elastic / elastic/detection-rules

[Rule Tuning] Node.js Pre or Post-Install Script Execution

Open Beginner friendly
#6,607 0 comments 0 reactions 0 assignees View on GitHub
community
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.