Skip to content

Commit c6f78ca

Browse files
committed
feat(supply-chain): add watch-install phantom dependency detector (v0.3.0)
Introduces — a drop-in wrapper for npm/yarn/pnpm/bun that monitors node_modules/ in real time during install and halts if a postinstall script exhibits dropper behaviour (create binary → execute → delete), as seen in the 2025 axios-ecosystem supply-chain attack. - New command: watch-install <pm> [args...] with --no-fail flag - Detects phantom files (EphemeralFile) and executable drops (ExecutableDrop) - Configured via [supply_chain] in .greengate.toml: block_phantom_scripts, enforce_sandbox, allow_postinstall allowlist (warns but does not fail) - 250ms polling loop, zero new dependencies - 17 unit tests (WatchState logic, package_from_path, is_executable) - 11 integration tests covering phantom, exec-drop, allowlist, config, --no-fail - Docs: new watch-install command reference, roadmap (sandbox-install as Feature 2), updated README, getting-started, use-cases, ci-integration, VitePress sidebar
1 parent 16a3860 commit c6f78ca

File tree

15 files changed

+1307
-28
lines changed

15 files changed

+1307
-28
lines changed

.greengate.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,18 @@ target_dir = "."
6161
# current = "output/current.perf"
6262
# baseline = "output/baseline.perf"
6363
# threshold = 15.0 # % mean-time regression allowed
64+
65+
# ── Supply chain protection ───────────────────────────────────────────────────
66+
# Drives `greengate watch-install <pm> install`.
67+
# block_phantom_scripts — exit non-zero if a postinstall script creates and
68+
# then deletes a file inside node_modules/ (dropper signature).
69+
# enforce_sandbox — also flag new executables dropped in the project root
70+
# that were not present before the install began.
71+
# allow_postinstall — packages whose postinstall scripts legitimately create
72+
# temp files (e.g. native build tools that compile .node addons).
73+
# Findings from these are reported as warnings, not errors.
74+
#
75+
# [supply_chain]
76+
# block_phantom_scripts = true
77+
# enforce_sandbox = true
78+
# allow_postinstall = ["esbuild", "prisma", "@swc/core"]

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "greengate"
3-
version = "0.2.12"
3+
version = "0.3.0"
44
edition = "2024"
55

66
[dependencies]

README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818

1919
| Command | Purpose |
2020
|---|---|
21+
| `greengate watch-install` | **Supply-chain protection** — wraps npm/yarn/pnpm/bun and halts on phantom postinstall droppers |
2122
| `greengate scan` | Secrets, PII & AST-based SAST for JS/TS/Python/Go |
23+
| `greengate audit` | OSV dependency vulnerability audit |
2224
| `greengate review` | PR Complexity Score + new-code coverage gaps |
2325
| `greengate lint` | Kubernetes manifest linting |
2426
| `greengate docker-lint` | Dockerfile best-practice checks |
2527
| `greengate coverage` | LCOV / Cobertura coverage threshold gate |
26-
| `greengate audit` | OSV dependency vulnerability audit |
2728
| `greengate lighthouse` | PageSpeed Insights performance gate |
2829
| `greengate reassure` | React component render regression gate |
2930
| `greengate sbom` | CycloneDX 1.5 SBOM generation |
@@ -68,18 +69,21 @@ cargo install --git https://github.com/thinkgrid-labs/greengate
6869
## Quick start
6970

