@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4
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/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# PLAYBOOK — Remediating npm Findings
|
|
2
|
+
|
|
3
|
+
## Decision flow
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm audit finding
|
|
7
|
+
|
|
|
8
|
+
v
|
|
9
|
+
+-------------------+
|
|
10
|
+
| DIRECT or |
|
|
11
|
+
| TRANSITIVE? |
|
|
12
|
+
+---+-----------+---+
|
|
13
|
+
| |
|
|
14
|
+
DIRECT | | TRANSITIVE
|
|
15
|
+
v v
|
|
16
|
+
+----------+ +-----------------+
|
|
17
|
+
| semver- | | parent has new |
|
|
18
|
+
| minor | | version with |
|
|
19
|
+
| fix? | | floor above fix?|
|
|
20
|
+
+-+----+---+ +--+----------+---+
|
|
21
|
+
| | | |
|
|
22
|
+
YES NO YES NO
|
|
23
|
+
| | | |
|
|
24
|
+
v v v v
|
|
25
|
+
audit manual bump overrides
|
|
26
|
+
fix review parent block
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Per-runtime remediation patterns
|
|
30
|
+
|
|
31
|
+
### Frontend bundler (webpack / vite / next.js)
|
|
32
|
+
|
|
33
|
+
CVEs in build-time-only deps (loaders, transformers, dev plugins)
|
|
34
|
+
are lower priority than CVEs in runtime deps that ship in the
|
|
35
|
+
bundle.
|
|
36
|
+
|
|
37
|
+
- Build-time only: file ticket, fix in next sprint.
|
|
38
|
+
- Runtime in bundle: block release.
|
|
39
|
+
- Use `npm audit --omit=dev` to focus on runtime; this skill's
|
|
40
|
+
`--include-dev` flag toggles the inverse.
|
|
41
|
+
|
|
42
|
+
### Node server (Express, Fastify, Hapi)
|
|
43
|
+
|
|
44
|
+
Every advisory affects a long-running process. Treat all of them
|
|
45
|
+
as runtime, including those marked dev-only — `eslint` etc. usually
|
|
46
|
+
DON'T ship to production, but if they're in your Docker image at
|
|
47
|
+
runtime, they do.
|
|
48
|
+
|
|
49
|
+
Inspect your `Dockerfile`. If it copies `node_modules` whole, every
|
|
50
|
+
dep ships. If it uses `npm ci --omit=dev`, devDeps are pruned.
|
|
51
|
+
|
|
52
|
+
### Electron desktop app
|
|
53
|
+
|
|
54
|
+
Electron apps bundle Chromium + Node into a binary distributed to
|
|
55
|
+
end-users. A vulnerable dep ships to every user. Patches require a
|
|
56
|
+
full app release; you cannot ship a runtime hotpatch. CRITICAL
|
|
57
|
+
findings require an emergency release.
|
|
58
|
+
|
|
59
|
+
### AWS Lambda / serverless function
|
|
60
|
+
|
|
61
|
+
Each function invocation loads the dependency tree. The blast radius
|
|
62
|
+
of a compromised dep is whatever the Lambda's IAM role grants. Audit
|
|
63
|
+
the IAM role alongside the dep audit — a compromised `request` package
|
|
64
|
+
in a Lambda with `s3:*` permissions is an exfiltration vector to
|
|
65
|
+
every object in your S3 buckets.
|
|
66
|
+
|
|
67
|
+
### Monorepo (Nx / Turborepo / pnpm workspaces)
|
|
68
|
+
|
|
69
|
+
Audit each workspace separately. `npm audit --workspaces` exists in
|
|
70
|
+
npm 7+ but produces output keyed by workspace and is awkward to
|
|
71
|
+
parse. This skill currently scans one package.json at a time; for
|
|
72
|
+
monorepos, iterate over the workspace globs in your top-level
|
|
73
|
+
`package.json` and run the scanner per package.
|
|
74
|
+
|
|
75
|
+
## Override-block templates
|
|
76
|
+
|
|
77
|
+
### Single-package override
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"overrides": {
|
|
82
|
+
"minimist": "^1.2.6"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use when one transitive package needs a floor and the rest of the
|
|
88
|
+
parent's deps are fine.
|
|
89
|
+
|
|
90
|
+
### Nested override (specific parent only)
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"overrides": {
|
|
95
|
+
"express": {
|
|
96
|
+
"qs": "^6.10.3"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Force `qs@^6.10.3` only when pulled in through `express` — leave
|
|
103
|
+
other consumers of `qs` untouched. Use when an override breaks an
|
|
104
|
+
unrelated package and you need surgical control.
|
|
105
|
+
|
|
106
|
+
### Wildcard override (rare)
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"overrides": {
|
|
111
|
+
"lodash": "$lodash"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`$lodash` resolves to the version declared in your `dependencies`.
|
|
117
|
+
Use to enforce that every transitive use of `lodash` matches your
|
|
118
|
+
declared version. Reserve for cases where transitive duplication is
|
|
119
|
+
causing real bundle-size or behavior problems.
|
|
120
|
+
|
|
121
|
+
## Provider rotation procedures
|
|
122
|
+
|
|
123
|
+
Vulnerabilities sometimes overlap credential leaks: a CVE in a
|
|
124
|
+
package that handles auth tokens may incidentally expose tokens.
|
|
125
|
+
After fixing the dep, check whether the vulnerable version logged
|
|
126
|
+
or transmitted secrets in a way that requires rotation.
|
|
127
|
+
|
|
128
|
+
| Provider | Rotation surface |
|
|
129
|
+
|---|---|
|
|
130
|
+
| GitHub | Settings → Developer settings → Personal access tokens → Regenerate |
|
|
131
|
+
| npm | `npm token revoke <id>` + `npm token create` |
|
|
132
|
+
| AWS | IAM → Access keys → Deactivate → Delete → New access key |
|
|
133
|
+
| Stripe | Dashboard → Developers → API keys → Roll key |
|
|
134
|
+
| Sentry | Settings → Auth Tokens → Revoke + Create |
|
|
135
|
+
|
|
136
|
+
## GitHub Dependabot integration
|
|
137
|
+
|
|
138
|
+
Dependabot opens automatic PRs when GitHub's vulnerability database
|
|
139
|
+
flags a finding. This skill is complementary, not redundant:
|
|
140
|
+
|
|
141
|
+
- Dependabot is the long-running watcher.
|
|
142
|
+
- This skill is the PR-time gate that ensures Dependabot's findings
|
|
143
|
+
haven't accumulated unreviewed.
|
|
144
|
+
|
|
145
|
+
In CI:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
- name: npm audit
|
|
149
|
+
run: |
|
|
150
|
+
python3 plugins/security/penetration-tester/skills/auditing-npm-dependencies/scripts/audit_npm.py . \
|
|
151
|
+
--min-severity high --format markdown --output npm-audit.md
|
|
152
|
+
- name: Comment on PR
|
|
153
|
+
if: github.event_name == 'pull_request'
|
|
154
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
155
|
+
with:
|
|
156
|
+
path: npm-audit.md
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## SOC2 evidence retention
|
|
160
|
+
|
|
161
|
+
For Trust Service Category CC7 (System Operations) and CC8 (Change
|
|
162
|
+
Management), retain npm audit output as evidence that dependency
|
|
163
|
+
vulnerabilities are tracked:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
mkdir -p evidence/CC7/
|
|
167
|
+
python3 ./scripts/audit_npm.py . --include-dev --no-cache \
|
|
168
|
+
--format json \
|
|
169
|
+
--output evidence/CC7/npm-audit-$(date +%Y%m%d).json
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Keep at least one audit per quarter. Auditors will ask for evidence
|
|
173
|
+
that you ran the audit, that findings were triaged, and that
|
|
174
|
+
remediation timing matched your published vulnerability-response
|
|
175
|
+
policy.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# THEORY — Why npm Dependency Audits Matter
|
|
2
|
+
|
|
3
|
+
## The shape of the problem
|
|
4
|
+
|
|
5
|
+
A modern Node application is mostly other people's code. A typical
|
|
6
|
+
React + Next.js app installs ~30 direct dependencies and ends up
|
|
7
|
+
with ~1,500 packages in `node_modules` after npm resolves the
|
|
8
|
+
transitive closure. Every one of those packages can:
|
|
9
|
+
|
|
10
|
+
1. Ship a known CVE that gets disclosed after you installed.
|
|
11
|
+
2. Be hijacked through maintainer-account takeover (the npm registry
|
|
12
|
+
has had several published cases since 2017).
|
|
13
|
+
3. Be replaced by a typosquatted near-name package that someone
|
|
14
|
+
slipped into a lockfile during a careless `npm install`.
|
|
15
|
+
|
|
16
|
+
Because npm packages execute arbitrary JavaScript on install
|
|
17
|
+
(`postinstall` scripts), a single compromised package in your
|
|
18
|
+
transitive tree can read environment variables, exfiltrate secrets,
|
|
19
|
+
and pivot to other systems with no user interaction beyond the
|
|
20
|
+
install.
|
|
21
|
+
|
|
22
|
+
## Historical npm supply-chain compromises
|
|
23
|
+
|
|
24
|
+
These are the cases that drove the industry to treat dependency
|
|
25
|
+
auditing as a CI gate, not optional polish.
|
|
26
|
+
|
|
27
|
+
| Year | Event | Mechanism |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| 2018 | `event-stream` | Maintainer transferred to a new account that injected wallet-stealing code into a transitive dep (`flatmap-stream`). Affected Bitcoin wallet users. |
|
|
30
|
+
| 2021 | `ua-parser-js` | Maintainer account compromise. Crypto miner shipped to ~8M weekly downloads. |
|
|
31
|
+
| 2022 | `node-ipc` | Maintainer-installed protestware that targeted users in specific geographies. |
|
|
32
|
+
| 2022 | `colors.js` / `faker.js` | Maintainer self-sabotage; broke production builds globally for the affected versions. |
|
|
33
|
+
| 2024 | `lottie-player` | Cloudflare CDN compromise distributed wallet-drainer code. |
|
|
34
|
+
|
|
35
|
+
Each case ended with the same remediation: roll back to a known-good
|
|
36
|
+
version range, audit logs for exploitation evidence, publish an
|
|
37
|
+
incident postmortem. The presence of an audit gate would not have
|
|
38
|
+
prevented the original install (the malicious code was new), but
|
|
39
|
+
would have surfaced the compromise within hours of disclosure.
|
|
40
|
+
|
|
41
|
+
## Direct vs transitive: the remediation diff
|
|
42
|
+
|
|
43
|
+
A CVE in a direct dependency you require in `package.json` is
|
|
44
|
+
fixable by upgrading the version range. A CVE in a transitive
|
|
45
|
+
dependency requires either (a) a parent bump that pulls in the fix
|
|
46
|
+
or (b) an `overrides` block in your root `package.json`.
|
|
47
|
+
|
|
48
|
+
The `overrides` mechanism was introduced in npm 8.3 (December 2021).
|
|
49
|
+
Before that, only Yarn's `resolutions` field offered the equivalent
|
|
50
|
+
escape hatch. `overrides` lets you force the resolved version of a
|
|
51
|
+
package regardless of what the dependency graph requested:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"overrides": {
|
|
56
|
+
"minimist": "^1.2.6"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This is safe when the override pins to a SemVer-compatible version
|
|
62
|
+
of the package the parent expected. It is RISKY when the override
|
|
63
|
+
crosses a major-version boundary — the parent may rely on removed
|
|
64
|
+
APIs and break at runtime. The audit script tags transitive findings
|
|
65
|
+
with a `relationship: transitive` evidence field to flag them for
|
|
66
|
+
the operator's manual review.
|
|
67
|
+
|
|
68
|
+
## npm audit output schema diff (v1 vs v2)
|
|
69
|
+
|
|
70
|
+
npm 6 emitted findings under an `advisories` key, one per advisory.
|
|
71
|
+
npm 7+ rewrote the output to a `vulnerabilities` key, keyed by
|
|
72
|
+
package name, with the per-package record summarizing all advisories
|
|
73
|
+
affecting that package.
|
|
74
|
+
|
|
75
|
+
| Aspect | v1 (npm 6) | v2 (npm 7+) |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Top-level key | `advisories` | `vulnerabilities` |
|
|
78
|
+
| Keyed by | Advisory ID | Package name |
|
|
79
|
+
| Direct vs transitive | implicit | explicit via `via` field |
|
|
80
|
+
| Fix metadata | `patched_versions` string | `fixAvailable` object or boolean |
|
|
81
|
+
| CVE field | `cves` array | per-advisory `cve` field in `via[]` |
|
|
82
|
+
|
|
83
|
+
This skill's parser handles both shapes. Practically, most modern
|
|
84
|
+
projects run npm 8+; the v1 parser is kept for legacy CI runners
|
|
85
|
+
and engineering laptops that haven't upgraded.
|
|
86
|
+
|
|
87
|
+
## Why severity normalization matters
|
|
88
|
+
|
|
89
|
+
Different tools use different severity vocabularies. npm uses
|
|
90
|
+
`info/low/moderate/high/critical`. CVSS uses numeric scores.
|
|
91
|
+
GitHub uses 4 levels. PyPA uses something else again. The
|
|
92
|
+
penetration-tester `Severity` enum is the canonical mapping target
|
|
93
|
+
so downstream consumers (SOC2 evidence packages, security
|
|
94
|
+
dashboards, executive summary reports) see one vocabulary across
|
|
95
|
+
every tool. The `Severity.from_npm_audit` classmethod in
|
|
96
|
+
`lib/finding.py` does the npm-specific mapping.
|
|
97
|
+
|
|
98
|
+
## When `npm audit fix` is dangerous
|
|
99
|
+
|
|
100
|
+
`npm audit fix` rewrites `package-lock.json` to the closest
|
|
101
|
+
non-breaking versions that resolve advisories. Two failure modes:
|
|
102
|
+
|
|
103
|
+
1. **Semver-major fix required.** npm refuses to auto-apply and
|
|
104
|
+
warns "requires manual review." This is correct behavior; do not
|
|
105
|
+
pass `--force` casually. A major version bump can break the
|
|
106
|
+
parent's API contract.
|
|
107
|
+
2. **Lockfile churn on shared CI runners.** If the lockfile rewrite
|
|
108
|
+
happens in a CI step that doesn't commit back, the next CI run
|
|
109
|
+
sees the same vulnerabilities and re-applies the fix, producing
|
|
110
|
+
a stable but undocumented divergence between developer workstations
|
|
111
|
+
and CI. Either commit the fix in CI (with appropriate guards) or
|
|
112
|
+
require the fix to land via human PR.
|
|
113
|
+
|
|
114
|
+
## When to use the CISA KEV list
|
|
115
|
+
|
|
116
|
+
NIST's NVD scoring (CVSS) reflects intrinsic severity. CISA's Known
|
|
117
|
+
Exploited Vulnerabilities (KEV) catalog reflects observed exploitation
|
|
118
|
+
in the wild. A high-CVSS finding with no KEV listing is concerning;
|
|
119
|
+
a medium-CVSS finding WITH KEV listing is more concerning because
|
|
120
|
+
attackers are actively using it. The skill currently emits CVSS-derived
|
|
121
|
+
severity only; KEV enrichment is a planned addition via the
|
|
122
|
+
`mcp__pen-tester-cve` MCP server when wired.
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""auditing-npm-dependencies — wrap `npm audit --json` into canonical Findings.
|
|
3
|
+
|
|
4
|
+
Walks a Node.js project, runs `npm audit --json` in the target directory, parses
|
|
5
|
+
both the v1 (npm 6) and v2 (npm 7+) audit output shapes, and emits Findings via
|
|
6
|
+
lib/finding.py. Output formats (json/jsonl/markdown) and exit-code semantics are
|
|
7
|
+
shared with the rest of the penetration-tester v3 pack via lib/report.py.
|
|
8
|
+
|
|
9
|
+
The scanner deduplicates per-CVE across direct + transitive dependency paths,
|
|
10
|
+
classifies each finding as direct vs transitive (impacts remediation strategy),
|
|
11
|
+
and maps npm's severity vocabulary (info/low/moderate/high/critical) onto the
|
|
12
|
+
shared Severity enum.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python3 audit_npm.py PATH [--output FILE] [--format json|jsonl|markdown]
|
|
16
|
+
[--min-severity sev] [--include-dev] [--no-cache]
|
|
17
|
+
[--json-only]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
# --- Make lib/ importable regardless of CWD ----------------------------------
|
|
31
|
+
_LIB_ROOT = Path(__file__).resolve().parents[3]
|
|
32
|
+
sys.path.insert(0, str(_LIB_ROOT))
|
|
33
|
+
|
|
34
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
35
|
+
from lib import report # noqa: E402
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
SKILL_ID = "auditing-npm-dependencies"
|
|
39
|
+
CATEGORY = "dependency-vulnerability"
|
|
40
|
+
CWE_DEFAULT = "CWE-1104" # Use of unmaintained / vulnerable third-party component
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --- npm invocation ----------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _npm_present() -> bool:
|
|
47
|
+
"""Probe for an npm binary on PATH."""
|
|
48
|
+
return shutil.which("npm") is not None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _project_is_node(directory: Path) -> bool:
|
|
52
|
+
"""A directory is a node project if it has package.json at the root."""
|
|
53
|
+
return (directory / "package.json").exists()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_npm_audit(directory: Path, include_dev: bool, no_cache: bool) -> tuple[dict[str, Any] | None, str]:
|
|
57
|
+
"""Run `npm audit --json` in directory, return (parsed_json_or_None, raw_stdout).
|
|
58
|
+
|
|
59
|
+
Returns parsed dict on success, None when audit failed or output was not JSON.
|
|
60
|
+
The raw stdout is always returned so the caller can attach it to an INFO
|
|
61
|
+
Finding for debugging.
|
|
62
|
+
"""
|
|
63
|
+
cmd: list[str] = ["npm", "audit", "--json"]
|
|
64
|
+
if not include_dev:
|
|
65
|
+
cmd.append("--omit=dev")
|
|
66
|
+
if no_cache:
|
|
67
|
+
# npm 8+ honors --no-audit; older npm ignored. Combined with cache flag.
|
|
68
|
+
cmd.append("--no-fund")
|
|
69
|
+
try:
|
|
70
|
+
proc = subprocess.run( # noqa: S603 — npm is the audit tool
|
|
71
|
+
cmd,
|
|
72
|
+
cwd=str(directory),
|
|
73
|
+
capture_output=True,
|
|
74
|
+
text=True,
|
|
75
|
+
timeout=180,
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
except subprocess.TimeoutExpired:
|
|
79
|
+
return None, "npm audit timed out after 180s"
|
|
80
|
+
except FileNotFoundError:
|
|
81
|
+
return None, "npm binary not found"
|
|
82
|
+
|
|
83
|
+
stdout = proc.stdout or ""
|
|
84
|
+
# npm audit exits non-zero when vulns are found — that's expected; we
|
|
85
|
+
# parse the JSON regardless.
|
|
86
|
+
try:
|
|
87
|
+
data = json.loads(stdout)
|
|
88
|
+
except json.JSONDecodeError:
|
|
89
|
+
return None, stdout
|
|
90
|
+
return data, stdout
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- npm v2 schema parser (npm 7+) -------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parse_v2(data: dict[str, Any], target_label: str) -> list[Finding]:
|
|
97
|
+
"""Parse npm 7+ audit output. Schema is `vulnerabilities.<pkg> → record`.
|
|
98
|
+
|
|
99
|
+
Direct vs transitive distinction comes from `record.via`: if `via` contains
|
|
100
|
+
string entries (other package names), this is a transitive vulnerability
|
|
101
|
+
surfaced through those parents. If `via` contains dict entries, the package
|
|
102
|
+
itself is the source (direct vuln in the named package).
|
|
103
|
+
"""
|
|
104
|
+
findings: list[Finding] = []
|
|
105
|
+
vulns: dict[str, Any] = data.get("vulnerabilities", {}) or {}
|
|
106
|
+
|
|
107
|
+
for pkg_name, record in vulns.items():
|
|
108
|
+
severity_str = str(record.get("severity", "info"))
|
|
109
|
+
severity = Severity.from_npm_audit(severity_str)
|
|
110
|
+
|
|
111
|
+
via = record.get("via", [])
|
|
112
|
+
is_direct = any(isinstance(item, dict) for item in via)
|
|
113
|
+
source_label = "direct" if is_direct else "transitive"
|
|
114
|
+
|
|
115
|
+
# Extract CVE / GHSA from any dict entries in `via`.
|
|
116
|
+
cve_id: str | None = None
|
|
117
|
+
ghsa_id: str | None = None
|
|
118
|
+
title_summary: str | None = None
|
|
119
|
+
urls: list[str] = []
|
|
120
|
+
for item in via:
|
|
121
|
+
if not isinstance(item, dict):
|
|
122
|
+
continue
|
|
123
|
+
raw_source = item.get("source")
|
|
124
|
+
# npm audit v2 sometimes emits `source` as a list, sometimes as a
|
|
125
|
+
# scalar (string advisory ID or integer advisory ID). Normalize.
|
|
126
|
+
source_list: list[Any] = []
|
|
127
|
+
if isinstance(raw_source, list):
|
|
128
|
+
source_list = raw_source
|
|
129
|
+
elif raw_source is not None:
|
|
130
|
+
source_list = [raw_source]
|
|
131
|
+
for src in source_list:
|
|
132
|
+
if isinstance(src, str) and src.upper().startswith("CVE-"):
|
|
133
|
+
cve_id = cve_id or src
|
|
134
|
+
if isinstance(src, str) and src.upper().startswith("GHSA-"):
|
|
135
|
+
ghsa_id = ghsa_id or src
|
|
136
|
+
cve_id = cve_id or item.get("cve") or None
|
|
137
|
+
# When source is itself a GHSA string, surface it as ghsa_id.
|
|
138
|
+
source_str = raw_source if isinstance(raw_source, str) else None
|
|
139
|
+
ghsa_id = ghsa_id or item.get("ghsa") or source_str or None
|
|
140
|
+
title_summary = title_summary or item.get("title")
|
|
141
|
+
url = item.get("url")
|
|
142
|
+
if url:
|
|
143
|
+
urls.append(url)
|
|
144
|
+
|
|
145
|
+
affected_range = str(record.get("range", "unknown"))
|
|
146
|
+
fix_available = record.get("fixAvailable", False)
|
|
147
|
+
if isinstance(fix_available, dict):
|
|
148
|
+
fix_version_str = f"{fix_available.get('name', pkg_name)}@{fix_available.get('version', '?')}"
|
|
149
|
+
elif fix_available is True:
|
|
150
|
+
fix_version_str = "non-breaking upgrade available (npm audit fix)"
|
|
151
|
+
else:
|
|
152
|
+
fix_version_str = "no fix available"
|
|
153
|
+
|
|
154
|
+
title = title_summary or f"npm vulnerability in {pkg_name} ({severity_str})"
|
|
155
|
+
|
|
156
|
+
detail_lines = [
|
|
157
|
+
f"Affected package: {pkg_name}",
|
|
158
|
+
f"Affected range: {affected_range}",
|
|
159
|
+
f"Dependency relationship: {source_label}",
|
|
160
|
+
f"npm severity: {severity_str}",
|
|
161
|
+
]
|
|
162
|
+
if cve_id:
|
|
163
|
+
detail_lines.append(f"CVE: {cve_id}")
|
|
164
|
+
if ghsa_id:
|
|
165
|
+
detail_lines.append(f"GHSA: {ghsa_id}")
|
|
166
|
+
|
|
167
|
+
if is_direct:
|
|
168
|
+
remediation = (
|
|
169
|
+
f"1. Run `npm audit fix` in {target_label}.\n"
|
|
170
|
+
f"2. If fix requires a semver-major bump, evaluate breaking changes "
|
|
171
|
+
f"and decide whether to upgrade.\n"
|
|
172
|
+
f"3. Commit the updated package-lock.json."
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
remediation = (
|
|
176
|
+
f"1. Run `npm ls {pkg_name}` to identify which parent(s) pull in "
|
|
177
|
+
f"the vulnerable version.\n"
|
|
178
|
+
f"2. Check whether upgrading the parent picks up the fix.\n"
|
|
179
|
+
f"3. If not, add a root-level `overrides` block for {pkg_name} "
|
|
180
|
+
f"pinning to the fix version (requires npm 8.3+)."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if fix_version_str.startswith("no fix"):
|
|
184
|
+
remediation += (
|
|
185
|
+
"\n\nNO FIX AVAILABLE — subscribe to GHSA notifications and "
|
|
186
|
+
"consider vendoring + patching or replacing the package."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
evidence_items: list[tuple[str, Any]] = [
|
|
190
|
+
("package", pkg_name),
|
|
191
|
+
("range", affected_range),
|
|
192
|
+
("relationship", source_label),
|
|
193
|
+
("fix", fix_version_str),
|
|
194
|
+
]
|
|
195
|
+
if cve_id:
|
|
196
|
+
evidence_items.append(("cve", cve_id))
|
|
197
|
+
if ghsa_id:
|
|
198
|
+
evidence_items.append(("ghsa", ghsa_id))
|
|
199
|
+
|
|
200
|
+
# Bump severity to HIGH if no fix and originally moderate+ — operator
|
|
201
|
+
# has limited remediation surface.
|
|
202
|
+
if fix_version_str.startswith("no fix") and severity.numeric >= 3:
|
|
203
|
+
severity = max(severity, Severity.HIGH, key=lambda s: s.numeric)
|
|
204
|
+
|
|
205
|
+
findings.append(
|
|
206
|
+
Finding(
|
|
207
|
+
skill_id=SKILL_ID,
|
|
208
|
+
title=title,
|
|
209
|
+
severity=severity,
|
|
210
|
+
target=f"{target_label}::{pkg_name}",
|
|
211
|
+
detail="\n".join(detail_lines),
|
|
212
|
+
remediation=remediation,
|
|
213
|
+
cve_id=cve_id,
|
|
214
|
+
cwe_id=CWE_DEFAULT,
|
|
215
|
+
references=tuple(urls or []),
|
|
216
|
+
evidence=tuple(evidence_items),
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return findings
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# --- npm v1 schema parser (npm 6) --------------------------------------------
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _parse_v1(data: dict[str, Any], target_label: str) -> list[Finding]:
|
|
227
|
+
"""Parse npm 6 audit output. Schema is `advisories.<id> → record`."""
|
|
228
|
+
findings: list[Finding] = []
|
|
229
|
+
advisories: dict[str, Any] = data.get("advisories", {}) or {}
|
|
230
|
+
|
|
231
|
+
for adv_id, record in advisories.items():
|
|
232
|
+
severity_str = str(record.get("severity", "info"))
|
|
233
|
+
severity = Severity.from_npm_audit(severity_str)
|
|
234
|
+
pkg_name = record.get("module_name", "<unknown>")
|
|
235
|
+
affected_range = record.get("vulnerable_versions", "unknown")
|
|
236
|
+
patched_versions = record.get("patched_versions", "")
|
|
237
|
+
cves = record.get("cves") or []
|
|
238
|
+
cve_id = cves[0] if cves else None
|
|
239
|
+
title = record.get("title") or f"npm advisory {adv_id} in {pkg_name}"
|
|
240
|
+
url = record.get("url", "")
|
|
241
|
+
|
|
242
|
+
detail_lines = [
|
|
243
|
+
f"Affected package: {pkg_name}",
|
|
244
|
+
f"Affected versions: {affected_range}",
|
|
245
|
+
f"npm severity: {severity_str}",
|
|
246
|
+
f"Advisory ID: {adv_id}",
|
|
247
|
+
]
|
|
248
|
+
if cve_id:
|
|
249
|
+
detail_lines.append(f"CVE: {cve_id}")
|
|
250
|
+
|
|
251
|
+
remediation = (
|
|
252
|
+
f"1. Upgrade {pkg_name} to a version matching `{patched_versions}`.\n"
|
|
253
|
+
"2. Run `npm install` to refresh package-lock.json.\n"
|
|
254
|
+
"3. Re-run audit to confirm the finding is resolved."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
findings.append(
|
|
258
|
+
Finding(
|
|
259
|
+
skill_id=SKILL_ID,
|
|
260
|
+
title=title,
|
|
261
|
+
severity=severity,
|
|
262
|
+
target=f"{target_label}::{pkg_name}",
|
|
263
|
+
detail="\n".join(detail_lines),
|
|
264
|
+
remediation=remediation,
|
|
265
|
+
cve_id=cve_id,
|
|
266
|
+
cwe_id=CWE_DEFAULT,
|
|
267
|
+
references=(url,) if url else (),
|
|
268
|
+
evidence=(
|
|
269
|
+
("package", pkg_name),
|
|
270
|
+
("affected", affected_range),
|
|
271
|
+
("patched", patched_versions),
|
|
272
|
+
("advisory_id", adv_id),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return findings
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# --- Operational helpers -----------------------------------------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _info_finding(title: str, detail: str, target: str) -> Finding:
|
|
284
|
+
return Finding(
|
|
285
|
+
skill_id=SKILL_ID,
|
|
286
|
+
title=title,
|
|
287
|
+
severity=Severity.INFO,
|
|
288
|
+
target=target,
|
|
289
|
+
detail=detail,
|
|
290
|
+
remediation="Operational issue; no security action required.",
|
|
291
|
+
references=(),
|
|
292
|
+
evidence=(),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def audit_directory(directory: Path, include_dev: bool, no_cache: bool, json_only: bool) -> tuple[list[Finding], str]:
|
|
297
|
+
"""Run an audit, returning (findings, raw_stdout)."""
|
|
298
|
+
if not _npm_present():
|
|
299
|
+
return [
|
|
300
|
+
_info_finding(
|
|
301
|
+
"npm not installed",
|
|
302
|
+
"The npm binary was not found on PATH; cannot run audit.",
|
|
303
|
+
str(directory),
|
|
304
|
+
)
|
|
305
|
+
], ""
|
|
306
|
+
if not _project_is_node(directory):
|
|
307
|
+
return [
|
|
308
|
+
_info_finding(
|
|
309
|
+
"target is not a Node project",
|
|
310
|
+
f"No package.json at {directory}; skipping npm audit.",
|
|
311
|
+
str(directory),
|
|
312
|
+
)
|
|
313
|
+
], ""
|
|
314
|
+
|
|
315
|
+
data, raw = _run_npm_audit(directory, include_dev, no_cache)
|
|
316
|
+
if json_only:
|
|
317
|
+
sys.stdout.write(raw)
|
|
318
|
+
return [], raw
|
|
319
|
+
if data is None:
|
|
320
|
+
return [
|
|
321
|
+
_info_finding(
|
|
322
|
+
"npm audit returned non-JSON output",
|
|
323
|
+
f"Raw stdout (first 500 chars): {raw[:500]}",
|
|
324
|
+
str(directory),
|
|
325
|
+
)
|
|
326
|
+
], raw
|
|
327
|
+
|
|
328
|
+
target_label = directory.name or str(directory)
|
|
329
|
+
if "vulnerabilities" in data:
|
|
330
|
+
findings = _parse_v2(data, target_label)
|
|
331
|
+
elif "advisories" in data:
|
|
332
|
+
findings = _parse_v1(data, target_label)
|
|
333
|
+
else:
|
|
334
|
+
findings = []
|
|
335
|
+
|
|
336
|
+
if not findings:
|
|
337
|
+
findings = [
|
|
338
|
+
_info_finding(
|
|
339
|
+
"no npm vulnerabilities found",
|
|
340
|
+
"npm audit reported a clean dependency tree.",
|
|
341
|
+
str(directory),
|
|
342
|
+
)
|
|
343
|
+
]
|
|
344
|
+
return findings, raw
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# --- CLI ---------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
351
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
352
|
+
p.add_argument("path", help="Path to Node project root (contains package.json)")
|
|
353
|
+
p.add_argument("--output", default=None, help="Write findings to FILE (default: stdout)")
|
|
354
|
+
p.add_argument(
|
|
355
|
+
"--format",
|
|
356
|
+
default="markdown",
|
|
357
|
+
choices=["json", "jsonl", "markdown"],
|
|
358
|
+
help="Output format (default: markdown)",
|
|
359
|
+
)
|
|
360
|
+
p.add_argument(
|
|
361
|
+
"--min-severity",
|
|
362
|
+
default="info",
|
|
363
|
+
choices=["info", "low", "medium", "high", "critical"],
|
|
364
|
+
help="Filter out findings below this severity (default: info — emit all)",
|
|
365
|
+
)
|
|
366
|
+
p.add_argument(
|
|
367
|
+
"--include-dev",
|
|
368
|
+
action="store_true",
|
|
369
|
+
help="Audit devDependencies too (default: prod only)",
|
|
370
|
+
)
|
|
371
|
+
p.add_argument(
|
|
372
|
+
"--no-cache",
|
|
373
|
+
action="store_true",
|
|
374
|
+
help="Disable npm fund cache hint (slower; fresher data)",
|
|
375
|
+
)
|
|
376
|
+
p.add_argument(
|
|
377
|
+
"--json-only",
|
|
378
|
+
action="store_true",
|
|
379
|
+
help="Print raw npm audit --json and exit (debug)",
|
|
380
|
+
)
|
|
381
|
+
return p
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _filter_min_severity(findings: list[Finding], min_sev: str) -> list[Finding]:
|
|
385
|
+
floor = Severity(min_sev).numeric
|
|
386
|
+
return [f for f in findings if f.severity.numeric >= floor]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def main(argv: list[str] | None = None) -> int:
|
|
390
|
+
args = _build_arg_parser().parse_args(argv)
|
|
391
|
+
directory = Path(args.path).resolve()
|
|
392
|
+
|
|
393
|
+
findings, _raw = audit_directory(
|
|
394
|
+
directory,
|
|
395
|
+
include_dev=args.include_dev,
|
|
396
|
+
no_cache=args.no_cache,
|
|
397
|
+
json_only=args.json_only,
|
|
398
|
+
)
|
|
399
|
+
if args.json_only:
|
|
400
|
+
return 0
|
|
401
|
+
|
|
402
|
+
findings = _filter_min_severity(findings, args.min_severity)
|
|
403
|
+
report.emit(findings, args.output, args.format, scan_target=str(directory))
|
|
404
|
+
return report.exit_code(findings)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
sys.exit(main())
|