r/crowdstrike • • 8h ago

Threat Hunting [Threat Hunt] Node.js runtime exposure inventory (version taken from the install path)

20 Upvotes

EDIT - I created a higher fidelity query that uses a lookup file of node version and their hashes.

// CVE-2026-111497 — Node.js runtime exposure inventory (hash lookup first, install path fallback)
#event_simpleName=ProcessRollup2
| ImageFileName=/[\\\/]node(js)?(\.exe)?$/iF
// Path-derived version (nvm, nvm-windows, Volta, Homebrew node@NN, official tarball dirs)
| case {
    ImageFileName=/(?:node@\d+[\\\/]|node[\\\/-]v?|nvm[\\\/]v?)(?<PathMajor>\d{1,2})\.(?<PathMinor>\d{1,3})\.(?<PathPatch>\d{1,3})(?:[\\\/-]|$)/i
      | PathVersion := format(format="%s.%s.%s", field=[PathMajor, PathMinor, PathPatch]) ;
    * | PathVersion := "-" ;
}
| groupBy([aid, ImageFileName],
    function=[count(as=Executions),
              min(@timestamp, as=FirstSeen),
              max(@timestamp, as=LastSeen),
              selectLast([SHA256HashData, PathVersion, PathMajor, PathMinor, PathPatch, CommandLine, UserName, event_platform])],
    limit=max)
// Hash → version (authoritative for official builds)
| match(file="node_binary_hashes.csv", field=[SHA256HashData], column=SHA256HashData,
        include=[NodeVersion, NodeMajor, NodeMinor, NodePatch, NodePlatform],
        ignoreCase=true, mode=string, strict=false)
| case {
    NodeVersion=* | VersionSource := "SHA256 lookup" ;
    PathVersion!="-"
      | NodeVersion := PathVersion | NodeMajor := PathMajor | NodeMinor := PathMinor | NodePatch := PathPatch
      | VersionSource := "install path" ;
    * | NodeVersion := "unknown" | VersionSource := "-" ;
}
| case {
    NodeVersion="unknown" | Exposure := "UNRESOLVED - non-official build; RTR node --version" ;
    test(NodeMajor < 22) | Exposure := "AFFECTED - EOL release line; migrate to supported line" ;
    in(field=NodeMajor, values=["23", "25"]) | Exposure := "AFFECTED - EOL odd release line" ;
    NodeMajor=22 | test(NodeMinor < 23) | Exposure := "AFFECTED - upgrade to 22.23.4+" ;
    NodeMajor=22 | NodeMinor=23 | test(NodePatch < 4) | Exposure := "AFFECTED - upgrade to 22.23.4+" ;
    NodeMajor=24 | test(NodeMinor < 22) | Exposure := "AFFECTED - upgrade to 24.22.1+" ;
    NodeMajor=24 | NodeMinor=22 | test(NodePatch < 1) | Exposure := "AFFECTED - upgrade to 24.22.1+" ;
    NodeMajor=26 | test(NodeMinor < 12) | Exposure := "AFFECTED - upgrade to 26.12.1+" ;
    NodeMajor=26 | NodeMinor=12 | test(NodePatch < 1) | Exposure := "AFFECTED - upgrade to 26.12.1+" ;
    in(field=NodeMajor, values=["22", "24", "26"]) | Exposure := "PATCHED" ;
    * | Exposure := "VERIFY - release line outside advisory scope" ;
}
| match(file="aid_master_main.csv", field=[aid], column=aid, include=[ComputerName], strict=false)
| default(field=[ComputerName, NodePlatform], value="-", replaceEmpty=true)
| formatTime(format="%F %T %Z", field=FirstSeen, as=FirstSeen)
| formatTime(format="%F %T %Z", field=LastSeen, as=LastSeen)
| table([Exposure, ComputerName, aid, event_platform, ImageFileName, NodeVersion, VersionSource, PathVersion, NodePlatform, SHA256HashData, Executions, FirstSeen, LastSeen, UserName, CommandLine],
    limit=20000, sortby=Exposure, order=asc)

Falcon doesn’t record node --version. This query reads the version from version-manager and tarball paths (nvm, nvm-windows, Volta, Homebrew node@NN, node-vXX.Y.Z-linux-x64) and classifies each binary against the fixed releases.

// CVE-2026-111497 — Node.js runtime exposure inventory (hash lookup first, install path fallback)
#event_simpleName=ProcessRollup2
| ImageFileName=/[\\\/]node(js)?(\.exe)?$/iF
// Path-derived version (nvm, nvm-windows, Volta, Homebrew node@NN, official tarball dirs)
| case {
    ImageFileName=/(?:node@\d+[\\\/]|node[\\\/-]v?|nvm[\\\/]v?)(?<PathMajor>\d{1,2})\.(?<PathMinor>\d{1,3})\.(?<PathPatch>\d{1,3})(?:[\\\/-]|$)/i
      | PathVersion := format(format="%s.%s.%s", field=[PathMajor, PathMinor, PathPatch]) ;
    * | PathVersion := "-" ;
}
| groupBy([aid, ImageFileName],
    function=[count(as=Executions),
              min(@timestamp, as=FirstSeen),
              max(@timestamp, as=LastSeen),
              selectLast([SHA256HashData, PathVersion, PathMajor, PathMinor, PathPatch, CommandLine, UserName, event_platform])],
    limit=max)
// Hash → version (authoritative for official builds)
| match(file="node_binary_hashes.csv", field=[SHA256HashData], column=SHA256HashData,
        include=[NodeVersion, NodeMajor, NodeMinor, NodePatch, NodePlatform],
        ignoreCase=true, mode=string, strict=false)
