secret-blast-radius 0.2.0 → 0.3.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 +40 -7
- package/action.yml +6 -2
- package/bin/cli.js +12 -6
- package/package.json +9 -3
- package/src/extension-iocs.json +911 -0
- package/src/format.js +5 -1
- package/src/incidents.js +14 -3
- package/src/index.js +90 -38
- package/src/scanners.js +38 -7
- package/src/supply.js +2 -1
- package/src/util.js +1 -0
- package/src/v3.js +240 -0
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ 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.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.
|
|
38
|
+
|
|
37
39
|
## Why this exists
|
|
38
40
|
|
|
39
41
|
The last few weeks, in order:
|
|
@@ -63,12 +65,16 @@ Every advisory ends the same way: *"rotate everything the machine could access."
|
|
|
63
65
|
| `dotenv` | `.env*` files in your projects (names only, never values) |
|
|
64
66
|
| `env` | Variables a CI step inherits (`NPM_TOKEN`, `AWS_*`, recognised token formats) |
|
|
65
67
|
| `paas` | Railway, Vercel, Fly, Render, Netlify CLI tokens; Heroku in `~/.netrc`; which projects deploy where |
|
|
68
|
+
| `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
69
|
|
|
67
70
|
**Signs you're already hit:**
|
|
68
71
|
|
|
69
72
|
| Scanner | Detects |
|
|
70
73
|
|---|---|
|
|
71
74
|
| `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 |
|
|
75
|
+
| `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` |
|
|
76
|
+
| `npm-cache` | Tarballs of known-bad versions in the npm cache index: proof the machine downloaded one even if no lockfile shows it today |
|
|
77
|
+
| `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 |
|
|
72
78
|
| `persistence` | `gh-token-monitor` (systemd unit, LaunchAgent, Windows dir, script) |
|
|
73
79
|
| `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
80
|
| `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 +86,23 @@ Every advisory ends the same way: *"rotate everything the machine could access."
|
|
|
80
86
|
| `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
87
|
| `certs` | PEM/CRT files and `NODE_EXTRA_CA_CERTS` bundles containing or chaining to the distrusted **emSign ECC Root CA - G3** (eMudhra) |
|
|
82
88
|
| `install-scripts` | Which dependencies run install scripts, straight from the lockfile |
|
|
83
|
-
| `hardening` | No `ignore-scripts
|
|
89
|
+
| `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`) |
|
|
90
|
+
|
|
91
|
+
**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.
|
|
92
|
+
|
|
93
|
+
**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>`.
|
|
94
|
+
|
|
95
|
+
## Rotation plan
|
|
96
|
+
|
|
97
|
+
`--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.
|
|
98
|
+
|
|
99
|
+
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
100
|
|
|
85
101
|
## Incident mode
|
|
86
102
|
|
|
87
103
|
```bash
|
|
88
104
|
npx secret-blast-radius --list-incidents
|
|
89
|
-
npx secret-blast-radius --incident tensorlake # or: shai-hulud, subql, ghostaction, railway, php2shell, emudhra, sbi
|
|
105
|
+
npx secret-blast-radius --incident tensorlake # or: glassworm, shai-hulud, subql, ghostaction, railway, php2shell, emudhra, sbi
|
|
90
106
|
```
|
|
91
107
|
|
|
92
108
|
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 +115,7 @@ permissions:
|
|
|
99
115
|
security-events: write
|
|
100
116
|
steps:
|
|
101
117
|
- uses: actions/checkout@v4
|
|
102
|
-
- uses: Krrishkumar125/secret-blast-radius@v0.
|
|
118
|
+
- uses: Krrishkumar125/secret-blast-radius@v0.3.0
|
|
103
119
|
with:
|
|
104
120
|
fail-on: critical
|
|
105
121
|
- uses: github/codeql-action/upload-sarif@v3
|
|
@@ -114,9 +130,9 @@ SARIF only gets file locations for findings inside the repo. Home-directory find
|
|
|
114
130
|
## Safety
|
|
115
131
|
|
|
116
132
|
- **Read-only.** It only writes files you name (`--runbook-file`, `--sarif-file`).
|
|
117
|
-
- **Offline.** It makes no network requests. The IOC
|
|
133
|
+
- **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
134
|
- **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 -
|
|
135
|
+
- **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
136
|
|
|
121
137
|
## Options
|
|
122
138
|
|
|
@@ -128,6 +144,8 @@ SARIF only gets file locations for findings inside the repo. Home-directory find
|
|
|
128
144
|
| `--runbook`, `--runbook-file <f>` | Markdown rotation checklist |
|
|
129
145
|
| `--fail-on <level>` | Exit 1 at/above `critical`, `high`, `medium`, `low` (default `none`) |
|
|
130
146
|
| `--only` / `--skip <list>` | Choose scanners (names in the tables above) |
|
|
147
|
+
| `--history`, `--since <date>` | Lockfile exposure window from git history |
|
|
148
|
+
| `--osv` | Online, opt-in OSV malicious-package lookup |
|
|
131
149
|
| `--ioc-file <f>` | Extra IOCs, same shape as `src/iocs.json` (repeatable) |
|
|
132
150
|
| `--no-env`, `--no-exec`, `--depth <n>`, `--home <dir>`, `--no-color` | Tuning |
|
|
133
151
|
|
|
@@ -137,18 +155,33 @@ Exit codes: `0` ok, `1` findings at/above `--fail-on`, `2` usage error.
|
|
|
137
155
|
|
|
138
156
|
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
157
|
|
|
158
|
+
## Use it with these tools
|
|
159
|
+
|
|
160
|
+
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:
|
|
161
|
+
|
|
162
|
+
| Need | Use |
|
|
163
|
+
|---|---|
|
|
164
|
+
| 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) |
|
|
165
|
+
| Vulnerable dependencies (CVEs) across ecosystems | [OSV-Scanner](https://github.com/google/osv-scanner) |
|
|
166
|
+
| GitHub Actions risks: `pull_request_target`, unpinned actions, template injection, excessive permissions | [zizmor](https://github.com/zizmorcore/zizmor) |
|
|
167
|
+
| Blocking exfiltration from CI runners at runtime | [StepSecurity Harden-Runner](https://github.com/step-security/harden-runner) |
|
|
168
|
+
| Vetting packages before you install them | [Socket](https://socket.dev) |
|
|
169
|
+
|
|
140
170
|
## Limits
|
|
141
171
|
|
|
142
172
|
- It checks well-known locations. It can't see your OS keychain (that's the point) or browser profiles.
|
|
143
|
-
- The IOC
|
|
173
|
+
- The bundled IOC lists cover the incidents above, not every malicious package ever published. Use `--osv` for the full OpenSSF feed.
|
|
174
|
+
- `--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.
|
|
175
|
+
- Sleeper extensions were benign when Socket found them. They're flagged as high risk, not proven malicious.
|
|
144
176
|
- For distro PHP builds it can't tell whether your vendor backported the fix.
|
|
145
177
|
- A clean report shrinks your blast radius. It doesn't prove a machine is clean.
|
|
146
178
|
|
|
147
179
|
## Programmatic API
|
|
148
180
|
|
|
149
181
|
```js
|
|
150
|
-
import { scan, runbook, toSarif, INCIDENTS } from 'secret-blast-radius';
|
|
182
|
+
import { scan, scanAsync, runbook, toSarif, INCIDENTS } from 'secret-blast-radius';
|
|
151
183
|
const result = scan({ projects: ['.'], incident: 'railway', exec: false });
|
|
184
|
+
const withOsv = await scanAsync({ projects: ['.'], osv: true, history: { since: '2026-09-01' } });
|
|
152
185
|
```
|
|
153
186
|
|
|
154
187
|
MIT © Krrish Kumar
|
package/action.yml
CHANGED
|
@@ -36,6 +36,10 @@ runs:
|
|
|
36
36
|
SBR_SARIF: ${{ inputs.sarif-file }}
|
|
37
37
|
SBR_VERSION: ${{ inputs.version }}
|
|
38
38
|
run: |
|
|
39
|
-
|
|
39
|
+
ws="$(pwd)"
|
|
40
|
+
case "$SBR_SARIF" in /*|[A-Za-z]:*) sarif="$SBR_SARIF" ;; *) sarif="$ws/$SBR_SARIF" ;; esac
|
|
41
|
+
args=(--only "$SBR_ONLY" --fail-on "$SBR_FAIL_ON" --sarif-file "$sarif")
|
|
40
42
|
if [ -n "$SBR_INCIDENT" ]; then args+=(--incident "$SBR_INCIDENT"); fi
|
|
41
|
-
npx
|
|
43
|
+
# Run from a neutral directory so npx never resolves a local package.json instead of the registry.
|
|
44
|
+
cd "${RUNNER_TEMP:-/tmp}"
|
|
45
|
+
npx --yes "secret-blast-radius@${SBR_VERSION}" "${args[@]}" "$ws"
|
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 {
|
|
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,21 @@ 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, 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 -
|
|
19
|
+
--no-exec Never run helper commands (php -v, crontab -l, ps, git)
|
|
20
|
+
--history Exposure window: walk lockfile git history for known-bad versions (runs git)
|
|
21
|
+
--since <date> Limit --history to commits since a date (e.g. 2026-09-01)
|
|
22
|
+
--osv ONLINE, opt-in: send lockfile package names+versions (nothing else) to
|
|
23
|
+
api.osv.dev and report MAL- malicious-package advisories
|
|
20
24
|
--runbook [file] Print (or write to file) a Markdown rotation runbook
|
|
21
25
|
--fail-on <level> Exit 1 if any finding is at/above level: critical|high|medium|low|none (default: none)
|
|
22
26
|
--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
|
|
27
|
+
hardening,malicious,repo,host-persistence,php,paas,certs,install-scripts,
|
|
28
|
+
ecosystems,extensions,npm-cache,history
|
|
24
29
|
--skip <list> Comma list of scanners to skip
|
|
25
30
|
--no-env Do not inspect environment variables
|
|
26
31
|
--depth <n> How deep to look for .env files in project dirs (default 4)
|
|
@@ -36,7 +41,7 @@ try {
|
|
|
36
41
|
allowPositionals: true,
|
|
37
42
|
options: {
|
|
38
43
|
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' },
|
|
44
|
+
incident: { type: 'string' }, 'list-incidents': { type: 'boolean' }, 'ioc-file': { type: 'string', multiple: true }, 'no-exec': { type: 'boolean' }, history: { type: 'boolean' }, since: { type: 'string' }, osv: { type: 'boolean' }, runbook: { type: 'boolean' }, 'runbook-file': { type: 'string' },
|
|
40
45
|
'fail-on': { type: 'string', default: 'none' }, only: { type: 'string' }, skip: { type: 'string' },
|
|
41
46
|
'no-env': { type: 'boolean' }, depth: { type: 'string' }, home: { type: 'string' },
|
|
42
47
|
'no-color': { type: 'boolean' }, version: { type: 'boolean', short: 'v' }, help: { type: 'boolean', short: 'h' },
|
|
@@ -67,7 +72,8 @@ if (o['no-env']) skip.push('env');
|
|
|
67
72
|
|
|
68
73
|
let result;
|
|
69
74
|
try {
|
|
70
|
-
|
|
75
|
+
if (o.since && !/^\d{4}-\d{2}-\d{2}/.test(o.since)) throw new Error('--since expects a date like 2026-09-01');
|
|
76
|
+
result = await scanAsync({ 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
77
|
failsAt(result, o['fail-on']);
|
|
72
78
|
} catch (e) {
|
|
73
79
|
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.
|
|
3
|
+
"version": "0.3.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,13 @@
|
|
|
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"
|
|
30
36
|
],
|
|
31
37
|
"type": "module",
|
|
32
38
|
"bin": {
|
|
@@ -46,7 +52,7 @@
|
|
|
46
52
|
"node": ">=18.18"
|
|
47
53
|
},
|
|
48
54
|
"scripts": {
|
|
49
|
-
"test": "node --test test/scanners.test.js test/supply.test.js test/cli.test.js",
|
|
55
|
+
"test": "node --test test/scanners.test.js test/supply.test.js test/cli.test.js test/v3.test.js",
|
|
50
56
|
"check": "node --check bin/cli.js && npm test"
|
|
51
57
|
}
|
|
52
58
|
}
|