@clear-capabilities/agentic-security-scanner 0.134.0 → 0.136.9
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 +432 -0
- package/bin/agentic-security-audit.js +2 -1
- package/bin/agentic-security-consistency.js +2 -1
- package/bin/agentic-security.js +448 -74
- package/dist/113.index.js +16 -7
- package/dist/117.index.js +3 -1
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +5 -4
- package/dist/220.index.js +5 -3
- package/dist/238.index.js +4 -4
- package/dist/317.index.js +300 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +196 -21
- package/dist/444.index.js +20 -11
- package/dist/449.index.js +8 -1
- package/dist/513.index.js +7 -3
- package/dist/526.index.js +6 -6
- package/dist/637.index.js +1 -1
- package/dist/675.index.js +7 -5
- package/dist/839.index.js +4 -3
- package/dist/905.index.js +1173 -0
- package/dist/agentic-security.mjs +14 -14
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +32 -0
- package/dist/compliance-frameworks/eu-ai-act.json +51 -0
- package/dist/compliance-frameworks/gdpr.json +45 -0
- package/dist/compliance-frameworks/hipaa-security-rule.json +56 -0
- package/dist/compliance-frameworks/nist-ai-600-1.json +51 -0
- package/dist/compliance-frameworks/nist-csf-2.json +73 -0
- package/dist/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +79 -0
- package/dist/compliance-frameworks/owasp-llm-top-10.json +69 -0
- package/package.json +24 -12
- package/src/badge.js +2 -1
- package/src/dataflow/CLAUDE.md +10 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/cross-service-taint.js +2 -1
- package/src/dataflow/engine.js +324 -60
- package/src/dataflow/ifds-precise.js +6 -4
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +25 -8
- package/src/dataflow/index.js +2 -1
- package/src/dataflow/proven-clean.js +41 -0
- package/src/dataflow/sanitizer-gate.js +35 -9
- package/src/dataflow/sanitizer-proof.js +21 -3
- package/src/dataflow/stub-aware-filter.js +36 -13
- package/src/dataflow/summaries.js +21 -2
- package/src/discovery/CLAUDE.md +10 -0
- package/src/discovery/index.js +175 -3
- package/src/discovery/llm-invoke.js +90 -1
- package/src/discovery/memory.js +163 -0
- package/src/engine.js +247 -50
- package/src/integrations/tickets.js +7 -6
- package/src/ir/CLAUDE.md +4 -1
- package/src/ir/balanced-call.js +55 -0
- package/src/ir/ir-stats.js +1 -1
- package/src/ir/parser-cpp.js +1 -1
- package/src/ir/parser-cs.js +62 -9
- package/src/ir/parser-go.js +29 -11
- package/src/ir/parser-java.js +96 -19
- package/src/ir/parser-js.js +151 -20
- package/src/ir/parser-php.js +44 -9
- package/src/ir/parser-rb.js +37 -7
- package/src/ir/ssa.js +6 -1
- package/src/leaderboard.js +3 -2
- package/src/llm-validator/consistency.js +6 -2
- package/src/llm-validator/index.js +1 -2
- package/src/lsp/server.js +28 -2
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/audit.js +2 -1
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +159 -17
- package/src/posture/CLAUDE.md +45 -8
- package/src/posture/accuracy-scorecard.js +67 -1
- package/src/posture/agents-memory.js +5 -3
- package/src/posture/aibom.js +12 -8
- package/src/posture/auditor-walkthrough.js +111 -10
- package/src/posture/auth-posture-import.js +5 -4
- package/src/posture/autopilot.js +8 -1
- package/src/posture/calibration-drift.js +11 -5
- package/src/posture/calibration.js +24 -2
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/src/posture/compliance-policy.js +40 -10
- package/src/posture/confidence.js +44 -10
- package/src/posture/corpus-enroll.js +9 -5
- package/src/posture/corpus-match.js +19 -0
- package/src/posture/csharp-analysis.js +62 -3
- package/src/posture/custom-rules.js +7 -5
- package/src/posture/cve-alert-daemon.js +6 -5
- package/src/posture/dep-add-guard.js +2 -1
- package/src/posture/deploy-platform.js +4 -1
- package/src/posture/deterministic.js +3 -2
- package/src/posture/drift.js +7 -1
- package/src/posture/epss.js +13 -1
- package/src/posture/evidence-bundle.js +276 -0
- package/src/posture/exploitability-probability.js +15 -2
- package/src/posture/falsification.js +23 -2
- package/src/posture/feature-flags.js +3 -2
- package/src/posture/findings-memory.js +3 -3
- package/src/posture/fix-history.js +5 -2
- package/src/posture/fix-metrics.js +5 -5
- package/src/posture/fix-plan.js +2 -1
- package/src/posture/fix-verify-loop.js +10 -1
- package/src/posture/grader-calibration.js +3 -4
- package/src/posture/iac-reachability.js +14 -8
- package/src/posture/integrity.js +25 -7
- package/src/posture/intent-context.js +2 -1
- package/src/posture/learning.js +4 -3
- package/src/posture/license-attributions.js +5 -7
- package/src/posture/license-graph.js +2 -1
- package/src/posture/license-policy.js +2 -1
- package/src/posture/model-rescan.js +69 -3
- package/src/posture/mttr.js +5 -0
- package/src/posture/network-policy-import.js +3 -2
- package/src/posture/poc-inprocess.js +27 -8
- package/src/posture/pqc-migration-plan.js +7 -5
- package/src/posture/pr-augment.js +8 -5
- package/src/posture/privacy-framework.js +262 -0
- package/src/posture/regression-test-gen.js +23 -8
- package/src/posture/reverse-blast-radius.js +5 -1
- package/src/posture/risk-dollars.js +20 -3
- package/src/posture/router.js +5 -4
- package/src/posture/ruleset-version.js +2 -2
- package/src/posture/runtime-correlation.js +2 -1
- package/src/posture/sbom-diff.js +12 -3
- package/src/posture/sca-policy.js +7 -4
- package/src/posture/scan-checkpoint.js +15 -0
- package/src/posture/secret-history.js +20 -11
- package/src/posture/security-trend.js +7 -1
- package/src/posture/stack-playbook.js +22 -1
- package/src/posture/state-dir.js +34 -0
- package/src/posture/telemetry-ingest.js +4 -3
- package/src/posture/threat-model-auto.js +4 -1
- package/src/posture/threat-model-grounding.js +13 -3
- package/src/posture/time-to-fix.js +3 -2
- package/src/posture/triage-memory.js +3 -2
- package/src/posture/validator-metrics.js +10 -3
- package/src/posture/verifier.js +32 -57
- package/src/posture/waf-ingest.js +6 -5
- package/src/posture/watch-mode.js +4 -3
- package/src/report/index.js +183 -14
- package/src/runScan.js +1 -1
- package/src/sast/_comment-strip.js +15 -4
- package/src/sast/_secret-entropy.js +1 -1
- package/src/sast/authz.js +6 -4
- package/src/sast/bench-shape/index.js +2 -7
- package/src/sast/claude-md-prompt-injection.js +14 -3
- package/src/sast/cloud-iam.js +60 -7
- package/src/sast/code-injection-multilang.js +29 -0
- package/src/sast/cpp-bench-extras.js +1 -1
- package/src/sast/csrf.js +7 -5
- package/src/sast/env-hygiene.js +5 -2
- package/src/sast/iac-terraform.js +25 -0
- package/src/sast/java-bench-extras.js +1 -1
- package/src/sast/java-constant-fold.js +5 -5
- package/src/sast/llm-owasp.js +4 -2
- package/src/sast/mcp-audit.js +7 -0
- package/src/sast/pipeline.js +8 -0
- package/src/sast/prompt-template.js +8 -6
- package/src/sast/prototype-pollution.js +6 -2
- package/src/sast/redos-nfa.js +6 -6
- package/src/sast/secret-concat.js +13 -2
- package/src/sast/ssrf-cloud-metadata.js +6 -3
- package/src/sast/xss-reflected-multilang.js +1 -1
- package/src/sast/xxe.js +1 -1
- package/src/sca/CLAUDE.md +3 -4
- package/src/sca/container.js +35 -3
- package/src/sca/dep-confusion.js +9 -1
- package/src/sca/sarif-ingest.js +0 -187
package/src/sca/container.js
CHANGED
|
@@ -80,9 +80,14 @@ export function scanContainer(fp, raw) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
return findings;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _parsePackagesFromDockerfile(raw) {
|
|
83
87
|
// Pass 2: apt/apk packages — surface as components hint for the SCA pipeline.
|
|
84
88
|
// We do NOT query OSV here (the engine's SCA pass owns that). Just collect names.
|
|
85
89
|
const packages = [];
|
|
90
|
+
let m;
|
|
86
91
|
_APT_INSTALL_RE.lastIndex = 0;
|
|
87
92
|
while ((m = _APT_INSTALL_RE.exec(raw))) {
|
|
88
93
|
for (const tok of m[1].split(/\s+/)) {
|
|
@@ -101,7 +106,34 @@ export function scanContainer(fp, raw) {
|
|
|
101
106
|
if (/^[a-z0-9][\w.+-]*$/.test(name)) packages.push({ ecosystem: 'alpine', name, version: ver || '' });
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
return packages;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract Dockerfile-declared apt/apk packages across every Dockerfile in
|
|
113
|
+
// fileContents, as full SCA component entries — mirrors image-packages.js's
|
|
114
|
+
// extractImagePackages(fileContents) signature/shape so both plug into the
|
|
115
|
+
// engine's components merge the same way. Deliberately independent of
|
|
116
|
+
// scanContainer's return value: piggybacking package data on a *finding*
|
|
117
|
+
// meant no packages were ever surfaced for a Dockerfile whose FROM line
|
|
118
|
+
// wasn't itself flagged (the common case — most Dockerfiles pin a
|
|
119
|
+
// non-EOL base image), even though apt/apk lines were still present and
|
|
120
|
+
// parseable.
|
|
121
|
+
export function extractContainerPackages(fileContents) {
|
|
122
|
+
const comps = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const [file, content] of Object.entries(fileContents || {})) {
|
|
125
|
+
if (!_DOCKERFILE_RE.test(file.replace(/\\/g, '/'))) continue;
|
|
126
|
+
if (!content || content.length > 200_000) continue;
|
|
127
|
+
for (const p of _parsePackagesFromDockerfile(content)) {
|
|
128
|
+
const key = `${p.ecosystem}:${p.name}:${p.version}`;
|
|
129
|
+
if (seen.has(key)) continue;
|
|
130
|
+
seen.add(key);
|
|
131
|
+
comps.push({
|
|
132
|
+
name: p.name, version: p.version, group: '', scope: 'required',
|
|
133
|
+
purl: `pkg:${p.ecosystem === 'debian' ? 'deb/debian' : 'apk/alpine'}/${encodeURIComponent(p.name)}${p.version ? '@' + encodeURIComponent(p.version) : ''}`,
|
|
134
|
+
ecosystem: p.ecosystem, filePath: file, isUnpinned: !p.version, isOsPackage: true, reachable: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return comps;
|
|
107
139
|
}
|
package/src/sca/dep-confusion.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as path from 'node:path';
|
|
|
12
12
|
import * as yaml from '../util/yaml.js';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
|
|
15
|
+
import { statePath } from '../posture/state-dir.js';
|
|
15
16
|
const _require = createRequire(import.meta.url);
|
|
16
17
|
const _POPULAR = (() => {
|
|
17
18
|
try {
|
|
@@ -52,7 +53,7 @@ export function levenshtein(a, b, maxDistance = 2) {
|
|
|
52
53
|
function _loadInternalScopes(scanRoot) {
|
|
53
54
|
if (!scanRoot) return [];
|
|
54
55
|
for (const name of ['internal-scopes.yml', 'internal-scopes.yaml']) {
|
|
55
|
-
const p =
|
|
56
|
+
const p = statePath(scanRoot, name);
|
|
56
57
|
if (!fs.existsSync(p)) continue;
|
|
57
58
|
try {
|
|
58
59
|
const doc = yaml.load(fs.readFileSync(p, 'utf8'));
|
|
@@ -96,6 +97,12 @@ export function detectDepConfusion(components, scanRoot) {
|
|
|
96
97
|
id, kind: 'sca', severity: bestDist === 1 ? 'critical' : 'high',
|
|
97
98
|
vuln: `Possible typosquat: "${c.name}" (1–2 chars from "${bestMatch}")`,
|
|
98
99
|
cwe: 'CWE-1357', stride: 'Tampering',
|
|
100
|
+
// CMP-1 (Stage 6 follow-up): neither finding here set `family`,
|
|
101
|
+
// so both fell through to the generic `vulnerable-dep` default
|
|
102
|
+
// every unset supplyChain finding gets — invisible to any
|
|
103
|
+
// compliance control mapped to the more specific
|
|
104
|
+
// family:dependency-confusion (ccpa.json).
|
|
105
|
+
family: 'dependency-confusion',
|
|
99
106
|
file: c.filePath || 'package.json', line: 0,
|
|
100
107
|
snippet: `${c.ecosystem}:${c.name}@${c.version}`,
|
|
101
108
|
fix: `Verify "${c.name}" is the package you actually meant. The popular package "${bestMatch}" is ${bestDist} edit(s) away — typosquat malware commonly registers names like this. Double-check the publisher, weekly downloads, and recent changes before keeping this dep.`,
|
|
@@ -121,6 +128,7 @@ export function detectDepConfusion(components, scanRoot) {
|
|
|
121
128
|
id, kind: 'sca', severity: 'high',
|
|
122
129
|
vuln: `Internal-scoped package on public registry: "${c.name}"`,
|
|
123
130
|
cwe: 'CWE-1357', stride: 'Tampering',
|
|
131
|
+
family: 'dependency-confusion',
|
|
124
132
|
file: c.filePath || 'package.json', line: 0,
|
|
125
133
|
snippet: `${c.ecosystem}:${c.name}@${c.version}`,
|
|
126
134
|
fix: `"${c.name}" matches your internal scope "${scope}", but is being resolved from the public registry. Confirm whether your private registry is configured (e.g. .npmrc / .pypirc) — if a public copy exists with the same name an attacker could publish malicious updates and your installs would silently switch.`,
|
package/src/sca/sarif-ingest.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
// 0.7.0 Feat-7: SARIF 2.1.0 ingest — normalizes and merges external scanner findings into unified report.
|
|
2
|
-
//
|
|
3
|
-
// Reads SARIF 2.1.0 from gitleaks / Semgrep / Bandit / Trivy / Checkov / SonarQube
|
|
4
|
-
// and merges into our scan. Findings are normalised + deduplicated against our
|
|
5
|
-
// own findings via a fingerprint of (CWE/CVE, file, line ±2, rule), and a
|
|
6
|
-
// `sources[]` array is added to the merged finding so attribution is preserved.
|
|
7
|
-
//
|
|
8
|
-
// Pure ESM, no external deps. Reads JSON only — no schema validation library.
|
|
9
|
-
|
|
10
|
-
import * as fs from 'node:fs';
|
|
11
|
-
import * as crypto from 'node:crypto';
|
|
12
|
-
|
|
13
|
-
// Tool-name → (kind, defaultSeverityIfUnset)
|
|
14
|
-
const TOOL_PROFILE = {
|
|
15
|
-
'semgrep': { kind: 'sast', defSev: 'medium' },
|
|
16
|
-
'opengrep': { kind: 'sast', defSev: 'medium' },
|
|
17
|
-
'bandit': { kind: 'sast', defSev: 'medium' },
|
|
18
|
-
'eslint': { kind: 'sast', defSev: 'low' },
|
|
19
|
-
'sonarqube': { kind: 'sast', defSev: 'medium' },
|
|
20
|
-
'codeql': { kind: 'sast', defSev: 'high' },
|
|
21
|
-
'gitleaks': { kind: 'secret', defSev: 'high' },
|
|
22
|
-
'trufflehog': { kind: 'secret', defSev: 'high' },
|
|
23
|
-
'detect-secrets':{ kind: 'secret', defSev: 'high' },
|
|
24
|
-
'trivy': { kind: 'sca', defSev: 'high' },
|
|
25
|
-
'grype': { kind: 'sca', defSev: 'high' },
|
|
26
|
-
'osv-scanner': { kind: 'sca', defSev: 'high' },
|
|
27
|
-
'checkov': { kind: 'iac', defSev: 'medium' },
|
|
28
|
-
'tfsec': { kind: 'iac', defSev: 'medium' },
|
|
29
|
-
'kics': { kind: 'iac', defSev: 'medium' },
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// SARIF level → our severity tier
|
|
33
|
-
const LEVEL_MAP = { error: 'critical', warning: 'high', note: 'medium', none: 'low' };
|
|
34
|
-
|
|
35
|
-
// Normalize a tool name into a TOOL_PROFILE key (case + version-agnostic)
|
|
36
|
-
function _toolKey(rawName) {
|
|
37
|
-
if (!rawName) return null;
|
|
38
|
-
const n = String(rawName).toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
39
|
-
for (const k of Object.keys(TOOL_PROFILE)) if (n.includes(k)) return k;
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function _fingerprint(file, line, vuln, cwe) {
|
|
44
|
-
// Bucketed line (±2) so that off-by-one rule reports merge correctly.
|
|
45
|
-
const bucket = Math.floor((line || 0) / 2) * 2;
|
|
46
|
-
const key = `${file}:${bucket}:${(cwe || '').toUpperCase()}:${(vuln || '').replace(/\W+/g, '_').toLowerCase()}`;
|
|
47
|
-
return crypto.createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function _extractCWE(rule) {
|
|
51
|
-
// SARIF rules can carry CWE in `properties.tags`, `properties.cwe`, or `helpUri`.
|
|
52
|
-
const tags = rule?.properties?.tags || [];
|
|
53
|
-
for (const t of tags) {
|
|
54
|
-
const m = String(t).match(/CWE-?(\d+)/i);
|
|
55
|
-
if (m) return `CWE-${m[1]}`;
|
|
56
|
-
}
|
|
57
|
-
if (rule?.properties?.cwe) {
|
|
58
|
-
const m = String(rule.properties.cwe).match(/(\d+)/);
|
|
59
|
-
if (m) return `CWE-${m[1]}`;
|
|
60
|
-
}
|
|
61
|
-
if (rule?.helpUri) {
|
|
62
|
-
const m = rule.helpUri.match(/cwe\.mitre\.org\/data\/definitions\/(\d+)/i);
|
|
63
|
-
if (m) return `CWE-${m[1]}`;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function _extractCVE(result) {
|
|
69
|
-
// Trivy / Grype / osv-scanner put CVE in ruleId or properties.
|
|
70
|
-
const id = result?.ruleId || '';
|
|
71
|
-
const m = id.match(/(CVE-\d{4}-\d{4,7})/i);
|
|
72
|
-
if (m) return m[1].toUpperCase();
|
|
73
|
-
const tags = result?.properties?.tags || [];
|
|
74
|
-
for (const t of tags) {
|
|
75
|
-
const tm = String(t).match(/(CVE-\d{4}-\d{4,7})/i);
|
|
76
|
-
if (tm) return tm[1].toUpperCase();
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Convert a single SARIF result + its run context into our normalized finding.
|
|
82
|
-
function _resultToFinding(result, run, toolKey, defSev) {
|
|
83
|
-
const loc = result.locations?.[0]?.physicalLocation;
|
|
84
|
-
const file = loc?.artifactLocation?.uri || result?.fingerprints?.path || '(unknown)';
|
|
85
|
-
const line = loc?.region?.startLine || 0;
|
|
86
|
-
const ruleId = result.ruleId || '';
|
|
87
|
-
const rule = (run.tool?.driver?.rules || []).find(r => r.id === ruleId);
|
|
88
|
-
const message = result.message?.text || rule?.fullDescription?.text || rule?.shortDescription?.text || ruleId;
|
|
89
|
-
const sev = LEVEL_MAP[result.level] || defSev;
|
|
90
|
-
const cwe = _extractCWE(rule || result);
|
|
91
|
-
const cve = _extractCVE(result);
|
|
92
|
-
const profile = TOOL_PROFILE[toolKey] || { kind: 'sast', defSev: 'medium' };
|
|
93
|
-
return {
|
|
94
|
-
file: file.replace(/^file:\/\//, ''),
|
|
95
|
-
line,
|
|
96
|
-
severity: sev,
|
|
97
|
-
vuln: cve ? `${cve}: ${message}`.slice(0, 240) : message.slice(0, 240),
|
|
98
|
-
cwe,
|
|
99
|
-
cve,
|
|
100
|
-
kind: profile.kind,
|
|
101
|
-
ruleId,
|
|
102
|
-
snippet: result.locations?.[0]?.physicalLocation?.contextRegion?.snippet?.text
|
|
103
|
-
|| result.locations?.[0]?.physicalLocation?.region?.snippet?.text
|
|
104
|
-
|| '',
|
|
105
|
-
sources: [run.tool?.driver?.name || toolKey],
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Load and parse a SARIF file. Returns an array of normalized findings.
|
|
110
|
-
export function ingestSARIFFile(filePath) {
|
|
111
|
-
let raw;
|
|
112
|
-
try { raw = fs.readFileSync(filePath, 'utf8'); }
|
|
113
|
-
catch (e) { throw new Error(`Cannot read SARIF file ${filePath}: ${e.message}`); }
|
|
114
|
-
let doc;
|
|
115
|
-
try { doc = JSON.parse(raw); }
|
|
116
|
-
catch (e) { throw new Error(`Invalid JSON in SARIF file ${filePath}: ${e.message}`); }
|
|
117
|
-
if (!doc || !Array.isArray(doc.runs)) {
|
|
118
|
-
throw new Error(`Not a SARIF document (missing runs[]): ${filePath}`);
|
|
119
|
-
}
|
|
120
|
-
const out = [];
|
|
121
|
-
for (const run of doc.runs) {
|
|
122
|
-
const toolName = run.tool?.driver?.name;
|
|
123
|
-
const toolKey = _toolKey(toolName);
|
|
124
|
-
const profile = TOOL_PROFILE[toolKey] || { kind: 'sast', defSev: 'medium' };
|
|
125
|
-
for (const result of (run.results || [])) {
|
|
126
|
-
try { out.push(_resultToFinding(result, run, toolKey || (toolName||'').toLowerCase(), profile.defSev)); }
|
|
127
|
-
catch (_) {}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Merge an array of external findings into the scan result. Mutates `scan` in place.
|
|
134
|
-
// Behaviour:
|
|
135
|
-
// - Findings whose fingerprint matches an existing scan.findings entry have their
|
|
136
|
-
// source tool name appended to scan.findings[i].sources[] and (if the external
|
|
137
|
-
// finding's severity is higher) the existing severity is bumped.
|
|
138
|
-
// - New findings (no match) are appended to scan.findings, scan.secrets, or
|
|
139
|
-
// scan.supplyChain depending on `kind`.
|
|
140
|
-
export function mergeSARIFFindings(scan, externals) {
|
|
141
|
-
// Index existing findings by fingerprint
|
|
142
|
-
const existingFP = new Map();
|
|
143
|
-
for (const f of (scan.findings || [])) {
|
|
144
|
-
const fp = _fingerprint((f.file||'').split(' -> ').pop(), f.line||f.source?.line||f.sink?.line||0, f.vuln||f.type, f.cwe);
|
|
145
|
-
existingFP.set(fp, f);
|
|
146
|
-
}
|
|
147
|
-
let merged = 0, added = 0;
|
|
148
|
-
const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
|
149
|
-
for (const ext of externals) {
|
|
150
|
-
const fp = _fingerprint(ext.file, ext.line, ext.vuln, ext.cwe);
|
|
151
|
-
const existing = existingFP.get(fp);
|
|
152
|
-
if (existing) {
|
|
153
|
-
existing.sources = Array.from(new Set([...(existing.sources || ['agentic-security']), ...ext.sources]));
|
|
154
|
-
// Bump severity if the external tool reported higher
|
|
155
|
-
if ((SEV_RANK[ext.severity] || 0) > (SEV_RANK[existing.severity] || 0)) {
|
|
156
|
-
existing.severity = ext.severity;
|
|
157
|
-
}
|
|
158
|
-
merged++;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
// Append as new finding into the right bucket
|
|
162
|
-
const id = `sarif-ingest:${fp}`;
|
|
163
|
-
const newFinding = {
|
|
164
|
-
id, kind: ext.kind, severity: ext.severity, vuln: ext.vuln,
|
|
165
|
-
cwe: ext.cwe, file: ext.file, line: ext.line, snippet: ext.snippet || '',
|
|
166
|
-
sources: ext.sources, parser: 'SARIF',
|
|
167
|
-
};
|
|
168
|
-
if (ext.kind === 'secret') (scan.secrets = scan.secrets || []).push(newFinding);
|
|
169
|
-
else if (ext.kind === 'sca') (scan.supplyChain = scan.supplyChain || []).push({ ...newFinding, type: 'vulnerable_dep' });
|
|
170
|
-
else if (ext.kind === 'iac' || ext.kind === 'sast' || ext.kind === 'logic') (scan.findings = scan.findings || []).push(newFinding);
|
|
171
|
-
added++;
|
|
172
|
-
}
|
|
173
|
-
return { merged, added };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function ingestAndMerge(scan, sarifPaths) {
|
|
177
|
-
let totalMerged = 0, totalAdded = 0;
|
|
178
|
-
for (const p of sarifPaths) {
|
|
179
|
-
let externals;
|
|
180
|
-
try { externals = ingestSARIFFile(p); }
|
|
181
|
-
catch (e) { /* swallow per-file errors so one bad SARIF doesn't break the run */ continue; }
|
|
182
|
-
const { merged, added } = mergeSARIFFindings(scan, externals);
|
|
183
|
-
totalMerged += merged;
|
|
184
|
-
totalAdded += added;
|
|
185
|
-
}
|
|
186
|
-
return { merged: totalMerged, added: totalAdded };
|
|
187
|
-
}
|