@blamejs/exceptd-skills 0.12.9 → 0.12.10
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/CHANGELOG.md +37 -0
- package/data/_indexes/_meta.json +7 -7
- package/data/_indexes/activity-feed.json +10 -10
- package/data/_indexes/catalog-summaries.json +4 -4
- package/data/_indexes/chains.json +79 -0
- package/data/_indexes/frequency.json +2 -0
- package/data/cve-catalog.json +351 -1
- package/data/cwe-catalog.json +34 -0
- package/data/playbooks/library-author.json +14 -0
- package/data/zeroday-lessons.json +223 -1
- package/lib/refresh-external.js +74 -14
- package/lib/source-osv.js +493 -0
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/data/cwe-catalog.json
CHANGED
|
@@ -1302,5 +1302,39 @@
|
|
|
1302
1302
|
"real_requirement": "Argon2id (memory-hard, RFC 9106) with tuned m/t/p; scrypt as fallback; bcrypt with work factor ≥ 12 acceptable for legacy. PBKDF2 only with iteration count ≥ 600,000 (NIST SP 800-63B 2022 update).",
|
|
1303
1303
|
"lag_notes": "SP 800-63B updated iteration guidance in 2022; many compliance attestations still cite the 2017 numbers. Argon2id is RFC-9106 (2021) but absent from FIPS-approved lists, creating policy friction in federal contexts.",
|
|
1304
1304
|
"last_verified": "2026-05-13"
|
|
1305
|
+
},
|
|
1306
|
+
"CWE-506": {
|
|
1307
|
+
"id": "CWE-506",
|
|
1308
|
+
"name": "Embedded Malicious Code",
|
|
1309
|
+
"abstraction": "Class",
|
|
1310
|
+
"category": "Supply Chain",
|
|
1311
|
+
"description": "The application contains code that appears to perform a legitimate function but actually contains a payload that performs an additional, attacker-controlled action — typically credential theft, persistence, or remote loader logic. The class covers package-registry malware (PyPI / npm / RubyGems / Cargo / Maven typosquats, compromised maintainer accounts, forged-release-via-CI vectors).",
|
|
1312
|
+
"top_25_rank_2024": null,
|
|
1313
|
+
"top_25_rank_2025": null,
|
|
1314
|
+
"view_memberships": ["CWE-1000"],
|
|
1315
|
+
"related_attack_patterns_capec": ["CAPEC-442", "CAPEC-446", "CAPEC-538"],
|
|
1316
|
+
"skills_referencing": ["library-author", "supply-chain-integrity"],
|
|
1317
|
+
"evidence_cves": ["CVE-2026-45321", "MAL-2026-3083"],
|
|
1318
|
+
"framework_controls_partially_addressing": ["NIST-800-53-SA-12", "NIST-800-218-PS.1", "ISO-27001-2022-A.8.30", "SLSA-Level-3"],
|
|
1319
|
+
"real_requirement": "Provenance attestation at install time (Sigstore, in-toto, SLSA L3+); registry-side malware scanning on every uploaded artifact; install-time .pth / postinstall / preinstall hook auditing; differential analysis between consecutive releases of the same package (added files, new network egress, new file reads); cooldown periods on new releases of high-download packages so registry scanners and community detection have time to fire before mass install.",
|
|
1320
|
+
"lag_notes": "SA-12 contemplates the traditional supply chain but does not require differential-analysis between adjacent releases. The elementary-data 0.23.3 attack (April 2026) added exactly one file (a `.pth` install-time payload) versus 0.23.2 — a difference any naive diff would catch but no registry-side scanner currently runs at upload time by default.",
|
|
1321
|
+
"last_verified": "2026-05-13"
|
|
1322
|
+
},
|
|
1323
|
+
"CWE-88": {
|
|
1324
|
+
"id": "CWE-88",
|
|
1325
|
+
"name": "Improper Neutralization of Argument Delimiters in a Command",
|
|
1326
|
+
"abstraction": "Base",
|
|
1327
|
+
"category": "Injection",
|
|
1328
|
+
"description": "The product constructs a string for a downstream command (typically by concatenating user input into a shell command line, then splitting on whitespace to argv) without escaping argument-delimiter characters. Distinguished from CWE-77 (Command Injection) by the narrower attack surface: the attacker cannot run arbitrary commands but CAN inject additional flags / arguments to a command the application already invokes, which is often sufficient to break the security model (redirect kubectl to attacker-control, change kubectl namespace, etc.).",
|
|
1329
|
+
"top_25_rank_2024": null,
|
|
1330
|
+
"top_25_rank_2025": null,
|
|
1331
|
+
"view_memberships": ["CWE-1000", "CWE-1003"],
|
|
1332
|
+
"related_attack_patterns_capec": ["CAPEC-460"],
|
|
1333
|
+
"skills_referencing": ["mcp-agent-trust", "container-runtime-security"],
|
|
1334
|
+
"evidence_cves": ["CVE-2026-39884"],
|
|
1335
|
+
"framework_controls_partially_addressing": ["NIST-800-53-SI-10"],
|
|
1336
|
+
"real_requirement": "Pass arguments to spawned processes as an array, not a string. When a string-form command is unavoidable, use the runtime's argument-list API (Node `child_process.spawn(cmd, argsArray)`, Python `subprocess.run([cmd, ...args])`) or a vetted escape function. Linter rule that flags any `.split(' ')` followed by `spawn`/`exec` on user-tainted input.",
|
|
1337
|
+
"lag_notes": "SI-10 addresses input validation categorically but does not specify the argv-vs-string boundary that argument injection exploits. Many MCP servers and CI runners string-concatenate user input into shell commands without registering this as a code-review failure mode.",
|
|
1338
|
+
"last_verified": "2026-05-13"
|
|
1305
1339
|
}
|
|
1306
1340
|
}
|
|
@@ -757,6 +757,20 @@
|
|
|
757
757
|
"deterministic": true,
|
|
758
758
|
"attack_ref": "T1195.002"
|
|
759
759
|
},
|
|
760
|
+
{
|
|
761
|
+
"id": "gha-workflow-script-injection-sink",
|
|
762
|
+
"type": "file_path",
|
|
763
|
+
"value": "Within the release-workflows artifact (any file under .github/workflows/*.yml): a `run:` shell script body directly interpolates an attacker-controllable github.event field — ${{ github.event.comment.body }}, ${{ github.event.issue.body }}, ${{ github.event.issue.title }}, ${{ github.event.pull_request.body }}, ${{ github.event.pull_request.title }}, ${{ github.event.review.body }}, ${{ github.event.head_commit.message }}, ${{ github.head_ref }}, ${{ github.event.discussion.body }}, ${{ github.event.discussion.title }} — without first capturing the value into an env: variable. Grep regex (multi-line YAML aware): `run:\\s*\\|[\\s\\S]*?\\$\\{\\{\\s*github\\.(event\\.(comment|issue|pull_request|review|head_commit|discussion)\\.|head_ref)`. Corroborate via the branch-tag-protection artifact: if any workflow with this sink also triggers on `pull_request_target` / `issue_comment` / `pull_request_review_comment` AND its job has `permissions: contents: write` (or unrestricted GITHUB_TOKEN), the sink is exploitable by any GitHub user who can comment on the repo.",
|
|
764
|
+
"description": "GitHub Actions script-injection sink. Elementary-data 0.23.3 (April 2026) was forged via this exact pattern — `${{ github.event.comment.body }}` interpolated into a `run:` block in update_pylon_issue.yml, escalated via the workflow's GITHUB_TOKEN to publish a malicious release. Without this indicator, a publisher account compromise via attacker-controlled comments looks identical to a clean release at the consumer side.",
|
|
765
|
+
"confidence": "deterministic",
|
|
766
|
+
"deterministic": true,
|
|
767
|
+
"false_positive_checks_required": [
|
|
768
|
+
"If the run: block reads the github.event field via an `env:` variable first (env: COMMENT_BODY: ${{ github.event.comment.body }}) and then references $COMMENT_BODY in the shell — that is the documented-safe pattern; demote to miss.",
|
|
769
|
+
"If the workflow only runs in a sandboxed `pull_request` event (not `pull_request_target`) AND has default `permissions: contents: read` AND does not use secrets.* — the sink is not exploitable; demote to miss."
|
|
770
|
+
],
|
|
771
|
+
"attack_ref": "T1195.001",
|
|
772
|
+
"cve_ref": "MAL-2026-3083"
|
|
773
|
+
},
|
|
760
774
|
{
|
|
761
775
|
"id": "publish-workflow-no-id-token-write",
|
|
762
776
|
"type": "file_path",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_meta": {
|
|
3
3
|
"schema_version": "1.0.0",
|
|
4
|
-
"last_updated": "2026-05-
|
|
4
|
+
"last_updated": "2026-05-13",
|
|
5
5
|
"purpose": "Zero-day learning loop output. Each entry maps a CVE to: attack vector, defense chain analysis, framework coverage, new control requirements generated, and exposure scoring.",
|
|
6
6
|
"note": "Never delete entries. Closed gaps are marked status: closed. History is data.",
|
|
7
7
|
"tlp": "CLEAR",
|
|
@@ -466,5 +466,227 @@
|
|
|
466
466
|
"basis": "SLSA L3 + provenance + signing all pass on the malicious package. Standard supply-chain audits (SBOM check, provenance verify, signature verify) all give green. The architectural pre-condition (pull_request_target + id-token:write + shared actions/cache) is not in any compliance framework's control catalog. Combined ~150M+ weekly downloads across 42 packages = extremely broad exposure.",
|
|
467
467
|
"theater_pattern": "provenance_signed_therefore_safe"
|
|
468
468
|
}
|
|
469
|
+
},
|
|
470
|
+
"MAL-2026-3083": {
|
|
471
|
+
"name": "Elementary-Data PyPI Worm (Forged Release via GitHub Actions Script Injection)",
|
|
472
|
+
"lesson_date": "2026-05-13",
|
|
473
|
+
"attack_vector": {
|
|
474
|
+
"description": "GitHub Actions script-injection sink in `.github/workflows/update_pylon_issue.yml`. The workflow interpolated `${{ github.event.comment.body }}` directly into a `run:` block — any commenter could execute attacker-controlled shell with the workflow's elevated GITHUB_TOKEN. The attacker forged an orphan commit (b1e4b1f3...) and tagged v0.23.3, causing the project's legitimate publishing pipeline to emit a properly-signed PyPI release of code the maintainers never saw. The wheel differed from 0.23.2 by exactly one file: an `elementary.pth` Python startup hook that auto-executed on every interpreter invocation and harvested cloud + dbt + git credentials, exfiltrating to a single subdomain on skyhanni.cloud during an 8-hour in-the-wild window (2026-04-24 22:20Z → 2026-04-25 ~06:30Z).",
|
|
475
|
+
"privileges_required": "Any GitHub account that can comment on a public PR or issue in the target repo.",
|
|
476
|
+
"complexity": "low — comment-driven; no maintainer access required",
|
|
477
|
+
"ai_factor": "None observed. Conventional GitHub Actions script-injection tradecraft. The compounding factor is workflow-shaped: `${{ github.event.* }}` interpolated directly into `run:` is a documented anti-pattern, but it remains widespread."
|
|
478
|
+
},
|
|
479
|
+
"defense_chain": {
|
|
480
|
+
"prevention": {
|
|
481
|
+
"what_would_have_worked": "Treat `${{ github.event.* }}` as untrusted; pass it via env: into the script body rather than interpolating directly. Forbid workflows triggered by issue_comment / pull_request_target from holding `contents: write` permissions. Block release tags whose target is not an ancestor of the default branch (orphan-commit-driven release detection).",
|
|
482
|
+
"was_this_required": false,
|
|
483
|
+
"framework_requiring_it": null,
|
|
484
|
+
"adequacy": "Architectural — eliminates the primitive entirely. Auditing every workflow file for the anti-pattern is the hard part; this is what the library-author playbook's `gha-workflow-script-injection-sink` indicator looks for."
|
|
485
|
+
},
|
|
486
|
+
"detection": {
|
|
487
|
+
"what_would_have_worked": "Consumer-side fresh-publish cooldown (PyPI's pip --require-hashes against a known-good lockfile, or registry-mirror cooldown windows). Comparison-by-content: any pip install of a major-version-pinned package returning a wheel whose extracted contents differ from the previous patch version by an added .pth file should fail loud.",
|
|
488
|
+
"was_this_required": false,
|
|
489
|
+
"framework_requiring_it": null,
|
|
490
|
+
"adequacy": "Defense in depth — the malicious 0.23.3 was caught within hours. A 24-72h cooldown would have shielded most consumers."
|
|
491
|
+
},
|
|
492
|
+
"response": {
|
|
493
|
+
"what_would_have_worked": "Rotate every credential under the credential_paths_scanned list for any host that pip-installed elementary-data during the 8h window — dbt warehouse creds especially. The package was yanked, but extracted .pth files persist on disk until the affected venv is wiped.",
|
|
494
|
+
"was_this_required": false,
|
|
495
|
+
"framework_requiring_it": null,
|
|
496
|
+
"adequacy": "Reduces blast radius post-exploitation. Upgrading to 0.23.4 does NOT remove the planted elementary.pth from the existing site-packages — a venv recreate is required."
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
"framework_coverage": {
|
|
500
|
+
"SLSA-L3": {
|
|
501
|
+
"covered": true,
|
|
502
|
+
"adequate": false,
|
|
503
|
+
"gap": "Provenance valid, payload malicious — same shape as CVE-2026-45321. SLSA-L3 attests WHICH pipeline built the artifact, not that the pipeline was driven by trusted inputs."
|
|
504
|
+
},
|
|
505
|
+
"NIST-800-53-SA-12": {
|
|
506
|
+
"covered": true,
|
|
507
|
+
"adequate": false,
|
|
508
|
+
"gap": "Supply chain protection treats signed release as the trust anchor. The signature was valid; the input to the signing pipeline was attacker-controlled."
|
|
509
|
+
},
|
|
510
|
+
"NIST-800-218-PO.4": {
|
|
511
|
+
"covered": true,
|
|
512
|
+
"adequate": false,
|
|
513
|
+
"gap": "Define and use secure development security checks. Direct interpolation of github.event.* into run: scripts is a documented anti-pattern but is not framework-enforced."
|
|
514
|
+
},
|
|
515
|
+
"EU-CRA-Art13": {
|
|
516
|
+
"covered": true,
|
|
517
|
+
"adequate": false,
|
|
518
|
+
"gap": "Vulnerability handling provisions don't address the case where the maintainer was an unwitting publisher."
|
|
519
|
+
},
|
|
520
|
+
"NIS2-Art21-2d": {
|
|
521
|
+
"covered": true,
|
|
522
|
+
"adequate": false,
|
|
523
|
+
"gap": "Supply chain risk management presumes detectable signal at consumption. Valid signature neutralizes consumer-side checks."
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
"new_control_requirements": [
|
|
527
|
+
{
|
|
528
|
+
"id": "NEW-CTRL-011",
|
|
529
|
+
"name": "GHA-WORKFLOW-SCRIPT-INJECTION-SINK-BAN",
|
|
530
|
+
"description": "Forbid direct interpolation of `${{ github.event.* }}` (comment.body, issue.body, review.body, pull_request.title, head_ref, etc.) into any `run:` block. Pass via `env:` so the shell sees a quoted variable, not an interpolated string. Enforced via repository linter / required CI check.",
|
|
531
|
+
"evidence": "MAL-2026-3083 — the entire compromise hinges on this single primitive; no other infrastructure was breached.",
|
|
532
|
+
"gap_closes": [
|
|
533
|
+
"NIST-800-218-PO.4",
|
|
534
|
+
"SLSA-L3"
|
|
535
|
+
]
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"id": "NEW-CTRL-012",
|
|
539
|
+
"name": "ORPHAN-COMMIT-RELEASE-DETECTION",
|
|
540
|
+
"description": "Reject release tags whose target commit is not reachable from the default branch (`git merge-base --is-ancestor`). Forged orphan-commit releases are a signature of the maintainer-impersonation supply chain pattern.",
|
|
541
|
+
"evidence": "MAL-2026-3083 — the malicious release pointed at orphan commit b1e4b1f3aad0d489ab0e9208031c67402bbb8480, never on main.",
|
|
542
|
+
"gap_closes": [
|
|
543
|
+
"NIST-800-53-SA-12",
|
|
544
|
+
"EU-CRA-Art13"
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
"compliance_exposure_score": {
|
|
549
|
+
"percent_audit_passing_orgs_still_exposed": 92,
|
|
550
|
+
"basis": "PyPI signature + maintainer trust + provenance all pass on the malicious package. Audit programs measure SBOM presence, package-signing posture, and dependency-pin discipline — none of which catch a maintainer's own pipeline being weaponized via a comment. ~1.1M monthly downloads broaden the consumer footprint.",
|
|
551
|
+
"theater_pattern": "signed_release_therefore_safe"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
"CVE-2026-42208": {
|
|
555
|
+
"name": "BerriAI LiteLLM Proxy Auth SQL Injection",
|
|
556
|
+
"lesson_date": "2026-05-13",
|
|
557
|
+
"attack_vector": {
|
|
558
|
+
"description": "Authorization header value passed directly into a SQL query in the LiteLLM proxy's auth path. Crafted bearer-token-shape strings reach an error-logging pathway that executes SQL with the attacker-controlled value as a string-concatenated parameter — full pre-auth read/modify of the managed-credentials database. CISA KEV-listed 2026-05-08; in-wild exploitation evidence is the listing criterion.",
|
|
559
|
+
"privileges_required": "Network reachability to the LiteLLM proxy endpoint. No prior authentication.",
|
|
560
|
+
"complexity": "low — curl-able. POST /chat/completions with a SQLi payload in Authorization.",
|
|
561
|
+
"ai_factor": "Conventional human-security-research SQLi tradecraft. AI-stack relevance is downstream: LiteLLM IS the gateway in front of the model-provider keys that operators DO NOT want exfiltrated. The vulnerability is conventional; the impact class is AI-infrastructure."
|
|
562
|
+
},
|
|
563
|
+
"defense_chain": {
|
|
564
|
+
"prevention": {
|
|
565
|
+
"what_would_have_worked": "Parameterised queries throughout the auth path — no caller-supplied string ever string-concatenated into SQL. The 1.83.7 patch is exactly this: caller-supplied value becomes a SQL parameter, not part of the statement.",
|
|
566
|
+
"was_this_required": false,
|
|
567
|
+
"framework_requiring_it": "NIST-800-53-SI-10",
|
|
568
|
+
"adequacy": "Eliminates the class. SI-10's text requirement is satisfied by 'we validate inputs' regardless of whether the validation runs before the parameter binding — the framework gap is operational, not conceptual."
|
|
569
|
+
},
|
|
570
|
+
"detection": {
|
|
571
|
+
"what_would_have_worked": "WAF rule on Authorization headers containing SQL metacharacters or exceeding 100 bytes of non-base64-shape characters. LiteLLM error logs surface the injection string verbatim pre-1.83.7 — a log-pattern alert would have fired on the first probe.",
|
|
572
|
+
"was_this_required": false,
|
|
573
|
+
"framework_requiring_it": null,
|
|
574
|
+
"adequacy": "Detection layer. Operators running LiteLLM behind a default-deny WAF would not have been compromised."
|
|
575
|
+
},
|
|
576
|
+
"response": {
|
|
577
|
+
"what_would_have_worked": "Rotate every virtual key minted on the proxy since the patch ship date. Rotate every model-provider key the proxy held (openai, anthropic, etc.). Rotate LITELLM_MASTER_KEY and DATABASE_URL credentials. Audit LiteLLM_VerificationToken / LiteLLM_UserTable for admin-event-less inserts.",
|
|
578
|
+
"was_this_required": false,
|
|
579
|
+
"framework_requiring_it": null,
|
|
580
|
+
"adequacy": "Reduces blast radius post-exploitation. The DB primitive is read+write — assume tampering, not just disclosure."
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
"framework_coverage": {
|
|
584
|
+
"NIST-800-53-SI-10": {
|
|
585
|
+
"covered": true,
|
|
586
|
+
"adequate": false,
|
|
587
|
+
"gap": "Input validation control doesn't address argument-vs-statement distinction in SQL libraries."
|
|
588
|
+
},
|
|
589
|
+
"OWASP-LLM01": {
|
|
590
|
+
"covered": false,
|
|
591
|
+
"adequate": false,
|
|
592
|
+
"gap": "Prompt-injection control set doesn't address the AI-PROXY backend SQL surface — LiteLLM is the substrate that gates LLM API access, not the LLM itself."
|
|
593
|
+
},
|
|
594
|
+
"EU-AI-Act-Art-15": {
|
|
595
|
+
"covered": true,
|
|
596
|
+
"adequate": false,
|
|
597
|
+
"gap": "Robustness + cybersecurity requirement is undefined operationally for AI gateway infrastructure."
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
"new_control_requirements": [
|
|
601
|
+
{
|
|
602
|
+
"id": "NEW-CTRL-013",
|
|
603
|
+
"name": "AI-GATEWAY-CREDENTIAL-STORE-ISOLATION",
|
|
604
|
+
"description": "AI-API gateway substrates (LiteLLM, Portkey, Helicone, similar) must isolate the managed-credentials DB on a network segment unreachable from the API plane. The auth path may read but the API plane MUST NOT have raw-SQL connectivity to the credential store.",
|
|
605
|
+
"evidence": "CVE-2026-42208 — a single SQLi reaches the entire model-provider credential vault because the API plane and credential store share a process.",
|
|
606
|
+
"gap_closes": [
|
|
607
|
+
"OWASP-LLM01",
|
|
608
|
+
"EU-AI-Act-Art-15"
|
|
609
|
+
]
|
|
610
|
+
}
|
|
611
|
+
],
|
|
612
|
+
"compliance_exposure_score": {
|
|
613
|
+
"percent_audit_passing_orgs_still_exposed": 75,
|
|
614
|
+
"basis": "SI-10 audits accept 'we validate inputs' as compliance. Most operators run LiteLLM internet-reachable behind a thin proxy without a SQL-injection-aware WAF. KEV listing imposes a 21-day patch SLA on federal orgs; private-sector adoption lags.",
|
|
615
|
+
"theater_pattern": "input_validation_checkbox_without_parameterised_queries"
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
"CVE-2026-39884": {
|
|
619
|
+
"name": "Flux159 mcp-server-kubernetes Argument Injection via port_forward",
|
|
620
|
+
"lesson_date": "2026-05-13",
|
|
621
|
+
"attack_vector": {
|
|
622
|
+
"description": "AI assistant invokes the mcp-server-kubernetes `port_forward` MCP tool with a tainted resourceName (e.g. 'pod-name --address=0.0.0.0'). The server builds a string-form kubectl command and uses `.split(' ')` instead of an argv array, so the attacker-controlled flag lands as a distinct argv entry to kubectl. `--address=0.0.0.0` binds the port-forward to all interfaces; `-n kube-system` redirects to attacker-chosen namespaces. Exploitation is mediated by the AI assistant — adversarial input via prompt injection in retrieved docs / commit messages / upstream MCP tool responses is the upstream gate.",
|
|
623
|
+
"privileges_required": "AI assistant with mcp-server-kubernetes installed and port_forward enabled. Attacker needs only to influence the AI's input (PR comment, doc, retrieved RAG chunk).",
|
|
624
|
+
"complexity": "low — once the tainted string is in the AI's context, the tool call propagates it unchanged.",
|
|
625
|
+
"ai_factor": "AI-assistant-mediated argument injection. The vuln is conventional argv-injection; the AI is the channel that converts adversarial document content into infrastructure-tool flags."
|
|
626
|
+
},
|
|
627
|
+
"defense_chain": {
|
|
628
|
+
"prevention": {
|
|
629
|
+
"what_would_have_worked": "argv-array spawn — `execFile('kubectl', ['port-forward', resourceName, ...])` with no `.split(' ')` and no shell interpretation. The 3.5.0 patch does exactly this.",
|
|
630
|
+
"was_this_required": false,
|
|
631
|
+
"framework_requiring_it": "NIST-800-53-SI-10",
|
|
632
|
+
"adequacy": "Architectural fix — the class disappears."
|
|
633
|
+
},
|
|
634
|
+
"detection": {
|
|
635
|
+
"what_would_have_worked": "MCP audit log alerting on port_forward tool calls where resourceName contains whitespace or kubectl flag prefixes (`--`, `-n`). Process-level alerting on kubectl port-forward processes with --address=0.0.0.0 on hosts that should only port-forward to localhost.",
|
|
636
|
+
"was_this_required": false,
|
|
637
|
+
"framework_requiring_it": null,
|
|
638
|
+
"adequacy": "Detection layer — catches the exploit attempt before the listener binds externally."
|
|
639
|
+
},
|
|
640
|
+
"response": {
|
|
641
|
+
"what_would_have_worked": "Disable the port_forward tool in the MCP allowlist until upgraded to 3.5.0+. Most operator deployments don't rely on port_forward for routine work.",
|
|
642
|
+
"was_this_required": false,
|
|
643
|
+
"framework_requiring_it": null,
|
|
644
|
+
"adequacy": "Effective tool-disable mitigation; low operator cost."
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
"framework_coverage": {
|
|
648
|
+
"NIST-800-53-SI-10": {
|
|
649
|
+
"covered": true,
|
|
650
|
+
"adequate": false,
|
|
651
|
+
"gap": "Input validation control doesn't address the argv-vs-string boundary that argument injection exploits."
|
|
652
|
+
},
|
|
653
|
+
"OWASP-LLM01": {
|
|
654
|
+
"covered": false,
|
|
655
|
+
"adequate": false,
|
|
656
|
+
"gap": "Prompt-injection control set doesn't model the AI-assistant-as-channel pattern — the attacker doesn't compromise the MCP server, they feed adversarial input that the AI dutifully passes through."
|
|
657
|
+
},
|
|
658
|
+
"NIS2-Art21-2g": {
|
|
659
|
+
"covered": true,
|
|
660
|
+
"adequate": false,
|
|
661
|
+
"gap": "Patch management presumes traditional CVE timelines; MCP plugin ecosystem patch awareness lags."
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
"new_control_requirements": [
|
|
665
|
+
{
|
|
666
|
+
"id": "NEW-CTRL-014",
|
|
667
|
+
"name": "MCP-SERVER-ARGV-NOT-SHELLSTRING",
|
|
668
|
+
"description": "MCP servers spawning subprocesses MUST use argv-array spawn primitives (execFile / spawn with array args / posix_spawn) — never .split(' ') or shell concatenation of caller-supplied input. Treats every MCP tool argument as untrusted by default.",
|
|
669
|
+
"evidence": "CVE-2026-39884 — the entire vulnerability is .split(' ') on a caller-supplied string. The 3.5.0 patch is the argv-array refactor.",
|
|
670
|
+
"gap_closes": [
|
|
671
|
+
"NIST-800-53-SI-10",
|
|
672
|
+
"OWASP-LLM01"
|
|
673
|
+
]
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
"id": "NEW-CTRL-015",
|
|
677
|
+
"name": "MCP-TOOL-ALLOWLIST-ENFORCEMENT",
|
|
678
|
+
"description": "AI agent stacks must enforce an explicit allowlist of MCP tools — tools default to denied. High-risk tools (port_forward, exec, write_file, shell, kubectl) require operator consent per session.",
|
|
679
|
+
"evidence": "CVE-2026-39884 — temporary mitigation is exactly 'disable port_forward in the allowlist'. The control closes the class across future MCP plugins.",
|
|
680
|
+
"gap_closes": [
|
|
681
|
+
"OWASP-LLM01",
|
|
682
|
+
"NIS2-Art21-2g"
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
],
|
|
686
|
+
"compliance_exposure_score": {
|
|
687
|
+
"percent_audit_passing_orgs_still_exposed": 88,
|
|
688
|
+
"basis": "MCP ecosystem patch hygiene lags traditional CVE timelines. Most AI-agent operators do not maintain an explicit MCP tool allowlist; SI-10 audits accept the MCP plugin as a vendored dependency without auditing its argv handling.",
|
|
689
|
+
"theater_pattern": "vendored_mcp_plugin_inherits_vendor_trust"
|
|
690
|
+
}
|
|
469
691
|
}
|
|
470
692
|
}
|
package/lib/refresh-external.js
CHANGED
|
@@ -118,21 +118,25 @@ Modes:
|
|
|
118
118
|
--from-cache [<p>] read from prefetch cache (default .cache/upstream).
|
|
119
119
|
Combine with --apply to upsert against cached data
|
|
120
120
|
entirely offline. Cache must be pre-populated via --prefetch.
|
|
121
|
-
--source kev,epss scope to a comma-separated list (kev|epss|nvd|rfc|pins)
|
|
121
|
+
--source kev,epss scope to a comma-separated list (kev|epss|nvd|rfc|pins|ghsa|osv)
|
|
122
122
|
--from-fixture <p> use frozen fixture payloads (tests use this path)
|
|
123
123
|
--indexes-only rebuild data/_indexes/ only; no network. Equivalent to
|
|
124
124
|
\`exceptd refresh --indexes-only\`.
|
|
125
125
|
--swarm fan out sources across worker threads. Best with --from-cache.
|
|
126
|
-
--advisory <id> (v0.12.0) seed a single catalog entry from
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
(
|
|
131
|
-
|
|
126
|
+
--advisory <id> (v0.12.0) seed a single catalog entry from an advisory ID.
|
|
127
|
+
CVE-* and GHSA-* route through the GitHub Advisory
|
|
128
|
+
Database. MAL-*, SNYK-*, RUSTSEC-*, USN-*, UVI-*, GO-*,
|
|
129
|
+
MGASA-*, PYSEC-*, and other OSV-native namespaces route
|
|
130
|
+
through OSV.dev (v0.12.10). Writes a DRAFT to
|
|
131
|
+
data/cve-catalog.json marked with _auto_imported: true.
|
|
132
|
+
Editorial fields (framework_control_gaps, iocs,
|
|
133
|
+
atlas_refs, attack_refs) remain null pending review via:
|
|
132
134
|
exceptd run cve-curation --advisory <id>
|
|
133
135
|
Examples:
|
|
134
136
|
exceptd refresh --advisory CVE-2026-45321
|
|
135
137
|
exceptd refresh --advisory GHSA-xxxx-xxxx-xxxx --apply
|
|
138
|
+
exceptd refresh --advisory MAL-2026-3083
|
|
139
|
+
exceptd refresh --advisory RUSTSEC-2025-0001
|
|
136
140
|
|
|
137
141
|
Sources (default = all):
|
|
138
142
|
kev CISA Known Exploited Vulnerabilities
|
|
@@ -143,6 +147,10 @@ Sources (default = all):
|
|
|
143
147
|
ghsa (v0.12.0) GitHub Advisory Database — npm/PyPI/Maven/etc. Lands new CVE
|
|
144
148
|
IDs as DRAFTS (_auto_imported: true); catalog validator treats drafts
|
|
145
149
|
as warnings, not errors. Editorial review still required.
|
|
150
|
+
osv (v0.12.10) OSV.dev aggregator — OSSF Malicious Packages (MAL-*) + Snyk
|
|
151
|
+
+ GHSA + RustSec + Mageia + Go Vuln DB + Ubuntu USN. Unauthenticated.
|
|
152
|
+
Use --advisory MAL-* / RUSTSEC-* / SNYK-* / USN-* to seed a single
|
|
153
|
+
draft. Bulk import via package watchlist is a v0.13 follow-up.
|
|
146
154
|
|
|
147
155
|
Air-gap workflow:
|
|
148
156
|
1. On a connected host: \`exceptd refresh --prefetch\`
|
|
@@ -526,6 +534,48 @@ const GHSA_SOURCE = {
|
|
|
526
534
|
},
|
|
527
535
|
};
|
|
528
536
|
|
|
537
|
+
/**
|
|
538
|
+
* v0.12.10: OSV.dev source. Aggregates OSSF Malicious Packages (MAL-*) +
|
|
539
|
+
* Snyk (SNYK-*) + GitHub Advisory Database + RustSec (RUSTSEC-*) + Mageia
|
|
540
|
+
* + Go Vuln DB + Ubuntu USN into one unauthenticated API. Slot in for the
|
|
541
|
+
* package-compromise class that doesn't have a CVE yet — the MAL-*
|
|
542
|
+
* namespace is the canonical key for those (e.g. MAL-2026-3083, the
|
|
543
|
+
* elementary-data PyPI worm).
|
|
544
|
+
*
|
|
545
|
+
* Apply path mirrors GHSA: new entries land in data/cve-catalog.json as
|
|
546
|
+
* drafts (`_auto_imported: true` + `_draft: true`). Catalog key is either
|
|
547
|
+
* the CVE alias (when present) or the OSV id verbatim — preserving the
|
|
548
|
+
* existing CVE-keyed convention while accepting OSV's broader identifier
|
|
549
|
+
* shapes.
|
|
550
|
+
*/
|
|
551
|
+
const OSV_SOURCE = {
|
|
552
|
+
name: "osv",
|
|
553
|
+
description: "OSV.dev — OSSF Malicious Packages (MAL-*) + Snyk + GHSA + RustSec + Mageia + Go Vuln DB + Ubuntu USN. Unauthenticated. Slot in for the broader supply-chain-class disclosure space — covers package compromises that don't have CVEs yet.",
|
|
554
|
+
applies_to: "data/cve-catalog.json",
|
|
555
|
+
async fetchDiff(ctx) {
|
|
556
|
+
if (ctx.fixtures?.osv) return synthesizeFromFixture(ctx, "osv");
|
|
557
|
+
const osv = require("./source-osv");
|
|
558
|
+
return osv.buildDiff(ctx);
|
|
559
|
+
},
|
|
560
|
+
async applyDiff(ctx, diffs) {
|
|
561
|
+
// Same shape as GHSA applyDiff — skip overwrites, surface conflicts.
|
|
562
|
+
let updated = 0;
|
|
563
|
+
const errors = [];
|
|
564
|
+
for (const d of diffs) {
|
|
565
|
+
if (d.field !== "_new_entry") continue;
|
|
566
|
+
if (!d.after || !d.id) continue;
|
|
567
|
+
if (ctx.cveCatalog[d.id]) continue; // never overwrite existing entries
|
|
568
|
+
try {
|
|
569
|
+
ctx.cveCatalog[d.id] = d.after;
|
|
570
|
+
updated++;
|
|
571
|
+
} catch (e) {
|
|
572
|
+
errors.push(`${d.id}: ${e.message}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return { updated, errors };
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
529
579
|
const ALL_SOURCES = {
|
|
530
580
|
kev: KEV_SOURCE,
|
|
531
581
|
epss: EPSS_SOURCE,
|
|
@@ -533,6 +583,7 @@ const ALL_SOURCES = {
|
|
|
533
583
|
rfc: RFC_SOURCE,
|
|
534
584
|
pins: PINS_SOURCE,
|
|
535
585
|
ghsa: GHSA_SOURCE,
|
|
586
|
+
osv: OSV_SOURCE,
|
|
536
587
|
};
|
|
537
588
|
|
|
538
589
|
// --- Cache-mode helpers ------------------------------------------------
|
|
@@ -746,7 +797,7 @@ function loadCtx(opts) {
|
|
|
746
797
|
cacheDir: null,
|
|
747
798
|
};
|
|
748
799
|
if (opts.fromFixture) {
|
|
749
|
-
ctx.fixtures = { dir: path.resolve(opts.fromFixture), kev: true, epss: true, nvd: true, rfc: true, pins: true, ghsa: true };
|
|
800
|
+
ctx.fixtures = { dir: path.resolve(opts.fromFixture), kev: true, epss: true, nvd: true, rfc: true, pins: true, ghsa: true, osv: true };
|
|
750
801
|
} else if (opts.fromCache) {
|
|
751
802
|
const abs = path.resolve(opts.fromCache);
|
|
752
803
|
ctx.cacheDir = abs;
|
|
@@ -799,11 +850,20 @@ function chosenSources(opts) {
|
|
|
799
850
|
* when a draft is produced, signaling that editorial review is needed.
|
|
800
851
|
*/
|
|
801
852
|
async function seedSingleAdvisory(opts) {
|
|
802
|
-
const ghsa = require("./source-ghsa");
|
|
803
853
|
const id = opts.advisory;
|
|
804
|
-
|
|
854
|
+
// v0.12.10: route OSV-native ids (MAL-*, SNYK-*, RUSTSEC-*, USN-*, etc.)
|
|
855
|
+
// through source-osv. CVE-* and GHSA-* keep routing through GHSA because
|
|
856
|
+
// GHSA carries richer field coverage for those identifier shapes.
|
|
857
|
+
const osvMod = require("./source-osv");
|
|
858
|
+
const useOsv = osvMod.isOsvId(id) && !/^GHSA-/i.test(id);
|
|
859
|
+
const ghsa = require("./source-ghsa");
|
|
860
|
+
const sourceMod = useOsv ? osvMod : ghsa;
|
|
861
|
+
const sourceName = useOsv ? "osv" : "ghsa";
|
|
862
|
+
const fixtureEnv = useOsv ? "EXCEPTD_OSV_FIXTURE" : "EXCEPTD_GHSA_FIXTURE";
|
|
863
|
+
|
|
864
|
+
const result = await sourceMod.fetchAdvisoryById(id, {});
|
|
805
865
|
if (!result.ok) {
|
|
806
|
-
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: ${result.error}`, source: result.source, hint:
|
|
866
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: ${result.error}`, source: result.source, routed_to: sourceName, hint: `Verify the ID format (CVE-YYYY-NNNN, GHSA-*, MAL-*, SNYK-*, RUSTSEC-*, USN-*, etc.) and network reachability. Set ${fixtureEnv} for offline testing.` };
|
|
807
867
|
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
808
868
|
else process.stderr.write(`[refresh --advisory] ${err.error}\n hint: ${err.hint}\n`);
|
|
809
869
|
process.exitCode = 2;
|
|
@@ -811,15 +871,15 @@ async function seedSingleAdvisory(opts) {
|
|
|
811
871
|
}
|
|
812
872
|
const advisory = result.advisories[0];
|
|
813
873
|
if (!advisory) {
|
|
814
|
-
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: no matching advisory found`, source: result.source };
|
|
874
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: no matching advisory found`, source: result.source, routed_to: sourceName };
|
|
815
875
|
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
816
876
|
else process.stderr.write(`[refresh --advisory] ${err.error}\n`);
|
|
817
877
|
process.exitCode = 2;
|
|
818
878
|
return;
|
|
819
879
|
}
|
|
820
|
-
const normalized =
|
|
880
|
+
const normalized = sourceMod.normalizeAdvisory(advisory);
|
|
821
881
|
if (!normalized) {
|
|
822
|
-
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: advisory
|
|
882
|
+
const err = { ok: false, verb: "refresh", error: `--advisory ${id}: advisory could not be normalized (missing required fields)`, routed_to: sourceName, source_id: advisory.ghsa_id || advisory.id || null };
|
|
823
883
|
if (opts.json) process.stdout.write(JSON.stringify(err) + "\n");
|
|
824
884
|
else process.stderr.write(`[refresh --advisory] ${err.error}\n`);
|
|
825
885
|
process.exitCode = 2;
|