@clear-capabilities/agentic-security-scanner 0.140.0 → 0.142.0
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 +283 -0
- package/dist/113.index.js +79 -3
- package/dist/178.index.js +1 -1
- package/dist/238.index.js +77 -1
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +12 -0
- package/dist/526.index.js +79 -3
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +14 -14
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +34 -7
- package/dist/compliance-frameworks/eu-ai-act.json +65 -14
- package/dist/compliance-frameworks/gdpr.json +56 -12
- package/dist/compliance-frameworks/hipaa-security-rule.json +68 -15
- package/dist/compliance-frameworks/nist-ai-600-1.json +57 -12
- package/dist/compliance-frameworks/nist-csf-2.json +78 -16
- package/dist/compliance-frameworks/nist-privacy-1-1.json +3 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +91 -20
- package/dist/compliance-frameworks/owasp-llm-top-10.json +89 -20
- package/package.json +19 -5
- package/src/dataflow/CLAUDE.md +9 -0
- package/src/dataflow/catalog.js +61 -0
- package/src/dataflow/engine.js +95 -0
- package/src/dataflow/sanitizer-gate.js +61 -0
- package/src/engine.js +353 -31
- package/src/mcp/tools.js +12 -0
- package/src/posture/accuracy-scorecard.js +57 -0
- package/src/posture/aibom.js +110 -1
- package/src/posture/auditor-walkthrough.js +56 -17
- package/src/posture/compliance-frameworks/ccpa.json +34 -7
- package/src/posture/compliance-frameworks/eu-ai-act.json +65 -14
- package/src/posture/compliance-frameworks/gdpr.json +56 -12
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +68 -15
- package/src/posture/compliance-frameworks/nist-ai-600-1.json +57 -12
- package/src/posture/compliance-frameworks/nist-csf-2.json +78 -16
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +3 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +91 -20
- package/src/posture/compliance-frameworks/owasp-llm-top-10.json +89 -20
- package/src/posture/concurrency-checker.js +3 -3
- package/src/posture/coverage-strength.js +182 -0
- package/src/posture/epss.js +17 -1
- package/src/posture/family-registry.js +103 -0
- package/src/posture/family-resolve.js +47 -0
- package/src/posture/fix-coverage.js +113 -0
- package/src/posture/fix-metrics.js +76 -0
- package/src/posture/mcp-rug-pull.js +144 -0
- package/src/posture/poc-inprocess.js +217 -1
- package/src/posture/proof-coverage.js +162 -0
- package/src/posture/reachability-filter.js +44 -0
- package/src/posture/sbom.js +12 -3
- package/src/runScan.js +56 -5
- package/src/sast/CLAUDE.md +2 -2
- package/src/sast/claude-md-prompt-injection.js +47 -3
- package/src/sast/cloud-iam.js +23 -0
- package/src/sast/convention-deviation.js +66 -3
- package/src/sast/crypto-protocol.js +23 -0
- package/src/sast/dapp-frontend.js +20 -0
- package/src/sast/iac-cloud-templates.js +346 -0
- package/src/sast/k8s-admission.js +27 -0
- package/src/sast/ml-supply-chain.js +22 -0
- package/src/sast/ruby.js +132 -0
- package/src/sast/web3-advanced.js +26 -0
- package/src/sca/CLAUDE.md +21 -4
- package/src/sca/container.js +18 -1
- package/src/sca/dep-confusion.js +69 -3
package/src/sca/container.js
CHANGED
|
@@ -31,7 +31,16 @@ const _DOCKERFILE_RE = /(?:^|\/)(?:[Dd]ockerfile|[^/]+\.dockerfile)$/i;
|
|
|
31
31
|
const _FROM_RE = /^\s*FROM\s+(?:--platform=\S+\s+)?([\w./-]+?)(?::([\w.\-]+))?(?:@sha256:[a-f0-9]{64})?(?:\s+AS\s+\S+)?\s*$/im;
|
|
32
32
|
|
|
33
33
|
// FROM <image>:<tag> covering all FROM lines in the file
|
|
34
|
-
|
|
34
|
+
// The digest is CAPTURED, not merely tolerated. Discarding it made
|
|
35
|
+
// `FROM ubuntu@sha256:…` parse as image=ubuntu with no tag, which `_scoreTag`
|
|
36
|
+
// then treats as `latest` — so the most tightly pinned form a Dockerfile can
|
|
37
|
+
// use was reported as "ubuntu:latest (floating tag)". A false positive on the
|
|
38
|
+
// hardened configuration is worse than a miss: it tells the people who did the
|
|
39
|
+
// right thing that they did the wrong one.
|
|
40
|
+
//
|
|
41
|
+
// Found by bench/iac-coverage, whose verdict-flip scoring exists precisely to
|
|
42
|
+
// catch a rule that fires on both variants of a control.
|
|
43
|
+
const _ALL_FROM_RE = /^\s*FROM\s+(?:--platform=\S+\s+)?([\w./-]+?)(?::([\w.\-]+))?(?:@sha256:([a-f0-9]{64}))?(?:\s+AS\s+\S+)?\s*$/img;
|
|
35
44
|
|
|
36
45
|
// `apt-get install -y pkg pkg pkg` / `apk add pkg pkg`
|
|
37
46
|
const _APT_INSTALL_RE = /\bapt(?:-get)?\s+install\b[^\n]*?(?:--?[\w-]+\s+)*((?:[a-z0-9][\w.+-]*(?:=[\w.+:-]+)?\s*)+)/gi;
|
|
@@ -66,9 +75,17 @@ export function scanContainer(fp, raw) {
|
|
|
66
75
|
while ((m = _ALL_FROM_RE.exec(raw))) {
|
|
67
76
|
const image = m[1].split('/').pop(); // strip registry / namespace prefixes
|
|
68
77
|
const tag = m[2] || '';
|
|
78
|
+
const digest = m[3] || '';
|
|
69
79
|
const line = raw.substring(0, m.index).split('\n').length;
|
|
80
|
+
// Digest-pinned with no tag: there is nothing to score. The reference is
|
|
81
|
+
// immutable, which is the recommended form, and inventing a `latest` tag
|
|
82
|
+
// for it produces the exact opposite advice.
|
|
83
|
+
if (digest && !tag) continue;
|
|
70
84
|
const score = _scoreTag(image, tag);
|
|
71
85
|
if (!score) continue;
|
|
86
|
+
// `image:22.04@sha256:…` — the tag can still be end-of-life, and that is
|
|
87
|
+
// worth saying, but it is not a FLOATING tag: the digest pins it.
|
|
88
|
+
if (digest && !score.eol) continue;
|
|
72
89
|
findings.push({
|
|
73
90
|
id: `container-base:${fp}:${line}:${image}:${tag || 'latest'}`,
|
|
74
91
|
kind: 'container', severity: score.sev,
|
package/src/sca/dep-confusion.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// 0.9.0 Feat-15: Dependency confusion + typosquat detection (Levenshtein distance against top-1000 npm/PyPI packages).
|
|
2
2
|
//
|
|
3
|
-
// (a) Typosquat: Levenshtein distance
|
|
3
|
+
// (a) Typosquat: a small Damerau-Levenshtein distance from a popular package,
|
|
4
|
+
// where "small" is judged RELATIVE to the name length — see SIMILARITY
|
|
5
|
+
// below for why the absolute 1–2 this originally used produced 166
|
|
6
|
+
// critical/high false positives and zero true positives across 13 real
|
|
7
|
+
// dependency trees.
|
|
4
8
|
// (b) Confusion: internal-scoped names (`@your-org/...`) that also appear on the
|
|
5
9
|
// public registry — declared via .agentic-security/internal-scopes.yml.
|
|
6
10
|
//
|
|
@@ -50,6 +54,64 @@ export function levenshtein(a, b, maxDistance = 2) {
|
|
|
50
54
|
return prev[b.length];
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
// Damerau-Levenshtein (optimal string alignment): a TRANSPOSITION costs 1.
|
|
58
|
+
//
|
|
59
|
+
// Plain Levenshtein charges 2 for a transposition, which is exactly backwards
|
|
60
|
+
// for this problem — swapping two adjacent characters (`lodahs` for `lodash`,
|
|
61
|
+
// `electorn` for `electron`) is the single most common real typo, and it is the
|
|
62
|
+
// one plain distance scores as least similar. `levenshtein` above is kept
|
|
63
|
+
// unchanged and separately tested; this is a second measure for a different
|
|
64
|
+
// question.
|
|
65
|
+
export function damerauLevenshtein(a, b, maxDistance = 2) {
|
|
66
|
+
if (a === b) return 0;
|
|
67
|
+
if (Math.abs(a.length - b.length) > maxDistance) return maxDistance + 1;
|
|
68
|
+
const m = a.length, n = b.length;
|
|
69
|
+
if (!m) return n;
|
|
70
|
+
if (!n) return m;
|
|
71
|
+
const d = Array.from({ length: m + 1 }, (_, i) => {
|
|
72
|
+
const row = new Array(n + 1).fill(0);
|
|
73
|
+
row[0] = i;
|
|
74
|
+
return row;
|
|
75
|
+
});
|
|
76
|
+
for (let j = 0; j <= n; j++) d[0][j] = j;
|
|
77
|
+
for (let i = 1; i <= m; i++) {
|
|
78
|
+
let rowMin = Infinity;
|
|
79
|
+
for (let j = 1; j <= n; j++) {
|
|
80
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
81
|
+
let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
82
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
83
|
+
v = Math.min(v, d[i - 2][j - 2] + 1); // transposition
|
|
84
|
+
}
|
|
85
|
+
d[i][j] = v;
|
|
86
|
+
if (v < rowMin) rowMin = v;
|
|
87
|
+
}
|
|
88
|
+
if (rowMin > maxDistance) return maxDistance + 1;
|
|
89
|
+
}
|
|
90
|
+
return d[m][n];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// How much of a name may differ before it is a DIFFERENT name rather than a
|
|
94
|
+
// misspelling of this one.
|
|
95
|
+
//
|
|
96
|
+
// Measured, not chosen by taste. At the previous "distance 1–2, any length"
|
|
97
|
+
// rule, bench/sca-replay produced these across 13 real repositories:
|
|
98
|
+
//
|
|
99
|
+
// ms ~ ws (d1) acorn ~ cors (d2) ajv ~ ava (d2) six ~ tox (d2)
|
|
100
|
+
// abab ~ ava (d2) arg ~ yargs (d2) bail ~ babel (d2) aws4 ~ ws (d2)
|
|
101
|
+
//
|
|
102
|
+
// Every one is a legitimate, popular package, and `ms` is a top-50 npm
|
|
103
|
+
// package. The common factor is length: two edits on a four-character name
|
|
104
|
+
// changes half of it. A quarter of the shorter name is the ceiling — it keeps
|
|
105
|
+
// every genuine shape (one transposition, one dropped char, one doubled char
|
|
106
|
+
// on a name of ordinary length) and rejects all of the above.
|
|
107
|
+
const MAX_DIVERGENCE = 0.25;
|
|
108
|
+
|
|
109
|
+
function _isTyposquatCandidate(name, popular, distance) {
|
|
110
|
+
if (distance <= 0) return false;
|
|
111
|
+
const shorter = Math.min(name.length, popular.length);
|
|
112
|
+
return distance / shorter <= MAX_DIVERGENCE;
|
|
113
|
+
}
|
|
114
|
+
|
|
53
115
|
function _loadInternalScopes(scanRoot) {
|
|
54
116
|
if (!scanRoot) return [];
|
|
55
117
|
for (const name of ['internal-scopes.yml', 'internal-scopes.yaml']) {
|
|
@@ -84,10 +146,14 @@ export function detectDepConfusion(components, scanRoot) {
|
|
|
84
146
|
const lowerName = c.name.toLowerCase();
|
|
85
147
|
// (1) Typosquat — only run if the dep is NOT itself in the popular set
|
|
86
148
|
if (!popularSet.has(lowerName)) {
|
|
149
|
+
// Compare the BARE name. `@scope/react` is not a typosquat of `react`;
|
|
150
|
+
// the scope is the thing that identifies the publisher.
|
|
151
|
+
const bare = lowerName.replace(/^@[^/]+\//, '');
|
|
87
152
|
let bestMatch = null, bestDist = 3;
|
|
88
153
|
for (const popular of popularSet) {
|
|
89
|
-
const d =
|
|
90
|
-
if (
|
|
154
|
+
const d = damerauLevenshtein(bare, popular, 2);
|
|
155
|
+
if (!_isTyposquatCandidate(bare, popular, d)) continue;
|
|
156
|
+
if (d < bestDist) { bestMatch = popular; bestDist = d; }
|
|
91
157
|
}
|
|
92
158
|
if (bestMatch) {
|
|
93
159
|
const id = `dep-confusion:${c.ecosystem}:${c.name}@${c.version}:typosquat`;
|