@clear-capabilities/agentic-security-scanner 0.148.0 → 0.148.3
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 +150 -0
- package/bin/agentic-security.js +5 -1
- package/dist/4970.index.js +58 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +2 -0
- package/dist/compliance-frameworks/eu-ai-act.json +12 -16
- package/dist/compliance-frameworks/gdpr.json +8 -6
- package/dist/compliance-frameworks/hipaa-security-rule.json +9 -9
- package/dist/compliance-frameworks/nist-800-171-r3.json +6 -5
- package/dist/compliance-frameworks/nist-ai-600-1.json +6 -4
- package/dist/compliance-frameworks/nist-csf-2.json +6 -5
- package/dist/compliance-frameworks/nist-privacy-1-1.json +20 -10
- package/dist/compliance-frameworks/owasp-asvs-5.json +2 -0
- package/dist/compliance-frameworks/owasp-llm-top-10.json +10 -11
- package/package.json +5 -4
- package/src/engine.js +51 -4
- package/src/pipeline/assurance-mode.js +58 -2
- package/src/posture/accuracy-scorecard.js +34 -0
- package/src/posture/aibom.js +22 -0
- package/src/posture/artifact-registry.js +2 -0
- package/src/posture/auditor-walkthrough.js +91 -35
- package/src/posture/compliance-frameworks/ccpa.json +2 -0
- package/src/posture/compliance-frameworks/eu-ai-act.json +12 -16
- package/src/posture/compliance-frameworks/gdpr.json +8 -6
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +9 -9
- package/src/posture/compliance-frameworks/nist-800-171-r3.json +6 -5
- package/src/posture/compliance-frameworks/nist-ai-600-1.json +6 -4
- package/src/posture/compliance-frameworks/nist-csf-2.json +6 -5
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +20 -10
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +2 -0
- package/src/posture/compliance-frameworks/owasp-llm-top-10.json +10 -11
- package/src/posture/threat-model.js +20 -3
- package/src/posture/verifier.js +70 -0
- package/src/sast/python-sinks.js +24 -1
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
"publisher": "NIST",
|
|
5
5
|
"license": "public-domain (US Federal publication)",
|
|
6
6
|
"url": "https://csrc.nist.gov/pubs/sp/800/171/r3/final",
|
|
7
|
+
"sourceDoi": "https://doi.org/10.6028/NIST.SP.800-171r3",
|
|
8
|
+
"sourcePdfSha256": "3e4631df8b5d61f40a6e542b52779ef30ddbbfff31e09214fa94ad6e6f5e6d08",
|
|
9
|
+
"sourceVerificationNote": "Verified 2026-09-07: the CSRC landing page URL resolves and states title \"Protecting Controlled Unclassified Information in Nonfederal Systems and Organizations\", Rev. 3, published May 2024. The DOI resolves (302) to the PDF at nvlpubs.nist.gov, whose own embedded XMP metadata (pdfx:DOI, pdfx:Comments) confirms NIST.SP.800-171r3. sourcePdfSha256 is the SHA-256 of that exact downloaded PDF, so a future re-check can detect if NIST republishes under the same URL/DOI with different content. This does not verify the CSV control export against the PDF text control-by-control — see scripts/nist-800-171/README for that gap.",
|
|
7
10
|
"note": "Control text is generated from docs/standards/NIST_SP_800_171r3_Controls.csv by scripts/nist-800-171/build-catalog.py. The codeTestable rating is THIS ENGINE'S judgment, not NIST's. Unlike the AI 600-1 workbook, the 800-171 export rates no control for testability. Ratings and their rationales live in scripts/nist-800-171/code-testability.json.",
|
|
8
11
|
"scope": "FULL CATALOGUE — all 97 requirements of NIST SP 800-171 Rev. 3 are carried, deliberately including those this engine cannot assess, because omitting them would read as coverage. Automated evidence is PARTIAL and unevenly distributed: 16 requirements are rated code-testable, 38 partial, and 43 are organisational, physical or personnel controls NOT represented by any automated signal: all of Awareness and Training, Personnel Security and Physical Protection, and most of Incident Response, Maintenance and Media Protection. Those are forced to 'partial' with an explicit disclosure and can never read as satisfied. This is not a CMMC assessment and produces no SPRS score.",
|
|
9
|
-
"controlsDigest": "
|
|
12
|
+
"controlsDigest": "927295dcd1594189",
|
|
10
13
|
"controlCount": 97,
|
|
11
14
|
"controls": [
|
|
12
15
|
{
|
|
@@ -330,10 +333,8 @@
|
|
|
330
333
|
"summary": "Protection of Audit Information: a. Protect audit information and audit logging tools from unauthorized access, modification, and deletion. b. Authorize access to management of audit logging functionality to only a subset of…",
|
|
331
334
|
"codeTestable": "partial",
|
|
332
335
|
"evidence": [
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
"mapsTo": [
|
|
336
|
-
"module:integrity"
|
|
336
|
+
"No automated signal in this engine — this tool can sign and verify its OWN scan output (module:integrity), but that evidences this scanner's integrity, not whether the SCANNED PROJECT protects its own audit logs from unauthorized access, modification or deletion. No detector here inspects the scanned codebase for log-signing, tamper-evident storage, or access controls on audit data. Removed after a second-review pass found the original mapping was a category error (self-referential: about this tool, not the target).",
|
|
337
|
+
"Reported as an engine gap: this requirement is code-observable in principle (a detector could look for tamper-evident logging libraries or access-controlled log stores in application code), but no detector here decides it."
|
|
337
338
|
]
|
|
338
339
|
},
|
|
339
340
|
{
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
"publisher": "NIST",
|
|
5
5
|
"license": "public-domain (US Federal publication)",
|
|
6
6
|
"url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.600-1.pdf",
|
|
7
|
+
"sourceDoi": "https://doi.org/10.6028/NIST.AI.600-1",
|
|
8
|
+
"sourcePdfSha256": "6e73620ab6b64e90ef2c04bf0e0d6246185a2f4b1b13cab0df494496cff89b6a",
|
|
9
|
+
"sourceVerificationNote": "Verified 2026-09-07: the doi.org DOI resolves (302) to this exact url, and the downloaded PDF's own embedded XMP metadata (pdfx:Category, dc:title) confirms \"NIST AI 600-1\" / \"Artificial Intelligence Risk Management Framework: Generative Artificial Intelligence Profile\". sourcePdfSha256 is the SHA-256 of that exact downloaded PDF, so a future re-check can detect if NIST republishes under the same URL/DOI with different content.",
|
|
7
10
|
"scope": "SELECTIVE SUBSET. 6 actions from the Generative AI Profile with a code-observable signal. The profile is far larger; governance, provenance and incident-response actions are organisational and are NOT represented here. The full control catalogue is built and gated separately by scripts/nist-compliance/build-catalog.py.",
|
|
8
|
-
"controlsDigest": "
|
|
11
|
+
"controlsDigest": "6e7cc01c5ba8ae22",
|
|
9
12
|
"controlCount": 6,
|
|
10
13
|
"controls": [
|
|
11
14
|
{
|
|
@@ -84,11 +87,10 @@
|
|
|
84
87
|
"summary": "Establish content provenance / authenticity controls.",
|
|
85
88
|
"codeTestable": "partial",
|
|
86
89
|
"evidence": [
|
|
87
|
-
"
|
|
88
|
-
"
|
|
90
|
+
"ATTRIBUTIONS.md generated.",
|
|
91
|
+
"Removed module:sigstore-verify after a second-review pass (adversarial premortem Q1, 2026-09-07): sigstore-attestations/ is never written anywhere in this codebase — see the identical removal note on hipaa-security-rule.json's §164.312(c) for the full reasoning (home-directory cache by design, fire-and-forget async producer, not a quick artifact-path fix). Content-provenance / authenticity controls for model loads remain only partially evidenced by this control's surviving module:license-attributions leg."
|
|
89
92
|
],
|
|
90
93
|
"mapsTo": [
|
|
91
|
-
"module:sigstore-verify",
|
|
92
94
|
"module:license-attributions"
|
|
93
95
|
]
|
|
94
96
|
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
"publisher": "NIST",
|
|
5
5
|
"license": "public-domain (US Federal publication)",
|
|
6
6
|
"url": "https://www.nist.gov/cyberframework",
|
|
7
|
+
"sourceVerifiedAt": "2026-09-07",
|
|
8
|
+
"sourceVerificationNote": "Verified 2026-09-07 (WebFetch): confirmed NIST Cybersecurity Framework, CSF 2.0 stated as current throughout the page, matching this bundle. No content hash pinned: this is a resource hub page (quick-start guides, profiles, informative references, a downloadable PDF), not a single fixed document.",
|
|
7
9
|
"scope": "SELECTIVE SUBSET. 8 subcategories with a code-observable signal. CSF 2.0 has over 100 subcategories; GOVERN in particular is organisational and is NOT represented here.",
|
|
8
|
-
"controlsDigest": "
|
|
10
|
+
"controlsDigest": "1df76b5bde1d0044",
|
|
9
11
|
"controlCount": 8,
|
|
10
12
|
"controls": [
|
|
11
13
|
{
|
|
@@ -45,12 +47,11 @@
|
|
|
45
47
|
"summary": "Vulnerabilities and threats are identified.",
|
|
46
48
|
"codeTestable": "partial",
|
|
47
49
|
"evidence": [
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
+
"ATT&CK / ATLAS technique mapping on findings (last-scan.json).",
|
|
51
|
+
"Removed module:exploitability-probability (adversarial premortem Q2, 2026-09-07): that name is not in the ARTIFACT vocabulary table — exploitability-probability.js is a real, live annotator (posture/exploitability-probability.js), but it sets fields directly on findings in memory and writes no standalone artifact of its own; its output lands in last-scan.json, the exact file the surviving module:attack-taxonomy leg on this control already checks. This mapping could never read 'present' via this leg, on any project, since it shipped — removed rather than pointed at a duplicate check with no new signal."
|
|
50
52
|
],
|
|
51
53
|
"mapsTo": [
|
|
52
|
-
"module:attack-taxonomy"
|
|
53
|
-
"module:exploitability-probability"
|
|
54
|
+
"module:attack-taxonomy"
|
|
54
55
|
]
|
|
55
56
|
},
|
|
56
57
|
{
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"publisher": "NIST",
|
|
5
5
|
"license": "public-domain (US Federal publication)",
|
|
6
6
|
"url": "https://www.nist.gov/privacy-framework",
|
|
7
|
-
"note": "Each control carries
|
|
8
|
-
"scope": "FULL CATALOGUE, partially assessable. All 104 controls are present and each carries `codeTestable
|
|
9
|
-
"controlsDigest": "
|
|
7
|
+
"note": "Each control carries a `codeTestable` rating (yes/partial/no). This rating is HAND-AUTHORED BY THIS PROJECT, not NIST: the Privacy Framework document ships no machine-readable testability column (unlike NIST AI 600-1, whose source workbook has one, read by scripts/nist-compliance/build-catalog.py). The rating says whether a control COULD be assessed from source code, in this project's judgment, it does not say this engine assesses it, and it is not NIST's own claim. `mapsTo` is present only where this engine produces a real signal; a control rated testable but which this engine cannot see is reported as an ENGINE GAP rather than silently passed.",
|
|
8
|
+
"scope": "FULL CATALOGUE, partially assessable, transcribed from an Initial Public Draft (IPD) NOT yet finalized by NIST as of the last verification (see sourceVerificationNote). All 104 controls are present and each carries `codeTestable`, hand-authored by this project (see `note`), NOT a NIST-published rating. 48 are governance controls no scanner can assess; those report `manual`, never `satisfied`. See posture/privacy-framework.js.",
|
|
9
|
+
"controlsDigest": "3b91775abd5ffc1b",
|
|
10
10
|
"controlCount": 104,
|
|
11
11
|
"controls": [
|
|
12
12
|
{
|
|
@@ -494,8 +494,11 @@
|
|
|
494
494
|
"summary": "Log records are determined, documented, implemented, and reviewed in accordance with policy and incorporating the principle of data minimization.",
|
|
495
495
|
"codeTestable": "partial",
|
|
496
496
|
"mapsTo": [
|
|
497
|
-
"module:scan-history"
|
|
498
|
-
|
|
497
|
+
"module:scan-history"
|
|
498
|
+
],
|
|
499
|
+
"evidence": [
|
|
500
|
+
"Scan history retained.",
|
|
501
|
+
"Removed module:mcp-audit after a second-review pass: mcp-audit.log records calls to THIS tool's own MCP server, not the organisation's own log-record policy and review practice. Category error (self-referential: about this tool, not the target)."
|
|
499
502
|
]
|
|
500
503
|
},
|
|
501
504
|
{
|
|
@@ -605,8 +608,10 @@
|
|
|
605
608
|
"category": "Data Processing Awareness (CM.AW-P)",
|
|
606
609
|
"summary": "Records of data disclosures and sharing are maintained and can be accessed for review or transmission/disclosure.",
|
|
607
610
|
"codeTestable": "yes",
|
|
608
|
-
"mapsTo": [
|
|
609
|
-
|
|
611
|
+
"mapsTo": [],
|
|
612
|
+
"evidence": [
|
|
613
|
+
"No automated signal in this engine after a second-review pass removed the only prior mapping. module:mcp-audit pointed at THIS scanner's own MCP call log, not at the organisation's own records of data disclosures and sharing. Category error (self-referential: about this tool, not the target).",
|
|
614
|
+
"Reported as an engine gap: NIST rates this control code-testable (data-disclosure records could in principle be evidenced from a disclosure-log artifact in the scanned project), but no detector here looks for one."
|
|
610
615
|
]
|
|
611
616
|
},
|
|
612
617
|
{
|
|
@@ -623,8 +628,11 @@
|
|
|
623
628
|
"summary": "Data provenance and lineage are maintained and can be accessed for review or transmission/disclosure.",
|
|
624
629
|
"codeTestable": "partial",
|
|
625
630
|
"mapsTo": [
|
|
626
|
-
"module:sbom-diff"
|
|
627
|
-
|
|
631
|
+
"module:sbom-diff"
|
|
632
|
+
],
|
|
633
|
+
"evidence": [
|
|
634
|
+
"SBOM diff history retained.",
|
|
635
|
+
"Removed module:integrity after a second-review pass: last-scan.json.sig is this scanner signing its OWN scan output, not evidence of the organisation's own data provenance and lineage tracking. Category error (self-referential: about this tool, not the target), same class as the 03.03.08 fix in the NIST 800-171 framework."
|
|
628
636
|
]
|
|
629
637
|
},
|
|
630
638
|
{
|
|
@@ -845,5 +853,7 @@
|
|
|
845
853
|
"summary": "Adequate resource capacity to ensure availability is maintained.",
|
|
846
854
|
"codeTestable": "yes"
|
|
847
855
|
}
|
|
848
|
-
]
|
|
856
|
+
],
|
|
857
|
+
"sourceVerifiedAt": "2026-09-07",
|
|
858
|
+
"sourceVerificationNote": "Verified 2026-09-07 (WebFetch, two independent prompts against https://www.nist.gov/privacy-framework): NIST Privacy Framework 1.1 is currently an Initial Public Draft (IPD), not a finalized standard, per the page's own \"PF 1.1 IPD\" labeling. Version 1.0 is NIST's current final release. This bundled framework's 104 controls were transcribed from the 1.1 IPD text; if NIST finalizes 1.1 with control-text or numbering changes, this file will not automatically reflect them and needs a re-check against the final publication when it ships. No sourcePdfSha256 is pinned because the source is a navigation hub page, not a single fixed document, and because a draft is expected to change before finalization, unlike a published SP."
|
|
849
859
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"publisher": "OWASP Foundation",
|
|
5
5
|
"license": "Creative Commons Attribution-ShareAlike 4.0",
|
|
6
6
|
"url": "https://owasp.org/www-project-application-security-verification-standard/",
|
|
7
|
+
"sourceVerifiedAt": "2026-09-07",
|
|
8
|
+
"sourceVerificationNote": "Verified 2026-09-07 (WebFetch): confirmed OWASP ASVS, latest stable release listed as 5.0.0, matching this bundle (owasp-asvs-5), released at Global AppSec EU Barcelona 2025-05-30. No content hash pinned: this is a project navigation page linking to a versioned downloads page, not the standard itself.",
|
|
7
9
|
"scope": "SELECTIVE SUBSET. 10 verification requirements with a direct detector behind them. ASVS 5.0 has several hundred requirements across 17 chapters; the remainder are NOT represented here.",
|
|
8
10
|
"controlsDigest": "6c4371a08f010f91",
|
|
9
11
|
"controlCount": 10,
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
"publisher": "OWASP Foundation",
|
|
5
5
|
"license": "Creative Commons Attribution-ShareAlike 4.0",
|
|
6
6
|
"url": "https://owasp.org/www-project-top-10-for-large-language-model-applications/",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"sourceVerifiedAt": "2026-09-07",
|
|
8
|
+
"sourceVersionDriftDetected": true,
|
|
9
|
+
"sourceVerificationNote": "Verified 2026-09-07 (WebFetch): confirmed page is OWASP Top 10 for LLM Applications, but the CURRENT published edition is now 2026 (2026-08-04), not the 2025 edition this file bundles. See scope field for the disclosure and what remediation this requires.",
|
|
10
|
+
"scope": "FULL TOP-10. All ten categories are present. Each category is a broad class rather than a single testable requirement, so a mapping indicates a detector exists for part of the class, not that the class is fully covered. VERSION DRIFT, disclosed 2026-09-07: this bundle is the 2025 edition. OWASP published a 2026 edition on 2026-08-04 (confirmed live, the project page now archives 2025 and lists 2026 as current); the 10-category taxonomy may have changed. This file has NOT been re-mapped to the 2026 edition, that requires a deliberate review of the new categories against this engine's detector families, not a rushed relabel. Treat this bundle as 2025-accurate only until that review lands.",
|
|
11
|
+
"controlsDigest": "7500b9c4268eba6f",
|
|
9
12
|
"controlCount": 10,
|
|
10
13
|
"controls": [
|
|
11
14
|
{
|
|
@@ -101,11 +104,10 @@
|
|
|
101
104
|
"summary": "Excessive Agency — agent autonomy is bounded by explicit guards.",
|
|
102
105
|
"codeTestable": "partial",
|
|
103
106
|
"evidence": [
|
|
104
|
-
"
|
|
105
|
-
"
|
|
107
|
+
"Apply-fix path requires HMAC-verified scan.",
|
|
108
|
+
"Removed module:mcp-tools after a second-review pass: that path points at THIS scanner's own source file (scanner/src/mcp/tools.js), which only resolves inside this tool's own checkout, never inside an arbitrary assessed project. It cannot evidence that the assessed LLM application bounds ITS OWN agent's autonomy. Category error (self-referential: about this tool, not the target)."
|
|
106
109
|
],
|
|
107
110
|
"mapsTo": [
|
|
108
|
-
"module:mcp-tools",
|
|
109
111
|
"module:apply-fix"
|
|
110
112
|
]
|
|
111
113
|
},
|
|
@@ -114,13 +116,10 @@
|
|
|
114
116
|
"summary": "Overreliance — human review checkpoints exist.",
|
|
115
117
|
"codeTestable": "partial",
|
|
116
118
|
"evidence": [
|
|
117
|
-
"security-fixer agent
|
|
118
|
-
"
|
|
119
|
+
"No automated signal in this engine after a second-review pass removed both prior mappings. module:security-fixer pointed at THIS scanner's own agent definition file (agents/security-fixer.md) and module:pre-edit-bodyguard at THIS scanner's own installed hook — both resolve only inside this tool's own checkout/plugin install, never inside an arbitrary assessed project, and neither evidences that the assessed LLM application itself has human review checkpoints. Category error (self-referential: about this tool, not the target), same class as the 03.03.08 fix in the NIST 800-171 framework.",
|
|
120
|
+
"Reported as an engine gap: human review checkpoints around an LLM application's outputs are code-observable in principle (an approval-gate library, a human-in-the-loop wrapper in the assessed system's own code), but no detector here looks for that in the SCANNED project."
|
|
119
121
|
],
|
|
120
|
-
"mapsTo": [
|
|
121
|
-
"module:security-fixer",
|
|
122
|
-
"module:pre-edit-bodyguard"
|
|
123
|
-
]
|
|
122
|
+
"mapsTo": []
|
|
124
123
|
},
|
|
125
124
|
{
|
|
126
125
|
"id": "LLM10",
|
|
@@ -32,7 +32,13 @@ const ASSET_PATTERNS = [
|
|
|
32
32
|
// [regex, category, exposure]
|
|
33
33
|
[/(?:stripe|paddle|braintree)\.\w+\.create/g, 'payment-method', 'public-api'],
|
|
34
34
|
[/(?:User|users?)\.create\(|prisma\.user\.create/g, 'identity', 'public-api'],
|
|
35
|
-
|
|
35
|
+
// Real customer false positive: `session = requests.Session()` matched —
|
|
36
|
+
// the pattern only checked the LHS identifier, so an HTTP CLIENT object
|
|
37
|
+
// (requests.Session / aiohttp.ClientSession / httpx.Client) got reported
|
|
38
|
+
// as an "authenticated user session" asset. The negative lookahead
|
|
39
|
+
// excludes the specific RHS constructor shapes that mean "this is an
|
|
40
|
+
// outbound HTTP client", not "this holds a logged-in user's session".
|
|
41
|
+
[/(?:session|token|jwt)\s*=\s*(?!\s*(?:requests\.Session|aiohttp\.ClientSession|httpx\.(?:Client|AsyncClient))\s*\()/g, 'session', 'internal'],
|
|
36
42
|
[/process\.env\.([A-Z_]+(?:KEY|SECRET|TOKEN))/g, 'secret', 'internal'],
|
|
37
43
|
[/(?:aws-sdk|@aws-sdk).*\.upload|s3\.put/g, 'object-storage', 'public-api'],
|
|
38
44
|
[/(?:openai|anthropic|together|groq)\.(?:chat|messages|completions)/g, 'llm-egress', 'external-api'],
|
|
@@ -43,8 +49,19 @@ const TRUST_BOUNDARY_PATTERNS = [
|
|
|
43
49
|
[/router\.(?:get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g, 'http-route'],
|
|
44
50
|
[/@(?:Get|Post|Put|Patch|Delete)Mapping\s*\(/g, 'http-route-java'],
|
|
45
51
|
[/@(?:app|router)\.(?:get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g, 'http-route-py'],
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
// Real customer false positive: alternation without a shared group scopes
|
|
53
|
+
// ONLY the first branch to a queue library — `(?:kafka|pubsub|sqs|sns)\.produce
|
|
54
|
+
// |\.publish|\.sendMessage` is three TOP-LEVEL alternatives, and the last
|
|
55
|
+
// two are bare, unscoped substrings. `bundle.publisher_domain` and
|
|
56
|
+
// `bundle.published_at` both contain the literal substring ".publish" (the
|
|
57
|
+
// first 8 characters of "publisher"/"published"), so plain field access
|
|
58
|
+
// with no queue library, and no method call at all, was read as a
|
|
59
|
+
// queue-producer trust boundary. Moving the method-name alternation
|
|
60
|
+
// INSIDE the shared `(?:kafka|pubsub|sqs|sns)\.` prefix scopes every
|
|
61
|
+
// branch to an actual queue client, and requiring a trailing `(` excludes
|
|
62
|
+
// attribute access from matching a call-shaped rule.
|
|
63
|
+
[/(?:kafka|pubsub|sqs|sns)\.(?:consume|subscribe|receiveMessage)\s*\(/gi, 'queue-consumer'],
|
|
64
|
+
[/(?:kafka|pubsub|sqs|sns)\.(?:produce|publish|sendMessage)\s*\(/gi, 'queue-producer'],
|
|
48
65
|
[/grpc\.Server|new\s+Server\s*\(\s*\)/g, 'grpc-server'],
|
|
49
66
|
[/(?:db|pool|client)\.(?:query|execute|raw)\s*\(/g, 'db-edge'],
|
|
50
67
|
];
|
package/src/posture/verifier.js
CHANGED
|
@@ -32,6 +32,7 @@ import * as path from 'node:path';
|
|
|
32
32
|
import * as os from 'node:os';
|
|
33
33
|
import { runConfined, sandboxAvailable } from '../sandbox/index.js';
|
|
34
34
|
import { isExplicitlyNoPoc } from './poc-cwe-map.js';
|
|
35
|
+
import { isSafeStateDir, stateDir, statePath, stateWritesEnabled } from './state-dir.js';
|
|
35
36
|
|
|
36
37
|
// ─── PoC static validation ──────────────────────────────────────────────────
|
|
37
38
|
//
|
|
@@ -228,5 +229,74 @@ export function verifierCoverageSummary(findings) {
|
|
|
228
229
|
return out;
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
// ─── Run persistence (adversarial premortem Q1, 2026-09-07) ────────────────
|
|
233
|
+
//
|
|
234
|
+
// `compliance-frameworks/*.json` has mapped `module:verifier` to
|
|
235
|
+
// `verifier-runs/` since this framework's control-to-artifact table was
|
|
236
|
+
// written, but nothing ever wrote to it: `cmdVerify` (bin/agentic-security.js)
|
|
237
|
+
// ran the loop and printed a summary, but never persisted one. That made
|
|
238
|
+
// NIST 800-171 `03.12.01` (Security Assessment) and NIST CSF 2.0 `RC.RP`
|
|
239
|
+
// (Recovery plans executed and improved) permanently unable to read
|
|
240
|
+
// 'present' via this leg of their mapping, on any project, ever — worse than
|
|
241
|
+
// a self-referential mapping (which can at least clear, dishonestly), a
|
|
242
|
+
// mapping that can never clear at all. This closes that gap for real, one
|
|
243
|
+
// record per `agentic-security verify` run, same shape as fix-metrics.js's
|
|
244
|
+
// append-per-attempt pattern (that file uses one growing JSONL; this one
|
|
245
|
+
// uses one file per run, because the table entry it satisfies is a
|
|
246
|
+
// directory, not a file, and a per-run file is what a reader would expect
|
|
247
|
+
// under a name like "verifier-runs/").
|
|
248
|
+
const RUN_DIR = 'verifier-runs';
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Persist one record of an `agentic-security verify` invocation. Best-effort
|
|
252
|
+
* and silent on failure, same convention as every other posture writer: a
|
|
253
|
+
* verify run's own findings must never be lost because the record of having
|
|
254
|
+
* run could not be written.
|
|
255
|
+
*
|
|
256
|
+
* @returns {boolean} whether the record was written (for tests, not callers).
|
|
257
|
+
*/
|
|
258
|
+
export function recordVerifierRun(scanRoot, summary) {
|
|
259
|
+
if (!scanRoot || !summary || typeof summary !== 'object') return false;
|
|
260
|
+
try {
|
|
261
|
+
const dir = stateDir(scanRoot);
|
|
262
|
+
if (!isSafeStateDir(dir)) return false;
|
|
263
|
+
if (!stateWritesEnabled()) return false;
|
|
264
|
+
const runsDir = statePath(scanRoot, RUN_DIR);
|
|
265
|
+
fs.mkdirSync(runsDir, { recursive: true });
|
|
266
|
+
const now = new Date();
|
|
267
|
+
// Millisecond Date timestamps collide under back-to-back calls (two
|
|
268
|
+
// `verify` invocations in the same test, or a scripted loop). hrtime is
|
|
269
|
+
// monotonic and nanosecond-resolution within this process, so appending
|
|
270
|
+
// it guarantees both a distinct filename AND correct chronological sort
|
|
271
|
+
// order (alphabetical == call order), which a random suffix alone would
|
|
272
|
+
// not: two records written in the same millisecond would sort by random
|
|
273
|
+
// bytes, not by which call happened first.
|
|
274
|
+
const stamp = now.toISOString().replace(/[^0-9TZ]/g, '-');
|
|
275
|
+
const seq = process.hrtime.bigint().toString().padStart(20, '0');
|
|
276
|
+
const record = { timestamp: now.toISOString(), ...summary };
|
|
277
|
+
// One writeFileSync of one complete file: a concurrent reader sees a
|
|
278
|
+
// whole record or nothing (never a partial one, unlike an append target).
|
|
279
|
+
fs.writeFileSync(path.join(runsDir, `${stamp}-${seq}.json`), JSON.stringify(record, null, 2), 'utf8');
|
|
280
|
+
return true;
|
|
281
|
+
} catch { return false; }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Read every well-formed run record, oldest first. A file that does not
|
|
286
|
+
* parse is skipped, not thrown on — same tolerant-read convention as
|
|
287
|
+
* fix-metrics.js's readFixAttempts.
|
|
288
|
+
*/
|
|
289
|
+
export function readVerifierRuns(scanRoot) {
|
|
290
|
+
try {
|
|
291
|
+
const runsDir = statePath(scanRoot, RUN_DIR);
|
|
292
|
+
const files = fs.readdirSync(runsDir).filter((f) => f.endsWith('.json')).sort();
|
|
293
|
+
const out = [];
|
|
294
|
+
for (const f of files) {
|
|
295
|
+
try { out.push(JSON.parse(fs.readFileSync(path.join(runsDir, f), 'utf8'))); } catch { /* skip malformed */ }
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
} catch { return []; }
|
|
299
|
+
}
|
|
300
|
+
|
|
231
301
|
// For tests and the no-dead-modules check.
|
|
232
302
|
export const _internals = { MAX_POC_BYTES, BANNED_HOSTS, SANITIZER_TABLE, runSandboxed };
|
package/src/sast/python-sinks.js
CHANGED
|
@@ -41,7 +41,30 @@ const SQLA_RAW_EXEC_CONCAT_RE = /\b(?:cursor|conn|connection|session)\s*\.\s*exe
|
|
|
41
41
|
// quotes (single quotes inside double-quoted f-string and vice versa) so we
|
|
42
42
|
// use two parallel patterns rather than a single character class that excludes
|
|
43
43
|
// both quote kinds.
|
|
44
|
-
|
|
44
|
+
//
|
|
45
|
+
// Two real-world false positives, both from the same underlying defect —
|
|
46
|
+
// this regex checked for a co-occurring keyword-shaped substring and a `{}`
|
|
47
|
+
// interpolation, not an actual SQL statement being assigned:
|
|
48
|
+
//
|
|
49
|
+
// query = f"SELECT * FROM users WHERE id = {user_id}" -> should fire
|
|
50
|
+
// print(f"[{position}] {len(selected)} readable pages") -> was firing
|
|
51
|
+
// raise RuntimeError(f"Selected TAR members exceed {N} bytes") -> was firing
|
|
52
|
+
//
|
|
53
|
+
// Case-insensitive matching with no word boundary let the SQL keyword
|
|
54
|
+
// alternation match inside ordinary English/identifier text — "Select" is
|
|
55
|
+
// the first six letters of "Selected", and "select" is the first six of the
|
|
56
|
+
// variable name "selected". `\b...\b` fixes that: a word boundary requires a
|
|
57
|
+
// non-word/word transition, which does not exist between "Select" and the
|
|
58
|
+
// trailing "ed" (both are word characters), so "Selected"/"selected" no
|
|
59
|
+
// longer match while "SELECT"/"select" as complete tokens still do.
|
|
60
|
+
//
|
|
61
|
+
// The vuln title claims "f-string SQL ASSIGNED TO VARIABLE", but the regex
|
|
62
|
+
// never actually checked for an assignment — it fired on any f-string
|
|
63
|
+
// anywhere, including a bare argument to print()/raise. Requiring
|
|
64
|
+
// `IDENT = ` (optionally through an opening paren, for a wrapped assignment)
|
|
65
|
+
// immediately before the f-string makes the check match what its own name
|
|
66
|
+
// promises, and excludes the print()/raise-message shape entirely.
|
|
67
|
+
const SQLA_FSTRING_SQL_ASSIGN_RE = /\b[A-Za-z_]\w*\s*=\s*\(?\s*(?:f"[^"]*\b(?:SELECT|INSERT|UPDATE|DELETE)\b[^"]*\{[^}]*\}|f'[^']*\b(?:SELECT|INSERT|UPDATE|DELETE)\b[^']*\{[^}]*\})/gi;
|
|
45
68
|
|
|
46
69
|
// ─── Command injection ────────────────────────────────────────────────────
|
|
47
70
|
//
|