7071
```bash
72+
# Supply-chain safe install — detects postinstall droppers in real time
73+
greengate watch-install npm ci
74+
7175
# Scan for secrets and run SAST
7276
greengate scan
7377

78+
# Audit dependencies for known CVEs
79+
greengate audit
80+
7481
# Analyze a PR: complexity score + new-code coverage gaps
7582
greengate review --base main --coverage-file coverage/lcov.info
7683

7784
# Enforce 80% minimum coverage
7885
greengate coverage --file coverage/lcov.info --min 80
7986

80-
# Audit dependencies for known CVEs
81-
greengate audit
82-
8387
# Lint Kubernetes manifests
8488
greengate lint --dir ./k8s
8589

@@ -100,11 +104,18 @@ greengate run
100104
curl -sL https://github.com/thinkgrid-labs/greengate/releases/latest/download/greengate-linux-amd64 \
101105
-o /usr/local/bin/greengate && chmod +x /usr/local/bin/greengate
102106
107+
# Replaces plain `npm ci` — halts if a postinstall script drops and deletes a binary
108+
- name: Supply-chain safe install
109+
run: greengate watch-install npm ci
110+
103111
- name: Secret, PII & SAST scan
104112
run: greengate scan --annotate
105113
env:
106114
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
107115

116+
- name: Dependency audit (OSV)
117+
run: greengate audit
118+
108119
- name: PR review (complexity + coverage gaps)
109120
if: github.event_name == 'pull_request'
110121
run: |
@@ -120,9 +131,6 @@ greengate run
120131

121132
- name: Coverage gate
122133
run: greengate coverage --file coverage/lcov.info --min 80
123-
124-
- name: Dependency audit
125-
run: greengate audit
126134
```
127135
128136
> See [CI/CD Integration](https://thinkgrid-labs.github.io/greengate/guide/ci-integration) for full GitHub Actions, GitLab CI, Bitbucket, and CircleCI examples.
@@ -134,6 +142,11 @@ greengate run
134142
Create `.greengate.toml` in your repo root. All fields are optional:
135143

136144
```toml
145+
[supply_chain]
146+
block_phantom_scripts = true
147+
enforce_sandbox = true
148+
allow_postinstall = ["esbuild", "prisma", "@swc/core"]
149+
137150
[scan]
138151
exclude_patterns = ["tests/**", "*.test.ts", "vendor/**"]
139152
entropy = true
@@ -162,8 +175,8 @@ Full guides, command references, and CI examples live in the **[docs site](https
162175
- [Getting Started](https://thinkgrid-labs.github.io/greengate/guide/getting-started)
163176
- [CI/CD Integration](https://thinkgrid-labs.github.io/greengate/guide/ci-integration)
164177
- [Use Cases](https://thinkgrid-labs.github.io/greengate/guide/use-cases)
165-
- **Commands:** [scan](https://thinkgrid-labs.github.io/greengate/commands/scan) · [review](https://thinkgrid-labs.github.io/greengate/commands/review) · [coverage](https://thinkgrid-labs.github.io/greengate/commands/coverage) · [audit](https://thinkgrid-labs.github.io/greengate/commands/audit) · [lint](https://thinkgrid-labs.github.io/greengate/commands/lint) · [docker-lint](https://thinkgrid-labs.github.io/greengate/commands/docker-lint) · [lighthouse](https://thinkgrid-labs.github.io/greengate/commands/lighthouse) · [reassure](https://thinkgrid-labs.github.io/greengate/commands/reassure) · [sbom](https://thinkgrid-labs.github.io/greengate/commands/sbom) · [run](https://thinkgrid-labs.github.io/greengate/commands/run)
166-
- **Reference:** [Config](https://thinkgrid-labs.github.io/greengate/reference/config) · [Secret Patterns](https://thinkgrid-labs.github.io/greengate/reference/secret-patterns) · [SAST Rules](https://thinkgrid-labs.github.io/greengate/reference/sast-rules) · [Output Formats](https://thinkgrid-labs.github.io/greengate/reference/output-formats) · [Exit Codes](https://thinkgrid-labs.github.io/greengate/reference/exit-codes)
178+
- **Commands:** [watch-install](https://thinkgrid-labs.github.io/greengate/commands/watch-install) · [scan](https://thinkgrid-labs.github.io/greengate/commands/scan) · [audit](https://thinkgrid-labs.github.io/greengate/commands/audit) · [review](https://thinkgrid-labs.github.io/greengate/commands/review) · [coverage](https://thinkgrid-labs.github.io/greengate/commands/coverage) · [lint](https://thinkgrid-labs.github.io/greengate/commands/lint) · [docker-lint](https://thinkgrid-labs.github.io/greengate/commands/docker-lint) · [lighthouse](https://thinkgrid-labs.github.io/greengate/commands/lighthouse) · [reassure](https://thinkgrid-labs.github.io/greengate/commands/reassure) · [sbom](https://thinkgrid-labs.github.io/greengate/commands/sbom) · [run](https://thinkgrid-labs.github.io/greengate/commands/run)
179+
- **Reference:** [Config](https://thinkgrid-labs.github.io/greengate/reference/config) · [Secret Patterns](https://thinkgrid-labs.github.io/greengate/reference/secret-patterns) · [SAST Rules](https://thinkgrid-labs.github.io/greengate/reference/sast-rules) · [Output Formats](https://thinkgrid-labs.github.io/greengate/reference/output-formats) · [Exit Codes](https://thinkgrid-labs.github.io/greengate/reference/exit-codes) · [Roadmap](https://thinkgrid-labs.github.io/greengate/reference/roadmap)
167180

168181
---
169182

docs/.vitepress/config.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineConfig } from 'vitepress'
22

33
export default defineConfig({
44
title: 'GreenGate',
5-
description: 'Rust DevOps CLI: secret scanning, AST-based SAST, Kubernetes linting, coverage gates, dependency auditing, and web performance — single zero-dependency binary.',
5+
description: 'Rust DevOps CLI: supply-chain protection, secret scanning, AST-based SAST, Kubernetes linting, coverage gates, dependency auditing, and web performance — single zero-dependency binary.',
66
base: '/greengate/',
77

88
head: [
@@ -36,11 +36,12 @@ export default defineConfig({
3636
{
3737
text: 'Commands',
3838
items: [
39+
{ text: '🔒 watch-install', link: '/commands/watch-install' },
3940
{ text: 'scan', link: '/commands/scan' },
41+
{ text: 'audit', link: '/commands/audit' },
4042
{ text: 'lint', link: '/commands/lint' },
4143
{ text: 'docker-lint', link: '/commands/docker-lint' },
4244
{ text: 'coverage', link: '/commands/coverage' },
43-
{ text: 'audit', link: '/commands/audit' },
4445
{ text: 'install-hooks', link: '/commands/install-hooks' },
4546
{ text: 'lighthouse', link: '/commands/lighthouse' },
4647
{ text: 'reassure', link: '/commands/reassure' },
@@ -57,6 +58,7 @@ export default defineConfig({
5758
{ text: 'Exit Codes', link: '/reference/exit-codes' },
5859
{ text: 'Output Formats', link: '/reference/output-formats' },
5960
{ text: 'Limitations', link: '/reference/limitations' },
61+
{ text: 'Roadmap', link: '/reference/roadmap' },
6062
],
6163
},
6264
],

docs/commands/watch-install.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# watch-install
2+
3+
> **Supply-chain protection for npm, yarn, pnpm, and bun installs.**
4+
5+
`greengate watch-install` wraps your package manager and monitors `node_modules/` in real time during the install. If a postinstall script creates a file and then deletes it before the install finishes — the classic dropper signature used in attacks like the [2025 axios compromise](#background) — the install is halted and the offending package is named.
6+
7+
---
8+
9+
## Usage
10+
11+
```bash
12+
greengate watch-install <PACKAGE_MANAGER> [ARGS...]
13+
```
14+
15+
All arguments after the package manager name are forwarded verbatim:
16+
17+
```bash
18+
# Drop-in for npm install
19+
greengate watch-install npm install
20+
21+
# Frozen lockfile (CI)
22+
greengate watch-install npm ci
23+
24+
# pnpm with flags
25+
greengate watch-install pnpm install --frozen-lockfile
26+
27+
# yarn
28+
greengate watch-install yarn install --immutable
29+
30+
# bun
31+
greengate watch-install bun install
32+
```
33+
34+
---
35+
36+
## Flags
37+
38+
| Flag | Default | Description |
39+
|---|---|---|
40+
| `--no-fail` || Report findings to stderr but exit 0. Useful for audit-only pipelines that are not yet blocking. |
41+
42+
---
43+
44+
## What it detects
45+
46+
### 1. Phantom files (`PHANTOM`)
47+
48+
A file is created inside `node_modules/` during a postinstall script and deleted before the install completes. This is the primary dropper signature:
49+
50+
```
51+
postinstall → write binary to disk → execute → unlink to hide evidence
52+
```
53+
54+
GreenGate polls `node_modules/` every 250 ms while the package manager runs. Any file that appears in one poll and disappears in a later poll is flagged.
55+
56+
### 2. Executable drops (`EXEC_DROP`)
57+
58+
A new executable file (one with the execute bit set on Unix, or a `.exe/.bat/.cmd/.ps1` extension on Windows) appears in the project root after the install completes that was not there before. Legitimate package managers never place executables outside `node_modules/`.
59+
60+
---
61+
62+
## Example output
63+
64+
**Phantom detected:**
65+
66+
```
67+
🚨 greengate watch-install: 1 suspicious event(s) detected:
68+
69+
[PHANTOM ] evil-pkg
70+
path: node_modules/evil-pkg/.postinstall
71+
72+
Tip: if this package is a known native build tool (e.g. esbuild, swc),
73+
add it to [supply_chain] allow_postinstall in .greengate.toml to suppress.
74+
75+
Error: watch-install: 1 blocking event(s) detected — halting.
76+
```
77+
78+
**Clean install:**
79+
80+
```
81+
✅ watch-install: clean — no phantom files or executable drops detected.
82+
```
83+
84+
---
85+
86+
## Configuration
87+
88+
All options live under `[supply_chain]` in `.greengate.toml`:
89+
90+
```toml
91+
[supply_chain]
92+
# Halt the install when a phantom or exec-drop is detected (default: true).
93+
block_phantom_scripts = true
94+
95+
# Also monitor the project root for new executables (default: true).
96+
enforce_sandbox = true
97+
98+
# Packages whose postinstall scripts legitimately create temp files.
99+
# Native build tools (esbuild, @swc/core, prisma) compile .node addons
100+
# and may create intermediate files during the build. List them here to
101+
# downgrade their findings to warnings instead of errors.
102+
allow_postinstall = ["esbuild", "prisma", "@swc/core"]
103+
```
104+
105+
### allow_postinstall behaviour
106+
107+
Packages on the allowlist still appear in the output with `[allowlisted — warning only]` so you have a full audit trail, but they do not cause `block_phantom_scripts` to trigger a non-zero exit.
108+
109+
---
110+
111+
## CI usage
112+
113+
Replace your existing `npm install` / `npm ci` step with `greengate watch-install`:
114+
115+
```yaml
116+
- name: Install GreenGate
117+
run: |
118+
curl -sL https://github.com/thinkgrid-labs/greengate/releases/latest/download/greengate-linux-amd64 \
119+
-o /usr/local/bin/greengate && chmod +x /usr/local/bin/greengate
120+
121+
- name: Supply-chain safe install
122+
run: greengate watch-install npm ci
123+
```
124+
125+
For teams not yet ready to block on findings, start in audit-only mode:
126+
127+
```yaml
128+
- name: Supply-chain audit (non-blocking)
129+
run: greengate watch-install --no-fail npm ci
130+
```
131+
132+
---
133+
134+
## Layered defence with `audit`
135+
136+
`watch-install` and `greengate audit` are complementary, not redundant:
137+
138+
| Tool | When it runs | What it catches |
139+
|---|---|---|
140+
| `greengate audit` | Pre/post install | Known CVEs in OSV database for your lock file |
141+
| `greengate watch-install` | During install | Runtime dropper behaviour that CVE databases cannot see |
142+
143+
Run both:
144+
145+
```yaml
146+
- run: greengate watch-install npm ci # catches runtime behaviour
147+
- run: greengate audit # catches known CVEs
148+
```
149+
150+
---
151+
152+
## Background
153+
154+
In early 2025, the [axios](https://github.com/axios/axios) npm package was the subject of a supply-chain compromise discussion where attackers targeted postinstall hooks to execute and then self-delete malicious payloads. This attack pattern — write, execute, unlink — leaves no trace in `node_modules/` after the install finishes, making it invisible to static scanners and lock-file diffing tools.
155+
156+
`watch-install` catches it because the file system events happen _during_ the install window, not after.
157+
158+
---
159+
160+
## Limitations
161+
162+
- **Pure network exfiltration** — if a postinstall script sends data over the network without writing any file, there is no filesystem event to observe. Pair with network egress controls in CI for defence in depth.
163+
- **Windows** — phantom detection works on Windows via `std::fs` polling, but exec-drop detection uses file-extension heuristics (`.exe`, `.bat`, `.cmd`, `.ps1`) rather than the execute bit.
164+
- **Very fast droppers** — files created and deleted within a single 250 ms poll window may be missed. This is the theoretical lower bound; real-world payloads take longer to download and execute.
165+
166+
See also: [Roadmap](/reference/roadmap) for planned sandbox-level isolation (`greengate sandbox-install`).

docs/guide/ci-integration.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,18 @@ jobs:
2727
-o /usr/local/bin/greengate
2828
chmod +x /usr/local/bin/greengate
2929
30+
# Replaces plain `npm ci` — halts if a postinstall script drops and deletes a binary
31+
- name: Supply-chain safe install
32+
run: greengate watch-install npm ci
33+
3034
- name: Secret, PII & SAST Scan
3135
env:
3236
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3337
run: greengate scan --annotate
3438

39+
- name: Dependency Audit (OSV)
40+
run: greengate audit
41+
3542
- name: PR Review (Complexity + Coverage Gaps)
3643
if: github.event_name == 'pull_request'
3744
env:
@@ -51,9 +58,6 @@ jobs:
5158

5259
- name: Coverage Gate
5360
run: greengate coverage --file coverage/lcov.info --min 80
54-
55-
- name: Dependency Audit
56-
run: greengate audit
5761
```
5862
5963
## GitHub Actions — SARIF upload (alternative)

0 commit comments

Comments
 (0)