elastic / elastic/detection-rules
[Rule Tuning] Node.js Pre or Post-Install Script Execution
- Dominant language
- Python
- Stars
- 2.7k
- Forks
- 696
- Avg merge
- 4d 17h
- Merged PRs (30d)
- 87
Description
## Link to Rule
https://github.com/elastic/detection-rules/blob/main/rules/cross-platform/execution_nodejs_pre_or_post_install_script_execution.toml
## Rule Tuning Type
False Negatives - Enhancing detection of true threats that were previously missed.
## Description
This rule ("Node.js Pre or Post-Install Script Execution") currently only matches `process.args == "install"` for the first stage of its sequence. This misses several real, commonly-used npm subcommands that trigger the exact same pre/post-install lifecycle-script execution as `npm install`: **`npm ci`** (the standard, CI/CD-recommended clean-install command, arguably *more* likely to appear in the exact automated pipeline context this rule is meant to protect than interactive `npm install`), **`npm i`** (the universally-used shorthand alias for `install`), **`npm install-ci-test`**, and **`npm cit`** (its shorthand alias). An attacker-controlled malicious package with a postinstall hook doesn't care which of these subcommands triggered its execution. Restricting to the single literal string `"install"` creates a straightforward evasion gap (any pipeline or attacker instruction using `npm ci`/`npm i` instead of the exact word `install` would silently miss detection).
**Behavior-based**: this is an argument-completeness fix to an existing behavioral pattern, not a new detection concept.
**Real testing performed against all four variants plus a genuine negative control**: using a real local npm registry (Verdaccio) and a real, benign lab package whose `preinstall` script writes a timestamped marker file, generated and captured (via real Auditbeat execve auditing) four real scenarios: `npm install `, `npm ci`, `npm i`, each producing the identical real process chain (`npm-cli.js install/ci/i ...` spawning a child `sh -c 'node -e ...'`, which runs `node -e ...`, with real parent/child PID linkage confirmed in the captured audit record each time). As a **real false-positive control**, also ran `npm install lodash` and `npm ci` against the real public npm registry with a real package that has **no** install hook. Confirmed **no child process was spawned at all** in either case, the correct real-world negative control for the sequence's second stage.
**Validated the literal rule query, both before and after the tuning**: reshaped the 4 real captured command lines and real parent/child PID linkage into a scratch index preserving the rule's actual sequence shape, ran the literal sequence EQL. The current (untuned) rule matches only the `npm install` case; the tuned rule (`process.args in ("install", "ci", "i", "install-ci-test", "cit")`) matches all four real scenarios, while the real no-hook negative control matches neither version.
## Example Data
Real captured true positives (reshaped from real Auditbeat execve captures into ECS process-event shape, since Elastic Defend's own eBPF sensor isn't available on the test host's kernel):
```json
{
"event": { "category": ["process"], "type": ["start"] },
"process": { "name": "node", "args": ["node", "/usr/lib/node_modules/npm/bin/npm-cli.js", "ci"] }
}
```
```json
{
"event": { "category": ["process"], "type": ["start"] },
"process": { "name": "node", "args": ["node", "/usr/lib/node_modules/npm/bin/npm-cli.js", "i"] }
}
```
Each of these was followed, in the real captured chain, by a real child `sh -c 'node -e ...'` (the same second-stage shape the existing rule already checks for the `install` case).
Real negative control (a real `npm ci` against the public registry with a package that has no install hook; confirmed no child process spawned at all):
```json
{
"event": { "category": ["process"], "type": ["start"] },
"process": { "name": "node", "args": ["node", "/usr/lib/node_modules/npm/bin/npm-cli.js", "ci"] }
}
```
No corresponding second-stage event exists for this one, so the sequence correctly does not match.
Proposed change (first sequence stage only):
```eql
// current:
process.args == "install"
// proposed:
process.args in ("install", "ci", "i", "install-ci-test", "cit")
```
Contributor guide
Research direction
Open rules/cross-platform/execution_nodejs_pre_or_post_install_script_execution.toml and inspect the first stage of its existing sequence. Replace the single install argument match with the five proposed npm subcommands, then validate that the four hook-execution scenarios match while the no-hook npm install and npm ci controls do not.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js
- Domain
- security
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100