secret-blast-radius 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,10 @@ Rotation order if this machine is ever compromised: 1. persistence 2. npm 3. a
34
34
 
35
35
  *(Illustrative output: fingerprints and paths are examples.)*
36
36
 
37
+ **New in 0.4:** `--harden` audits the settings that would have blunted this month's attacks and prints the exact fix: cooldowns, `--require-hashes`, uv `exclude-newer`, digest-pinned images, OIDC instead of long-lived publish/cloud keys, unpinned MCP servers, `.env` files with secrets committed to git. Also new: Python lockfiles and virtualenvs (requirements, poetry.lock, uv.lock, Pipfile.lock, pylock.toml), Docker image references, and `--incident litellm` / `--incident zeabur`.
38
+
39
+ **New in 0.3:** `--history` shows the dates a bad version sat in your lockfile; GlassWorm editor-extension check; npm cache check; RubyGems/Cargo/Composer/Terraform/Pulumi credential files; opt-in `--osv` malicious-package lookup; a rotation plan with verify steps and provider docs.
40
+
37
41
  ## Why this exists
38
42
 
39
43
  The last few weeks, in order:
@@ -63,12 +67,18 @@ Every advisory ends the same way: *"rotate everything the machine could access."
63
67
  | `dotenv` | `.env*` files in your projects (names only, never values) |
64
68
  | `env` | Variables a CI step inherits (`NPM_TOKEN`, `AWS_*`, recognised token formats) |
65
69
  | `paas` | Railway, Vercel, Fly, Render, Netlify CLI tokens; Heroku in `~/.netrc`; which projects deploy where |
70
+ | `ecosystems` | RubyGems `~/.gem/credentials`, Cargo `credentials.toml` (the plaintext `cargo:token` store), Composer `auth.json` (global and project), Terraform `credentials.tfrc.json`, Pulumi `credentials.json`, local `.tfstate` files |
66
71
 
67
72
  **Signs you're already hit:**
68
73
 
69
74
  | Scanner | Detects |
70
75
  |---|---|
71
76
  | `malicious` | Known-bad versions in `package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml` (v5–v9), `yarn.lock` (classic + Berry), `bun.lock`, and in `node_modules`: tensorlake + native packages, `@subql/common@5.8.3`, `@7nohe/openapi-react-query-codegen` (10 versions), the keyv/cacheable wave; the `@opensearch/setup` worm marker |
77
+ | `extensions` | Installed VS Code / Cursor / Windsurf / VSCodium / Kiro / remote-server extensions against 149 GlassWorm IOCs from Socket (malicious, version-scoped, and flagged sleepers), including ones pulled in through `extensionPack` |
78
+ | `npm-cache` | Tarballs of known-bad versions in the npm cache index: proof the machine downloaded one even if no lockfile shows it today |
79
+ | `history` (`--history`) | **Exposure window.** Walks each lockfile's git history and reports when a known-bad version entered and left (`2026-10-08 → 2026-10-10`), so you know which CI runs and machines to treat as exposed. `--since 2026-09-01` narrows it |
80
+ | `python` | Known-bad PyPI versions (litellm 1.107.0, 1.82.7/1.82.8) in requirements*.txt, poetry.lock, uv.lock, Pipfile.lock, pdm.lock, pylock.toml and project virtualenvs; the litellm `cook.pth` startup hook |
81
+ | `images` | Dockerfile `FROM` and compose `image:` references to compromised image digests/tags, and floating tags (`latest`, `main-stable`) that pointed at a compromised image during a known window |
72
82
  | `persistence` | `gh-token-monitor` (systemd unit, LaunchAgent, Windows dir, script) |
73
83
  | `host-persistence` | Crontab lines that pipe a server into `bash` (GhostAction `#ghc2`), the `manifest-cache.js --warm` process and `/tmp/tmp.ts018051808.lock` (@subql) |
74
84
  | `repo` | Workflows that contact GhostAction infrastructure, dump `git log -p --all`, or serialise `toJSON(secrets)` to the network; `.vscode/tasks.json` that auto-runs a "font"/image; Claude Code hooks that run disguised files; the tensorlake worm branch |
