|
| 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`). |
0 commit comments