secret-blast-radius 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +153 -2
- package/action.yml +41 -0
- package/bin/cli.js +93 -0
- package/package.json +50 -4
- package/src/format.js +51 -0
- package/src/incidents.js +92 -0
- package/src/index.js +133 -0
- package/src/iocs.json +251 -0
- package/src/sarif.js +37 -0
- package/src/scanners.js +433 -0
- package/src/supply.js +409 -0
- package/src/util.js +130 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Krrish Kumar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,154 @@
|
|
|
1
|
-
#
|
|
1
|
+
# secret-blast-radius
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/Krrishkumar125/secret-blast-radius/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/secret-blast-radius)
|
|
5
|
+
|
|
6
|
+
**One bad `npm install` can read every secret on your laptop. This tells you what it would get, whether it already happened, and what to rotate first.**
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx secret-blast-radius
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
✖ CRITICAL Known-malicious package @subql/common@5.8.3 (subql)
|
|
14
|
+
~/code/indexer/package-lock.json
|
|
15
|
+
Resolved in this lockfile. Treat every machine/CI runner that installed it as compromised…
|
|
16
|
+
|
|
17
|
+
✖ CRITICAL Plaintext npm credential for registry.npmjs.org
|
|
18
|
+
~/.npmrc sha256:3f1c9a0b
|
|
19
|
+
Any install script can read this and republish every package you maintain (this is how Shai-Hulud spreads).
|
|
20
|
+
|
|
21
|
+
✖ CRITICAL VS Code task "eslint-check" auto-runs on folder open and executes a non-code file
|
|
22
|
+
~/code/old-portfolio/.vscode/tasks.json
|
|
23
|
+
Matches the GhostAction / PolinRider launcher (tasks.json → `node public/fonts/*.woff2`)…
|
|
24
|
+
|
|
25
|
+
▲ HIGH MCP server "github" has a literal secret in env.GITHUB_PERSONAL_ACCESS_TOKEN
|
|
26
|
+
~/.cursor/mcp.json sha256:9b07e6c1
|
|
27
|
+
|
|
28
|
+
● MEDIUM api deploys to Railway (railway.toml)
|
|
29
|
+
Railway disclosed on Oct 21, 2026 that env vars may have been read and decrypted…
|
|
30
|
+
|
|
31
|
+
Summary: 3 critical, 1 high, 1 medium, 0 low.
|
|
32
|
+
Rotation order if this machine is ever compromised: 1. persistence 2. npm 3. ai 4. paas
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
*(Illustrative output: fingerprints and paths are examples.)*
|
|
36
|
+
|
|
37
|
+
## Why this exists
|
|
38
|
+
|
|
39
|
+
The last few weeks, in order:
|
|
40
|
+
|
|
41
|
+
| When (2026) | What | What it took |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Oct 5 | [`@subql/common@5.8.3`](https://osv.dev/vulnerability/MAL-2026-17571) shipped through the project's own trusted-publishing workflow | AWS (SSM + Secrets Manager, all regions), GitHub, npm, SSH, K8s, Vault. Runs on import too, so `--ignore-scripts` isn't enough |
|
|
44
|
+
| Oct 7–15 | [GhostAction](https://thehackernews.com/2026/10/credential-stealing-github-actions.html): ~1,900 hijacked GitHub accounts pushed "security audit" workflows to 30,000+ repos, then a [VS Code fake-font stealer](https://opensourcemalware.com/blog/ghostaction-copycats-polinrider-by-hiding-a-stealer-in-a-fake-font-file) | Actions secrets, everything in git history, `~/.npmrc`, `~/.aws`, `~/.netrc`, SSH keys, Claude/Gemini logins, plus a cron backdoor |
|
|
45
|
+
| Oct 8 | [`tensorlake@0.5.144`](https://github.com/tensorlakeai/tensorlake/security/advisories/GHSA-8g63-53c9-65j2) (Mini Shai-Hulud) | Everything above plus AI-tool and MCP configs. Installs `gh-token-monitor`, which **wipes your home directory if you revoke the stolen GitHub token** |
|
|
46
|
+
| Oct 21 | [Railway](https://blog.railway.com/p/security-disclosure-wednesday-october-21st): customer env vars may have been decrypted | Every secret you stored on the platform |
|
|
47
|
+
| Oct 21 | [PHP2Shell](https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching) (CVE-2026-109618/-109619), unauthenticated RCE | Whatever your PHP servers can reach |
|
|
48
|
+
|
|
49
|
+
Every advisory ends the same way: *"rotate everything the machine could access."* Hardly anyone has that list. This tool builds it, checks for the known infections, and puts the steps in an order that won't make things worse.
|
|
50
|
+
|
|
51
|
+
## What it checks
|
|
52
|
+
|
|
53
|
+
**Your credentials** (what a stealer reads first):
|
|
54
|
+
|
|
55
|
+
| Scanner | Looks at |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `npm` | `~/.npmrc`, project `.npmrc`, `~/.pypirc`: literal tokens (not `${VAR}`) |
|
|
58
|
+
| `github` | gh `hosts.yml` (plaintext fallback), `~/.git-credentials`, `~/.netrc` |
|
|
59
|
+
| `cloud` | `~/.aws/credentials` (long-lived vs session), gcloud ADC, Azure token cache |
|
|
60
|
+
| `kubernetes` | `~/.kube/config` embedded tokens/keys, `~/.docker/config.json` without `credsStore` |
|
|
61
|
+
| `ai` | MCP configs for Claude Code/Desktop, Cursor, Windsurf, VS Code, Zed, Kiro, Gemini; Claude, Gemini and Codex CLI logins |
|
|
62
|
+
| `ssh` | Private keys without a passphrase (PEM, PKCS#8, OpenSSH) |
|
|
63
|
+
| `dotenv` | `.env*` files in your projects (names only, never values) |
|
|
64
|
+
| `env` | Variables a CI step inherits (`NPM_TOKEN`, `AWS_*`, recognised token formats) |
|
|
65
|
+
| `paas` | Railway, Vercel, Fly, Render, Netlify CLI tokens; Heroku in `~/.netrc`; which projects deploy where |
|
|
66
|
+
|
|
67
|
+
**Signs you're already hit:**
|
|
68
|
+
|
|
69
|
+
| Scanner | Detects |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `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 |
|
|
72
|
+
| `persistence` | `gh-token-monitor` (systemd unit, LaunchAgent, Windows dir, script) |
|
|
73
|
+
| `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
|
+
| `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 |
|
|
75
|
+
|
|
76
|
+
**Exposure to this month's other incidents:**
|
|
77
|
+
|
|
78
|
+
| Scanner | Detects |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `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
|
+
| `certs` | PEM/CRT files and `NODE_EXTRA_CA_CERTS` bundles containing or chaining to the distrusted **emSign ECC Root CA - G3** (eMudhra) |
|
|
82
|
+
| `install-scripts` | Which dependencies run install scripts, straight from the lockfile |
|
|
83
|
+
| `hardening` | No `ignore-scripts`, no dependency cooldown (`min-release-age` / `minimumReleaseAge`) |
|
|
84
|
+
|
|
85
|
+
## Incident mode
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx secret-blast-radius --list-incidents
|
|
89
|
+
npx secret-blast-radius --incident tensorlake # or: shai-hulud, subql, ghostaction, railway, php2shell, emudhra, sbi
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
## In CI and GitHub code scanning
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
permissions:
|
|
98
|
+
contents: read
|
|
99
|
+
security-events: write
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v4
|
|
102
|
+
- uses: Krrishkumar125/secret-blast-radius@v0.2.0
|
|
103
|
+
with:
|
|
104
|
+
fail-on: critical
|
|
105
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
106
|
+
if: always()
|
|
107
|
+
with:
|
|
108
|
+
sarif_file: secret-blast-radius.sarif
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or without the action: `npx secret-blast-radius@0 --only malicious,repo,php,env --fail-on critical --sarif-file sbr.sarif`.
|
|
112
|
+
SARIF only gets file locations for findings inside the repo. Home-directory findings are reported without a path.
|
|
113
|
+
|
|
114
|
+
## Safety
|
|
115
|
+
|
|
116
|
+
- **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`.
|
|
118
|
+
- **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.
|
|
120
|
+
|
|
121
|
+
## Options
|
|
122
|
+
|
|
123
|
+
| Option | Meaning |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `[project-dir…]` | Projects to scan for `.env`, lockfiles, Dockerfiles, workflows (default: cwd) |
|
|
126
|
+
| `--incident <name>`, `--list-incidents` | Incident checklists |
|
|
127
|
+
| `--json`, `--sarif`, `--sarif-file <f>` | Machine output |
|
|
128
|
+
| `--runbook`, `--runbook-file <f>` | Markdown rotation checklist |
|
|
129
|
+
| `--fail-on <level>` | Exit 1 at/above `critical`, `high`, `medium`, `low` (default `none`) |
|
|
130
|
+
| `--only` / `--skip <list>` | Choose scanners (names in the tables above) |
|
|
131
|
+
| `--ioc-file <f>` | Extra IOCs, same shape as `src/iocs.json` (repeatable) |
|
|
132
|
+
| `--no-env`, `--no-exec`, `--depth <n>`, `--home <dir>`, `--no-color` | Tuning |
|
|
133
|
+
|
|
134
|
+
Exit codes: `0` ok, `1` findings at/above `--fail-on`, `2` usage error.
|
|
135
|
+
|
|
136
|
+
## Keeping false positives low
|
|
137
|
+
|
|
138
|
+
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
|
+
|
|
140
|
+
## Limits
|
|
141
|
+
|
|
142
|
+
- 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.
|
|
144
|
+
- For distro PHP builds it can't tell whether your vendor backported the fix.
|
|
145
|
+
- A clean report shrinks your blast radius. It doesn't prove a machine is clean.
|
|
146
|
+
|
|
147
|
+
## Programmatic API
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
import { scan, runbook, toSarif, INCIDENTS } from 'secret-blast-radius';
|
|
151
|
+
const result = scan({ projects: ['.'], incident: 'railway', exec: false });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
MIT © Krrish Kumar
|
package/action.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: secret-blast-radius
|
|
2
|
+
description: Find what a malicious install could steal in this job and repo (worm IOCs, GhostAction workflows, PHP2Shell, exposed CI secrets). Read-only; never prints secrets.
|
|
3
|
+
author: Krrish Kumar
|
|
4
|
+
branding:
|
|
5
|
+
icon: shield
|
|
6
|
+
color: red
|
|
7
|
+
inputs:
|
|
8
|
+
only:
|
|
9
|
+
description: Comma list of scanners. Default is the set that makes sense on a CI runner.
|
|
10
|
+
default: malicious,repo,php,certs,install-scripts,env,npm,dotenv
|
|
11
|
+
fail-on:
|
|
12
|
+
description: critical | high | medium | low | none
|
|
13
|
+
default: critical
|
|
14
|
+
incident:
|
|
15
|
+
description: Optional incident name (php2shell, railway, tensorlake, shai-hulud, subql, ghostaction, emudhra, sbi)
|
|
16
|
+
default: ''
|
|
17
|
+
sarif-file:
|
|
18
|
+
description: Where to write SARIF (upload it with github/codeql-action/upload-sarif)
|
|
19
|
+
default: secret-blast-radius.sarif
|
|
20
|
+
version:
|
|
21
|
+
description: npm version or dist-tag of secret-blast-radius to run
|
|
22
|
+
default: '0'
|
|
23
|
+
outputs:
|
|
24
|
+
sarif-file:
|
|
25
|
+
description: Path of the SARIF report
|
|
26
|
+
value: ${{ inputs.sarif-file }}
|
|
27
|
+
runs:
|
|
28
|
+
using: composite
|
|
29
|
+
steps:
|
|
30
|
+
- name: Run secret-blast-radius
|
|
31
|
+
shell: bash
|
|
32
|
+
env:
|
|
33
|
+
SBR_ONLY: ${{ inputs.only }}
|
|
34
|
+
SBR_FAIL_ON: ${{ inputs.fail-on }}
|
|
35
|
+
SBR_INCIDENT: ${{ inputs.incident }}
|
|
36
|
+
SBR_SARIF: ${{ inputs.sarif-file }}
|
|
37
|
+
SBR_VERSION: ${{ inputs.version }}
|
|
38
|
+
run: |
|
|
39
|
+
args=(--only "$SBR_ONLY" --fail-on "$SBR_FAIL_ON" --sarif-file "$SBR_SARIF")
|
|
40
|
+
if [ -n "$SBR_INCIDENT" ]; then args+=(--incident "$SBR_INCIDENT"); fi
|
|
41
|
+
npx --yes "secret-blast-radius@${SBR_VERSION}" "${args[@]}" .
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { writeFileSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { scan, failsAt, formatText, formatMarkdown, runbook, toSarif, INCIDENTS } from '../src/index.js';
|
|
5
|
+
|
|
6
|
+
const HELP = `Usage: secret-blast-radius [options] [project-dir...]
|
|
7
|
+
|
|
8
|
+
Lists every credential an npm install script (or any process you run) could read
|
|
9
|
+
on this machine or CI runner, ranks it, and writes an ordered rotation runbook.
|
|
10
|
+
Read-only. Never prints secret values.
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--incident <name> Tailored checklist + only the relevant checks. Names:
|
|
14
|
+
php2shell, railway, tensorlake, shai-hulud, subql, ghostaction, emudhra, sbi
|
|
15
|
+
--list-incidents Show incident names and status
|
|
16
|
+
--json Machine-readable output
|
|
17
|
+
--sarif [file] SARIF 2.1.0 for GitHub code scanning (stdout, or --sarif-file <f>)
|
|
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)
|
|
20
|
+
--runbook [file] Print (or write to file) a Markdown rotation runbook
|
|
21
|
+
--fail-on <level> Exit 1 if any finding is at/above level: critical|high|medium|low|none (default: none)
|
|
22
|
+
--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
|
|
24
|
+
--skip <list> Comma list of scanners to skip
|
|
25
|
+
--no-env Do not inspect environment variables
|
|
26
|
+
--depth <n> How deep to look for .env files in project dirs (default 4)
|
|
27
|
+
--home <dir> Home directory to inspect (default: your home)
|
|
28
|
+
--no-color Disable colours
|
|
29
|
+
-v, --version Print version
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
let args;
|
|
34
|
+
try {
|
|
35
|
+
args = parseArgs({
|
|
36
|
+
allowPositionals: true,
|
|
37
|
+
options: {
|
|
38
|
+
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' },
|
|
40
|
+
'fail-on': { type: 'string', default: 'none' }, only: { type: 'string' }, skip: { type: 'string' },
|
|
41
|
+
'no-env': { type: 'boolean' }, depth: { type: 'string' }, home: { type: 'string' },
|
|
42
|
+
'no-color': { type: 'boolean' }, version: { type: 'boolean', short: 'v' }, help: { type: 'boolean', short: 'h' },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error(e.message + '\n\n' + HELP);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
const { values: o, positionals } = args;
|
|
50
|
+
if (o.help) { process.stdout.write(HELP); process.exit(0); }
|
|
51
|
+
if (o.version) {
|
|
52
|
+
console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
// `--runbook out.md`: parseArgs treats the file as positional; detect a .md positional right after.
|
|
56
|
+
let runbookFile = o['runbook-file'];
|
|
57
|
+
let projects = positionals;
|
|
58
|
+
if (o.runbook && !runbookFile && positionals.length && /\.md$/i.test(positionals[0])) { runbookFile = positionals[0]; projects = positionals.slice(1); }
|
|
59
|
+
|
|
60
|
+
if (o['list-incidents']) {
|
|
61
|
+
for (const [k, v] of Object.entries(INCIDENTS)) console.log(`${k.padEnd(12)} ${v.title}\n${' '.repeat(13)}${v.status}`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
const list = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined);
|
|
65
|
+
const skip = list(o.skip) ?? [];
|
|
66
|
+
if (o['no-env']) skip.push('env');
|
|
67
|
+
|
|
68
|
+
let result;
|
|
69
|
+
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 });
|
|
71
|
+
failsAt(result, o['fail-on']);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(`secret-blast-radius: ${e.message}`);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
78
|
+
if (o.sarif || o['sarif-file']) {
|
|
79
|
+
const sarif = JSON.stringify(toSarif(result, version), null, 2) + '\n';
|
|
80
|
+
if (o['sarif-file']) { writeFileSync(o['sarif-file'], sarif); if (!o.json) console.error(`SARIF written to ${o['sarif-file']}`); } else process.stdout.write(sarif);
|
|
81
|
+
}
|
|
82
|
+
if (o.sarif && !o['sarif-file']) {
|
|
83
|
+
// stdout already used for SARIF
|
|
84
|
+
} else if (o.json) {
|
|
85
|
+
process.stdout.write(JSON.stringify({ ...result, runbook: runbook(result).map(({ items, ...r }) => ({ ...r, findings: items.map((i) => i.id) })) }, null, 2) + '\n');
|
|
86
|
+
} else if (o.runbook) {
|
|
87
|
+
const md = formatMarkdown(result);
|
|
88
|
+
if (runbookFile) { writeFileSync(runbookFile, md); console.log(`Runbook written to ${runbookFile}`); } else process.stdout.write(md + '\n');
|
|
89
|
+
} else {
|
|
90
|
+
const color = !o['no-color'] && process.stdout.isTTY && !process.env.NO_COLOR;
|
|
91
|
+
console.log(formatText(result, { color }));
|
|
92
|
+
}
|
|
93
|
+
process.exit(failsAt(result, o['fail-on']) ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "secret-blast-radius",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"author": "Krrish Kumar (https://github.com/Krrishkumar125)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Krrishkumar125/secret-blast-radius.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Krrishkumar125/secret-blast-radius#readme",
|
|
12
|
+
"bugs": "https://github.com/Krrishkumar125/secret-blast-radius/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"security",
|
|
15
|
+
"supply-chain",
|
|
16
|
+
"npm",
|
|
17
|
+
"shai-hulud",
|
|
18
|
+
"secrets",
|
|
19
|
+
"credentials",
|
|
20
|
+
"rotation",
|
|
21
|
+
"incident-response",
|
|
22
|
+
"mcp",
|
|
23
|
+
"ci",
|
|
24
|
+
"devsecops",
|
|
25
|
+
"sarif",
|
|
26
|
+
"php2shell",
|
|
27
|
+
"ghostaction",
|
|
28
|
+
"railway",
|
|
29
|
+
"github-action"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"bin": {
|
|
33
|
+
"secret-blast-radius": "bin/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": "./src/index.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin",
|
|
40
|
+
"src",
|
|
41
|
+
"action.yml",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.18"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "node --test test/scanners.test.js test/supply.test.js test/cli.test.js",
|
|
50
|
+
"check": "node --check bin/cli.js && npm test"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { runbook } from './index.js';
|
|
2
|
+
import { INCIDENTS } from './incidents.js';
|
|
3
|
+
|
|
4
|
+
const ICON = { critical: '✖', high: '▲', medium: '●', low: '·' };
|
|
5
|
+
|
|
6
|
+
export function formatText(result, { color = false } = {}) {
|
|
7
|
+
const c = (code, s) => (color ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
8
|
+
const sev = { critical: (s) => c('1;31', s), high: (s) => c('31', s), medium: (s) => c('33', s), low: (s) => c('2', s) };
|
|
9
|
+
const lines = [];
|
|
10
|
+
const { counts } = result;
|
|
11
|
+
lines.push(c('1', 'secret-blast-radius') + ' — what a malicious install script could steal from this machine right now');
|
|
12
|
+
lines.push('');
|
|
13
|
+
if (result.incident) {
|
|
14
|
+
const inc = INCIDENTS[result.incident];
|
|
15
|
+
lines.push(c('1', inc.title), c('2', inc.status), '');
|
|
16
|
+
inc.checklist.forEach((x, i) => lines.push(` ${i + 1}. ${x}`));
|
|
17
|
+
lines.push('', ...inc.refs.map((r) => ` ${c('2', r)}`), '', c('1', 'What this machine/project has that matters for this incident:'), '');
|
|
18
|
+
}
|
|
19
|
+
if (!result.findings.length) {
|
|
20
|
+
lines.push(result.incident ? 'Nothing relevant found by these checks.' : 'No reachable secrets found in the places checked. Nice.');
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
23
|
+
for (const f of result.findings) {
|
|
24
|
+
lines.push(`${sev[f.severity](`${ICON[f.severity]} ${f.severity.toUpperCase().padEnd(8)}`)} ${f.title}`);
|
|
25
|
+
lines.push(` ${c('2', f.location + (f.fingerprint ? ` ${f.fingerprint}` : '') + (f.ageDays != null ? ` (${f.ageDays}d old)` : ''))}`);
|
|
26
|
+
lines.push(` ${f.detail}`);
|
|
27
|
+
lines.push('');
|
|
28
|
+
}
|
|
29
|
+
lines.push(`Summary: ${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low.`);
|
|
30
|
+
const rb = runbook(result);
|
|
31
|
+
if (rb.length) lines.push(`Rotation order if this machine is ever compromised: ${rb.map((r, i) => `${i + 1}. ${r.category}`).join(' ')}`);
|
|
32
|
+
lines.push('Run with --runbook for step-by-step rotation, --json for machine output. Secret values are never printed.');
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatMarkdown(result) {
|
|
37
|
+
const out = ['# Secret rotation runbook', '', `Generated ${result.scannedAt} by secret-blast-radius. Values are never included; fingerprints let you match items.`, ''];
|
|
38
|
+
if (result.incident) {
|
|
39
|
+
const inc = INCIDENTS[result.incident];
|
|
40
|
+
out.push(`## Incident: ${inc.title}`, '', `_${inc.status}_`, '', ...inc.checklist.map((x) => `- [ ] ${x}`), '', ...inc.refs.map((r) => `- ${r}`), '');
|
|
41
|
+
}
|
|
42
|
+
const rb = runbook(result);
|
|
43
|
+
if (!rb.length) return out.concat('Nothing to rotate: no reachable secrets were found.').join('\n');
|
|
44
|
+
out.push('Work top to bottom. The order matters: stop persistence first, then the credentials a worm uses to spread (npm, GitHub), then everything else.', '');
|
|
45
|
+
rb.forEach((step, i) => {
|
|
46
|
+
out.push(`## ${i + 1}. ${step.label}`, '');
|
|
47
|
+
for (const it of step.items) out.push(`- [ ] **${it.title}** — \`${it.location}\`${it.fingerprint ? ` (${it.fingerprint})` : ''}`);
|
|
48
|
+
out.push('', ...step.steps.map((s) => `1. ${s}`), '', `Reference: ${step.url}`, '');
|
|
49
|
+
});
|
|
50
|
+
return out.join('\n');
|
|
51
|
+
}
|
package/src/incidents.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Incident playbooks. Every claim here is sourced; `status` marks what is confirmed vs still under investigation. */
|
|
2
|
+
export const INCIDENTS = {
|
|
3
|
+
php2shell: {
|
|
4
|
+
title: 'PHP2Shell — CVE-2026-109618 / CVE-2026-109619 (Oct 21, 2026)',
|
|
5
|
+
status: 'Confirmed by the PHP project and CISA. CISA had not observed exploitation at publication; public PoCs were circulating.',
|
|
6
|
+
scanners: ['php'],
|
|
7
|
+
checklist: [
|
|
8
|
+
'Inventory every PHP runtime: servers, containers, base images, and products that bundle PHP (CISA: "prevalence may not be directly observable").',
|
|
9
|
+
'Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 (or your distro\'s patched build) and restart every PHP-FPM pool / Apache worker.',
|
|
10
|
+
'PHP 8.1 and older get no fix: upgrade, or take internet-facing systems offline until you can (CISA).',
|
|
11
|
+
'Rebuild images with `--pull --no-cache`; a floating `php:8.3` tag does not update images you already built.',
|
|
12
|
+
'Watch for PHP worker processes spawning shells or downloading tools.',
|
|
13
|
+
],
|
|
14
|
+
refs: ['https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/', 'https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching'],
|
|
15
|
+
},
|
|
16
|
+
railway: {
|
|
17
|
+
title: 'Railway environment-variable exposure (disclosed Oct 21, 2026)',
|
|
18
|
+
status: 'Confirmed by Railway; scope still being determined ("may be limited to a subset of users"). Railway advises all users to act.',
|
|
19
|
+
scanners: ['paas', 'dotenv', 'env'],
|
|
20
|
+
checklist: [
|
|
21
|
+
'Roll every environment variable stored in Railway: API keys, database passwords, webhook secrets, OAuth client secrets.',
|
|
22
|
+
'Rotate private container-registry credentials configured in Railway.',
|
|
23
|
+
'Review provider logs (database, Stripe, cloud, AI providers) for use of the old keys from unknown IPs.',
|
|
24
|
+
'Redeploy once builds resume: running services keep the old values until restart.',
|
|
25
|
+
'Watch for Railway\'s direct email if your account is in the confirmed set, and status.railway.com for updates.',
|
|
26
|
+
],
|
|
27
|
+
refs: ['https://blog.railway.com/p/security-disclosure-wednesday-october-21st'],
|
|
28
|
+
},
|
|
29
|
+
tensorlake: {
|
|
30
|
+
title: 'tensorlake@0.5.144 — Mini Shai-Hulud worm with gh-token-monitor dead-man\'s switch (Oct 8, 2026)',
|
|
31
|
+
status: 'Confirmed by the maintainer (GHSA-8g63-53c9-65j2), Socket, StepSecurity and Aikido.',
|
|
32
|
+
scanners: ['malicious', 'persistence', 'host-persistence', 'repo', 'npm', 'github', 'cloud', 'kubernetes', 'ssh', 'ai', 'dotenv', 'env'],
|
|
33
|
+
checklist: [
|
|
34
|
+
'Search lockfiles and CI logs for tensorlake@0.5.144 and tensorlake-native-*@0.5.144 (live 01:12–02:54 UTC, Oct 8).',
|
|
35
|
+
'If it ran: disconnect, then REMOVE gh-token-monitor before revoking any GitHub token (revocation triggers `rm -rf ~`).',
|
|
36
|
+
'From a clean machine rotate npm, GitHub, cloud, Kubernetes, Vault, SSH/GPG, .env, AI-tool/MCP credentials.',
|
|
37
|
+
'Check packages you publish for versions you did not release; check repos for commits by claude@users.noreply.github.com and the branch dependabot/github_actions/format/setup-formatter.',
|
|
38
|
+
'Rebuild the machine from a trusted image.',
|
|
39
|
+
],
|
|
40
|
+
refs: ['https://github.com/tensorlakeai/tensorlake/security/advisories/GHSA-8g63-53c9-65j2', 'https://www.stepsecurity.io/blog/tensorlake-npm-compromised-hostage-token-worm'],
|
|
41
|
+
},
|
|
42
|
+
subql: {
|
|
43
|
+
title: '@subql/common@5.8.3 — credential stealer + remote shell (Oct 5, 2026)',
|
|
44
|
+
status: 'Confirmed: GHSA-9333-3c4x-x3h5 / MAL-2026-17571; analysed by StepSecurity and GMO Flatt Security.',
|
|
45
|
+
scanners: ['malicious', 'host-persistence', 'cloud', 'github', 'npm', 'ssh', 'kubernetes', 'env', 'dotenv'],
|
|
46
|
+
checklist: [
|
|
47
|
+
'Search lockfiles, caches and CI logs for @subql/common@5.8.3. 5.8.2 is the last known-good release.',
|
|
48
|
+
'`--ignore-scripts` is NOT enough: importing the package also starts the payload.',
|
|
49
|
+
'Look for a running `manifest-cache.js --warm` process and /tmp/tmp.ts018051808.lock; block ci-artifacts.dev.',
|
|
50
|
+
'Rotate AWS credentials first (SSM Parameter Store and Secrets Manager were read across all regions), then GitHub, npm, SSH, Kubernetes, Vault.',
|
|
51
|
+
'Rebuild affected hosts/runners; uninstalling does not stop the detached worker.',
|
|
52
|
+
],
|
|
53
|
+
refs: ['https://osv.dev/vulnerability/MAL-2026-17571', 'https://www.stepsecurity.io/blog/subql-ecosystem-compromised', 'https://flatt.tech/research/posts/subql-common-npm-supply-chain-attack/'],
|
|
54
|
+
},
|
|
55
|
+
ghostaction: {
|
|
56
|
+
title: 'GhostAction — malicious "security audit" workflows + VS Code fake-font stealer (Oct 7–15, 2026)',
|
|
57
|
+
status: 'Confirmed by StepSecurity, Socket, GitGuardian and OpenSourceMalware. ~1,900 accounts, 30,000+ repos targeted.',
|
|
58
|
+
scanners: ['repo', 'host-persistence', 'github', 'npm', 'cloud', 'kubernetes', 'ssh', 'ai'],
|
|
59
|
+
checklist: [
|
|
60
|
+
'Revoke GitHub sessions, PATs, OAuth grants (incl. client ID Ov23ctrqCqZ79UaYKwyq), SSH and deploy keys FIRST: deleting the workflow does not revoke the token that pushed it.',
|
|
61
|
+
'In every repo you can write to, remove workflows contacting 193.32.204.199 (security-audit.yml, lint.yml, ci-check.yml, …), .vscode/tasks.json "eslint-check" tasks and public/fonts/*.woff2 files that are really JS.',
|
|
62
|
+
'Rotate the repo\'s Actions secrets and every credential ever committed to any branch (Stage 1 reads `git log -p --all`).',
|
|
63
|
+
'If you opened an infected repo in VS Code with automatic tasks allowed: run `crontab -l` and remove lines ending in #ghc2, then rotate from a clean device.',
|
|
64
|
+
'Require approval for workflow runs and protect .github/workflows with required reviews.',
|
|
65
|
+
],
|
|
66
|
+
refs: ['https://thehackernews.com/2026/10/credential-stealing-github-actions.html', 'https://opensourcemalware.com/blog/ghostaction-copycats-polinrider-by-hiding-a-stealer-in-a-fake-font-file', 'https://blog.gitguardian.com/ghostaction-github-actions-supply-chain-attack-returns/'],
|
|
67
|
+
},
|
|
68
|
+
emudhra: {
|
|
69
|
+
title: 'eMudhra — weak keys in 8 intermediates under emSign ECC Root CA - G3 (reported Oct 17–18, 2026)',
|
|
70
|
+
status: 'Confirmed by Google, Mozilla and eMudhra. eMudhra says no certificates were issued from the intermediates; root cause still under investigation.',
|
|
71
|
+
scanners: ['certs'],
|
|
72
|
+
checklist: [
|
|
73
|
+
'Website operators: if your TLS chain ends at emSign ECC Root CA - G3, get a certificate from another CA before Chrome removes the root (Oct 27, 2026 PDT).',
|
|
74
|
+
'Remove emSign ECC Root CA - G3 from custom CA bundles (NODE_EXTRA_CA_CERTS, Java truststores, pinned bundles in apps/IoT).',
|
|
75
|
+
'Clients that do not enforce Certificate Transparency or consume CRLSets/OneCRL need a trust-store update to stop trusting the intermediates.',
|
|
76
|
+
'eMudhra states its Indian DSC/eSign services run on separate hierarchies and are unaffected.',
|
|
77
|
+
],
|
|
78
|
+
refs: ['https://blog.google/security/protecting-chrome-and-android-users-from-compromised-emudhra-intermediate-ca-certificates/', 'https://emudhra.com/en/post/certificate-security-incident/'],
|
|
79
|
+
},
|
|
80
|
+
sbi: {
|
|
81
|
+
title: 'SBI — customer records found on the dark web (disclosed Oct 21, 2026)',
|
|
82
|
+
status: 'Under investigation. SBI\'s preliminary analysis of ~375,000 records found 3,897 customer records with PII; source not yet determined.',
|
|
83
|
+
scanners: ['dotenv', 'env', 'ai'],
|
|
84
|
+
checklist: [
|
|
85
|
+
'Nothing to scan on a developer machine for this one: it is a data exposure, not a software flaw.',
|
|
86
|
+
'Customers: expect targeted calls/SMS using real names, phone numbers, addresses and loan details. Banks never ask for OTP, PIN or remote-access apps.',
|
|
87
|
+
'Fintech/lending developers: CERT-In\'s Oct 9 and Oct 15 advisories cite exposed credentials, test APIs and plaintext payment-API keys. The secrets inventory below is where to start.',
|
|
88
|
+
],
|
|
89
|
+
refs: ['https://economictimes.indiatimes.com/industry/banking/finance/banking/sbi-probes-suspected-data-breach-after-3-75-lakh-records-surface-on-dark-web/articleshow/135083347.cms'],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
INCIDENTS['shai-hulud'] = { ...INCIDENTS.tensorlake, title: 'Mini Shai-Hulud npm worm family (keyv/cacheable Aug 2026, @7nohe/openapi-react-query-codegen, tensorlake Oct 2026)' };
|