@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/dist/113.index.js +79 -3
  3. package/dist/178.index.js +1 -1
  4. package/dist/238.index.js +77 -1
  5. package/dist/384.index.js +1 -1
  6. package/dist/435.index.js +12 -0
  7. package/dist/526.index.js +79 -3
  8. package/dist/637.index.js +1 -1
  9. package/dist/agentic-security.mjs +14 -14
  10. package/dist/agentic-security.mjs.sha256 +1 -1
  11. package/dist/compliance-frameworks/ccpa.json +34 -7
  12. package/dist/compliance-frameworks/eu-ai-act.json +65 -14
  13. package/dist/compliance-frameworks/gdpr.json +56 -12
  14. package/dist/compliance-frameworks/hipaa-security-rule.json +68 -15
  15. package/dist/compliance-frameworks/nist-ai-600-1.json +57 -12
  16. package/dist/compliance-frameworks/nist-csf-2.json +78 -16
  17. package/dist/compliance-frameworks/nist-privacy-1-1.json +3 -0
  18. package/dist/compliance-frameworks/owasp-asvs-5.json +91 -20
  19. package/dist/compliance-frameworks/owasp-llm-top-10.json +89 -20
  20. package/package.json +19 -5
  21. package/src/dataflow/CLAUDE.md +9 -0
  22. package/src/dataflow/catalog.js +61 -0
  23. package/src/dataflow/engine.js +95 -0
  24. package/src/dataflow/sanitizer-gate.js +61 -0
  25. package/src/engine.js +353 -31
  26. package/src/mcp/tools.js +12 -0
  27. package/src/posture/accuracy-scorecard.js +57 -0
  28. package/src/posture/aibom.js +110 -1
  29. package/src/posture/auditor-walkthrough.js +56 -17
  30. package/src/posture/compliance-frameworks/ccpa.json +34 -7
  31. package/src/posture/compliance-frameworks/eu-ai-act.json +65 -14
  32. package/src/posture/compliance-frameworks/gdpr.json +56 -12
  33. package/src/posture/compliance-frameworks/hipaa-security-rule.json +68 -15
  34. package/src/posture/compliance-frameworks/nist-ai-600-1.json +57 -12
  35. package/src/posture/compliance-frameworks/nist-csf-2.json +78 -16
  36. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +3 -0
  37. package/src/posture/compliance-frameworks/owasp-asvs-5.json +91 -20
  38. package/src/posture/compliance-frameworks/owasp-llm-top-10.json +89 -20
  39. package/src/posture/concurrency-checker.js +3 -3
  40. package/src/posture/coverage-strength.js +182 -0
  41. package/src/posture/epss.js +17 -1
  42. package/src/posture/family-registry.js +103 -0
  43. package/src/posture/family-resolve.js +47 -0
  44. package/src/posture/fix-coverage.js +113 -0
  45. package/src/posture/fix-metrics.js +76 -0
  46. package/src/posture/mcp-rug-pull.js +144 -0
  47. package/src/posture/poc-inprocess.js +217 -1
  48. package/src/posture/proof-coverage.js +162 -0
  49. package/src/posture/reachability-filter.js +44 -0
  50. package/src/posture/sbom.js +12 -3
  51. package/src/runScan.js +56 -5
  52. package/src/sast/CLAUDE.md +2 -2
  53. package/src/sast/claude-md-prompt-injection.js +47 -3
  54. package/src/sast/cloud-iam.js +23 -0
  55. package/src/sast/convention-deviation.js +66 -3
  56. package/src/sast/crypto-protocol.js +23 -0
  57. package/src/sast/dapp-frontend.js +20 -0
  58. package/src/sast/iac-cloud-templates.js +346 -0
  59. package/src/sast/k8s-admission.js +27 -0
  60. package/src/sast/ml-supply-chain.js +22 -0
  61. package/src/sast/ruby.js +132 -0
  62. package/src/sast/web3-advanced.js +26 -0
  63. package/src/sca/CLAUDE.md +21 -4
  64. package/src/sca/container.js +18 -1
  65. package/src/sca/dep-confusion.js +69 -3
@@ -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
- const _ALL_FROM_RE = /^\s*FROM\s+(?:--platform=\S+\s+)?([\w./-]+?)(?::([\w.\-]+))?(?:@sha256:[a-f0-9]{64})?(?:\s+AS\s+\S+)?\s*$/img;
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,
@@ -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 1–2 from a popular package.
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 = levenshtein(lowerName, popular, 2);
90
- if (d > 0 && d <= 2 && d < bestDist) { bestMatch = popular; bestDist = d; }
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`;