@@ -80,13 +90,44 @@ Every advisory ends the same way: *"rotate everything the machine could access."
80
90
  | `php` | PHP2Shell-vulnerable PHP in Dockerfiles, compose files, `composer.json` platform pins and the local `php` binary. Distro builds are flagged *high, verify backport* rather than critical |
81
91
  | `certs` | PEM/CRT files and `NODE_EXTRA_CA_CERTS` bundles containing or chaining to the distrusted **emSign ECC Root CA - G3** (eMudhra) |
82
92
  | `install-scripts` | Which dependencies run install scripts, straight from the lockfile |
83
- | `hardening` | No `ignore-scripts`, no dependency cooldown (`min-release-age` / `minimumReleaseAge`) |
93
+ | `hardening` | No `ignore-scripts`; no dependency cooldown, with the right setting for the package manager you use (npm `min-release-age`, pnpm `minimumReleaseAge`, Yarn `npmMinimalAgeGate`, Bun `install.minimumReleaseAge`) |
94
+
95
+ **Opt-in online check:** `--osv` sends the package names and versions from your lockfiles (nothing else) to the [OSV API](https://google.github.io/osv.dev/post-v1-querybatch/) and reports `MAL-` advisories from the [OpenSSF malicious-packages](https://github.com/ossf/malicious-packages) feed. That covers far more than the bundled list. Vulnerability (CVE/GHSA) results are ignored on purpose; use OSV-Scanner for those.
96
+
97
+ **Small extras:** least-privilege hints from a token's public prefix (classic `ghp_` PAT, long-lived `AKIA` key), and MCP servers started with an unpinned `npx -y <pkg>`.
98
+
99
+ ## `--harden`: prevent the next one
100
+
101
+ ```bash
102
+ npx secret-blast-radius --harden
103
+ ```
104
+
105
+ Read-only audit of preventive settings. Every finding has a `fix:` line with the exact change:
106
+
107
+ | Check | Why (Oct 2026 evidence) | Fix it prints |
108
+ |---|---|---|
109
+ | npm `ignore-scripts`, cooldown for npm/pnpm/Yarn/Bun | tensorlake ran from `preinstall` and was live under 2h | `ignore-scripts=true`, `min-release-age=3`, `minimumReleaseAge: 4320`, … |
110
+ | pnpm `dangerouslyAllowAllBuilds: true` | pnpm blocks dependency builds by default; this turns that off | remove it, allow-list with `allowBuilds`/`onlyBuiltDependencies` |
111
+ | requirements not pinned / no `--hash` | an unpinned `litellm` resolved to 1.107.0 | `uv pip compile --generate-hashes`, `pip install --require-hashes` |
112
+ | uv without `exclude-newer` | litellm 1.107.0 was live ~2h | `exclude-newer = "3 days"` under `[tool.uv]` |
113
+ | images not pinned by digest | litellm's `latest`/`main-stable` served a stealer for ~2.5h | `image:tag@sha256:…` + Renovate/Dependabot |
114
+ | long-lived `NPM_TOKEN`, PyPI `password:`, AWS keys in workflows | stolen long-lived tokens drove LiteLLM, tensorlake, GhostAction | npm/PyPI trusted publishing, AWS `role-to-assume` (OIDC) |
115
+ | MCP servers run with `npx -y <pkg>` | pulls whatever is latest on every start | pin `pkg@x.y.z` |
116
+ | `.env` with secret-looking values committed or not ignored (needs git) | CERT-In Oct 15: agents got in via plaintext credentials in repos | `git rm --cached`, `.gitignore`, rotate |
117
+
118
+ Checks only fire on files you have. Settings that are already safe by default (pnpm build blocking) are not nagged about. Cargo cooldowns land in Rust 1.100 (Nov 12), so they aren't checked yet.
119
+
120
+ ## Rotation plan
121
+
122
+ `--runbook` writes an ordered plan: stop the malware, then registry tokens (npm, PyPI, RubyGems, crates.io), GitHub, cloud and IaC, Kubernetes, deploy platforms, CI env, AI keys, SSH, app secrets. Each step has the exact commands, a **verify** check that the old credential is dead, a **redeploy** reminder where running processes keep old values, and links to the provider's own docs. One example of why the details matter: npm says token revocation can take up to an hour.
123
+
124
+ Trusted publishing and provenance tell you *where* a package was built. They don't tell you it's safe: `@subql/common@5.8.3` and `tensorlake@0.5.144` both had valid provenance.
84
125
 
85
126
  ## Incident mode
86
127
 
87
128
  ```bash
88
129
  npx secret-blast-radius --list-incidents
89
- npx secret-blast-radius --incident tensorlake # or: shai-hulud, subql, ghostaction, railway, php2shell, emudhra, sbi
130
+ npx secret-blast-radius --incident tensorlake # or: glassworm, shai-hulud, subql, ghostaction, railway, php2shell, emudhra, sbi
90
131
  ```
91
132
 
92
133
  Runs only the checks that matter for that incident and prints a sourced checklist, marking what's confirmed and what's still under investigation. `--runbook` turns it into a Markdown checklist you can paste into an incident ticket.
@@ -99,7 +140,7 @@ permissions:
99
140
  security-events: write
100
141
  steps:
101
142
  - uses: actions/checkout@v4
102
- - uses: Krrishkumar125/secret-blast-radius@v0.2.1
143
+ - uses: Krrishkumar125/secret-blast-radius@v0.4.0
103
144
  with:
104
145
  fail-on: critical
105
146
  - uses: github/codeql-action/upload-sarif@v3
@@ -114,9 +155,9 @@ SARIF only gets file locations for findings inside the repo. Home-directory find
114
155
  ## Safety
115
156
 
116
157
  - **Read-only.** It only writes files you name (`--runbook-file`, `--sarif-file`).
117
- - **Offline.** It makes no network requests. The IOC list is bundled (`src/iocs.json`, dated). Add your own with `--ioc-file extra.json`.
158
+ - **Offline by default.** It makes no network requests unless you pass `--osv`, which sends package names and versions only. The IOC lists are bundled (`src/iocs.json`, `src/extension-iocs.json`, dated). Add your own with `--ioc-file extra.json` (`packages` and `extensions` keys).
118
159
  - **Secret values are never printed.** Findings show a location and a `sha256:xxxxxxxx` fingerprint. Every output format is tested for leaks.
119
- - **Zero dependencies.** Helper commands (`php -r`, `crontab -l`, `ps -eo args`) are run read-only with a timeout; `--no-exec` disables them.
160
+ - **Zero dependencies.** Helper commands (`php -v`, `crontab -l`, `ps -eo args`, and `git log`/`git show` for `--history`) are run read-only with a timeout; `--no-exec` disables them.
120
161
 
121
162
  ## Options
122
163
 
@@ -128,6 +169,8 @@ SARIF only gets file locations for findings inside the repo. Home-directory find
128
169
  | `--runbook`, `--runbook-file <f>` | Markdown rotation checklist |
129
170
  | `--fail-on <level>` | Exit 1 at/above `critical`, `high`, `medium`, `low` (default `none`) |
130
171
  | `--only` / `--skip <list>` | Choose scanners (names in the tables above) |
172
+ | `--history`, `--since <date>` | Lockfile exposure window from git history |
173
+ | `--osv` | Online, opt-in OSV malicious-package lookup |
131
174
  | `--ioc-file <f>` | Extra IOCs, same shape as `src/iocs.json` (repeatable) |
132
175
  | `--no-env`, `--no-exec`, `--depth <n>`, `--home <dir>`, `--no-color` | Tuning |
133
176
 
@@ -137,18 +180,33 @@ Exit codes: `0` ok, `1` findings at/above `--fail-on`, `2` usage error.
137
180
 
138
181
  These are skipped on purpose: `${VAR}`, `${{ secrets.X }}`, `op://…`, `[YOUR_KEY]`, `your_api_key_here`, `not-a-secret`, prose values like `this-is-a-secret-value`, credential-free URLs (`redis://localhost`, `file:./dev.db`), `.env.example/.sample/.template/.demo`, `node_modules`, ordinary `folderOpen` tasks, and Claude hooks that run normal tools. Every rule above came from running the tool on real repos (TanStack Router, symfony/demo, vercel/next.js, docker-library/wordpress, Railway templates). If you hit a false positive, please open an issue.
139
182
 
183
+ ## Use it with these tools
184
+
185
+ secret-blast-radius answers one question: *what could have been stolen, and what do I rotate first?* For the rest, use the tools built for it:
186
+
187
+ | Need | Use |
188
+ |---|---|
189
+ | Secrets committed to git history | [gitleaks](https://github.com/gitleaks/gitleaks) or [TruffleHog](https://github.com/trufflesecurity/trufflehog) (TruffleHog can also verify whether a key is live) |
190
+ | Vulnerable dependencies (CVEs) across ecosystems | [OSV-Scanner](https://github.com/google/osv-scanner) |
191
+ | GitHub Actions risks: `pull_request_target`, unpinned actions, template injection, excessive permissions | [zizmor](https://github.com/zizmorcore/zizmor) |
192
+ | Blocking exfiltration from CI runners at runtime | [StepSecurity Harden-Runner](https://github.com/step-security/harden-runner) |
193
+ | Vetting packages before you install them | [Socket](https://socket.dev) |
194
+
140
195
  ## Limits
141
196
 
142
197
  - It checks well-known locations. It can't see your OS keychain (that's the point) or browser profiles.
143
- - The IOC list covers the incidents above, not every malicious package ever published. Pair it with `npm audit`, OSV or Socket.
198
+ - The bundled IOC lists cover the incidents above, not every malicious package ever published. Use `--osv` for the full OpenSSF feed.
199
+ - `--history` follows lockfiles that exist today; a lockfile that was deleted or renamed is not followed. On partial (`--filter=blob:none`) clones it fetches old blobs and is slow.
200
+ - Sleeper extensions were benign when Socket found them. They're flagged as high risk, not proven malicious.
144
201
  - For distro PHP builds it can't tell whether your vendor backported the fix.
145
202
  - A clean report shrinks your blast radius. It doesn't prove a machine is clean.
146
203
 
147
204
  ## Programmatic API
148
205
 
149
206
  ```js
150
- import { scan, runbook, toSarif, INCIDENTS } from 'secret-blast-radius';
207
+ import { scan, scanAsync, runbook, toSarif, INCIDENTS } from 'secret-blast-radius';
151
208
  const result = scan({ projects: ['.'], incident: 'railway', exec: false });
209
+ const withOsv = await scanAsync({ projects: ['.'], osv: true, history: { since: '2026-09-01' } });
152
210
  ```
153
211
 
154
212
  MIT © Krrish Kumar
package/bin/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
3
  import { writeFileSync, readFileSync } from 'node:fs';
4
- import { scan, failsAt, formatText, formatMarkdown, runbook, toSarif, INCIDENTS } from '../src/index.js';
4
+ import { scanAsync, failsAt, formatText, formatMarkdown, runbook, toSarif, INCIDENTS } from '../src/index.js';
5
5
 
6
6
  const HELP = `Usage: secret-blast-radius [options] [project-dir...]
7
7
 
@@ -11,16 +11,23 @@ Read-only. Never prints secret values.
11
11
 
12
12
  Options:
13
13
  --incident <name> Tailored checklist + only the relevant checks. Names:
14
- php2shell, railway, tensorlake, shai-hulud, subql, ghostaction, emudhra, sbi
14
+ php2shell, litellm, zeabur, glassworm, railway, tensorlake, shai-hulud, subql, ghostaction, emudhra, sbi
15
15
  --list-incidents Show incident names and status
16
16
  --json Machine-readable output
17
17
  --sarif [file] SARIF 2.1.0 for GitHub code scanning (stdout, or --sarif-file <f>)
18
18
  --ioc-file <f> Extra IOC JSON (same shape as src/iocs.json); repeatable
19
- --no-exec Never run helper commands (php -r, crontab -l, ps)
19
+ --no-exec Never run helper commands (php -v, crontab -l, ps, git)
20
+ --harden Audit preventive settings (cooldowns, hashes, digest pins, OIDC,
21
+ unpinned MCP servers, .env in git) and print the exact fix
22
+ --history Exposure window: walk lockfile git history for known-bad versions (runs git)
23
+ --since <date> Limit --history to commits since a date (e.g. 2026-09-01)
24
+ --osv ONLINE, opt-in: send lockfile package names+versions (nothing else) to
25
+ api.osv.dev and report MAL- malicious-package advisories
20
26
  --runbook [file] Print (or write to file) a Markdown rotation runbook
21
27
  --fail-on <level> Exit 1 if any finding is at/above level: critical|high|medium|low|none (default: none)
22
28
  --only <list> Comma list of scanners: persistence,npm,github,cloud,kubernetes,env,ai,ssh,dotenv,
23
- hardening,malicious,repo,host-persistence,php,paas,certs,install-scripts
29
+ hardening,malicious,repo,host-persistence,php,paas,certs,install-scripts,
30
+ ecosystems,extensions,npm-cache,history,python,images,harden
24
31
  --skip <list> Comma list of scanners to skip
25
32
  --no-env Do not inspect environment variables
26
33
  --depth <n> How deep to look for .env files in project dirs (default 4)
@@ -36,7 +43,7 @@ try {
36
43
  allowPositionals: true,
37
44
  options: {
38
45
  json: { type: 'boolean' }, sarif: { type: 'boolean' }, 'sarif-file': { type: 'string' },
39
- incident: { type: 'string' }, 'list-incidents': { type: 'boolean' }, 'ioc-file': { type: 'string', multiple: true }, 'no-exec': { type: 'boolean' }, runbook: { type: 'boolean' }, 'runbook-file': { type: 'string' },
46
+ incident: { type: 'string' }, 'list-incidents': { type: 'boolean' }, 'ioc-file': { type: 'string', multiple: true }, 'no-exec': { type: 'boolean' }, history: { type: 'boolean' }, harden: { type: 'boolean' }, since: { type: 'string' }, osv: { type: 'boolean' }, runbook: { type: 'boolean' }, 'runbook-file': { type: 'string' },
40
47
  'fail-on': { type: 'string', default: 'none' }, only: { type: 'string' }, skip: { type: 'string' },
41
48
  'no-env': { type: 'boolean' }, depth: { type: 'string' }, home: { type: 'string' },
42
49
  'no-color': { type: 'boolean' }, version: { type: 'boolean', short: 'v' }, help: { type: 'boolean', short: 'h' },
@@ -67,7 +74,8 @@ if (o['no-env']) skip.push('env');
67
74
 
68
75
  let result;
69
76
  try {
70
- result = scan({ projects, home: o.home, only: list(o.only), skip, depth: o.depth ? Number(o.depth) : undefined, incident: o.incident, iocFiles: o['ioc-file'] ?? [], exec: o['no-exec'] ? false : undefined });
77
+ if (o.since && !/^\d{4}-\d{2}-\d{2}/.test(o.since)) throw new Error('--since expects a date like 2026-09-01');
78
+ result = await scanAsync({ harden: o.harden, osv: o.osv, history: o.history || o.since || list(o.only)?.includes('history') ? { since: o.since } : null, projects, home: o.home, only: list(o.only), skip, depth: o.depth ? Number(o.depth) : undefined, incident: o.incident, iocFiles: o['ioc-file'] ?? [], exec: o['no-exec'] ? false : undefined });
71
79
  failsAt(result, o['fail-on']);
72
80
  } catch (e) {
73
81
  console.error(`secret-blast-radius: ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secret-blast-radius",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "What would a malicious npm install steal from this machine or CI runner? Ranked inventory of reachable credentials, known-worm IOCs (Shai-Hulud, @subql, GhostAction), PHP2Shell and PaaS exposure checks, ordered rotation runbook, SARIF. Read-only, offline, never prints secrets.",
5
5
  "license": "MIT",
6
6
  "author": "Krrish Kumar (https://github.com/Krrishkumar125)",
@@ -26,7 +26,17 @@
26
26
  "php2shell",
27
27
  "ghostaction",
28
28
  "railway",
29
- "github-action"
29
+ "github-action",
30
+ "vscode-extensions",
31
+ "glassworm",
32
+ "osv",
33
+ "rubygems",
34
+ "cargo",
35
+ "terraform",
36
+ "pypi",
37
+ "litellm",
38
+ "docker",
39
+ "hardening"
30
40
  ],
31
41
  "type": "module",
32
42
  "bin": {
@@ -46,7 +56,7 @@
46
56
  "node": ">=18.18"
47
57
  },
48
58
  "scripts": {
49
- "test": "node --test test/scanners.test.js test/supply.test.js test/cli.test.js",
59
+ "test": "node --test test/scanners.test.js test/supply.test.js test/cli.test.js test/v3.test.js test/v4.test.js",
50
60
  "check": "node --check bin/cli.js && npm test"
51
61
  }
52
62
  }