@blamejs/exceptd-skills 0.18.5 → 0.18.7
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 +24 -0
- package/bin/exceptd.js +277 -60
- package/data/_indexes/_meta.json +22 -3
- package/data/cve-catalog.json +25 -0
- package/data/playbooks/framework.json +2 -2
- package/data/playbooks/post-quantum-migration.json +1 -1
- package/lib/auto-discovery.js +30 -10
- package/lib/collectors/ai-api.js +9 -2
- package/lib/collectors/cicd-pipeline-compromise.js +24 -5
- package/lib/collectors/cred-stores.js +17 -4
- package/lib/collectors/crypto.js +9 -2
- package/lib/collectors/hardening.js +9 -2
- package/lib/collectors/library-author.js +24 -3
- package/lib/collectors/mcp.js +9 -2
- package/lib/collectors/runtime.js +9 -2
- package/lib/collectors/sbom.js +28 -15
- package/lib/collectors/scan-excludes.js +25 -0
- package/lib/collectors/secrets.js +40 -4
- package/lib/cve-curation.js +84 -8
- package/lib/lint-skills.js +75 -3
- package/lib/playbook-runner.js +375 -40
- package/lib/prefetch.js +6 -1
- package/lib/refresh-external.js +32 -1
- package/lib/refresh-network.js +201 -24
- package/lib/schemas/cve-catalog.schema.json +5 -0
- package/lib/scoring.js +106 -13
- package/lib/sign.js +107 -29
- package/lib/source-advisories.js +23 -5
- package/lib/source-ghsa.js +25 -1
- package/lib/source-osv.js +26 -1
- package/lib/upstream-check.js +1 -1
- package/lib/validate-cve-catalog.js +19 -3
- package/lib/validate-indexes.js +62 -1
- package/lib/validate-playbooks.js +4 -1
- package/lib/validate-vendor.js +69 -8
- package/manifest.json +53 -53
- package/orchestrator/index.js +69 -13
- package/package.json +1 -1
- package/sbom.cdx.json +124 -124
- package/scripts/audit-cross-skill.js +1 -1
- package/scripts/bootstrap.js +1 -0
- package/scripts/build-indexes.js +58 -4
- package/scripts/check-agents-md-collectors.js +41 -13
- package/scripts/check-changelog-extract.js +4 -4
- package/scripts/check-codebase-patterns.js +19 -5
- package/scripts/check-manifest-snapshot.js +74 -30
- package/scripts/check-sbom-currency.js +25 -5
- package/scripts/check-test-count.js +26 -7
- package/scripts/check-test-coverage.js +44 -4
- package/scripts/check-version-tags.js +27 -8
- package/scripts/predeploy.js +1 -1
- package/scripts/refresh-manifest-snapshot.js +14 -4
- package/scripts/refresh-reverse-refs.js +7 -1
- package/scripts/refresh-sbom.js +1 -1
- package/scripts/release.js +3 -3
- package/scripts/run-e2e-scenarios.js +18 -8
- package/scripts/validate-vendor-online.js +20 -2
- package/scripts/verify-shipped-tarball.js +65 -6
- package/sources/validators/cve-validator.js +17 -1
- package/vendor/blamejs/_PROVENANCE.json +4 -2
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-06-
|
|
3
|
+
"generated_at": "2026-06-20T16:35:33.578Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 64,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "6e8a5ac562c706b0142ad12b07a62982bb83d500870ca33a5d865bd17a16d23a",
|
|
8
8
|
"README.md": "e7b854e7db9a364a1b368b5084b4f0c2a8282f0459ce39800ac1d1dabdc06074",
|
|
9
9
|
"data/atlas-ttps.json": "5bc59e23d6c2defa54168de161a0825299b9cc4a49c6b26df2dae70b4f42eedf",
|
|
10
10
|
"data/attack-techniques.json": "53c6f248760eecb11a0354f74ab467a5814e95075a686b9b3bf18c34e2f7435e",
|
|
11
|
-
"data/cve-catalog.json": "
|
|
11
|
+
"data/cve-catalog.json": "06ca53e69071dfe94867c10717f3ff2962c50341defcaf050f1a96236b5be51c",
|
|
12
12
|
"data/cwe-catalog.json": "359263361fa52069e2856cc352d8f1c757d614ea840db6ef3ff5e696185ca220",
|
|
13
13
|
"data/d3fend-catalog.json": "349d14b2777342d38e5f8b0149a9ebd7703ee59f67a9efee200d1e19313088ac",
|
|
14
14
|
"data/dlp-controls.json": "d2406c482dddd30e49203879999dc4b3a7fd4d0494d6a61d86b91ee76415df19",
|
|
@@ -71,6 +71,25 @@
|
|
|
71
71
|
},
|
|
72
72
|
"skill_count": 51,
|
|
73
73
|
"catalog_count": 11,
|
|
74
|
+
"outputs": [
|
|
75
|
+
"activity-feed.json",
|
|
76
|
+
"catalog-summaries.json",
|
|
77
|
+
"chains.json",
|
|
78
|
+
"currency.json",
|
|
79
|
+
"did-ladders.json",
|
|
80
|
+
"frequency.json",
|
|
81
|
+
"handoff-dag.json",
|
|
82
|
+
"jurisdiction-clocks.json",
|
|
83
|
+
"jurisdiction-map.json",
|
|
84
|
+
"recipes.json",
|
|
85
|
+
"section-offsets.json",
|
|
86
|
+
"stale-content.json",
|
|
87
|
+
"summary-cards.json",
|
|
88
|
+
"theater-fingerprints.json",
|
|
89
|
+
"token-budget.json",
|
|
90
|
+
"trigger-table.json",
|
|
91
|
+
"xref.json"
|
|
92
|
+
],
|
|
74
93
|
"index_stats": {
|
|
75
94
|
"xref_entries": {
|
|
76
95
|
"cwe_refs": 53,
|
package/data/cve-catalog.json
CHANGED
|
@@ -1322,6 +1322,7 @@
|
|
|
1322
1322
|
"CVE-2025-53773": {
|
|
1323
1323
|
"name": "GitHub Copilot / VS Code 'YOLO mode' Prompt Injection RCE",
|
|
1324
1324
|
"type": "RCE-via-prompt-injection",
|
|
1325
|
+
"attack_class": "prompt-injection",
|
|
1325
1326
|
"cvss_score": 7.8,
|
|
1326
1327
|
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
|
|
1327
1328
|
"cvss_correction_note": "v0.12.6 source audit corrected from CVSS 9.6/AV:N (network) to CVSS 7.8/AV:L (local) — the attack is local-vector via developer-side IDE interaction; the attacker does not reach in over the network. NVD authoritative.",
|
|
@@ -1436,6 +1437,7 @@
|
|
|
1436
1437
|
"CVE-2026-30615": {
|
|
1437
1438
|
"name": "Windsurf MCP Local-Vector RCE via Adversarial Tool Response",
|
|
1438
1439
|
"type": "RCE-supply-chain",
|
|
1440
|
+
"attack_class": "mcp-supply-chain",
|
|
1439
1441
|
"cvss_score": 8,
|
|
1440
1442
|
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H",
|
|
1441
1443
|
"cvss_correction_note": "v0.12.9 (2026-05-13): catalog previously stored CVSS 9.8 / AV:N. NVD authoritative is 8.0 / AV:L (local attack vector; attacker must control HTML content the Windsurf MCP client processes — not a network-vector zero-interaction RCE as initially cataloged). Source: https://nvd.nist.gov/vuln/detail/CVE-2026-30615 (published 2026-04-15, last_modified 2026-04-27, vulnStatus: Deferred). Recompute RWEP with blast_radius reduced from 30→20 to reflect local-vector + Scope:U.",
|
|
@@ -1699,6 +1701,7 @@
|
|
|
1699
1701
|
"CVE-2026-39884": {
|
|
1700
1702
|
"name": "Flux159 mcp-server-kubernetes Argument Injection via port_forward",
|
|
1701
1703
|
"type": "argument-injection",
|
|
1704
|
+
"attack_class": "mcp-supply-chain",
|
|
1702
1705
|
"cvss_score": 8.3,
|
|
1703
1706
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L",
|
|
1704
1707
|
"cisa_kev": false,
|
|
@@ -3555,6 +3558,7 @@
|
|
|
3555
3558
|
"ai_assisted_weaponization": false,
|
|
3556
3559
|
"name": "Anthropic MCP SDK stdio command-injection",
|
|
3557
3560
|
"type": "command-injection",
|
|
3561
|
+
"attack_class": "mcp-supply-chain",
|
|
3558
3562
|
"cvss_score": 8.8,
|
|
3559
3563
|
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
|
|
3560
3564
|
"cisa_kev": false,
|
|
@@ -5742,6 +5746,7 @@
|
|
|
5742
5746
|
"id": "CVE-2025-38352",
|
|
5743
5747
|
"name": "Android / Linux Kernel POSIX CPU Timer Race (sandbox-escape LPE)",
|
|
5744
5748
|
"type": "race-condition-lpe",
|
|
5749
|
+
"attack_class": "kernel-lpe",
|
|
5745
5750
|
"cvss_score": 7.4,
|
|
5746
5751
|
"cvss_vector": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
|
|
5747
5752
|
"cisa_kev": true,
|
|
@@ -6689,6 +6694,7 @@
|
|
|
6689
6694
|
"id": "CVE-2025-22225",
|
|
6690
6695
|
"name": "VMware ESXi Arbitrary Kernel Write (VM-escape chain, ransomware-active)",
|
|
6691
6696
|
"type": "arbitrary-kernel-write-vm-escape",
|
|
6697
|
+
"attack_class": "kernel-lpe",
|
|
6692
6698
|
"cvss_score": 8.2,
|
|
6693
6699
|
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H",
|
|
6694
6700
|
"cisa_kev": true,
|
|
@@ -7256,6 +7262,7 @@
|
|
|
7256
7262
|
"id": "CVE-2025-55319",
|
|
7257
7263
|
"name": "Visual Studio Code Agentic-AI Command Injection (ZeroPath AI-discovered)",
|
|
7258
7264
|
"type": "command-injection-agentic",
|
|
7265
|
+
"attack_class": "ai-c2",
|
|
7259
7266
|
"cvss_score": 7.4,
|
|
7260
7267
|
"cvss_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
|
|
7261
7268
|
"cisa_kev": false,
|
|
@@ -11564,6 +11571,7 @@
|
|
|
11564
11571
|
"CVE-2026-25592": {
|
|
11565
11572
|
"name": "Microsoft Semantic Kernel SessionsPythonPlugin Path Traversal — Prompt-Injection to Host RCE",
|
|
11566
11573
|
"type": "RCE",
|
|
11574
|
+
"attack_class": "prompt-injection",
|
|
11567
11575
|
"cvss_score": 9.9,
|
|
11568
11576
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
|
|
11569
11577
|
"cvss_note": "NVD CVSS v3.1 base 9.9 (CRITICAL), Scope:Changed. The high score reflects worst-case impact; the RWEP score is intentionally lower (Hard Rule #3) because there is no confirmed in-the-wild exploitation or KEV listing and a patch shipped at disclosure.",
|
|
@@ -11681,6 +11689,7 @@
|
|
|
11681
11689
|
"CVE-2025-49596": {
|
|
11682
11690
|
"name": "MCP Inspector Missing Authentication — Unauthenticated RCE via the Inspector Proxy",
|
|
11683
11691
|
"type": "RCE",
|
|
11692
|
+
"attack_class": "mcp-supply-chain",
|
|
11684
11693
|
"cvss_score": 8.3,
|
|
11685
11694
|
"cvss_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
|
|
11686
11695
|
"cvss_note": "Operator v3.1 estimate (NVD has not assessed v3.1; the GitHub CNA published CVSS v4.0 9.4 CRITICAL). Exploitation lures a victim's browser to reach the locally-bound, unauthenticated MCP Inspector proxy (the 0.0.0.0-day / DNS-rebinding class), hence AC:H / UI:R; once reached, commands run over stdio with no auth.",
|
|
@@ -11787,6 +11796,7 @@
|
|
|
11787
11796
|
"CVE-2025-54136": {
|
|
11788
11797
|
"name": "Cursor MCPoison — Persistent RCE via Modified Already-Trusted MCP Config",
|
|
11789
11798
|
"type": "RCE",
|
|
11799
|
+
"attack_class": "mcp-supply-chain",
|
|
11790
11800
|
"cvss_score": 8.8,
|
|
11791
11801
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
|
|
11792
11802
|
"cvss_note": "NVD CVSS v3.1 base 8.8 (HIGH). The 'trust-on-first-use, never-re-validated' design lets a one-time approval become silent persistent code execution.",
|
|
@@ -11894,6 +11904,7 @@
|
|
|
11894
11904
|
"CVE-2026-22252": {
|
|
11895
11905
|
"name": "LibreChat MCP stdio Transport — Authenticated Arbitrary Command Execution as Root",
|
|
11896
11906
|
"type": "RCE",
|
|
11907
|
+
"attack_class": "mcp-supply-chain",
|
|
11897
11908
|
"cvss_score": 9.9,
|
|
11898
11909
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
|
|
11899
11910
|
"cvss_note": "NVD CVSS v3.1 base 9.9 (CRITICAL), Scope:Changed (the GitHub CNA scored 9.1 with PR:H). The MCP stdio transport runs the supplied command as root inside the container, so any authenticated user reaches host-class execution.",
|
|
@@ -11998,6 +12009,7 @@
|
|
|
11998
12009
|
"CVE-2026-22688": {
|
|
11999
12010
|
"name": "Tencent WeKnora MCP stdio Command Injection",
|
|
12000
12011
|
"type": "RCE",
|
|
12012
|
+
"attack_class": "mcp-supply-chain",
|
|
12001
12013
|
"cvss_score": 8.8,
|
|
12002
12014
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
|
|
12003
12015
|
"cvss_note": "NVD CVSS v3.1 base 8.8 (HIGH); the GitHub CNA scored 9.9 (Scope:Changed). Authenticated command injection through the MCP stdio settings.",
|
|
@@ -12099,6 +12111,7 @@
|
|
|
12099
12111
|
"CVE-2026-40933": {
|
|
12100
12112
|
"name": "FlowiseAI Flowise MCP Custom Config Command Injection",
|
|
12101
12113
|
"type": "RCE",
|
|
12114
|
+
"attack_class": "mcp-supply-chain",
|
|
12102
12115
|
"cvss_score": 9.9,
|
|
12103
12116
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
|
|
12104
12117
|
"cvss_note": "NVD/CNA CVSS v3.1 base 9.9 (CRITICAL, Scope:Changed). Authenticated command injection via the Custom MCP configuration.",
|
|
@@ -12199,6 +12212,7 @@
|
|
|
12199
12212
|
"CVE-2026-30624": {
|
|
12200
12213
|
"name": "Agent Zero MCP Server Config Command Injection",
|
|
12201
12214
|
"type": "RCE",
|
|
12215
|
+
"attack_class": "mcp-supply-chain",
|
|
12202
12216
|
"cvss_score": 8.6,
|
|
12203
12217
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H",
|
|
12204
12218
|
"cvss_note": "NVD CVSS v3.1 base 8.6 (HIGH). Remote code execution through malicious MCP server configurations executed without adequate validation.",
|
|
@@ -12293,6 +12307,7 @@
|
|
|
12293
12307
|
"CVE-2026-30616": {
|
|
12294
12308
|
"name": "Jaaz MCP stdio Command Execution RCE",
|
|
12295
12309
|
"type": "RCE",
|
|
12310
|
+
"attack_class": "mcp-supply-chain",
|
|
12296
12311
|
"cvss_score": 7.3,
|
|
12297
12312
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
|
|
12298
12313
|
"cvss_note": "CISA-ADP CVSS v3.1 base 7.3 (HIGH). Remote code execution in MCP stdio command execution handling.",
|
|
@@ -12387,6 +12402,7 @@
|
|
|
12387
12402
|
"CVE-2026-30617": {
|
|
12388
12403
|
"name": "Langchain-Chatchat MCP Management Interface stdio RCE",
|
|
12389
12404
|
"type": "RCE",
|
|
12405
|
+
"attack_class": "mcp-supply-chain",
|
|
12390
12406
|
"cvss_score": 8.6,
|
|
12391
12407
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H",
|
|
12392
12408
|
"cvss_note": "NVD CVSS v3.1 base 8.6 (HIGH). RCE through an exposed MCP management interface that configures malicious stdio server commands.",
|
|
@@ -12481,6 +12497,7 @@
|
|
|
12481
12497
|
"CVE-2026-30625": {
|
|
12482
12498
|
"name": "Upsonic MCP Task Allowed-Command Argument Injection RCE",
|
|
12483
12499
|
"type": "RCE",
|
|
12500
|
+
"attack_class": "mcp-supply-chain",
|
|
12484
12501
|
"cvss_score": 9.8,
|
|
12485
12502
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
|
12486
12503
|
"cvss_note": "NVD CVSS v3.1 base 9.8 (CRITICAL). RCE via MCP task creation where allow-listed commands (npm, npx) accept flags that enable arbitrary OS command execution.",
|
|
@@ -12575,6 +12592,7 @@
|
|
|
12575
12592
|
"CVE-2026-26015": {
|
|
12576
12593
|
"name": "DocsGPT MCP stdio Unauthenticated Remote Code Execution",
|
|
12577
12594
|
"type": "RCE",
|
|
12595
|
+
"attack_class": "mcp-supply-chain",
|
|
12578
12596
|
"cvss_score": 9.8,
|
|
12579
12597
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
|
12580
12598
|
"cvss_note": "NVD CVSS v3.1 base 9.8 (CRITICAL); the GitHub advisory scores 10.0. Unauthenticated: a crafted payload bypasses the MCP test behavior to execute commands.",
|
|
@@ -12783,6 +12801,7 @@
|
|
|
12783
12801
|
"CVE-2025-23254": {
|
|
12784
12802
|
"name": "NVIDIA TensorRT-LLM Python Executor Deserialization RCE (ShadowMQ)",
|
|
12785
12803
|
"type": "RCE",
|
|
12804
|
+
"attack_class": "ai-c2",
|
|
12786
12805
|
"cvss_score": 8.8,
|
|
12787
12806
|
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
|
|
12788
12807
|
"cvss_note": "NVIDIA/NVD CVSS v3.1 base 8.8 (HIGH, Scope:Changed). Insecure deserialization in the TensorRT-LLM Python executor.",
|
|
@@ -12885,6 +12904,7 @@
|
|
|
12885
12904
|
"CVE-2025-30165": {
|
|
12886
12905
|
"name": "vLLM V0 Engine ZeroMQ Deserialization RCE (ShadowMQ)",
|
|
12887
12906
|
"type": "RCE",
|
|
12907
|
+
"attack_class": "ai-c2",
|
|
12888
12908
|
"cvss_score": 8,
|
|
12889
12909
|
"cvss_vector": "CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
|
|
12890
12910
|
"cvss_note": "NVD CVSS v3.1 base 8.0 (HIGH, AV:Adjacent). Unsafe deserialization over ZeroMQ in multi-node V0-engine deployments.",
|
|
@@ -12988,6 +13008,7 @@
|
|
|
12988
13008
|
"CVE-2024-50050": {
|
|
12989
13009
|
"name": "Meta Llama Stack Socket Deserialization RCE (ShadowMQ)",
|
|
12990
13010
|
"type": "RCE",
|
|
13011
|
+
"attack_class": "ai-c2",
|
|
12991
13012
|
"cvss_score": 6.3,
|
|
12992
13013
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
|
|
12993
13014
|
"cvss_note": "NVD CISA-ADP CVSS v3.1 base 6.3 (MEDIUM); Oligo and Snyk originally scored the same flaw 9.3 (CRITICAL) — a documented CVSS dispute. The serialization format was replaced with JSON in the fix.",
|
|
@@ -13091,6 +13112,7 @@
|
|
|
13091
13112
|
"CVE-2025-60455": {
|
|
13092
13113
|
"name": "Modular Max Server KVCache-Agent Deserialization RCE (ShadowMQ)",
|
|
13093
13114
|
"type": "RCE",
|
|
13115
|
+
"attack_class": "ai-c2",
|
|
13094
13116
|
"cvss_score": 8.4,
|
|
13095
13117
|
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
|
13096
13118
|
"cvss_note": "NVD CVSS v3.1 base 8.4 (HIGH). Unsafe deserialization reachable when --experimental-enable-kvcache-agent is enabled.",
|
|
@@ -17799,6 +17821,7 @@
|
|
|
17799
17821
|
"CVE-2024-5565": {
|
|
17800
17822
|
"name": "Vanna.AI Prompt Injection to Remote Code Execution",
|
|
17801
17823
|
"type": "RCE",
|
|
17824
|
+
"attack_class": "prompt-injection",
|
|
17802
17825
|
"cvss_score": 8.1,
|
|
17803
17826
|
"cvss_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
|
17804
17827
|
"cvss_note": "JFrog (CNA) CVSS v3.1 base 8.1 (HIGH); the GitHub advisory (GHSA-7735-w2jp-gvg6) rates it 9.2 (CRITICAL); NVD has not published its own assessed score. Prompt injection through the text-to-SQL ask method - with visualization enabled, the default - makes the LLM emit attacker-chosen Python that Vanna runs to build the Plotly figure, giving remote code execution (CWE-94 / CWE-77).",
|
|
@@ -17907,6 +17930,7 @@
|
|
|
17907
17930
|
"CVE-2024-12366": {
|
|
17908
17931
|
"name": "PandasAI Prompt Injection to Remote Code Execution",
|
|
17909
17932
|
"type": "RCE",
|
|
17933
|
+
"attack_class": "prompt-injection",
|
|
17910
17934
|
"cvss_score": 9.8,
|
|
17911
17935
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
|
17912
17936
|
"cvss_note": "CISA-ADP CVSS v3.1 base 9.8 (CRITICAL); the GitHub advisory (GHSA-vv2h-2w3q-3fx7) also rates it Critical; NVD has not published its own assessed score. PandasAI's interactive prompt (chat) fails to distinguish legitimate from malicious input, so prompt injection drives the natural-language interface into executing arbitrary Python - remote code execution (CWE-94), no authentication or user interaction required.",
|
|
@@ -47286,6 +47310,7 @@
|
|
|
47286
47310
|
"CVE-2026-42271": {
|
|
47287
47311
|
"name": "BerriAI LiteLLM MCP Test-Endpoint Authenticated Command Injection (CISA KEV)",
|
|
47288
47312
|
"type": "RCE",
|
|
47313
|
+
"attack_class": "mcp-supply-chain",
|
|
47289
47314
|
"cvss_score": 8.8,
|
|
47290
47315
|
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
|
|
47291
47316
|
"cvss_note": "NIST (NVD) CVSS v3.1 base 8.8 (HIGH); the GitHub (CNA) advisory rates it CVSS v4.0 8.7 (HIGH). The MCP REST test endpoints accept attacker-supplied server configurations that spawn arbitrary subprocesses; any authenticated caller, including a low-privilege internal-user key, reaches command execution on the proxy host (CWE-77 / CWE-78).",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"feeds_into": [
|
|
43
43
|
{
|
|
44
44
|
"playbook_id": "sbom",
|
|
45
|
-
"condition": "
|
|
45
|
+
"condition": "analyze.compliance_theater_check.verdict == 'theater' AND analyze.blast_radius_score >= 4"
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"fed_by": [
|
|
@@ -717,7 +717,7 @@
|
|
|
717
717
|
"action": "raise_severity"
|
|
718
718
|
},
|
|
719
719
|
{
|
|
720
|
-
"condition": "
|
|
720
|
+
"condition": "analyze.compliance_theater_check.verdict == 'theater' AND blast_radius_score >= 3",
|
|
721
721
|
"action": "trigger_playbook",
|
|
722
722
|
"target_playbook": "sbom"
|
|
723
723
|
}
|
|
@@ -817,7 +817,7 @@
|
|
|
817
817
|
],
|
|
818
818
|
"escalation_criteria": [
|
|
819
819
|
{
|
|
820
|
-
"condition": "no-cryptographic-asset-register == fired AND jurisdiction_obligations contains
|
|
820
|
+
"condition": "no-cryptographic-asset-register == fired AND jurisdiction_obligations contains 'EU'",
|
|
821
821
|
"action": "notify_legal"
|
|
822
822
|
},
|
|
823
823
|
{
|
package/lib/auto-discovery.js
CHANGED
|
@@ -411,19 +411,39 @@ async function fetchDatatracker(url, ctx) {
|
|
|
411
411
|
if ((ctx && ctx.airGap === true) || process.env.EXCEPTD_AIR_GAP === "1") {
|
|
412
412
|
return { ok: false, error: "air-gap-blocked", source: "datatracker" };
|
|
413
413
|
}
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
// Adopt the vendored blamejs retry primitive, matching source-osv /
|
|
415
|
+
// source-ghsa / source-advisories / refresh-network: a transient Datatracker
|
|
416
|
+
// failure (HTTP 429/5xx, ECONNRESET/ETIMEDOUT family) backs off with
|
|
417
|
+
// exponential jitter instead of being dropped on the first hiccup, which
|
|
418
|
+
// silently yielded no discovered working groups. A genuine 4xx (e.g. 404 on
|
|
419
|
+
// a renamed WG acronym) carries a non-retryable statusCode, so the classifier
|
|
420
|
+
// does not waste attempts on it. The caller contract is unchanged: discovery
|
|
421
|
+
// never throws — it returns drafts, not errors — so an exhausted retry budget
|
|
422
|
+
// (or any non-retryable failure) collapses to the same `null` the
|
|
423
|
+
// single-fetch path returned, which discoverNewRfcs treats as one WG skip.
|
|
424
|
+
const { withRetry } = require("../vendor/blamejs/retry.js");
|
|
425
|
+
const once = async () => {
|
|
426
|
+
const ac = new AbortController();
|
|
427
|
+
const t = setTimeout(() => ac.abort(), TIMEOUT_MS);
|
|
428
|
+
try {
|
|
429
|
+
const res = await fetch(url, {
|
|
430
|
+
signal: ac.signal,
|
|
431
|
+
headers: { "User-Agent": USER_AGENT, Accept: "application/json" },
|
|
432
|
+
});
|
|
433
|
+
if (!res.ok) {
|
|
434
|
+
const e = new Error(`HTTP ${res.status} from ${url}`);
|
|
435
|
+
e.statusCode = res.status; // let withRetry's classifier retry 429/5xx
|
|
436
|
+
throw e;
|
|
437
|
+
}
|
|
438
|
+
return await res.json();
|
|
439
|
+
} finally {
|
|
440
|
+
clearTimeout(t);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
416
443
|
try {
|
|
417
|
-
|
|
418
|
-
signal: ac.signal,
|
|
419
|
-
headers: { "User-Agent": USER_AGENT, Accept: "application/json" },
|
|
420
|
-
});
|
|
421
|
-
if (!res.ok) return null;
|
|
422
|
-
return await res.json();
|
|
444
|
+
return await withRetry(once, { maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 2000, jitterFactor: 0.5 });
|
|
423
445
|
} catch {
|
|
424
446
|
return null;
|
|
425
|
-
} finally {
|
|
426
|
-
clearTimeout(t);
|
|
427
447
|
}
|
|
428
448
|
}
|
|
429
449
|
|
package/lib/collectors/ai-api.js
CHANGED
|
@@ -26,11 +26,18 @@ const os = require("node:os");
|
|
|
26
26
|
const COLLECTOR_ID = "ai-api";
|
|
27
27
|
|
|
28
28
|
function readSafe(full, max = 256 * 1024) {
|
|
29
|
+
let fd;
|
|
29
30
|
try {
|
|
30
|
-
|
|
31
|
+
fd = fs.openSync(full, "r");
|
|
32
|
+
const s = fs.fstatSync(fd);
|
|
31
33
|
if (s.size > max) return null;
|
|
32
|
-
|
|
34
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
35
|
+
// fewer than s.size bytes on network/FUSE/sync-backed fds, which would
|
|
36
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
37
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
38
|
+
return fs.readFileSync(fd, "utf8");
|
|
33
39
|
} catch { return null; }
|
|
40
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
function fileExists(full) {
|
|
@@ -32,11 +32,18 @@ const OIDC_WALK_EXCLUDES = codeExcludeSet();
|
|
|
32
32
|
const COLLECTOR_ID = "cicd-pipeline-compromise";
|
|
33
33
|
|
|
34
34
|
function readSafe(p, max = 512 * 1024) {
|
|
35
|
+
let fd;
|
|
35
36
|
try {
|
|
36
|
-
|
|
37
|
+
fd = fs.openSync(p, "r");
|
|
38
|
+
const s = fs.fstatSync(fd);
|
|
37
39
|
if (s.size > max) return null;
|
|
38
|
-
|
|
40
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
41
|
+
// fewer than s.size bytes on network/FUSE/sync-backed fds, which would
|
|
42
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
43
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
44
|
+
return fs.readFileSync(fd, "utf8");
|
|
39
45
|
} catch { return null; }
|
|
46
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
function walkWorkflows(root) {
|
|
@@ -206,8 +213,14 @@ function scanWorkflow(content, rel) {
|
|
|
206
213
|
// Pull-request-from-forks (without target) requires runtime info
|
|
207
214
|
// to detect fork status — left to operator evidence.
|
|
208
215
|
if (hasPRTarget) {
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
// Compare the captured secret NAME exactly to GITHUB_TOKEN. An unanchored
|
|
217
|
+
// /secrets\.GITHUB_TOKEN/ substring test also matched custom secrets such
|
|
218
|
+
// as GITHUB_TOKEN_PROD, silently treating them as the built-in token and
|
|
219
|
+
// dropping a real fork-PR secret exposure (false negative).
|
|
220
|
+
const nonDefault = [];
|
|
221
|
+
for (const m of content.matchAll(/\$\{\{\s*secrets\.([A-Z_][A-Z0-9_]*)\s*\}\}/g)) {
|
|
222
|
+
if (m[1] !== "GITHUB_TOKEN") nonDefault.push(m[0]);
|
|
223
|
+
}
|
|
211
224
|
if (nonDefault.length > 0) {
|
|
212
225
|
hits["secret-exposed-to-fork-pr"].push({ file: rel, snippet: `pull_request_target + ${nonDefault.length} secrets.* reference(s)` });
|
|
213
226
|
}
|
|
@@ -243,7 +256,13 @@ function scanOidcPolicies(root) {
|
|
|
243
256
|
if (!e.isFile() || !/\.json$/i.test(e.name)) continue;
|
|
244
257
|
const text = readSafe(full);
|
|
245
258
|
if (!text) continue;
|
|
246
|
-
|
|
259
|
+
// The authoritative OIDC sub-claim detection is the self-anchored patterns
|
|
260
|
+
// below — each is bound to the leading `"` of the JSON key
|
|
261
|
+
// (`"token.actions.githubusercontent.com:sub"`), so a lookalike issuer
|
|
262
|
+
// such as `"eviltoken.actions…"` cannot match. No separate substring
|
|
263
|
+
// pre-filter on the issuer host: it added nothing the anchored patterns
|
|
264
|
+
// don't already enforce, and a bare host-substring check reads as an
|
|
265
|
+
// incomplete URL sanitization.
|
|
247
266
|
// sub-claim wildcards. Cover the three shapes the playbook lists.
|
|
248
267
|
const subWildcard =
|
|
249
268
|
/"token\.actions\.githubusercontent\.com:sub"\s*:\s*"\*"/.test(text) ||
|
|
@@ -26,11 +26,18 @@ const os = require("node:os");
|
|
|
26
26
|
const COLLECTOR_ID = "cred-stores";
|
|
27
27
|
|
|
28
28
|
function readSafe(full, max = 512 * 1024) {
|
|
29
|
+
let fd;
|
|
29
30
|
try {
|
|
30
|
-
|
|
31
|
+
fd = fs.openSync(full, "r");
|
|
32
|
+
const s = fs.fstatSync(fd);
|
|
31
33
|
if (s.size > max) return null;
|
|
32
|
-
|
|
34
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
35
|
+
// fewer than s.size bytes on network/FUSE/sync-backed fds, which would
|
|
36
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
37
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
38
|
+
return fs.readFileSync(fd, "utf8");
|
|
33
39
|
} catch { return null; }
|
|
40
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
function statSafe(full) {
|
|
@@ -133,8 +140,14 @@ function parseKubeConfig(content) {
|
|
|
133
140
|
let blockHasStatic = false;
|
|
134
141
|
for (const m of block.matchAll(userKvRe)) {
|
|
135
142
|
const upto = block.slice(0, m.index);
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
// Indentation-agnostic key anchors: a kubeconfig may use 2-space, 4-space,
|
|
144
|
+
// or tab indentation. Hardcoding "\n user:" missed a user-level static
|
|
145
|
+
// token under any other indentation and mislabeled it as an auth-provider
|
|
146
|
+
// (dynamic) token.
|
|
147
|
+
const userMatches = [...upto.matchAll(/\n[ \t]*user\s*:/g)];
|
|
148
|
+
const lastUserAt = userMatches.length ? userMatches[userMatches.length - 1].index : -1;
|
|
149
|
+
const apMatches = [...upto.matchAll(/\n[ \t]*auth-provider\s*:/g)];
|
|
150
|
+
const lastAuthProviderAt = apMatches.length ? apMatches[apMatches.length - 1].index : -1;
|
|
138
151
|
// Reject when the closest enclosing key is auth-provider rather
|
|
139
152
|
// than user — those are dynamic tokens, not static credentials.
|
|
140
153
|
if (lastAuthProviderAt > lastUserAt) continue;
|
package/lib/collectors/crypto.js
CHANGED
|
@@ -28,11 +28,18 @@ const { spawnSync } = require("node:child_process");
|
|
|
28
28
|
const COLLECTOR_ID = "crypto";
|
|
29
29
|
|
|
30
30
|
function readFileSafe(p, max = 256 * 1024) {
|
|
31
|
+
let fd;
|
|
31
32
|
try {
|
|
32
|
-
|
|
33
|
+
fd = fs.openSync(p, "r");
|
|
34
|
+
const st = fs.fstatSync(fd);
|
|
33
35
|
if (st.size > max) return null;
|
|
34
|
-
|
|
36
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
37
|
+
// fewer than st.size bytes on network/FUSE/sync-backed fds, which would
|
|
38
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
39
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
40
|
+
return fs.readFileSync(fd, "utf8");
|
|
35
41
|
} catch { return null; }
|
|
42
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
// Same sshd_config Include-expansion logic as the hardening
|
|
@@ -25,11 +25,18 @@ function readSysctl(p) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function readFileSafe(p, max = 256 * 1024) {
|
|
28
|
+
let fd;
|
|
28
29
|
try {
|
|
29
|
-
|
|
30
|
+
fd = fs.openSync(p, "r");
|
|
31
|
+
const st = fs.fstatSync(fd);
|
|
30
32
|
if (st.size > max) return null;
|
|
31
|
-
|
|
33
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
34
|
+
// fewer than st.size bytes on network/FUSE/sync-backed fds, which would
|
|
35
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
36
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
37
|
+
return fs.readFileSync(fd, "utf8");
|
|
32
38
|
} catch { return null; }
|
|
39
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
// Expand the base sshd_config into the effective directive stream by
|
|
@@ -43,11 +43,18 @@ const DEFAULT_MAX_DEPTH = 6;
|
|
|
43
43
|
const DEFAULT_EXCLUDES = codeExcludeSet();
|
|
44
44
|
|
|
45
45
|
function readSafe(full, max = 512 * 1024) {
|
|
46
|
+
let fd;
|
|
46
47
|
try {
|
|
47
|
-
|
|
48
|
+
fd = fs.openSync(full, "r");
|
|
49
|
+
const s = fs.fstatSync(fd);
|
|
48
50
|
if (s.size > max) return null;
|
|
49
|
-
|
|
51
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
52
|
+
// fewer than s.size bytes on network/FUSE/sync-backed fds, which would
|
|
53
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
54
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
55
|
+
return fs.readFileSync(fd, "utf8");
|
|
50
56
|
} catch { return null; }
|
|
57
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
function walkWorkflows(root) {
|
|
@@ -307,9 +314,23 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
307
314
|
scan: (text) => {
|
|
308
315
|
const j = JSON.parse(text);
|
|
309
316
|
let missing = 0;
|
|
317
|
+
// Only a REMOTE-tarball entry (`resolved` is an http(s)/git URL) is
|
|
318
|
+
// expected to carry an integrity hash. Workspace links — npm v3 emits
|
|
319
|
+
// them as `{ "resolved": "<relative-path>", "link": true }` — and
|
|
320
|
+
// file:/link:/workspace:/git: refs legitimately have NO integrity (a
|
|
321
|
+
// local symlink is not a registry artifact). Counting them flips
|
|
322
|
+
// lockfile-missing-integrity to a false hit on every npm-workspaces
|
|
323
|
+
// monorepo even when every registry dep is sha512-pinned. Mirrors
|
|
324
|
+
// sbom.js's local-ref demotion; the bare-relative-path + `link:true`
|
|
325
|
+
// shape is the canonical workspace link.
|
|
310
326
|
const walkObj = (obj) => {
|
|
311
327
|
if (!obj || typeof obj !== "object") return;
|
|
312
|
-
if (
|
|
328
|
+
if (
|
|
329
|
+
obj.resolved &&
|
|
330
|
+
obj.integrity == null &&
|
|
331
|
+
obj.link !== true &&
|
|
332
|
+
/^(?:https?:|git\+|git:|ssh:)/i.test(String(obj.resolved))
|
|
333
|
+
) missing++;
|
|
313
334
|
for (const v of Object.values(obj)) if (v && typeof v === "object") walkObj(v);
|
|
314
335
|
};
|
|
315
336
|
walkObj(j.packages || j.dependencies || {});
|
package/lib/collectors/mcp.js
CHANGED
|
@@ -31,11 +31,18 @@ const os = require("node:os");
|
|
|
31
31
|
const COLLECTOR_ID = "mcp";
|
|
32
32
|
|
|
33
33
|
function readSafe(full, max = 1024 * 1024) {
|
|
34
|
+
let fd;
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
+
fd = fs.openSync(full, "r");
|
|
37
|
+
const s = fs.fstatSync(fd);
|
|
36
38
|
if (s.size > max) return null;
|
|
37
|
-
|
|
39
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
40
|
+
// fewer than s.size bytes on network/FUSE/sync-backed fds, which would
|
|
41
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
42
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
43
|
+
return fs.readFileSync(fd, "utf8");
|
|
38
44
|
} catch { return null; }
|
|
45
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
function readJson(full) {
|
|
@@ -20,11 +20,18 @@ const path = require("node:path");
|
|
|
20
20
|
const COLLECTOR_ID = "runtime";
|
|
21
21
|
|
|
22
22
|
function readFileSafe(p, max = 512 * 1024) {
|
|
23
|
+
let fd;
|
|
23
24
|
try {
|
|
24
|
-
|
|
25
|
+
fd = fs.openSync(p, "r");
|
|
26
|
+
const s = fs.fstatSync(fd);
|
|
25
27
|
if (s.size > max) return null;
|
|
26
|
-
|
|
28
|
+
// readFileSync(fd) loops read() to EOF — a single readSync may return
|
|
29
|
+
// fewer than s.size bytes on network/FUSE/sync-backed fds, which would
|
|
30
|
+
// leave the buffer tail NUL-filled and silently drop trailing content.
|
|
31
|
+
// Reading via the already-open fd keeps the fstat-then-read TOCTOU-free.
|
|
32
|
+
return fs.readFileSync(fd, "utf8");
|
|
27
33
|
} catch { return null; }
|
|
34
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } } }
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
const NOPASSWD_WILDCARD_RE = /\bNOPASSWD:\s*(?:ALL|\/[^,\n]*\*)/;
|
package/lib/collectors/sbom.js
CHANGED
|
@@ -283,22 +283,35 @@ function findSbomDocuments(cwd) {
|
|
|
283
283
|
const found = [];
|
|
284
284
|
for (const s of SBOM_FORMATS) {
|
|
285
285
|
const p = path.join(cwd, s.file);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
286
|
+
let fd;
|
|
287
|
+
// Open once and fstat the descriptor instead of existsSync→statSync→read:
|
|
288
|
+
// an absent file (ENOENT) is skipped just as the existsSync(false) path did,
|
|
289
|
+
// but there is no TOCTOU window between the check, the size stat, and the read.
|
|
290
|
+
try { fd = fs.openSync(p, "r"); }
|
|
291
|
+
catch (e) { if (e.code === "ENOENT") continue; found.push({ file: s.file, format: s.format, error: e.message }); continue; }
|
|
292
|
+
try {
|
|
293
|
+
const stat = fs.fstatSync(fd);
|
|
294
|
+
let content;
|
|
295
|
+
// Read from the already-open descriptor (no re-open by path → no
|
|
296
|
+
// TOCTOU window, preserving the fstat-on-fd hardening). readFileSync(fd)
|
|
297
|
+
// loops internally to EOF; a single readSync(fd, buf, …, stat.size) does
|
|
298
|
+
// NOT — it can return a short read (network/FUSE mount, signal), leaving
|
|
299
|
+
// the buffer NUL-padded and truncating valid JSON, so JSON.parse would
|
|
300
|
+
// throw and component_count silently fall back to null on a present,
|
|
301
|
+
// parseable SBOM. The fd stays open for the finally{} closeSync.
|
|
302
|
+
try { content = fs.readFileSync(fd, "utf8"); } catch { content = null; }
|
|
303
|
+
let component_count = null;
|
|
304
|
+
if (content && s.format === "cyclonedx-1.x") {
|
|
305
|
+
try {
|
|
306
|
+
const j = JSON.parse(content);
|
|
307
|
+
component_count = (j.components || []).length;
|
|
308
|
+
} catch {}
|
|
301
309
|
}
|
|
310
|
+
found.push({ file: s.file, format: s.format, size_bytes: stat.size, component_count });
|
|
311
|
+
} catch (e) {
|
|
312
|
+
found.push({ file: s.file, format: s.format, error: e.message });
|
|
313
|
+
} finally {
|
|
314
|
+
if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* non-fatal */ } }
|
|
302
315
|
}
|
|
303
316
|
}
|
|
304
317
|
return found;
|