Fabricated Candidate Personas and Portfolio-Site Infrastructure

WindowsmacOSNetwork

IOC Hunt low DPRK IT Worker FraudFamous Chollima

A recruiting-side hunt: the free site builders that host fabricated candidate portfolios, the identity-reuse patterns across applications, and the profile tells that repeat across personas.

What this hunt looks for

This hunt runs before hire, over recruiting-workstation traffic rather than endpoint telemetry on employees. ReliaQuest documents fabricated candidate profiles on free site builders, with AI-generated biographies, up to twelve years of copied experience, near-absent posting history, a near-verbatim skill list, GitHub repositories with no commits by their supposed owner, and profiles kept open-to-work after hire.

Mandiant adds background-check patterns: personas sharing one email address, United States addresses paired with education from institutions outside North America, universities that differ between the check and the resume, resumes overlapping published ones, and testimonials illustrated with other people's images. Its one published hosted persona is a Netlify subdomain.

The queries cover the web-facing half. The identity-reuse patterns, such as one email address across personas, need an applicant-tracking export and are an HR-system check rather than a log query.

Technique
Establish Accounts and develop fabricated personas ahead of access (MITRE ATT&CK T1585, T1585.001, T1587.001) in support of fraudulently obtained employment (T1078).
Data sources
Proxy logs from recruiting workstations, DNS logs, EDR network telemetry
Last updated
2026-10-04

Indicators

The atomic values this hunt matches on, listed separately from the queries so they can be lifted into tooling that speaks none of the query languages below. Every value here is reported by one of the sources cited at the foot of this page.

TypeValueNote
Hosting platform jimdosite.com Free site builder ReliaQuest names as hosting fabricated candidate profiles.
Hosting platform netlify.app Named by ReliaQuest; Mandiant publishes daniel-ayala.netlify.app as a specific example.
URL daniel-ayala.netlify.app Persona site published in Mandiant's indicator list (2024-09-23).

Logic

Every query below implements these steps, so the same hunt returns the same result in each product.

  1. Collect: DNS lookups and proxy URLs from recruiting workstations on Windows and macOS, last 90 days
  2. Match: the host is daniel-ayala.netlify.app, or any subdomain of jimdosite.com or netlify.app
  3. Exclude: devices outside the recruiting-workstation group (placeholder list)
  4. Report: devices and users with the candidate sites reached and a count of hits on the published persona

Queries

Defender / Sentinel KQL

Written for this site

Recruiting workstations that reached the published persona site or another free portfolio site, persona hits first.

// Replace the placeholder with the recruiting workstations.
let RecruitingDevices = dynamic(["recruiting-ws-01"]);
DeviceNetworkEvents
| where Timestamp > ago(90d)
| where DeviceName in~ (RecruitingDevices)
| where RemoteUrl has "daniel-ayala.netlify.app"
     // optional: any free portfolio site on the two builders ReliaQuest names, which every such candidate matches
     or RemoteUrl has_any ("jimdosite.com", "netlify.app")
| extend Site = tolower(extract(@"(?i)(?:^|[./@])([a-z0-9-]+\.(?:jimdosite\.com|netlify\.app))(?:[:/?#]|$)", 1, RemoteUrl))
| summarize Requests = count(), Sites = make_set(Site, 20),
            PublishedPersona = countif(Site == "daniel-ayala.netlify.app"), LastSeen = max(Timestamp)
        by DeviceName, InitiatingProcessAccountName
| sort by PublishedPersona desc, LastSeen desc
Lower-fidelity parts (1)
  • the jimdosite.com and netlify.app subdomain match — these free builders host millions of ordinary sites, so every candidate with a portfolio on one is returned and only the published persona is a known indicator. Remove: Delete the line beginning // optional: and the or RemoteUrl has_any line after it.

Splunk SPL

Written for this site

Recruiting workstations and users that reached the published persona site or another free portfolio site, in proxy or DNS data.

```sourcetype placeholders: index=proxy (url, src, user) and index=dns (query, src); recruiting_workstations.csv has a src column```
```optional: the OR isnotnull(site) match covers any free portfolio site on the two builders ReliaQuest names, which every such candidate matches```
(index=proxy OR index=dns) (url=* OR query=*) earliest=-90d
| lookup recruiting_workstations.csv src OUTPUT team
| where isnotnull(team)
| eval target=lower(coalesce(url, query))
| rex field=target "(?:^|[./@])(?<site>[a-z0-9-]+\.(?:jimdosite\.com|netlify\.app))(?:[:/?#]|$)"
| eval published=if(site="daniel-ayala.netlify.app", 1, 0)
| where published=1 OR isnotnull(site)
| stats count AS requests values(site) AS sites sum(published) AS published_persona max(_time) AS last_seen BY src, user
| sort 0 - published_persona, - last_seen
| convert ctime(last_seen)
Lower-fidelity parts (1)
  • the jimdosite.com and netlify.app subdomain match — these free builders host millions of ordinary sites, so every candidate with a portfolio on one is returned and only the published persona is a known indicator. Remove: Delete OR isnotnull(site) from the second where line and the optional comment above it.

Sigma

Written for this site

A lookup of the published persona site or of a free portfolio site, to be scoped to recruiting workstations in the SIEM.

