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.
| Type | Value | Note |
|---|---|---|
| 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.
- Collect: DNS lookups and proxy URLs from recruiting workstations on Windows and macOS, last 90 days
- Match: the host is daniel-ayala.netlify.app, or any subdomain of jimdosite.com or netlify.app
- Exclude: devices outside the recruiting-workstation group (placeholder list)
- 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 siteRecruiting 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 siteRecruiting 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 siteA 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 siteRecruiting 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 siteRecruiting 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 siteRecruiting 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
- Threat Spotlight: Red Flags for Red Star Hackers: Hunting for North Korean Insiders — ReliaQuest, 2025-05-08
- Staying a Step Ahead: Mitigating the DPRK IT Worker Threat — Google Cloud (Mandiant), 2024-09-23
- Jasper Sleet: North Korean remote IT workers' evolving tactics to infiltrate organizations — Microsoft Security Blog, 2025-06-30
- Famous Chollima Threat Actor Profile — Huntress Threat Library
- North Korean IT Workers Conducting Data Extortion — FBI Internet Crime Complaint Center, 2025-01-23