secret-blast-radius 0.1.0 → 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/README.md +105 -76
- package/action.yml +41 -0
- package/bin/cli.js +24 -5
- package/package.json +41 -9
- package/src/format.js +12 -1
- package/src/incidents.js +92 -0
- package/src/index.js +45 -3
- package/src/iocs.json +251 -0
- package/src/sarif.js +37 -0
- package/src/scanners.js +17 -2
- package/src/supply.js +409 -0
- package/src/util.js +4 -0
package/README.md
CHANGED
|
@@ -3,123 +3,152 @@
|
|
|
3
3
|
[](https://github.com/Krrishkumar125/secret-blast-radius/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/secret-blast-radius)
|
|
5
5
|
|
|
6
|
-
**
|
|
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
7
|
|
|
8
8
|
```bash
|
|
9
9
|
npx secret-blast-radius
|
|
10
10
|
```
|
|
11
11
|
|
|
12
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
|
+
|
|
13
17
|
✖ CRITICAL Plaintext npm credential for registry.npmjs.org
|
|
14
18
|
~/.npmrc sha256:3f1c9a0b
|
|
15
19
|
Any install script can read this and republish every package you maintain (this is how Shai-Hulud spreads).
|
|
16
20
|
|
|
17
|
-
✖ CRITICAL
|
|
18
|
-
|
|
19
|
-
|
|
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`)…
|
|
20
24
|
|
|
21
25
|
▲ HIGH MCP server "github" has a literal secret in env.GITHUB_PERSONAL_ACCESS_TOKEN
|
|
22
26
|
~/.cursor/mcp.json sha256:9b07e6c1
|
|
23
27
|
|
|
24
|
-
● MEDIUM
|
|
25
|
-
|
|
28
|
+
● MEDIUM api deploys to Railway (railway.toml)
|
|
29
|
+
Railway disclosed on Oct 21, 2026 that env vars may have been read and decrypted…
|
|
26
30
|
|
|
27
|
-
Summary:
|
|
28
|
-
Rotation order if this machine is ever compromised: 1.
|
|
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
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
*(Illustrative output: fingerprints and paths are examples.)*
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
npm, GitHub, cloud, Kubernetes, SSH, `.env` **and coding-agent/MCP config** credentials, then used the npm token to
|
|
35
|
-
republish itself ([advisory](https://github.com/tensorlakeai/tensorlake/security/advisories/GHSA-8g63-53c9-65j2),
|
|
36
|
-
[StepSecurity](https://www.stepsecurity.io/blog/tensorlake-npm-compromised-hostage-token-worm)). Railway told every
|
|
37
|
-
customer to rotate secrets after env vars may have been decrypted
|
|
38
|
-
([disclosure](https://blog.railway.com/p/security-disclosure-wednesday-october-21st)).
|
|
37
|
+
## Why this exists
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
for you, **before** an incident (so you can shrink it) and **during** one (so you rotate in the right order).
|
|
39
|
+
The last few weeks, in order:
|
|
42
40
|
|
|
43
|
-
What
|
|
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 |
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
**not a repo secret scanner** (gitleaks, trufflehog scan git history). It inventories the credentials *reachable on the
|
|
47
|
-
machine or CI runner*, which is exactly what an install script can read.
|
|
48
|
-
- **One report across every place:** npm, GitHub CLI and git, AWS/GCP/Azure, kubeconfig and docker, SSH keys, MCP configs for
|
|
49
|
-
Claude, Cursor, Windsurf, VS Code, Zed, Kiro and Gemini, project `.env` files, and the environment variables a CI step inherits.
|
|
50
|
-
- **An ordered rotation runbook.** Order matters: the worm's `gh-token-monitor` wipes your home directory when you revoke
|
|
51
|
-
the stolen GitHub token, so persistence comes first, then the credentials a worm spreads with (npm, GitHub), then the rest.
|
|
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.
|
|
52
50
|
|
|
53
|
-
##
|
|
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:**
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
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`) |
|
|
59
84
|
|
|
60
|
-
##
|
|
85
|
+
## Incident mode
|
|
61
86
|
|
|
62
87
|
```bash
|
|
63
|
-
npx secret-blast-radius
|
|
64
|
-
npx secret-blast-radius
|
|
65
|
-
npx secret-blast-radius --runbook # Markdown rotation checklist to stdout
|
|
66
|
-
npx secret-blast-radius --runbook-file rotate.md --runbook
|
|
67
|
-
npx secret-blast-radius --json # machine-readable
|
|
68
|
-
npx secret-blast-radius --fail-on high # exit 1 if anything high or critical (CI gate)
|
|
69
|
-
npx secret-blast-radius --only npm,github,ai # pick scanners
|
|
88
|
+
npx secret-blast-radius --list-incidents
|
|
89
|
+
npx secret-blast-radius --incident tensorlake # or: shai-hulud, subql, ghostaction, railway, php2shell, emudhra, sbi
|
|
70
90
|
```
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|---|---|
|
|
74
|
-
| `--json` | JSON report including the ordered runbook |
|
|
75
|
-
| `--runbook`, `--runbook-file <f>` | Markdown rotation checklist |
|
|
76
|
-
| `--fail-on <level>` | `critical`, `high`, `medium`, `low` or `none` (default) |
|
|
77
|
-
| `--only` / `--skip <list>` | `persistence,npm,github,cloud,kubernetes,env,ai,ssh,dotenv,hardening` |
|
|
78
|
-
| `--no-env` | Skip environment variables |
|
|
79
|
-
| `--depth <n>` | How deep to search project dirs for `.env*` (default 4; skips `node_modules`, `.git`, build dirs) |
|
|
80
|
-
| `--home <dir>` | Inspect another home directory (e.g. a mounted disk image) |
|
|
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.
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
## In CI and GitHub code scanning
|
|
83
95
|
|
|
84
96
|
```yaml
|
|
85
|
-
|
|
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
|
|
86
109
|
```
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
90
113
|
|
|
91
|
-
##
|
|
114
|
+
## Safety
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| `github` | gh `hosts.yml`, `~/.git-credentials` | Tokens stored in plaintext instead of the keyring |
|
|
98
|
-
| `cloud` | `~/.aws/credentials`, gcloud ADC, Azure token cache | Long-lived keys (critical) vs session keys |
|
|
99
|
-
| `kubernetes` | `~/.kube/config`, `~/.docker/config.json` | Embedded tokens/client keys, registry auth without `credsStore` |
|
|
100
|
-
| `env` | Current process environment | Known token formats and secret-named variables |
|
|
101
|
-
| `ai` | MCP configs for Claude Code/Desktop, Cursor, Windsurf, VS Code, Zed, Kiro, Gemini; Codex auth | Literal keys in `env`, `headers`, `args`, URL query |
|
|
102
|
-
| `ssh` | `~/.ssh/*` private keys | Keys without a passphrase (PEM, PKCS#8 and OpenSSH formats) |
|
|
103
|
-
| `dotenv` | `.env*` in project dirs (not `.env.example`) | Secret-named keys and recognised tokens; URLs only when they embed a password |
|
|
104
|
-
| `hardening` | `.npmrc`, `pnpm-workspace.yaml` | No `ignore-scripts`, no dependency cooldown (`min-release-age` / `minimumReleaseAge`) |
|
|
105
|
-
|
|
106
|
-
Placeholders such as `${VAR}`, `${{ secrets.X }}`, `${env:X}`, `op://…`, `<token>`, `[YOUR_KEY]` and `your_api_key_here` are ignored.
|
|
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.
|
|
107
120
|
|
|
108
|
-
##
|
|
121
|
+
## Options
|
|
109
122
|
|
|
110
|
-
|
|
111
|
-
|
|
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 |
|
|
112
133
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
117
139
|
|
|
118
140
|
## Limits
|
|
119
141
|
|
|
120
|
-
It checks well-known locations
|
|
121
|
-
|
|
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.
|
|
122
146
|
|
|
123
|
-
##
|
|
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
|
+
```
|
|
124
153
|
|
|
125
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
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 } from '../src/index.js';
|
|
4
|
+
import { scan, 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
|
|
|
@@ -10,10 +10,17 @@ on this machine or CI runner, ranks it, and writes an ordered rotation runbook.
|
|
|
10
10
|
Read-only. Never prints secret values.
|
|
11
11
|
|
|
12
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
|
|
13
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)
|
|
14
20
|
--runbook [file] Print (or write to file) a Markdown rotation runbook
|
|
15
21
|
--fail-on <level> Exit 1 if any finding is at/above level: critical|high|medium|low|none (default: none)
|
|
16
|
-
--only <list> Comma list of scanners: persistence,npm,github,cloud,kubernetes,env,ai,ssh,dotenv,
|
|
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
|
|
17
24
|
--skip <list> Comma list of scanners to skip
|
|
18
25
|
--no-env Do not inspect environment variables
|
|
19
26
|
--depth <n> How deep to look for .env files in project dirs (default 4)
|
|
@@ -28,7 +35,8 @@ try {
|
|
|
28
35
|
args = parseArgs({
|
|
29
36
|
allowPositionals: true,
|
|
30
37
|
options: {
|
|
31
|
-
json: { type: 'boolean' },
|
|
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' },
|
|
32
40
|
'fail-on': { type: 'string', default: 'none' }, only: { type: 'string' }, skip: { type: 'string' },
|
|
33
41
|
'no-env': { type: 'boolean' }, depth: { type: 'string' }, home: { type: 'string' },
|
|
34
42
|
'no-color': { type: 'boolean' }, version: { type: 'boolean', short: 'v' }, help: { type: 'boolean', short: 'h' },
|
|
@@ -49,20 +57,31 @@ let runbookFile = o['runbook-file'];
|
|
|
49
57
|
let projects = positionals;
|
|
50
58
|
if (o.runbook && !runbookFile && positionals.length && /\.md$/i.test(positionals[0])) { runbookFile = positionals[0]; projects = positionals.slice(1); }
|
|
51
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
|
+
}
|
|
52
64
|
const list = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined);
|
|
53
65
|
const skip = list(o.skip) ?? [];
|
|
54
66
|
if (o['no-env']) skip.push('env');
|
|
55
67
|
|
|
56
68
|
let result;
|
|
57
69
|
try {
|
|
58
|
-
result = scan({ projects, home: o.home, only: list(o.only), skip, depth: o.depth ? Number(o.depth) : undefined });
|
|
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 });
|
|
59
71
|
failsAt(result, o['fail-on']);
|
|
60
72
|
} catch (e) {
|
|
61
73
|
console.error(`secret-blast-radius: ${e.message}`);
|
|
62
74
|
process.exit(2);
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
|
|
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) {
|
|
66
85
|
process.stdout.write(JSON.stringify({ ...result, runbook: runbook(result).map(({ items, ...r }) => ({ ...r, findings: items.map((i) => i.id) })) }, null, 2) + '\n');
|
|
67
86
|
} else if (o.runbook) {
|
|
68
87
|
const md = formatMarkdown(result);
|
package/package.json
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "secret-blast-radius",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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
5
|
"license": "MIT",
|
|
6
6
|
"author": "Krrish Kumar (https://github.com/Krrishkumar125)",
|
|
7
|
-
"repository": {
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Krrishkumar125/secret-blast-radius.git"
|
|
10
|
+
},
|
|
8
11
|
"homepage": "https://github.com/Krrishkumar125/secret-blast-radius#readme",
|
|
9
12
|
"bugs": "https://github.com/Krrishkumar125/secret-blast-radius/issues",
|
|
10
|
-
"keywords": [
|
|
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
|
+
],
|
|
11
31
|
"type": "module",
|
|
12
|
-
"bin": {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
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
|
+
},
|
|
16
48
|
"scripts": {
|
|
17
|
-
"test": "node --test test/scanners.test.js test/cli.test.js",
|
|
49
|
+
"test": "node --test test/scanners.test.js test/supply.test.js test/cli.test.js",
|
|
18
50
|
"check": "node --check bin/cli.js && npm test"
|
|
19
51
|
}
|
|
20
52
|
}
|
package/src/format.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runbook } from './index.js';
|
|
2
|
+
import { INCIDENTS } from './incidents.js';
|
|
2
3
|
|
|
3
4
|
const ICON = { critical: '✖', high: '▲', medium: '●', low: '·' };
|
|
4
5
|
|
|
@@ -9,8 +10,14 @@ export function formatText(result, { color = false } = {}) {
|
|
|
9
10
|
const { counts } = result;
|
|
10
11
|
lines.push(c('1', 'secret-blast-radius') + ' — what a malicious install script could steal from this machine right now');
|
|
11
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
|
+
}
|
|
12
19
|
if (!result.findings.length) {
|
|
13
|
-
lines.push('No reachable secrets found in the places checked. Nice.');
|
|
20
|
+
lines.push(result.incident ? 'Nothing relevant found by these checks.' : 'No reachable secrets found in the places checked. Nice.');
|
|
14
21
|
return lines.join('\n');
|
|
15
22
|
}
|
|
16
23
|
for (const f of result.findings) {
|
|
@@ -28,6 +35,10 @@ export function formatText(result, { color = false } = {}) {
|
|
|
28
35
|
|
|
29
36
|
export function formatMarkdown(result) {
|
|
30
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
|
+
}
|
|
31
42
|
const rb = runbook(result);
|
|
32
43
|
if (!rb.length) return out.concat('Nothing to rotate: no reachable secrets were found.').join('\n');
|
|
33
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.', '');
|
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)' };
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { SCANNERS } from './scanners.js';
|
|
5
|
+
import { SUPPLY_SCANNERS, loadIocs } from './supply.js';
|
|
6
|
+
import { INCIDENTS } from './incidents.js';
|
|
7
|
+
|
|
8
|
+
export const ALL_SCANNERS = { ...SCANNERS, ...SUPPLY_SCANNERS };
|
|
9
|
+
|
|
10
|
+
function defaultExec(cmd, args) {
|
|
11
|
+
try {
|
|
12
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 4000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
13
|
+
return r.status === 0 ? r.stdout : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
|
|
5
19
|
export const SEVERITY = ['critical', 'high', 'medium', 'low'];
|
|
6
20
|
|
|
@@ -50,6 +64,21 @@ export const ROTATION = {
|
|
|
50
64
|
steps: ['Generate a new key (`ssh-keygen -t ed25519`) with a passphrase.', 'Replace the public key on GitHub/GitLab and in every server\'s authorized_keys; remove the old one.'],
|
|
51
65
|
url: 'https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent',
|
|
52
66
|
},
|
|
67
|
+
paas: {
|
|
68
|
+
order: 5.5, label: 'Deploy platforms (Railway, Vercel, Fly, Render, Heroku, Netlify)',
|
|
69
|
+
steps: ['Rotate every secret stored in the platform\'s env store at its provider (DB, Stripe, AI keys), then update the variable.', 'Redeploy: services read env vars at start-up.', 'Log the CLI out and back in, and revoke old platform tokens.'],
|
|
70
|
+
url: 'https://blog.railway.com/p/security-disclosure-wednesday-october-21st',
|
|
71
|
+
},
|
|
72
|
+
php: {
|
|
73
|
+
order: 9, label: 'Patch PHP (PHP2Shell)',
|
|
74
|
+
steps: ['Update to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 or later; PHP ≤ 8.1 must be upgraded.', 'Rebuild images with --pull --no-cache and restart all PHP workers.'],
|
|
75
|
+
url: 'https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching',
|
|
76
|
+
},
|
|
77
|
+
certs: {
|
|
78
|
+
order: 10, label: 'Replace certificates under emSign ECC Root CA - G3',
|
|
79
|
+
steps: ['Issue a replacement certificate from another CA before Oct 27, 2026.', 'Remove the root from custom trust bundles.'],
|
|
80
|
+
url: 'https://blog.google/security/protecting-chrome-and-android-users-from-compromised-emudhra-intermediate-ca-certificates/',
|
|
81
|
+
},
|
|
53
82
|
dotenv: {
|
|
54
83
|
order: 8, label: 'Application secrets in .env files',
|
|
55
84
|
steps: ['Rotate database passwords, payment keys and webhook secrets at each provider.', 'Redeploy: most platforms only read env vars at process start.'],
|
|
@@ -63,17 +92,27 @@ export function scan(options = {}) {
|
|
|
63
92
|
projects: (options.projects?.length ? options.projects : [process.cwd()]).map((p) => resolve(p)),
|
|
64
93
|
env: options.env ?? process.env,
|
|
65
94
|
depth: options.depth ?? 4,
|
|
95
|
+
iocs: options.iocs ?? loadIocs(options.iocFiles ?? []),
|
|
96
|
+
exec: options.exec === false ? null : options.exec ?? defaultExec,
|
|
97
|
+
checkTmp: options.checkTmp ?? options.home === undefined,
|
|
66
98
|
};
|
|
67
|
-
|
|
99
|
+
let onlyList = options.only;
|
|
100
|
+
if (options.incident) {
|
|
101
|
+
const inc = INCIDENTS[options.incident];
|
|
102
|
+
if (!inc) throw new Error(`Unknown incident "${options.incident}". Use one of: ${Object.keys(INCIDENTS).join(', ')}`);
|
|
103
|
+
onlyList = onlyList ? onlyList.filter((s) => inc.scanners.includes(s)) : inc.scanners;
|
|
104
|
+
}
|
|
105
|
+
for (const s of [...(onlyList ?? []), ...(options.skip ?? [])]) if (!ALL_SCANNERS[s]) throw new Error(`Unknown scanner "${s}". Use: ${Object.keys(ALL_SCANNERS).join(', ')}`);
|
|
106
|
+
const only = onlyList ? new Set(onlyList) : null;
|
|
68
107
|
const skip = new Set(options.skip ?? []);
|
|
69
108
|
const findings = [];
|
|
70
|
-
for (const [name, fn] of Object.entries(
|
|
109
|
+
for (const [name, fn] of Object.entries(ALL_SCANNERS)) {
|
|
71
110
|
if ((only && !only.has(name)) || skip.has(name)) continue;
|
|
72
111
|
findings.push(...fn(ctx));
|
|
73
112
|
}
|
|
74
113
|
findings.sort((a, b) => SEVERITY.indexOf(a.severity) - SEVERITY.indexOf(b.severity) || (ROTATION[a.category]?.order ?? 99) - (ROTATION[b.category]?.order ?? 99));
|
|
75
114
|
const counts = Object.fromEntries(SEVERITY.map((s) => [s, findings.filter((f) => f.severity === s).length]));
|
|
76
|
-
return { scannedAt: new Date().toISOString(), home: ctx.home, projects: ctx.projects, counts, findings };
|
|
115
|
+
return { scannedAt: new Date().toISOString(), home: ctx.home, projects: ctx.projects, iocsUpdated: ctx.iocs.updated, incident: options.incident, counts, findings };
|
|
77
116
|
}
|
|
78
117
|
|
|
79
118
|
export function runbook(result) {
|
|
@@ -89,3 +128,6 @@ export function failsAt(result, level) {
|
|
|
89
128
|
}
|
|
90
129
|
|
|
91
130
|
export { formatText, formatMarkdown } from './format.js';
|
|
131
|
+
export { toSarif } from './sarif.js';
|
|
132
|
+
export { INCIDENTS } from './incidents.js';
|
|
133
|
+
export { loadIocs, parseLockfile, phpStatus } from './supply.js';
|