SIEM
Security Information and Event Management (SIEM) systems collect logs from many devices into one store.This store can be queried for incidents, policy gaps, and inventory.
NVM for Windows Advanced Logging writes Node.js activity in an SIEM-friendly format, in a dedicated NVM for Windows/Operational channel. Send that channel to your SIEM to see which Node.js versions run in the organization, who installed or removed them, when a firewall or malware rule blocked a package, and much more.
Advanced logging produces structured logs with metadata fields. These are explicitly designed for querying.
Without this, SIEM collectors would require custom parsers against plaintext messages, with no syntax guarantees. The plaintext logging available without advanced logging is designed for individual user readability, not SIEM or analytics. Plaintext logs will change over time with community feedback. Advanced Logging exists to provide a reliable log source for critical auditing capabilities.
This guide shows how to collect the channel, then gives starter queries.
Setup the collector
| Item | Value |
|---|---|
| Channel | NVM for Windows/Operational |
| Provider | NVM for Windows |
| Provider GUID | {4c0f8d8e-2d6b-4f93-9f0f-3f0d7b4e2d11} |
Point your collector at the NVM for Windows/Operational channel. Do not collect only the Windows Application log. Application warnings, such as NVM4101, are a separate stream.
Most EventName / Payload events need the Advanced Logging add-on. Without that license, classic events still include Code. They do not include EventName or Payload.
Exception: nodejs.executed (Event ID 120) is written whenever log_executions is on. Community Builds and Certified Builds. No Advanced Logging license. Certified Builds also write package_manager.executed the same way.
How to query
There is a full list of Error Codes (and explanations) for use in SIEM pipelines. When querying, drop the NVM prefix. For example, NVM4312 is EventData.Code == 4312.
| You want | Filter |
|---|---|
| Structured info, warning, or error | Event ID 120, 121, or 122 |
| An NVM code | Code (number only) |
| A named lifecycle or audit event | EventName, then parse Payload as JSON |
Each node.exe launch | Event ID 120 nodejs.executed when log_executions is on. Event 200 unused. |
| Field | Detail |
|---|---|
Payload | JSON string - Do not look for the NVM number inside this field. |
Code | Separate UInt32 - Code is 0 when the event is not an NVM#### condition. |
Collectors flatten EventData in different ways. Use the names your platform created if a sample field is missing.
| Microsoft Sentinel | Splunk | Elastic | Event Viewer | |
|---|---|---|---|---|
| Channel | EventLog | Channel | winlog.channel | log name |
| Event ID | EventID | EventCode | winlog.event_id | EventID |
| Event name | EventData.EventName | EventData.EventName | winlog.event_data.EventName | Data[@Name='EventName'] |
| Payload | EventData.Payload | EventData.Payload | winlog.event_data.Payload | Data[@Name='Payload'] |
| Code | EventData.Code | EventData.Code | winlog.event_data.Code | Data[@Name='Code'] |
| Computer | Computer | Computer | winlog.computer_name | — |
Example Queries
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventData.Code == 4312
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventData.Code=4312
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_data.Code == 4312
wevtutil qe "NVM for Windows/Operational" /q:"*[EventData[Data[@Name='Code']='4312']]" /f:text /c:20
Node.js versions in use
Event ID 120 nodejs.executed records each shim launch when log_executions is on.
Payload.NodeVersion is the version. Event ID 200 is unused.
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| where EventData.EventName == "nodejs.executed"
| extend Payload = parse_json(tostring(EventData.Payload))
| summarize Launches = count() by tostring(Payload.NodeVersion), Computer
| order by Launches desc
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.EventName="nodejs.executed"
| eval Payload=json_decode(EventData.Payload)
| stats count as Launches by Payload.NodeVersion, Computer
| sort -Launches
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.EventName == "nodejs.executed"
| EVAL Payload = TO_JSON(winlog.event_data.Payload)
| STATS Launches = COUNT(*) BY version = Payload.NodeVersion, host = winlog.computer_name
| SORT Launches DESC
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='EventName']='nodejs.executed']]" /f:text /c:20
Host count for the same event:
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| where EventData.EventName == "nodejs.executed"
| extend Payload = parse_json(tostring(EventData.Payload))
| summarize Launches = dcount(Computer) by tostring(EventData.EventName)
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.EventName="nodejs.executed"
| stats dc(Computer) as Hosts
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.EventName == "nodejs.executed"
| STATS Hosts = COUNT_DISTINCT(winlog.computer_name)
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='EventName']='nodejs.executed']]" /f:text /c:20
Who installed a Node.js version
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| where EventData.EventName == "nvm.system.changed"
| extend Payload = parse_json(tostring(EventData.Payload))
| where Payload.Action == "install"
| where Payload.Outcome == "Succeeded"
| summarize Installs = count() by tostring(Payload.NodeVersion), tostring(Payload.User), tostring(Payload.hostname)
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.EventName="nvm.system.changed"
| eval action=json_extract('EventData.Payload',"Action"), outcome=json_extract('EventData.Payload',"Outcome"), node_version=json_extract('EventData.Payload',"NodeVersion"), user=json_extract('EventData.Payload',"User"), hostname=json_extract('EventData.Payload',"hostname")
| search action=install outcome=Succeeded
| stats count as Installs by node_version, user, hostname
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.EventName == "nvm.system.changed" AND winlog.event_data.Payload LIKE "*\"Action\":\"install\"*" AND winlog.event_data.Payload LIKE "*\"Outcome\":\"Succeeded\"*"
| STATS Installs = COUNT(*) BY host = winlog.computer_name
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='EventName']='nvm.system.changed']]" /f:text /c:20
Who installed Node.js 20
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| extend Payload = parse_json(tostring(EventData.Payload))
| where EventData.EventName == "nvm.system.changed"
| where Payload.Action == "install"
| where Payload.Outcome == "Succeeded"
| where tostring(Payload.NodeVersion) startswith "20."
| summarize People = dcount(tostring(Payload.User))
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.EventName="nvm.system.changed"
| eval action=json_extract('EventData.Payload',"Action"), outcome=json_extract('EventData.Payload',"Outcome"), node_version=json_extract('EventData.Payload',"NodeVersion"), user=json_extract('EventData.Payload',"User")
| search action=install outcome=Succeeded
| search node_version="20.*"
| stats dc(user) as People
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.EventName == "nvm.system.changed" AND winlog.event_data.Payload LIKE "*\"Action\":\"install\"*" AND winlog.event_data.Payload LIKE "*\"Outcome\":\"Succeeded\"*" AND winlog.event_data.Payload LIKE "*\"NodeVersion\":\"20.*"
| STATS People = COUNT_DISTINCT(winlog.computer_name)
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='EventName']='nvm.system.changed']]" /f:text /c:20
Change 20. to another major version, or to an EOL line your policy tracks.
The event does not mark EOL for you.
Who uninstalled a Node.js version
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| where EventData.EventName == "nvm.system.changed"
| extend Payload = parse_json(tostring(EventData.Payload))
| where Payload.Action == "uninstall"
| where Payload.Outcome == "Succeeded"
| summarize Uninstalls = count() by tostring(Payload.NodeVersion), tostring(Payload.User), tostring(Payload.hostname)
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.EventName="nvm.system.changed"
| eval action=json_extract('EventData.Payload',"Action"), outcome=json_extract('EventData.Payload',"Outcome"), node_version=json_extract('EventData.Payload',"NodeVersion"), user=json_extract('EventData.Payload',"User"), hostname=json_extract('EventData.Payload',"hostname")
| search action=uninstall outcome=Succeeded
| stats count as Uninstalls by node_version, user, hostname
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.EventName == "nvm.system.changed" AND winlog.event_data.Payload LIKE "*\"Action\":\"uninstall\"*" AND winlog.event_data.Payload LIKE "*\"Outcome\":\"Succeeded\"*"
| STATS Uninstalls = COUNT(*) BY host = winlog.computer_name
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='EventName']='nvm.system.changed']]" /f:text /c:20
Which version became the default
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| where EventData.EventName == "nvm.version.activated"
| extend Payload = parse_json(tostring(EventData.Payload))
| summarize Activations = count() by tostring(Payload.version), tostring(Payload.user), tostring(Payload.hostname)
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.EventName="nvm.version.activated"
| eval version=json_extract('EventData.Payload',"version"), user=json_extract('EventData.Payload',"user"), hostname=json_extract('EventData.Payload',"hostname")
| stats count as Activations by version, user, hostname
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.EventName == "nvm.version.activated"
| STATS Activations = COUNT(*) BY host = winlog.computer_name
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='EventName']='nvm.version.activated']]" /f:text /c:20
Package-manager installs allowed or blocked
Certified Builds write package_manager.install when the module firewall allows or denies an install.
NVM4408 is an allow.
NVM4403 is a deny.
A malware block does not write this event. Use NVM4602 instead.
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID in (120, 122)
| where EventData.EventName == "package_manager.install"
| summarize Events = count() by Computer, EventData.Code
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode IN (120,122) EventData.EventName="package_manager.install"
| stats count as Events by Computer, EventData.Code
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id IN (120, 122) AND winlog.event_data.EventName == "package_manager.install"
| STATS Events = COUNT(*) BY host = winlog.computer_name, code = winlog.event_data.Code
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120 or EventID=122)] and EventData[Data[@Name='EventName']='package_manager.install']]" /f:text /c:20
Malware blocks
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| where EventData.Code == 4602
| where EventData.EventName == "malware.package_blocked"
| summarize Blocks = count() by Computer
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.Code=4602 EventData.EventName="malware.package_blocked"
| stats count as Blocks by Computer
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.Code == 4602 AND winlog.event_data.EventName == "malware.package_blocked"
| STATS Blocks = COUNT(*) BY host = winlog.computer_name
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='Code']='4602'] and EventData[Data[@Name='EventName']='malware.package_blocked']]" /f:text /c:20
Inspect Payload for the package name, version when known, command, source, and reason.
Integrity verification failures
Authenticode / trust failures are NVM4301. A swapped node.exe whose FileVersion does not match the folder is NVM4314.
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventData.Code in (4301, 4314)
| summarize Failures = count() by Computer, EventID, EventData.EventName, EventData.Code
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventData.Code IN (4301,4314)
| stats count as Failures by Computer, EventCode, EventData.EventName, EventData.Code
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_data.Code IN (4301, 4314)
| STATS Failures = COUNT(*) BY host = winlog.computer_name, event_id = winlog.event_id, name = winlog.event_data.EventName, code = winlog.event_data.Code
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[EventData[Data[@Name='Code']='4301' or Data[@Name='Code']='4314']]" /f:text /c:20
Version firewall blocks
Governance VersionAllowList / VersionBlockList denies are NVM4411.
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventData.Code == 4411
| where EventData.EventName == "node.security.version_firewall_blocked"
| summarize Blocks = count() by Computer
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventData.Code=4411 EventData.EventName="node.security.version_firewall_blocked"
| stats count as Blocks by Computer
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_data.Code == 4411 AND winlog.event_data.EventName == "node.security.version_firewall_blocked"
| STATS Blocks = COUNT(*) BY host = winlog.computer_name
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[EventData[Data[@Name='Code']='4411'] and EventData[Data[@Name='EventName']='node.security.version_firewall_blocked']]" /f:text /c:20
Lifecycle scripts blocked
HKLM BlockPackageManagerLifecycleScripts writes NVM4412 (package_manager.lifecycle_scripts_blocked) on Certified Builds with Advanced Logging when the proxy rewrites an install-class command (install, ci, add, update, rebuild, and similar; npx with a package; bare yarn). npm --version and npm run do not write it. The lock still applies to those commands. This is audit, not a failed install. Community Builds do not write it. Inspect Payload.command, Payload.verb, and Payload.action.
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventData.Code == 4412
| where EventData.EventName == "package_manager.lifecycle_scripts_blocked"
| summarize Locks = count() by Computer, tostring(EventData.EventName)
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventData.Code=4412 EventData.EventName="package_manager.lifecycle_scripts_blocked"
| stats count as Locks by Computer
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_data.Code == 4412 AND winlog.event_data.EventName == "package_manager.lifecycle_scripts_blocked"
| STATS Locks = COUNT(*) BY host = winlog.computer_name
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[EventData[Data[@Name='Code']='4412'] and EventData[Data[@Name='EventName']='package_manager.lifecycle_scripts_blocked']]" /f:text /c:20
Configuration changes
- Microsoft Sentinel
- Splunk
- Elastic
- Event Viewer
Event
| where EventLog == "NVM for Windows/Operational"
| where EventID == 120
| where EventData.EventName == "nvm.configuration.changed"
| extend Payload = parse_json(tostring(EventData.Payload))
| summarize Changes = count() by tostring(Payload.Configuration), tostring(Payload.User), tostring(Payload.hostname)
Change `index=wineventlog` to your Splunk index.
index=wineventlog Channel="NVM for Windows/Operational" EventCode=120 EventData.EventName="nvm.configuration.changed"
| eval configuration=json_extract('EventData.Payload',"Configuration"), user=json_extract('EventData.Payload',"User"), hostname=json_extract('EventData.Payload',"hostname")
| stats count as Changes by configuration, user, hostname
Change `FROM logs-*` to your Elastic data stream.
FROM logs-*
| WHERE winlog.channel == "NVM for Windows/Operational" AND winlog.event_id == 120 AND winlog.event_data.EventName == "nvm.configuration.changed"
| STATS Changes = COUNT(*) BY host = winlog.computer_name
Event Viewer lists matching events. It does not count or group them.
wevtutil qe "NVM for Windows/Operational" /q:"*[System[(EventID=120)] and EventData[Data[@Name='EventName']='nvm.configuration.changed']]" /f:text /c:20
Event ID templates
The Windows Event ID is the template. It is not the NVM number.
| Event ID | Writer | Fields |
|---|---|---|
| 100 | nvm.exe info | Source, Message, Code |
| 101 | nvm.exe warning | Source, Message, Code |
| 102 | nvm.exe error | Source, Message, Code |
| 120 | structured info | Source, EventName, Payload, Code |
| 121 | structured warning | Source, EventName, Payload, Code |
| 122 | structured error | Source, EventName, Payload, Code |
| 200 | unused | Manifest leftover. Launch logging is Event ID 120 nodejs.executed. |
| 210 | shim info | Source, Message, Code |
| 211 | shim warning | Source, Message, Code |
| 212 | shim error | Source, Message, Code |
Event ID 200 is unused. Launch logging uses Event ID 120.
Security events from the shims include user, sid, hostname, parent_process, parent_pid, project_name, and project_path when the process can resolve them.
An empty value is unknown.
Event names with Code 0
Query these by EventName.
| EventName | What it records |
|---|---|
nvm.system.changed | Install, uninstall, and similar lifecycle results. Payload: Action, NodeVersion, Outcome, User, sid, hostname, correlation_id. |
nvm.configuration.changed | A setting change. Payload: Configuration, Value, Old, Outcome. |
nvm.version.activated | The active Node.js version changed. Payload: version, previous_version, user, hostname. |
nodejs.executed | A node launch when LogExecutions is 1. Source node-shim. When permission allow flags were supplied, applied, or removed, the payload includes permission_allow.supplied, permission_allow.applied, and permission_allow.removed. |
package_manager.executed | An npm, yarn, pnpm, or similar launch on a Certified Build when LogExecutions is 1. Source proxy. |
Event names that carry an NVM code
Match Code to the number in Error Codes.
| EventName | Code | Source |
|---|---|---|
verify_key.open_failed | 4312 | nvm.exe on a Certified Build. A Community Build writes Event ID 102 with the same code. |
node.security.verification_failed | 4301 | node-shim or proxy |
node.security.activation_blocked | 4302 or 4305 | nvm.exe uses 4302. The proxy uses 4305. |
node.security.cache_state_changed | 4303 | node-shim or proxy |
node.security.full_verification_recovered | 4304 | node-shim or proxy |
node.security.version_mismatch | 4314 | node-shim or proxy. PE FileVersion does not match the install folder. |
node.security.version_firewall_blocked | 4411 | node-shim or proxy. Governance VersionAllowList / VersionBlockList denied the version. |
package_manager.lifecycle_scripts_blocked | 4412 | proxy. Advanced Logging only. HKLM lifecycle-script lock on an install-class command. |
network.deadline_exceeded | 4501 | nvm.exe or sync.exe |
malware.definitions_updated | 4601 | sync.exe. Advanced Logging only. |
malware.package_blocked | 4602 | Shim. Advanced Logging only. |
firewall.elevation_required | 4404 | nvm.exe |
firewall.invalid_rule | 4405 | nvm.exe |
firewall.untrusted_module_changed | 4406 | nvm.exe or proxy |
firewall.policy_mutated | 4410 | nvm.exe |
firewall.policy_mutate_failed | 4410 | nvm.exe |
package_manager.install | 4403 or 4408 | proxy on a Certified Build. 4403 is a deny. 4408 is an allow. |