Suspicious Package Install During an Interview Take-Home Assignment
WindowsmacOSLinux
TTP Hunt medium Contagious InterviewSupply Chain
npm, node, pip or python spawning a shell, downloader or the other interpreter just after an install, and the interpreter reaching a staging host minutes later, whether the payload runs in a child process or entirely in memory.
What this hunt looks for
Socket tracks more than seventeen hundred malicious packages across npm, PyPI, Go, Rust and PHP linked to Contagious Interview. A candidate installs an assessment repository whose postinstall hook, or an obfuscated function in a plausible dependency, fetches a second stage such as BeaverTail. Node or Python spawning a shell or downloader straight after an install is a strong signal.
On PyPI the malicious logic often fires on import or call rather than at install. Socket also describes in-memory loaders that never spawn a child: hex strings passed to eval (HexEval), XORed byte tables (XORIndex), and an AES-256-CBC ciphertext hidden in a file such as LICENSE.
Those loaders run inside the interpreter, so the network match watches node and Python for ten minutes after an install for staging on vercel.app, npoint.io, raw addresses and the historical port 1224. The JavaScript-to-Python handoff puts python among the child processes.
- Technique
- Compromise of software dependencies through npm and PyPI package abuse (MITRE ATT&CK T1195.002) leading to user execution of a downloaded second stage (T1204.002), including in-memory loaders that never spawn a child process (T1027.013).
- Data sources
- EDR process telemetry, Package registry telemetry, EDR network telemetry, Package inventory
- 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 |
|---|---|---|
| npm package | epxreso, epxresso, epxressoo, dotevn, boby_parser, vaildator, cors-validator, http-helmet, morgan-logger, nodemailer-helper, nodemon-pkg |
Typosquats and lookalikes of express, dotenv, body-parser, validator, cors, helmet, morgan, nodemailer and nodemon among the 338 packages Socket attributed to the campaign in October 2025. |
| npm package | ethrs.js, ethres.js, we3.js, wb3.js, truffel, truflee, ganacche, ganacha, ganachee, ganachhe, gannache, foudry, foundary, foundrey, foundri, fundry, metamask-api |
Web3 tooling lookalikes of ethers, web3, truffle, ganache and foundry, plus a MetaMask brand impersonation (Socket). |
| npm package | react-router-html, react-redirect-router, vite-plugin-react-ping, vvite-plugin-react-ping, vitejs-plugin-react-refresh, nextjs-babel-toastify, webpack-css-branch-loader, redux-saga-sentinel, eslint-detector, jito-components, components-flexibility |
Frontend toolchain lookalikes from the same set (Socket). |
| URL | 0927.vercel.app/api/ipcheck, ip-check-server.vercel.app/api/ip-check/208, json-project-hazel.vercel.app/apikey/QWERTYU890T12HML, log-server-lovat.vercel.app/api/ipcheck/703, process-log.vercel.app/api/ipcheck, process-log-update.vercel.app/api/ipcheck |
Staging endpoints on Vercel, named to pass as health checks or logging hooks (Socket). |
| URL | api.npoint.io/b964566497d98298d32c, fashdefi[.]store:6168/defy/v7 |
Staging on a JSON-hosting service and on a raw domain with a non-standard port (Socket). |
| IP | 135.181.123.177, 138.201.50.5, 144.172.105.235, 144.172.112.106, 146.70.253.107, 23.127.202.249, 23.227.202.244 |
Raw-address endpoints in the same packages (Socket). 23.227.202.244 and 138.201.50.5 also appear in Talos's BeaverTail and OtterCookie reporting. |
Logic
Every query below implements these steps, so the same hunt returns the same result in each product.
- Collect: process starts and network connections on Windows, macOS and Linux endpoints; installed npm packages through osquery
- Match: an npm, yarn, pnpm or pip install command ran on the same device in the previous 10 minutes
- Match: and node, npm, python or pip starts a shell, downloader or script host (PowerShell, curl, certutil, wscript, osascript, cmd, sh)
- Match: or node or python connects to a published staging host, raw address or port 1224 or 6168 (addresses rotate)
- Match: or an installed npm package is on the published typosquat list (osquery inventory, no install window)
- Report: devices with the signal kinds seen, parent processes and examples, newest first
Queries
Defender / Sentinel KQL
Written for this site, from indicators reported by SocketJoins an install command to a spawn or staging connection on the same device within 10 minutes, one row per device.
// Package install followed within 10 minutes by a spawn from node or python, or by a connection to a staging endpoint
let Installs = DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in~ ("node.exe", "node", "npm.cmd", "npm", "yarn", "pnpm") or FileName startswith "python" or FileName startswith "pip"
| where ProcessCommandLine matches regex @'(?i)(^|[^a-z0-9])(npm|yarn|pnpm)(-cli[.]js|[.]cmd)?[^a-z0-9 ]?[ ]+(install|add|i)([^a-z0-9]|$)|(^|[^a-z0-9])pip[0-9.]*([.]exe)?[^a-z0-9 ]?[ ]+install([^a-z0-9]|$)|-m[ ]+pip[ ]+install'
| project DeviceName, InstallTime = Timestamp;
let Candidates = union
(DeviceProcessEvents
| where Timestamp > ago(7d)
| where (InitiatingProcessFileName in~ ("node.exe", "node", "npm.cmd", "npm") and (FileName in~ ("powershell.exe", "curl.exe", "curl", "certutil.exe", "wscript.exe", "osascript") or FileName startswith "python"))
or ((InitiatingProcessFileName startswith "python" or InitiatingProcessFileName startswith "pip") and FileName in~ ("powershell.exe", "curl.exe", "curl", "certutil.exe", "wscript.exe", "osascript"))
// optional: every npm lifecycle script and many pip builds run through a shell
or ((InitiatingProcessFileName in~ ("node.exe", "node", "npm.cmd", "npm") or InitiatingProcessFileName startswith "python" or InitiatingProcessFileName startswith "pip") and FileName in~ ("cmd.exe", "sh", "bash", "zsh"))
| project Timestamp, DeviceName, Signal = "spawn", Parent = InitiatingProcessFileName, Detail = ProcessCommandLine),
(DeviceNetworkEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName in~ ("node.exe", "node") or InitiatingProcessFileName startswith "python"
| where RemoteUrl has_any ("0927.vercel.app", "ip-check-server.vercel.app", "json-project-hazel.vercel.app", "log-server-lovat.vercel.app", "process-log.vercel.app", "process-log-update.vercel.app", "api.npoint.io", "fashdefi.store")
// optional: raw addresses from the same packages, they rotate
or RemoteIP in ("135.181.123.177", "138.201.50.5", "144.172.105.235", "144.172.112.106", "146.70.253.107", "23.127.202.249", "23.227.202.244")
// optional: ports shared with ordinary services
or (RemotePort in (1224, 6168) and not(ipv4_is_private(RemoteIP)))
// optional: any vercel.app host is broad, it also serves ordinary applications
or RemoteUrl contains ".vercel.app"
| project Timestamp, DeviceName, Signal = "staging", Parent = InitiatingProcessFileName, Detail = strcat(RemoteUrl, " ", RemoteIP, ":", tostring(RemotePort)));
Installs
| join kind=inner Candidates on DeviceName
| where Timestamp between (InstallTime .. (InstallTime + 10m))
| summarize Signals = make_set(Signal), Events = count(), Parents = make_set(Parent, 5), Examples = make_set(Detail, 5), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by DeviceName
| sort by LastSeen desc
Lower-fidelity parts (4)
- the shell names cmd.exe, sh, bash and zsh as children — every npm lifecycle script and many pip builds run through a shell; removing them also drops payloads started through child_process.exec. Remove: delete the `// optional: every npm lifecycle script` line and the `or ((InitiatingProcessFileName in~ ...` line below it.
- the seven raw staging addresses — listed in the packages and likely to rotate. Remove: delete the `// optional: raw addresses` line and the `or RemoteIP in (...)` line below it.
- the ports 1224 and 6168 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `// optional: ports` line and the `or (RemotePort in (1224, 6168) ...)` line below it.
- the broad `.vercel.app` suffix match — Vercel hosts many ordinary applications and developer tools. Remove: delete the `// optional: any vercel.app host` line and the `or RemoteUrl contains ".vercel.app"` line below it.
Splunk SPL
Written for this site, from indicators reported by SocketSorts process and network events by time and keeps those with an install command earlier on the same host in a 10-minute window.
```index=endpoint is a placeholder for EDR process and network events carrying the Endpoint data model fields```
index=endpoint earliest=-7d
| eval proc=lower(process_name), parent=lower(parent_process_name), cmd=lower(process), web=lower(coalesce(url, query))
| eval is_install=if(match(proc, "^(node|npm|yarn|pnpm)(\\.exe|\\.cmd)?$|^(python|pip)") AND match(cmd, "(^|[^a-z0-9])(npm|yarn|pnpm)(-cli[.]js|[.]cmd)?[^a-z0-9 ]?[ ]+(install|add|i)([^a-z0-9]|$)|(^|[^a-z0-9])pip[0-9.]*([.]exe)?[^a-z0-9 ]?[ ]+install([^a-z0-9]|$)|-m[ ]+pip[ ]+install"), 1, 0)
| eval is_spawn=case(
match(parent, "^(node|npm)(\\.exe|\\.cmd)?$") AND match(proc, "^(powershell|curl|certutil|wscript|osascript)(\\.exe)?$|^python"), 1,
match(parent, "^(python|pip)") AND match(proc, "^(powershell|curl|certutil|wscript|osascript)(\\.exe)?$"), 1,
```optional: every npm lifecycle script and many pip builds run through a shell```
match(parent, "^(node|npm)(\\.exe|\\.cmd)?$|^(python|pip)") AND match(proc, "^(cmd\\.exe|sh|bash|zsh)$"), 1,
true(), 0)
| eval is_initiator=if(match(proc, "^(node(\\.exe)?$|python)"), 1, 0)
| eval is_staging=case(
is_initiator=1 AND match(web, "(0927|ip-check-server|json-project-hazel|log-server-lovat|process-log|process-log-update)\\.vercel\\.app|api\\.npoint\\.io|fashdefi\\.store"), 1,
```optional: raw addresses from the same packages, they rotate```
is_initiator=1 AND in(dest_ip, "135.181.123.177", "138.201.50.5", "144.172.105.235", "144.172.112.106", "146.70.253.107", "23.127.202.249", "23.227.202.244"), 1,
```optional: ports shared with ordinary services```
is_initiator=1 AND in(tostring(dest_port), "1224", "6168") AND NOT (cidrmatch("10.0.0.0/8", dest_ip) OR cidrmatch("172.16.0.0/12", dest_ip) OR cidrmatch("192.168.0.0/16", dest_ip) OR cidrmatch("127.0.0.0/8", dest_ip)), 1,
```optional: any vercel.app host is broad, it also serves ordinary applications```
is_initiator=1 AND match(web, "\\.vercel\\.app"), 1,
true(), 0)
| where is_install=1 OR is_spawn=1 OR is_staging=1
| sort 0 host _time
| streamstats time_window=10m current=f sum(is_install) AS installs_10m BY host
| where (is_spawn=1 OR is_staging=1) AND installs_10m > 0
| eval signal=if(is_spawn=1, "spawn", "staging"), detail=coalesce(process, web)
| stats count AS events values(signal) AS signals values(parent_process_name) AS parents values(detail) AS examples min(_time) AS first_seen max(_time) AS last_seen BY host
| convert ctime(first_seen) ctime(last_seen)
| sort - last_seen
Lower-fidelity parts (4)
- the shell names cmd.exe, sh, bash and zsh as children — every npm lifecycle script and many pip builds run through a shell; removing them also drops payloads started through child_process.exec. Remove: delete the ```optional: every npm lifecycle script``` comment and the `match(parent, ...) AND match(proc, "^(cmd\\.exe|sh|bash|zsh)$"), 1,` line below it.
- the seven raw staging addresses — listed in the packages and likely to rotate. Remove: delete the ```optional: raw addresses``` comment and the `is_initiator=1 AND in(dest_ip, ...)` line below it.
- the ports 1224 and 6168 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the ```optional: ports``` comment and the `is_initiator=1 AND in(tostring(dest_port) ...` line below it.
- the broad `.vercel.app` suffix match — Vercel hosts many ordinary applications and developer tools. Remove: delete the ```optional: any vercel.app host``` comment and the `is_initiator=1 AND match(web, "\\.vercel\\.app"), 1,` line below it.
Sigma
Written for this site, from indicators reported by SocketThe single-event core only, because a Sigma correlation cannot be converted for Kusto, so the 10-minute install condition is applied in the KQL, SPL and EQL queries.
title: Node or Python Starts a Shell, Downloader or Interpreter
id: 5b41db11-0453-5d90-869e-79698f334542
status: experimental
description: >
node or npm starting a downloader, script host or interpreter, or python or
pip starting one, as a malicious postinstall hook or in-package payload of a
Contagious Interview take-home package does. The single-event core of the hunt:
the 10-minute install condition is added in the KQL, SPL and EQL queries.
references:
- https://socket.dev/blog/north-korea-contagious-interview-campaign-338-malicious-npm-packages
- https://socket.dev/blog/contagious-interview-campaign-spreads-across-5-ecosystems
logsource:
category: process_creation
detection:
parent_node:
ParentImage|re: '(?i)[\\/](node|npm)(\.exe|\.cmd)?$'
parent_python:
ParentImage|re: '(?i)[\\/](pythonw?[0-9.]*|pip[0-9.]*)(\.exe)?$'
child_core:
Image|re: '(?i)[\\/](powershell|curl|certutil|wscript|osascript)(\.exe)?$'
child_python:
Image|re: '(?i)[\\/]pythonw?[0-9.]*(\.exe)?$'
# optional: every npm lifecycle script and many pip builds run through a shell
child_shell:
Image|re: '(?i)[\\/](cmd\.exe|sh|bash|zsh)$'
condition: (parent_node and (child_core or child_python)) or (parent_python and child_core) or ((parent_node or parent_python) and child_shell)
falsepositives:
- Build tooling that shells out during install, for example node-gyp and husky
- Packages that call a downloader for an optional native build step
level: medium
---
title: Node or Python Connects to a Reported Staging Endpoint
id: 2d7c9e41-8b35-5a06-9f1e-4c3a6b8d0e72
status: experimental
description: >
node or python connecting to a staging host Socket reports for the Contagious
Interview packages, to one of the raw addresses in the same packages or to the
ports 1224 and 6168, for payloads that run in memory and spawn no child.
references:
- https://socket.dev/blog/north-korea-contagious-interview-campaign-338-malicious-npm-packages
logsource:
category: network_connection
detection:
initiator:
Image|re: '(?i)[\\/](node|pythonw?[0-9.]*)(\.exe)?$'
selection_host:
DestinationHostname|endswith:
- '0927.vercel.app'
- 'ip-check-server.vercel.app'
- 'json-project-hazel.vercel.app'
- 'log-server-lovat.vercel.app'
- 'process-log.vercel.app'
- 'process-log-update.vercel.app'
- 'api.npoint.io'
- 'fashdefi.store'
# optional: raw addresses from the same packages, they rotate
selection_ip:
DestinationIp:
- '135.181.123.177'
- '138.201.50.5'
- '144.172.105.235'
- '144.172.112.106'
- '146.70.253.107'
- '23.127.202.249'
- '23.227.202.244'
# optional: ports shared with ordinary services
selection_port:
DestinationPort:
- 1224
- 6168
# optional: any vercel.app host is broad, it also serves ordinary applications
selection_vercel:
DestinationHostname|endswith: '.vercel.app'
filter_private:
DestinationIp|cidr:
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
- '127.0.0.0/8'
condition: initiator and (selection_host or selection_ip or selection_vercel or (selection_port and not filter_private))
falsepositives:
- Developer tools and applications that use Vercel or npoint.io legitimately
level: medium
Lower-fidelity parts (4)
- the shell names cmd.exe, sh, bash and zsh as children — every npm lifecycle script and many pip builds run through a shell; removing them also drops payloads started through child_process.exec. Remove: delete the `# optional: every npm lifecycle script` comment, the `child_shell` block and `or ((parent_node or parent_python) and child_shell)` from the first rule's condition.
- the seven raw staging addresses — listed in the packages and likely to rotate. Remove: delete the `# optional: raw addresses` comment, the `selection_ip` block and `or selection_ip` from the second rule's condition.
- the ports 1224 and 6168 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `# optional: ports` comment, the `selection_port` and `filter_private` blocks and `or (selection_port and not filter_private)` from the second rule's condition.
- the broad `.vercel.app` suffix match — Vercel hosts many ordinary applications and developer tools. Remove: delete the `# optional: any vercel.app host` comment, the `selection_vercel` block and `or selection_vercel` from the second rule's condition.
CrowdStrike LogScale
Written for this site, from indicators reported by SocketPairs an install with a spawn or staging lookup inside one 10-minute bucket, and because Falcon DNS events name no process the staging lookup is not limited to node and python.
// Fixed 10-minute buckets stand in for the sliding window of the KQL, SPL and EQL queries; Falcon DNS events name no process
(#event_simpleName=/^(Synthetic)?ProcessRollup2$/ or #event_simpleName=DnsRequest or #event_simpleName=NetworkConnectIP4)
| case {
#event_simpleName=/ProcessRollup2$/ ImageFileName=/[\\\/](node|npm|yarn|pnpm|python[0-9.]*w?|pip[0-9.]*)(\.exe|\.cmd)?$/i CommandLine=/(^|[^a-z0-9])(npm|yarn|pnpm)(-cli[.]js|[.]cmd)?[^a-z0-9 ]?[ ]+(install|add|i)([^a-z0-9]|$)|(^|[^a-z0-9])pip[0-9.]*([.]exe)?[^a-z0-9 ]?[ ]+install([^a-z0-9]|$)|-m[ ]+pip[ ]+install/i | kind := "install";
#event_simpleName=/ProcessRollup2$/ ParentBaseFileName=/^(node|npm)(\.exe|\.cmd)?$/i ImageFileName=/[\\\/](powershell|curl|certutil|wscript|osascript|python[0-9.]*w?)(\.exe)?$/i | kind := "spawn";
#event_simpleName=/ProcessRollup2$/ ParentBaseFileName=/^(python[0-9.]*w?|pip[0-9.]*)(\.exe)?$/i ImageFileName=/[\\\/](powershell|curl|certutil|wscript|osascript)(\.exe)?$/i | kind := "spawn";
// optional: every npm lifecycle script and many pip builds run through a shell
#event_simpleName=/ProcessRollup2$/ ParentBaseFileName=/^(node|npm|python[0-9.]*w?|pip[0-9.]*)(\.exe|\.cmd)?$/i ImageFileName=/[\\\/](cmd\.exe|sh|bash|zsh)$/i | kind := "spawn";
#event_simpleName=DnsRequest DomainName=/((0927|ip-check-server|json-project-hazel|log-server-lovat|process-log|process-log-update)\.vercel\.app|api\.npoint\.io|fashdefi\.store)$/i | kind := "staging";
// optional: raw addresses from the same packages, they rotate
#event_simpleName=NetworkConnectIP4 in(RemoteAddressIP4, values=["135.181.123.177", "138.201.50.5", "144.172.105.235", "144.172.112.106", "146.70.253.107", "23.127.202.249", "23.227.202.244"]) | kind := "staging";
// optional: ports shared with ordinary services
#event_simpleName=NetworkConnectIP4 in(RemotePort, values=[1224, 6168]) RemoteAddressIP4!=/^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.)/ | kind := "staging";
// optional: any vercel.app host is broad, it also serves ordinary applications
#event_simpleName=DnsRequest DomainName=/\.vercel\.app$/i | kind := "staging";
}
| kind=*
| is_install := if(kind == "install", then=1, else=0)
| is_hit := if(kind == "install", then=0, else=1)
| bucket(span=10m, field=[ComputerName, aid], function=[sum(is_install, as=installs), sum(is_hit, as=hits), collect([kind, CommandLine, DomainName]), min(@timestamp, as=first_seen), max(@timestamp, as=last_seen)])
| installs > 0
| hits > 0
| sort(last_seen, order=desc)
Lower-fidelity parts (4)
- the shell names cmd.exe, sh, bash and zsh as children — every npm lifecycle script and many pip builds run through a shell; removing them also drops payloads started through child_process.exec. Remove: delete the `// optional: every npm lifecycle script` line and the `kind := "spawn";` branch line below it.
- the seven raw staging addresses — listed in the packages and likely to rotate. Remove: delete the `// optional: raw addresses` line and the `NetworkConnectIP4 in(RemoteAddressIP4 ...` branch line below it.
- the ports 1224 and 6168 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `// optional: ports` line and the `NetworkConnectIP4 in(RemotePort ...` branch line below it.
- the broad `.vercel.app` suffix match — Vercel hosts many ordinary applications and developer tools. Remove: delete the `// optional: any vercel.app host` line and the `DomainName=/\.vercel\.app$/i` branch line below it.
Elastic
Written for this site, from indicators reported by SocketAn EQL sequence on host.name, install first and spawn or staging second within 10 minutes, with the install pattern written as wildcards because EQL regular expressions lack \b and \s.
sequence by host.name with maxspan=10m
[process where event.type == "start" and
process.name : ("node.exe", "node", "npm.cmd", "npm", "yarn", "pnpm", "python*", "pip*") and
process.command_line : ("*npm* install*", "*npm* add *", "*npm* i", "*npm* i *", "*yarn* add *", "*yarn* install*", "*pnpm* add *", "*pnpm* install*", "*pnpm* i", "*pnpm* i *", "*pip* install*", "*-m pip install*")]
[any where
(event.category == "process" and event.type == "start" and (
(process.parent.name : ("node.exe", "node", "npm.cmd", "npm") and process.name : ("powershell.exe", "curl.exe", "curl", "certutil.exe", "wscript.exe", "osascript", "python*"))
or (process.parent.name : ("python*", "pip*") and process.name : ("powershell.exe", "curl.exe", "curl", "certutil.exe", "wscript.exe", "osascript"))
// optional: every npm lifecycle script and many pip builds run through a shell
or (process.parent.name : ("node.exe", "node", "npm.cmd", "npm", "python*", "pip*") and process.name : ("cmd.exe", "sh", "bash", "zsh"))
))
or (event.category == "network" and process.name : ("node.exe", "node", "python*") and (
dns.question.name : ("*0927.vercel.app", "*ip-check-server.vercel.app", "*json-project-hazel.vercel.app", "*log-server-lovat.vercel.app", "*process-log.vercel.app", "*process-log-update.vercel.app", "*api.npoint.io", "*fashdefi.store")
// optional: raw addresses from the same packages, they rotate
or cidrMatch(destination.ip, "135.181.123.177/32", "138.201.50.5/32", "144.172.105.235/32", "144.172.112.106/32", "146.70.253.107/32", "23.127.202.249/32", "23.227.202.244/32")
// optional: ports shared with ordinary services
or (destination.port in (1224, 6168) and not cidrMatch(destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"))
// optional: any vercel.app host is broad, it also serves ordinary applications
or dns.question.name : "*.vercel.app"
))]
Lower-fidelity parts (4)
- the shell names cmd.exe, sh, bash and zsh as children — every npm lifecycle script and many pip builds run through a shell; removing them also drops payloads started through child_process.exec. Remove: delete the `// optional: every npm lifecycle script` line and the `or (process.parent.name : (...) and process.name : ("cmd.exe" ...` line below it.
- the seven raw staging addresses — listed in the packages and likely to rotate. Remove: delete the `// optional: raw addresses` line and the `or cidrMatch(destination.ip, ...)` line below it.
- the ports 1224 and 6168 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `// optional: ports` line and the `or (destination.port in (1224, 6168) ...)` line below it.
- the broad `.vercel.app` suffix match — Vercel hosts many ordinary applications and developer tools. Remove: delete the `// optional: any vercel.app host` line and the `or dns.question.name : "*.vercel.app"` line below it.
Cortex XQL
Written for this site, from indicators reported by SocketPairs an install with a spawn or staging connection inside one 10-minute bin per host.
// Fixed 10-minute buckets stand in for the sliding window of the KQL, SPL and EQL queries
config case_sensitive = false
| dataset = xdr_data
| filter event_type = ENUM.PROCESS or event_type = ENUM.NETWORK
| alter f_install = if(event_type = ENUM.PROCESS and action_process_image_name ~= "^(node|npm|yarn|pnpm)([.]exe|[.]cmd)?$|^(python|pip)" and action_process_image_command_line ~= "(^|[^a-z0-9])(npm|yarn|pnpm)(-cli[.]js|[.]cmd)?[^a-z0-9 ]?[ ]+(install|add|i)([^a-z0-9]|$)|(^|[^a-z0-9])pip[0-9.]*([.]exe)?[^a-z0-9 ]?[ ]+install([^a-z0-9]|$)|-m[ ]+pip[ ]+install", 1, 0)
| alter f_spawn = if(event_type = ENUM.PROCESS and ((actor_process_image_name ~= "^(node|npm)([.]exe|[.]cmd)?$" and action_process_image_name ~= "^(powershell|curl|certutil|wscript|osascript)([.]exe)?$|^python") or (actor_process_image_name ~= "^(python|pip)" and action_process_image_name ~= "^(powershell|curl|certutil|wscript|osascript)([.]exe)?$")), 1, 0)
| alter f_staging = if(event_type = ENUM.NETWORK and actor_process_image_name ~= "^(node([.]exe)?$|python)" and (action_external_hostname ~= "((0927|ip-check-server|json-project-hazel|log-server-lovat|process-log|process-log-update)[.]vercel[.]app|api[.]npoint[.]io|fashdefi[.]store)$" or dns_query_name ~= "((0927|ip-check-server|json-project-hazel|log-server-lovat|process-log|process-log-update)[.]vercel[.]app|api[.]npoint[.]io|fashdefi[.]store)$"), 1, 0)
// optional: every npm lifecycle script and many pip builds run through a shell
| alter f_shell = if(event_type = ENUM.PROCESS and actor_process_image_name ~= "^(node|npm|python|pip)" and action_process_image_name ~= "^(cmd[.]exe|sh|bash|zsh)$", 1, 0)
// optional: raw addresses from the same packages, they rotate
| alter f_ip = if(event_type = ENUM.NETWORK and actor_process_image_name ~= "^(node([.]exe)?$|python)" and action_remote_ip in ("135.181.123.177", "138.201.50.5", "144.172.105.235", "144.172.112.106", "146.70.253.107", "23.127.202.249", "23.227.202.244"), 1, 0)
// optional: ports shared with ordinary services
| alter f_port = if(event_type = ENUM.NETWORK and actor_process_image_name ~= "^(node([.]exe)?$|python)" and action_remote_port in (1224, 6168) and not (action_remote_ip ~= "^(10[.]|192[.]168[.]|172[.](1[6-9]|2[0-9]|3[01])[.]|127[.])"), 1, 0)
// optional: any vercel.app host is broad, it also serves ordinary applications
| alter f_vercel = if(event_type = ENUM.NETWORK and actor_process_image_name ~= "^(node([.]exe)?$|python)" and (action_external_hostname ~= "[.]vercel[.]app$" or dns_query_name ~= "[.]vercel[.]app$"), 1, 0)
| bin _time span = 10m
| comp sum(f_install) as installs, sum(f_spawn) as spawns, sum(f_staging) as stagings, sum(f_shell) as shells, sum(f_ip) as ips, sum(f_port) as ports, sum(f_vercel) as vercels by agent_hostname, _time
| filter installs > 0 and (spawns > 0 or stagings > 0 or shells > 0 or ips > 0 or ports > 0 or vercels > 0)
| sort desc _time
Lower-fidelity parts (4)
- the shell names cmd.exe, sh, bash and zsh as children — every npm lifecycle script and many pip builds run through a shell; removing them also drops payloads started through child_process.exec. Remove: delete the `// optional: every npm lifecycle script` line and the `alter f_shell` line below it, then `sum(f_shell) as shells,` from `comp` and `or shells > 0` from the last `filter`.
- the seven raw staging addresses — listed in the packages and likely to rotate. Remove: delete the `// optional: raw addresses` line and the `alter f_ip` line below it, then `sum(f_ip) as ips,` from `comp` and `or ips > 0` from the last `filter`.
- the ports 1224 and 6168 on a public address — ordinary services use these ports, so it matches on port alone. Remove: delete the `// optional: ports` line and the `alter f_port` line below it, then `sum(f_port) as ports,` from `comp` and `or ports > 0` from the last `filter`.
- the broad `.vercel.app` suffix match — Vercel hosts many ordinary applications and developer tools. Remove: delete the `// optional: any vercel.app host` line and the `alter f_vercel` line below it, then `sum(f_vercel) as vercels` and its comma from `comp` and `or vercels > 0` from the last `filter`.
osquery
Written for this site, from indicators reported by SocketLists installed copies of the 39 published typosquat packages, constrain npm_packages with a directory = '<project path>' test on osquery versions that require it.
SELECT 'package' AS signal, path, name || '@' || version AS detail
FROM npm_packages
WHERE name IN ('epxreso', 'epxresso', 'epxressoo', 'dotevn', 'boby_parser', 'vaildator', 'cors-validator', 'http-helmet', 'morgan-logger', 'nodemailer-helper', 'nodemon-pkg',
'ethrs.js', 'ethres.js', 'we3.js', 'wb3.js', 'truffel', 'truflee', 'ganacche', 'ganacha', 'ganachee', 'ganachhe', 'gannache', 'foudry', 'foundary', 'foundrey', 'foundri', 'fundry', 'metamask-api',
'react-router-html', 'react-redirect-router', 'vite-plugin-react-ping', 'vvite-plugin-react-ping', 'vitejs-plugin-react-refresh', 'nextjs-babel-toastify', 'webpack-css-branch-loader',
'redux-saga-sentinel', 'eslint-detector', 'jito-components', 'components-flexibility')
-- optional: heuristic for the AES loader, a LICENSE file large enough to hide a ciphertext; each % covers one directory level
UNION ALL
SELECT 'license' AS signal, path, CAST(size AS TEXT) AS detail
FROM file
WHERE (path LIKE '/Users/%/%/node_modules/%/LICENSE' OR path LIKE '/home/%/%/node_modules/%/LICENSE' OR path LIKE 'C:\Users\%\%\node_modules\%\LICENSE')
AND size > 65536
Lower-fidelity parts (1)
- the oversized LICENSE heuristic for the AES loader — a large LICENSE file is only a hint and the % wildcards cover a fixed directory depth. Remove: delete from the `-- optional: heuristic` line to the end of the query, which removes the UNION ALL and the second SELECT.
False positives and tuning
Native-module build steps, node-gyp and husky among them, shell out during npm install, so this is noisy on a developer machine; scope it to active-candidate or new-hire devices and check package name and registry age.
Scientific and machine-learning PyPI packages occasionally shell out for native builds, and the PyPI trigger is more often import-time, so correlate with low-reputation package names and interview context.
The network match is bounded by the ten-minute install window and the listed endpoints, so it is quieter; the vercel.app suffix match is broad and should be dropped if it is noisy.
Sources
- North Korea's Contagious Interview Campaign Escalates: 338 Malicious npm Packages, 50,000 Downloads — Socket, 2025-10-10
- Contagious Interview, DeceptiveDevelopment, Gwisin Gang, Tenacious Pungsan, DEV#POPPER, PurpleBravo, TAG-121, Group G1052 — The MITRE Corporation (MITRE ATT&CK), 2026-07-31
- North Korea's Contagious Interview Campaign Spreads Across 5 Ecosystems, Delivering Staged RAT Payloads — Socket, 2026-04-07
- N. Korean Hackers Spread 1,700 Malicious Packages Across npm, PyPI, Go, Rust — The Hacker News, 2026-04-01
- Hacking Employers and Seeking Employment: Two Job-Related Campaigns Bear Hallmarks of North Korean Threat Actors — Unit 42 (Palo Alto Networks), 2023-11-21
- North Korean Malware, September 2025 — GitLab Threat Intelligence, 2025-09-30