SOCLIFE
SOCLIFE

Evidence-led security analysis

Published knowledge

Search SOC//LIFE

DetectionsDET-009

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

  1. Collect Node JS process executions from the user-profile Nodejs directory.
  2. Require a JavaScript argument.
  3. Build a thirty-day device baseline for user-space Node JS.
  4. Keep devices with no historical user-space Node JS activity.
  5. Collect Run/RunOnce values targeting Node JS or ProgramData.
  6. Correlate persistence within ten minutes before to thirty minutes after Node JS execution.
  7. Preserve process, parent, registry target, hashes, and device/user context.

Primary analytic

Query

KQL and SPL express the same analytical intent using source-specific schemas.
Q-01Detection logic

New 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 desc
SPL
(
 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_time

What 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.

  • Timestamp
  • DeviceId
  • DeviceName
  • FileName
  • FolderPath
  • ProcessId
  • ProcessUniqueId
  • ProcessCommandLine
  • AccountUpn
  • SHA1
  • SHA256
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessUniqueId
registry

Endpoint registry creation/modification telemetry.

  • Timestamp
  • DeviceId
  • DeviceName
  • ActionType
  • RegistryKey
  • RegistryValueName
  • RegistryValueData
  • PreviousRegistryValueData
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessAccountUpn
  • InitiatingProcessUniqueId
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

Use the candidate context to reconstruct what executed, what changed, and what communicated next.
View query — Trace photo shortcut and PowerShell staging
Q-02Drilldown

Trace 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 asc
SPL
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 _time

What 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.

  • Timestamp
  • DeviceId
  • DeviceName
  • FileName
  • FolderPath
  • ProcessId
  • ProcessUniqueId
  • ProcessCommandLine
  • AccountUpn
  • SHA1
  • SHA256
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessUniqueId
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-03Drilldown

Check 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 asc
SPL
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 _time

What 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.

  • Timestamp
  • DeviceId
  • DeviceName
  • FileName
  • FolderPath
  • ProcessId
  • ProcessUniqueId
  • ProcessCommandLine
  • AccountUpn
  • SHA1
  • SHA256
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessUniqueId
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-04Drilldown

Correlate 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 asc
SPL
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 cmd

What 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.

  • Timestamp
  • DeviceId
  • DeviceName
  • FileName
  • FolderPath
  • ProcessId
  • ProcessUniqueId
  • ProcessCommandLine
  • AccountUpn
  • SHA1
  • SHA256
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessUniqueId
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-05Drilldown

Inspect 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 asc
SPL
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 _time

What 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.

  • Timestamp
  • DeviceId
  • DeviceName
  • RemoteUrl
  • RemoteIP
  • RemotePort
  • Protocol
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessAccountUpn
  • InitiatingProcessUniqueId
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

Similar activity can be legitimate. Confirm the approved purpose and expected context before escalating.
  • 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.
View threat hunt

Technical boundary

Telemetry and limitations

Process

Endpoint process creation telemetry.

Required fields
  • Timestamp
  • DeviceId
  • DeviceName
  • FileName
  • FolderPath
  • ProcessId
  • ProcessUniqueId
  • ProcessCommandLine
  • AccountUpn
  • SHA1
  • SHA256
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessUniqueId
Registry

Endpoint registry creation/modification telemetry.

Required fields
  • Timestamp
  • DeviceId
  • DeviceName
  • ActionType
  • RegistryKey
  • RegistryValueName
  • RegistryValueData
  • PreviousRegistryValueData
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessAccountUpn
  • InitiatingProcessUniqueId
Network

Endpoint network telemetry.

Required fields
  • Timestamp
  • DeviceId
  • DeviceName
  • RemoteUrl
  • RemoteIP
  • RemotePort
  • Protocol
  • InitiatingProcessFileName
  • InitiatingProcessFolderPath
  • InitiatingProcessCommandLine
  • InitiatingProcessAccountUpn
  • InitiatingProcessUniqueId

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

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.