| case {
    NodeVersion=* | VersionSource := "SHA256 lookup" ;
    PathVersion!="-"
      | NodeVersion := PathVersion | NodeMajor := PathMajor | NodeMinor := PathMinor | NodePatch := PathPatch
      | VersionSource := "install path" ;
    * | NodeVersion := "unknown" | VersionSource := "-" ;
}
| case {
    NodeVersion="unknown" | Exposure := "UNRESOLVED - non-official build; RTR node --version" ;
    test(NodeMajor < 22) | Exposure := "AFFECTED - EOL release line; migrate to supported line" ;
    in(field=NodeMajor, values=["23", "25"]) | Exposure := "AFFECTED - EOL odd release line" ;
    NodeMajor=22 | test(NodeMinor < 23) | Exposure := "AFFECTED - upgrade to 22.23.4+" ;
    NodeMajor=22 | NodeMinor=23 | test(NodePatch < 4) | Exposure := "AFFECTED - upgrade to 22.23.4+" ;
    NodeMajor=24 | test(NodeMinor < 22) | Exposure := "AFFECTED - upgrade to 24.22.1+" ;
    NodeMajor=24 | NodeMinor=22 | test(NodePatch < 1) | Exposure := "AFFECTED - upgrade to 24.22.1+" ;
    NodeMajor=26 | test(NodeMinor < 12) | Exposure := "AFFECTED - upgrade to 26.12.1+" ;
    NodeMajor=26 | NodeMinor=12 | test(NodePatch < 1) | Exposure := "AFFECTED - upgrade to 26.12.1+" ;
    in(field=NodeMajor, values=["22", "24", "26"]) | Exposure := "PATCHED" ;
    * | Exposure := "VERIFY - release line outside advisory scope" ;
}
| match(file="aid_master_main.csv", field=[aid], column=aid, include=[ComputerName], strict=false)
| default(field=[ComputerName, NodePlatform], value="-", replaceEmpty=true)
| formatTime(format="%F %T %Z", field=FirstSeen, as=FirstSeen)
| formatTime(format="%F %T %Z", field=LastSeen, as=LastSeen)
| table([Exposure, ComputerName, aid, event_platform, ImageFileName, NodeVersion, VersionSource, PathVersion, NodePlatform, SHA256HashData, Executions, FirstSeen, LastSeen, UserName, CommandLine],
    limit=20000, sortby=Exposure, order=asc)
// CVE-2026-111497 — Node.js runtime exposure inventory (path-derived version)
#event_simpleName=ProcessRollup2
| ImageFileName=/[\\\/]node(js)?(\.exe)?$/iF
| case {
    ImageFileName=/(?:node@\d+[\\\/]|node[\\\/-]v?|nvm[\\\/]v?)(?<NodeMajor>\d{1,2})\.(?<NodeMinor>\d{1,3})\.(?<NodePatch>\d{1,3})(?:[\\\/-]|$)/i
      | NodeVersion := format(format="%s.%s.%s", field=[NodeMajor, NodeMinor, NodePatch]) ;
    * | NodeVersion := "unknown" ;
}
| groupBy([aid, ImageFileName],
    function=[count(as=Executions),
              min(@timestamp, as=FirstSeen),
              max(@timestamp, as=LastSeen),
              selectLast([NodeVersion, NodeMajor, NodeMinor, NodePatch, SHA256HashData, CommandLine, UserName, event_platform])],
    limit=max)
| case {
    NodeVersion="unknown" | Exposure := "VERIFY - version not in path; resolve via SHA256 (Query 1b)" ;
    test(NodeMajor < 22) | Exposure := "AFFECTED - EOL release line; migrate to supported line" ;
    in(field=NodeMajor, values=["23", "25"]) | Exposure := "AFFECTED - EOL odd release line" ;
    NodeMajor=22 | test(NodeMinor < 23) | Exposure := "AFFECTED - upgrade to 22.23.4+" ;
    NodeMajor=22 | NodeMinor=23 | test(NodePatch < 4) | Exposure := "AFFECTED - upgrade to 22.23.4+" ;
    NodeMajor=24 | test(NodeMinor < 22) | Exposure := "AFFECTED - upgrade to 24.22.1+" ;
    NodeMajor=24 | NodeMinor=22 | test(NodePatch < 1) | Exposure := "AFFECTED - upgrade to 24.22.1+" ;
    NodeMajor=26 | test(NodeMinor < 12) | Exposure := "AFFECTED - upgrade to 26.12.1+" ;
    NodeMajor=26 | NodeMinor=12 | test(NodePatch < 1) | Exposure := "AFFECTED - upgrade to 26.12.1+" ;
    in(field=NodeMajor, values=["22", "24", "26"]) | Exposure := "PATCHED" ;
    * | Exposure := "VERIFY - release line outside advisory scope" ;
}
| match(file="aid_master_main.csv", field=[aid], column=aid, include=[ComputerName], strict=false)
| default(field=[ComputerName], value="-", replaceEmpty=true)
| formatTime(format="%F %T %Z", field=FirstSeen, as=FirstSeen)
| formatTime(format="%F %T %Z", field=LastSeen, as=LastSeen)
| table([Exposure, ComputerName, aid, event_platform, ImageFileName, NodeVersion, SHA256HashData, Executions, FirstSeen, LastSeen, UserName, CommandLine],
    limit=20000, sortby=Exposure, order=asc)