Behavior-based detection engineering
New User-Space Node JS with Run-Key Persistence
Detects first-seen Node JS JavaScript execution from a user profile followed by Run or RunOnce persistence targeting Node JS or a ProgramData payload.
Behavior
What it detects
A device without recent user-space Node JS history executes JavaScript from the user profile and creates nearby Run/RunOnce persistence pointing to Node JS or ProgramData.
Engineering decision
Why this detection
Node JS is legitimate. Run keys are common. The signal is the combination: a device that does not normally run user-space Node JS begins executing JavaScript from the user profile and establishes startup persistence nearby.
Campaign-specific shortcut names, PowerShell variants, compiler activity, Defender exclusions, and non-standard ports stay in drilldowns so the primary detection survives infrastructure and syntax rotation.
Signal chain
Detection logic
- Collect Node JS process executions from the user-profile Nodejs directory.
- Require a JavaScript argument.
- Build a thirty-day device baseline for user-space Node JS.
- Keep devices with no historical user-space Node JS activity.
- Collect Run/RunOnce values targeting Node JS or ProgramData.
- Correlate persistence within ten minutes before to thirty minutes after Node JS execution.
- Preserve process, parent, registry target, hashes, and device/user context.
Primary analytic
Query
Q-01Detection logicNew user-space Node JS followed by Run-key persistence
What this checks
Generate a candidate when a device with no recent user-space Node JS history executes JavaScript and creates nearby startup persistence.
KQL
let current_window=1d;
let baseline_window=30d;
let history =
DeviceProcessEvents
| where Timestamp between (ago(baseline_window) .. ago(current_window))
| where FileName =~ "node.exe"
| where FolderPath has @"\AppData\Local\Nodejs\"
| summarize HistoricalUserSpaceNode=count() by DeviceId;
let nodes =
DeviceProcessEvents
| where Timestamp >= ago(current_window)
| where FileName =~ "node.exe"
| where FolderPath has @"\AppData\Local\Nodejs\"
| where ProcessCommandLine has ".js"
| project NodeTime=Timestamp,DeviceId,DeviceName,AccountUpn,NodePath=FolderPath,
NodeCommandLine=ProcessCommandLine,NodeProcessUniqueId=ProcessUniqueId,
NodeSHA1=SHA1,NodeSHA256=SHA256,NodeParent=InitiatingProcessFileName,
NodeParentCommandLine=InitiatingProcessCommandLine;
let persistence =
DeviceRegistryEvents
| where Timestamp >= ago(current_window)
| where RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\Run"
or RegistryKey has @"\Software\Microsoft\Windows\CurrentVersion\RunOnce"
| where RegistryValueData has_any (@"\AppData\Local\Nodejs\",@"\ProgramData\")
| extend PersistenceKind=case(
RegistryKey endswith @"\RunOnce","RunOnce",
RegistryKey endswith @"\Run","Run","Other")
| project PersistenceTime=Timestamp,DeviceId,PersistenceKind,RegistryKey,
RegistryValueName,RegistryValueData,
PersistenceProcess=InitiatingProcessFileName,
PersistenceCommandLine=InitiatingProcessCommandLine;
nodes
| join kind=leftouter history on DeviceId
| extend HistoricalUserSpaceNode=coalesce(HistoricalUserSpaceNode,0)
| where HistoricalUserSpaceNode == 0
| join kind=inner persistence on DeviceId
| where PersistenceTime between (NodeTime - 10m .. NodeTime + 30m)
| summarize PersistenceKinds=make_set(PersistenceKind,5),
PersistenceValues=make_set(RegistryValueData,20),
PersistenceKeys=make_set(RegistryKey,10),
FirstPersistence=min(PersistenceTime),
LastPersistence=max(PersistenceTime)
by NodeTime,DeviceId,DeviceName,AccountUpn,NodePath,NodeCommandLine,
NodeProcessUniqueId,NodeSHA1,NodeSHA256,NodeParent,NodeParentCommandLine,
HistoricalUserSpaceNode
| order by NodeTime descSPL
(
index=<endpoint_process_index> sourcetype=<process_events_sourcetype> earliest=-30d
)
OR
(
index=<endpoint_registry_index> sourcetype=<registry_events_sourcetype> earliest=-1d
)
| eval device=coalesce(device,DeviceName,host),
user=lower(coalesce(user,AccountUpn,InitiatingProcessAccountUpn)),
process_name=lower(coalesce(process_name,FileName,InitiatingProcessFileName)),
process_path=lower(coalesce(process_path,FolderPath,InitiatingProcessFolderPath)),
cmd=coalesce(process_command_line,ProcessCommandLine,InitiatingProcessCommandLine),
registry_key=coalesce(registry_key,RegistryKey),
value_data=coalesce(value_data,RegistryValueData),
event_type=case(
process_name="node.exe" AND like(process_path,"%\\appdata\\local\\nodejs\\%") AND like(lower(cmd),"%.js%"),"user_node",
(like(lower(registry_key),"%\\currentversion\\run") OR like(lower(registry_key),"%\\currentversion\\runonce"))
AND (like(lower(value_data),"%\\appdata\\local\\nodejs\\%") OR like(lower(value_data),"%\\programdata\\%")),"persistence",
true(),"other"),
is_current=if(_time>=relative_time(now(),"-1d"),1,0)
| where event_type!="other"
| eventstats count(eval(event_type="user_node" AND is_current=0)) as historical_user_node by device
| sort 0 device _time
| streamstats current=f
last(eval(if(event_type="user_node" AND is_current=1,_time,null()))) as node_time
last(eval(if(event_type="user_node" AND is_current=1,process_path,null()))) as node_path
last(eval(if(event_type="user_node" AND is_current=1,cmd,null()))) as node_cmd
by device
| where event_type="persistence" AND is_current=1 AND historical_user_node=0
AND isnotnull(node_time) AND _time>=node_time-600 AND _time<=node_time+1800
| stats min(_time) as first_persistence max(_time) as last_persistence
values(registry_key) as persistence_keys values(value_data) as persistence_values
by device user node_time node_path node_cmd historical_user_node
| convert ctime(node_time) ctime(first_persistence) ctime(last_persistence)
| sort - node_timeWhat to look for
A result that materially raises or lowers confidence in persistent malicious Node JS activity.
Technical details
Tested signal
First-seen user-space Node JS JavaScript plus Run/RunOnce persistence.
Assumptions
- Required endpoint telemetry is available and device roles can be baselined.
Data requirements and relevant fields
- process
Endpoint process creation telemetry.
TimestampDeviceIdDeviceNameFileNameFolderPathProcessIdProcessUniqueIdProcessCommandLineAccountUpnSHA1SHA256InitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessUniqueId
- registry
Endpoint registry creation/modification telemetry.
TimestampDeviceIdDeviceNameActionTypeRegistryKeyRegistryValueNameRegistryValueDataPreviousRegistryValueDataInitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessAccountUpnInitiatingProcessUniqueId
KQL schema
Validate table availability, field population, and retention.
SPL schema
Replace placeholders and map process/registry/network fields locally.
Limitations
- Legitimate developer and packaged-application Node JS activity must be tuned explicitly.
KQL uses Microsoft Defender XDR endpoint telemetry. SPL is a normalized raw-event scaffold requiring local field mapping.
Analyst workflow
What the analyst should look for
- Is the device a developer/build endpoint?
- Is the user-space Node JS runtime expected and managed?
- What JavaScript file is executed and who created it?
- What process launched the runtime?
- Does the startup value point to Node JS or an unexplained ProgramData binary?
- Are both Run and RunOnce present?
- Was suspicious PowerShell or a fake image shortcut observed earlier?
- Did the process connect to unusual destinations or non-standard ports?
Expected result
A result that materially raises or lowers confidence in persistent malicious Node JS activity.
Investigation pivots
Drilldowns
View query — Trace photo shortcut and PowerShell staging
Q-02DrilldownTrace photo shortcut and PowerShell staging
What this checks
Search the candidate device for fake PNG shortcut naming and the downloader chain.
KQL
let target_device="<DEVICE_NAME>";
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where DeviceName =~ target_device
| where
(ProcessCommandLine has ".png.lnk" and ProcessCommandLine has_any ("IMG-","PHOTO-"))
or
(FileName in~ ("powershell.exe","pwsh.exe") and
ProcessCommandLine has_any ("Invoke-WebRequest"," iwr ","iwr ") and
ProcessCommandLine has ".ps1")
| project Timestamp,DeviceName,AccountUpn,FileName,FolderPath,ProcessCommandLine,
SHA1,SHA256,InitiatingProcessFileName,InitiatingProcessCommandLine
| order by Timestamp ascSPL
index=<endpoint_process_index> sourcetype=<process_events_sourcetype> earliest=-7d
| eval device=coalesce(device,DeviceName,host),
process_name=lower(coalesce(process_name,FileName)),
cmd=coalesce(process_command_line,ProcessCommandLine)
| where device="<DEVICE_NAME>" AND (
(like(lower(cmd),"%.png.lnk%") AND (like(cmd,"%IMG-%") OR like(cmd,"%PHOTO-%")))
OR
(process_name IN ("powershell.exe","pwsh.exe") AND
(like(lower(cmd),"%invoke-webrequest%") OR like(lower(cmd),"%iwr %")) AND
like(lower(cmd),"%.ps1%")))
| table _time device process_name cmd InitiatingProcessFileName InitiatingProcessCommandLine SHA1 SHA256
| sort 0 _timeWhat to look for
A result that materially raises or lowers confidence in persistent malicious Node JS activity.
Technical details
Tested signal
Campaign-style LNK or PowerShell script staging before Node JS.
Assumptions
- Required endpoint telemetry is available and device roles can be baselined.
Data requirements and relevant fields
- process
Endpoint process creation telemetry.
TimestampDeviceIdDeviceNameFileNameFolderPathProcessIdProcessUniqueIdProcessCommandLineAccountUpnSHA1SHA256InitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessUniqueId
KQL schema
Validate table availability, field population, and retention.
SPL schema
Replace placeholders and map process/registry/network fields locally.
Limitations
- Legitimate developer and packaged-application Node JS activity must be tuned explicitly.
KQL uses Microsoft Defender XDR endpoint telemetry. SPL is a normalized raw-event scaffold requiring local field mapping.
View query — Check Wave 2 compile-after-delivery
Q-03DrilldownCheck Wave 2 compile-after-delivery
What this checks
Find PowerShell-adjacent C# compiler activity on the candidate endpoint.
KQL
let target_device="<DEVICE_NAME>";
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where DeviceName =~ target_device
| where FileName in~ ("csc.exe","cvtres.exe")
| where InitiatingProcessFileName in~ ("powershell.exe","pwsh.exe")
or InitiatingProcessFolderPath has @"\AppData\Local\Temp\"
| project Timestamp,DeviceName,FileName,FolderPath,ProcessCommandLine,
InitiatingProcessFileName,InitiatingProcessFolderPath,InitiatingProcessCommandLine
| order by Timestamp ascSPL
index=<endpoint_process_index> sourcetype=<process_events_sourcetype> earliest=-7d
| eval device=coalesce(device,DeviceName,host),
process_name=lower(coalesce(process_name,FileName)),
parent_name=lower(coalesce(parent_name,InitiatingProcessFileName)),
parent_path=lower(coalesce(parent_path,InitiatingProcessFolderPath))
| where device="<DEVICE_NAME>" AND process_name IN ("csc.exe","cvtres.exe")
AND (parent_name IN ("powershell.exe","pwsh.exe") OR like(parent_path,"%\\appdata\\local\\temp\\%"))
| table _time device process_name ProcessCommandLine parent_name parent_path InitiatingProcessCommandLine
| sort 0 _timeWhat to look for
A result that materially raises or lowers confidence in persistent malicious Node JS activity.
Technical details
Tested signal
csc/cvtres execution from PowerShell or temporary context.
Assumptions
- Required endpoint telemetry is available and device roles can be baselined.
Data requirements and relevant fields
- process
Endpoint process creation telemetry.
TimestampDeviceIdDeviceNameFileNameFolderPathProcessIdProcessUniqueIdProcessCommandLineAccountUpnSHA1SHA256InitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessUniqueId
KQL schema
Validate table availability, field population, and retention.
SPL schema
Replace placeholders and map process/registry/network fields locally.
Limitations
- Legitimate developer and packaged-application Node JS activity must be tuned explicitly.
KQL uses Microsoft Defender XDR endpoint telemetry. SPL is a normalized raw-event scaffold requiring local field mapping.
View query — Correlate Defender exclusions with Temp execution
Q-04DrilldownCorrelate Defender exclusions with Temp execution
What this checks
Identify Defender process exclusions followed by user-temp executable launches.
KQL
let target_device="<DEVICE_NAME>";
let e=DeviceProcessEvents
| where Timestamp >= ago(7d) and DeviceName =~ target_device
| where FileName in~ ("powershell.exe","pwsh.exe")
| where ProcessCommandLine has "Add-MpPreference" and ProcessCommandLine has "-ExclusionProcess"
| project DeviceId,ExclusionTime=Timestamp,ExclusionCmd=ProcessCommandLine;
let x=DeviceProcessEvents
| where Timestamp >= ago(7d) and DeviceName =~ target_device
| where FolderPath has @"\AppData\Local\Temp\" and FileName endswith ".exe"
| project DeviceId,ExecTime=Timestamp,FileName,FolderPath,ProcessCommandLine;
e
| join kind=inner x on DeviceId
| where ExecTime between (ExclusionTime .. ExclusionTime+30m)
| order by ExclusionTime ascSPL
index=<endpoint_process_index> sourcetype=<process_events_sourcetype> earliest=-7d
| eval device=coalesce(device,DeviceName,host),
process_name=lower(coalesce(process_name,FileName)),
process_path=lower(coalesce(process_path,FolderPath)),
cmd=coalesce(process_command_line,ProcessCommandLine),
event_type=case(
process_name IN ("powershell.exe","pwsh.exe") AND like(lower(cmd),"%add-mppreference%") AND like(lower(cmd),"%-exclusionprocess%"),"exclusion",
like(process_path,"%\\appdata\\local\\temp\\%") AND like(process_name,"%.exe"),"temp_exec",
true(),"other")
| where device="<DEVICE_NAME>" AND event_type!="other"
| sort 0 device _time
| streamstats current=f last(eval(if(event_type="exclusion",_time,null()))) as exclusion_time by device
| where event_type="temp_exec" AND isnotnull(exclusion_time) AND _time<=exclusion_time+1800
| table _time exclusion_time device process_name process_path cmdWhat to look for
A result that materially raises or lowers confidence in persistent malicious Node JS activity.
Technical details
Tested signal
Security-control modification tightly preceding payload execution.
Assumptions
- Required endpoint telemetry is available and device roles can be baselined.
Data requirements and relevant fields
- process
Endpoint process creation telemetry.
TimestampDeviceIdDeviceNameFileNameFolderPathProcessIdProcessUniqueIdProcessCommandLineAccountUpnSHA1SHA256InitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessUniqueId
KQL schema
Validate table availability, field population, and retention.
SPL schema
Replace placeholders and map process/registry/network fields locally.
Limitations
- Legitimate developer and packaged-application Node JS activity must be tuned explicitly.
KQL uses Microsoft Defender XDR endpoint telemetry. SPL is a normalized raw-event scaffold requiring local field mapping.
View query — Inspect non-standard network beaconing
Q-05DrilldownInspect non-standard network beaconing
What this checks
Search source-reported high ports from Node JS or suspicious user-space payload locations.
KQL
let target_device="<DEVICE_NAME>";
DeviceNetworkEvents
| where Timestamp >= ago(7d)
| where DeviceName =~ target_device
| where RemotePort in (8443,8445,8453,5555,56001,56002,56003)
| where InitiatingProcessFileName =~ "node.exe"
or InitiatingProcessFolderPath has @"\AppData\Local\Temp\"
or InitiatingProcessFolderPath has @"\AppData\Local\Nodejs\"
or InitiatingProcessFolderPath has @"\ProgramData\"
| project Timestamp,DeviceName,RemoteUrl,RemoteIP,RemotePort,Protocol,
InitiatingProcessFileName,InitiatingProcessFolderPath,InitiatingProcessCommandLine
| order by Timestamp ascSPL
index=<endpoint_network_index> sourcetype=<endpoint_network_events_sourcetype> earliest=-7d
| eval device=coalesce(device,DeviceName,host),
remote_port=coalesce(remote_port,RemotePort,dest_port),
process_name=lower(coalesce(process_name,InitiatingProcessFileName)),
process_path=lower(coalesce(process_path,InitiatingProcessFolderPath))
| where device="<DEVICE_NAME>" AND remote_port IN (8443,8445,8453,5555,56001,56002,56003)
AND (process_name="node.exe" OR like(process_path,"%\\appdata\\local\\%") OR like(process_path,"%\\programdata\\%"))
| table _time device process_name process_path RemoteIP RemoteUrl remote_port
| sort 0 _timeWhat to look for
A result that materially raises or lowers confidence in persistent malicious Node JS activity.
Technical details
Tested signal
Node JS/user-space process communication on campaign-observed non-standard ports.
Assumptions
- Required endpoint telemetry is available and device roles can be baselined.
Data requirements and relevant fields
- network
Endpoint network telemetry.
TimestampDeviceIdDeviceNameRemoteUrlRemoteIPRemotePortProtocolInitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessAccountUpnInitiatingProcessUniqueId
KQL schema
Validate table availability, field population, and retention.
SPL schema
Replace placeholders and map process/registry/network fields locally.
Limitations
- Legitimate developer and packaged-application Node JS activity must be tuned explicitly.
KQL uses Microsoft Defender XDR endpoint telemetry. SPL is a normalized raw-event scaffold requiring local field mapping.
Legitimate resemblance
What the analyst should confirm
A developer uses portable Node JS from a user profile and an approved tool creates startup persistence.
The device is a developer/build asset, the runtime/workload is owned, the startup value is documented, and no suspicious PowerShell or network activity follows.A packaged enterprise application bundles Node JS in user space and creates Run/RunOnce entries.
The parent installer, signer/hash, management platform, app owner, and network destinations match an approved deployment.A security-research endpoint reproduces the campaign.
The device is an approved lab asset and the test window is documented.
Confirmed match
Action after a confirmed match
- Isolate the endpoint and preserve process/registry/network evidence.
- Remove both startup persistence paths and payload targets after evidence collection.
- Remove attacker-deployed JavaScript and user-space Node JS components that are not business-required.
- Revert unauthorized Defender exclusions.
- Search all endpoints for the same behavior and confirmed infrastructure.
- Tune only documented developer/build populations rather than globally suppressing Node JS.
Threat hunt
Could this be happening elsewhere?
Hunt for this behavior across the environment.Technical boundary
Telemetry and limitations
- Process
Endpoint process creation telemetry.
Required fieldsTimestampDeviceIdDeviceNameFileNameFolderPathProcessIdProcessUniqueIdProcessCommandLineAccountUpnSHA1SHA256InitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessUniqueId
- Registry
Endpoint registry creation/modification telemetry.
Required fieldsTimestampDeviceIdDeviceNameActionTypeRegistryKeyRegistryValueNameRegistryValueDataPreviousRegistryValueDataInitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessAccountUpnInitiatingProcessUniqueId
- Network
Endpoint network telemetry.
Required fieldsTimestampDeviceIdDeviceNameRemoteUrlRemoteIPRemotePortProtocolInitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessAccountUpnInitiatingProcessUniqueId
Blind spots
- Existing legitimate user-space Node JS can hide compromise behind the baseline.
- Registry telemetry gaps can hide one persistence branch.
- The actor can move or rename the runtime.
- Ports and domains can rotate quickly.
Behavior mapping
MITRE ATT&CK
T1059.007· JavaScriptThe candidate requires Node JS executing a JavaScript payload.
T1547.001· Registry Run Keys / Startup FolderRun/RunOnce persistence elevates the unusual runtime into a high-value candidate.
T1059.001· PowerShellPowerShell staging is exposed as an investigation drilldown.
T1571· Non-Standard PortNon-standard-port beaconing is retained as a post-detection network drilldown.
Mappings describe the behavior examined by this analytic. They do not prove attribution, deployment, or technique-wide coverage.
Review boundary
Sources and limits
- External sources
- 4
Exact fields, retention, and operational thresholds remain environment-specific.