title: Recruiting Workstation Lookup of a Fabricated Candidate Portfolio Site
id: b6d3e815-0f7a-4c92-91e4-3a5c8d2f7e10
status: experimental
description: >
  Detects a lookup of daniel-ayala.netlify.app, the persona site Mandiant
  published as a DPRK IT-worker indicator, or of any subdomain of the free
  site builders ReliaQuest names as hosting fabricated candidate profiles,
  jimdosite.com and netlify.app. Scope the rule to recruiting workstations,
  because these platforms host millions of legitimate sites.
references:
  - https://reliaquest.com/blog/threat-spotlight-identifying-north-korean-insider-threats
  - https://cloud.google.com/blog/topics/threat-intelligence/mitigating-dprk-it-worker-threat
logsource:
  category: dns_query
detection:
  selection_persona:
    QueryName: 'daniel-ayala.netlify.app'
  # optional: any free portfolio site on the two builders ReliaQuest names, which every such candidate matches
  selection_builders:
    QueryName|endswith:
      - '.jimdosite.com'
      - '.netlify.app'
  condition: 1 of selection_*
falsepositives:
  - Candidates who host an ordinary portfolio on either platform and are reviewed by recruiters
level: low
Lower-fidelity parts (1)
  • the jimdosite.com and netlify.app subdomain match — these free builders host millions of ordinary sites, so every candidate with a portfolio on one is returned and only the published persona is a known indicator. Remove: Delete the # optional: line and selection_builders, leaving selection_persona.

Elastic

Written for this site

Recruiting workstations and users that reached the published persona site or another free portfolio site.

FROM logs-*
| WHERE @timestamp > NOW() - 90 days AND host.name IN ("recruiting-ws-01")
| EVAL target = TO_LOWER(COALESCE(url.original, dns.question.name, destination.domain))
| WHERE target IS NOT NULL
| GROK target "(?:^|[./@])(?<site>[a-z0-9-]+\\.(?:jimdosite\\.com|netlify\\.app))(?:[:/?#]|$)"
| WHERE site IS NOT NULL
| EVAL published = CASE(site == "daniel-ayala.netlify.app", 1, 0)
| STATS requests = COUNT(*), sites = VALUES(site), published_persona = SUM(published), last_seen = MAX(@timestamp) BY host.name, user.name
| SORT published_persona DESC, last_seen DESC
| LIMIT 200
Lower-fidelity parts (1)
  • the jimdosite.com and netlify.app subdomain match — these free builders host millions of ordinary sites, so every candidate with a portfolio on one is returned and only the published persona is a known indicator. Remove: Change the line WHERE site IS NOT NULL to WHERE site == "daniel-ayala.netlify.app".

CrowdStrike LogScale

Written for this site

Recruiting workstations that looked up the published persona site or another free portfolio site, persona hits first.

// Replace the placeholder with the recruiting workstations.
#event_simpleName=DnsRequest
| in(field=ComputerName, values=["recruiting-ws-01"], ignoreCase=true)
| DomainName=/^daniel-ayala\.netlify\.app$/i
    // optional: any free portfolio site on the two builders ReliaQuest names, which every such candidate matches
    or DomainName=/\.(jimdosite\.com|netlify\.app)$/i
| lower(field=DomainName, as=site)
| case { site="daniel-ayala.netlify.app" | published := 1; * | published := 0 }
| groupBy([aid, ComputerName], function=[count(as=requests), collect(site), sum(published, as=published_persona), max(@timestamp, as=last_seen)])
| sort(published_persona, order=desc)
Lower-fidelity parts (1)
  • the jimdosite.com and netlify.app subdomain match — these free builders host millions of ordinary sites, so every candidate with a portfolio on one is returned and only the published persona is a known indicator. Remove: Delete the line beginning // optional: and the or DomainName line after it.

Cortex XQL

Written for this site

Recruiting workstations and users that looked up the published persona site or another free portfolio site, persona hits first.

// Replace the placeholder with the recruiting workstations.
dataset = xdr_data
| filter event_type = ENUM.NETWORK and dns_query_name != null and agent_hostname in ("recruiting-ws-01")
| alter site = lowercase(dns_query_name)
| filter site = "daniel-ayala.netlify.app"
    // optional: any free portfolio site on the two builders ReliaQuest names, which every such candidate matches
    or site ~= "\.(jimdosite\.com|netlify\.app)$"
| alter published = if(site = "daniel-ayala.netlify.app", 1, 0)
| comp count() as requests, values(site) as sites, sum(published) as published_persona, max(_time) as last_seen by agent_hostname, actor_effective_username
| sort desc published_persona
Lower-fidelity parts (1)
  • the jimdosite.com and netlify.app subdomain match — these free builders host millions of ordinary sites, so every candidate with a portfolio on one is returned and only the published persona is a known indicator. Remove: Delete the line beginning // optional: and the or site ~= line after it.

False positives and tuning

Netlify and comparable platforms host an enormous number of ordinary personal sites, so a candidate hosting a portfolio on one is normal; hence the scoping to recruiting data rather than web traffic.

The identity-reuse patterns assume an applicant-tracking export most organisations must build, and have innocent explanations: agencies resubmit candidates, referrals share a phone number, families share an address.

Nothing here should reject an applicant on its own; each signal decides where to verify more carefully.

Handling candidate data this way carries legal and privacy obligations that vary by jurisdiction.

Sources

Tuning required before use This hunt is a starting point. Adapt the queries to your schema, validate them against your baseline and tune them for the false positives noted above. Indicators go stale as operators rotate hosts.