@clear-capabilities/agentic-security-scanner 0.153.2 → 0.155.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 (136) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/bin/agentic-security.js +131 -19
  3. package/dist/1444.index.js +64 -19
  4. package/dist/1905.index.js +46 -3
  5. package/dist/1919.index.js +445 -0
  6. package/dist/2862.index.js +325 -0
  7. package/dist/3499.index.js +5 -1
  8. package/dist/3531.index.js +261 -0
  9. package/dist/3596.index.js +103 -0
  10. package/dist/4113.index.js +15 -3
  11. package/dist/4171.index.js +319 -0
  12. package/dist/5637.index.js +10 -4
  13. package/dist/6359.index.js +433 -0
  14. package/dist/7838.index.js +15 -3
  15. package/dist/8551.index.js +1192 -0
  16. package/dist/8587.index.js +410 -0
  17. package/dist/8714.index.js +98 -0
  18. package/dist/8798.index.js +218 -0
  19. package/dist/9220.index.js +35 -3
  20. package/dist/9560.index.js +26 -3
  21. package/dist/agentic-security.mjs +51 -51
  22. package/dist/agentic-security.mjs.sha256 +1 -1
  23. package/package.json +20 -3
  24. package/src/dataflow/CLAUDE.md +9 -0
  25. package/src/dataflow/catalog-haskell.js +37 -0
  26. package/src/dataflow/catalog.js +4 -0
  27. package/src/dataflow/engine.js +33 -3
  28. package/src/dataflow/privacy-catalog-haskell.js +50 -0
  29. package/src/dataflow/privacy-catalog.js +5 -1
  30. package/src/discovery/CLAUDE.md +6 -0
  31. package/src/discovery/index.js +12 -3
  32. package/src/discovery/lenses.js +4 -1
  33. package/src/discovery/partition.js +28 -0
  34. package/src/egress/redact.js +8 -0
  35. package/src/engine.js +125 -14
  36. package/src/ir/index.js +39 -2
  37. package/src/language/CLAUDE.md +74 -0
  38. package/src/language/accuracy.js +90 -0
  39. package/src/language/aibom.js +307 -0
  40. package/src/language/assurance.js +193 -0
  41. package/src/language/bom-validate.js +227 -0
  42. package/src/language/bom.js +236 -0
  43. package/src/language/bridges.js +392 -0
  44. package/src/language/compliance-map.js +61 -0
  45. package/src/language/context.js +199 -0
  46. package/src/language/contracts.js +339 -0
  47. package/src/language/discovery.js +359 -0
  48. package/src/language/engine-pass.js +120 -0
  49. package/src/language/fix-lifecycle.js +120 -0
  50. package/src/language/haskell-adapter.js +51 -0
  51. package/src/language/haskell-disclosure.js +170 -0
  52. package/src/language/haskell-findings.js +44 -0
  53. package/src/language/haskell-fix.js +409 -0
  54. package/src/language/haskell-grammar.js +78 -0
  55. package/src/language/haskell-guards.js +382 -0
  56. package/src/language/haskell-ir.js +1377 -0
  57. package/src/language/haskell-llm.js +128 -0
  58. package/src/language/haskell-manifests.js +1290 -0
  59. package/src/language/haskell-models.js +278 -0
  60. package/src/language/haskell-parser.js +1073 -0
  61. package/src/language/haskell-resolved-graph.js +414 -0
  62. package/src/language/haskell-sca.js +387 -0
  63. package/src/language/haskell-security-rules.js +332 -0
  64. package/src/language/haskell-supply.js +140 -0
  65. package/src/language/haskell-sweep.js +85 -0
  66. package/src/language/haskell-syntax.js +895 -0
  67. package/src/language/haskell-web.js +749 -0
  68. package/src/language/nix-adapter.js +42 -0
  69. package/src/language/nix-agents.js +155 -0
  70. package/src/language/nix-build-trust.js +790 -0
  71. package/src/language/nix-closure.js +331 -0
  72. package/src/language/nix-eval-isolation.js +274 -0
  73. package/src/language/nix-fix.js +268 -0
  74. package/src/language/nix-grammar.js +97 -0
  75. package/src/language/nix-inventory.js +379 -0
  76. package/src/language/nix-ir.js +642 -0
  77. package/src/language/nix-parser.js +908 -0
  78. package/src/language/nix-privacy.js +114 -0
  79. package/src/language/nix-sca.js +358 -0
  80. package/src/language/nix-script-taint.js +885 -0
  81. package/src/language/nix-secrets.js +396 -0
  82. package/src/language/nixos-hardening.js +564 -0
  83. package/src/language/nixos-module-resolver.js +595 -0
  84. package/src/language/nixos-option-catalog.js +96 -0
  85. package/src/language/pragma.js +150 -0
  86. package/src/language/resolved-pass.js +174 -0
  87. package/src/language/secrets.js +323 -0
  88. package/src/language/state-artifacts.js +78 -0
  89. package/src/language/support-registry.js +289 -0
  90. package/src/language/trusted-inputs.js +38 -0
  91. package/src/language/witness.js +185 -0
  92. package/src/lineage/CLAUDE.md +8 -0
  93. package/src/lineage/coverage.js +2 -0
  94. package/src/lineage/cross-repo-link.js +22 -0
  95. package/src/lineage/graph-builder.js +5 -1
  96. package/src/lineage/haskell-view.js +319 -0
  97. package/src/lineage/index.js +8 -1
  98. package/src/lineage/nix-view.js +198 -0
  99. package/src/lineage/sink-registry.js +10 -1
  100. package/src/lineage/source-registry.js +3 -0
  101. package/src/lineage/transform-catalog.js +12 -0
  102. package/src/llm-validator/index.js +16 -2
  103. package/src/lsp/server.js +69 -9
  104. package/src/mcp/CLAUDE.md +7 -0
  105. package/src/mcp/tools.js +26 -3
  106. package/src/pipeline/coverage-ledger.js +0 -0
  107. package/src/pipeline/scan-health.js +19 -1
  108. package/src/posture/accuracy-scorecard.js +51 -0
  109. package/src/posture/aibom.js +61 -3
  110. package/src/posture/artifact-registry.js +12 -0
  111. package/src/posture/auditor-walkthrough.js +41 -0
  112. package/src/posture/clustering.js +2 -2
  113. package/src/posture/evidence-bundle.js +7 -1
  114. package/src/posture/execution-proof.js +15 -0
  115. package/src/posture/falsification.js +7 -0
  116. package/src/posture/family-resolve.js +7 -0
  117. package/src/posture/fix-history.js +40 -0
  118. package/src/posture/fix-verify.js +15 -3
  119. package/src/posture/provenance/git-evidence.js +24 -0
  120. package/src/posture/provenance/origin-resolver.js +14 -2
  121. package/src/posture/provenance/predicate-replay.js +11 -2
  122. package/src/posture/provenance/sca-origin.js +20 -3
  123. package/src/posture/sbom-diff.js +7 -6
  124. package/src/posture/sbom.js +85 -32
  125. package/src/posture/sca-policy.js +10 -1
  126. package/src/posture/sca-verdict.js +8 -1
  127. package/src/posture/secret-history.js +62 -18
  128. package/src/posture/stable-id.js +21 -2
  129. package/src/posture/state-dir.js +21 -0
  130. package/src/posture/threat-model-auto.js +4 -4
  131. package/src/posture/triage.js +18 -1
  132. package/src/posture/watch-mode.js +34 -3
  133. package/src/pr-delta.js +9 -4
  134. package/src/report/index.js +35 -9
  135. package/src/runScan.js +56 -5
  136. package/src/sast/pipeline.js +34 -1
@@ -0,0 +1,128 @@
1
+ // LLM / agent-tool / prompt-safety analysis for Haskell (X-003).
2
+ //
3
+ // Two kinds of finding, kept separate on purpose:
4
+ //
5
+ // * TAINT findings the engine already produced, interpreted in AI context (annotateHaskellLlm):
6
+ // llm-prompt flows untrusted text reaching a model request body (CWE-1427, OWASP LLM01). The sink is an
7
+ // ordinary HTTP body setter, so the finding is KEPT only when the file shows AI evidence
8
+ // (an AI endpoint, a model literal, an AI SDK import) and DROPPED everywhere else.
9
+ // model-output flows a model response (`getResponseBody` in an AI file) reaching a command, SQL, code,
10
+ // path or HTML sink (OWASP LLM05 / improper output handling). The model is untrusted input.
11
+ // Both get an OWASP-LLM label, and when one function carries BOTH a prompt flow and a model-output flow an
12
+ // `hs-llm-agent-chain` finding links them. That chain is honest about its limit: the model is a
13
+ // non-deterministic hop between two proven flows, so it is a risk path, not an end-to-end proof.
14
+ //
15
+ // * Source rules over the Haskell tokens (analyzeHaskellLlmRules): a tool the model can choose that is dispatched
16
+ // to a shell/file/SQL primitive (OWASP LLM06 excessive agency). A tool dispatched to a fixed, pure lookup is not.
17
+ //
18
+ // Inert mentions (a provider name in a log string, a URL in a comment) never create AI context: the AI-context
19
+ // decision is made by the AI-BOM extractor on tokens, which skips comments and treats strings as data.
20
+
21
+ import { extractHaskellAI } from './aibom.js';
22
+ import { tokenizeHaskell } from './secrets.js';
23
+
24
+ export const HS_LLM_VERSION = 'haskell-llm/1';
25
+ const OWASP = Object.freeze({ prompt: 'LLM01', output: 'LLM05', agency: 'LLM06' });
26
+ const OUTPUT_FAMILIES = new Set(['cmd', 'sql', 'path', 'xss', 'url']);
27
+ const OUTPUT_SINK_CWES = { 'CWE-78': 'command', 'CWE-88': 'command', 'CWE-89': 'SQL query', 'CWE-22': 'file path', 'CWE-79': 'HTML output', 'CWE-94': 'code evaluation' };
28
+
29
+ /** Files with AI evidence, by the AI-BOM extractor (so the two never disagree about what is AI code). */
30
+ export function aiFileSet(files) {
31
+ const r = extractHaskellAI(files);
32
+ const out = new Set();
33
+ for (const list of [r.models, r.endpoints, r.frameworks, r.embeddings]) for (const c of list) for (const e of c.evidence || []) if (e.file && /\.l?hs$/i.test(e.file)) out.add(e.file);
34
+ for (const u of r.unresolved) if (u.file) out.add(u.file);
35
+ return out;
36
+ }
37
+
38
+ const isPrompt = (f) => f && (f.family === 'prompt-injection-untrusted-text-in-a-mod' || f.cwe === 'CWE-1427' || (f.sink && /llm-prompt/.test(String(f.sink.family || ''))));
39
+ const isModelOutput = (f) => f && f.source && /remote HTTP response/.test(String(f.source.label || ''));
40
+
41
+ /**
42
+ * Mutates `findings` in place: drops llm-prompt findings outside AI context, labels the rest, and appends
43
+ * agent-chain findings. Returns counts.
44
+ */
45
+ export function annotateHaskellLlm(findings, files) {
46
+ const stats = { promptKept: 0, promptDropped: 0, modelOutput: 0, chains: 0 };
47
+ if (!Array.isArray(findings) || !findings.length) return stats;
48
+ let ai;
49
+ try { ai = aiFileSet(files); } catch { ai = new Set(); }
50
+ const kept = [];
51
+ for (const f of findings) {
52
+ if (!f || f.parser !== 'IR-TAINT' || !/\.l?hs$/i.test(f.file || '')) { kept.push(f); continue; }
53
+ if (isPrompt(f)) {
54
+ if (!ai.has(f.file)) { stats.promptDropped++; continue; } // an HTTP body, not a prompt
55
+ f.owaspLlm = OWASP.prompt; f.llmRole = 'prompt-input'; f.language = f.language || 'haskell';
56
+ f.llmContext = 'Untrusted text is placed in a model request. The model cannot reliably tell instructions from data.';
57
+ stats.promptKept++;
58
+ } else if (isModelOutput(f) && ai.has(f.file) && (OUTPUT_SINK_CWES[f.cwe] || OUTPUT_FAMILIES.has(f.family))) {
59
+ f.owaspLlm = OWASP.output; f.llmRole = 'model-output';
60
+ f.llmContext = `A model response reaches a ${OUTPUT_SINK_CWES[f.cwe] || 'sensitive'} sink. Treat the response as untrusted input: it can carry instructions injected through the prompt or retrieved content.`;
61
+ f.description = `${f.description || f.vuln} ${f.llmContext}`.trim();
62
+ stats.modelOutput++;
63
+ }
64
+ kept.push(f);
65
+ }
66
+ findings.splice(0, findings.length, ...kept);
67
+ // one chain per file that has both halves
68
+ const byFile = new Map();
69
+ for (const f of findings) if (f && f.parser === 'IR-TAINT' && (f.llmRole === 'prompt-input' || f.llmRole === 'model-output')) { if (!byFile.has(f.file)) byFile.set(f.file, { prompt: [], out: [] }); byFile.get(f.file)[f.llmRole === 'prompt-input' ? 'prompt' : 'out'].push(f); }
70
+ for (const [file, { prompt, out }] of byFile) {
71
+ if (!prompt.length || !out.length) continue;
72
+ const p = prompt[0]; const o = out[0];
73
+ findings.push({
74
+ id: `hs-llm-agent-chain:${file}:${p.line}:${o.line}`, file, line: p.line, parser: 'HS-LLM', family: 'llm-agent-chain', rule: 'hs-llm-agent-chain',
75
+ severity: 'critical', cwe: 'CWE-1427', vuln: 'Untrusted input steers a model whose output reaches a dangerous sink', owaspLlm: `${OWASP.prompt}+${OWASP.output}`,
76
+ description: `Untrusted input (${(p.source && p.source.label) || 'input'}, line ${p.line}) reaches a model request, and the response reaches a ${OUTPUT_SINK_CWES[o.cwe] || 'sensitive'} sink (line ${o.line}). Each half is a taint finding on its own; the model between them is non-deterministic, so this is a risk path, not a proven end-to-end exploit.`,
77
+ remediation: 'Break either half: constrain what untrusted text can reach the model, and never pass a model response to a command, query or file path without a strict allow-list.',
78
+ language: 'haskell', capability: 'sast', analysisKind: 'application', evidenceKind: 'source', confidence: 0.55,
79
+ chain: [{ file, line: p.line, label: `source: ${(p.source && p.source.label) || 'input'}`, kind: 'source' }, { file, line: p.line, label: 'model request (prompt)', kind: 'prompt' }, { file, line: o.line, label: `model response -> ${OUTPUT_SINK_CWES[o.cwe] || 'sink'}`, kind: 'sink' }],
80
+ chainNote: 'the model is a non-deterministic hop between two proven flows', llmRole: 'agent-chain', end2endProof: false,
81
+ });
82
+ stats.chains++;
83
+ }
84
+ return stats;
85
+ }
86
+
87
+ // ── excessive agency: tools the model can choose ─────────────────────────────
88
+ const DANGEROUS_PRIMITIVES = new Map([
89
+ ['callCommand', 'run a shell command'], ['system', 'run a shell command'], ['callProcess', 'run a process'], ['readProcess', 'run a process'], ['rawSystem', 'run a process'], ['spawnProcess', 'run a process'],
90
+ ['writeFile', 'write a file'], ['appendFile', 'write a file'], ['removeFile', 'delete a file'], ['removeDirectoryRecursive', 'delete a directory tree'],
91
+ ['execute_', 'run SQL'], ['query_', 'run SQL'], ['rawExecute', 'run SQL'], ['rawSql', 'run SQL'],
92
+ ]);
93
+ const TOOL_NAME = /^(?:run_?(?:shell|command|cmd)|exec(?:ute)?(?:_?(?:command|shell|code))?|shell|bash|sh|write_?file|delete_?file|remove_?file|sql(?:_?query)?|run_?sql|eval(?:uate)?|python|code_?interpreter)$/i;
94
+
95
+ export function analyzeHaskellLlmRules(files) {
96
+ const out = [];
97
+ const ai = (() => { try { return aiFileSet(files); } catch { return new Set(); } })();
98
+ for (const [file, text] of Object.entries(files)) {
99
+ if (!/\.l?hs$/i.test(file) || typeof text !== 'string' || !ai.has(file)) continue;
100
+ const toks = tokenizeHaskell(text);
101
+ const lineOf = (idx) => { let l = 1; for (let i = 0; i < idx; i++) if (text.charCodeAt(i) === 10) l++; return l; };
102
+ const declared = toks.filter((t) => t.k === 's' && TOOL_NAME.test(t.v));
103
+ for (let i = 0; i < toks.length; i++) {
104
+ const t = toks[i];
105
+ if (t.k !== 's' || !TOOL_NAME.test(t.v)) continue;
106
+ // a dispatch: "tool_name" -> ... or "tool_name" = ... followed shortly by a dangerous primitive
107
+ const next = toks[i + 1];
108
+ if (!(next && next.k === 'o' && /^(?:->|=)$/.test(next.v))) continue;
109
+ let prim = null;
110
+ for (let j = i + 2; j < Math.min(toks.length, i + 14); j++) {
111
+ if (toks[j].k === 's' && TOOL_NAME.test(toks[j].v) && toks[j + 1] && toks[j + 1].k === 'o' && /^(?:->|=)$/.test(toks[j + 1].v)) break; // the next alternative begins
112
+ if (toks[j].k === 'i') { const base = toks[j].v.replace(/^.*\./, ''); if (DANGEROUS_PRIMITIVES.has(base)) { prim = { name: base, what: DANGEROUS_PRIMITIVES.get(base), line: lineOf(toks[j].start) }; break; } }
113
+ }
114
+ if (!prim) continue;
115
+ out.push({
116
+ id: `hs-llm-excessive-agency:${file}:${lineOf(t.start)}`, file, line: lineOf(t.start), parser: 'HS-LLM', family: 'llm-excessive-agency', rule: 'hs-llm-excessive-agency',
117
+ severity: 'high', cwe: 'CWE-250', vuln: 'A model-selectable tool is dispatched to a powerful primitive', owaspLlm: OWASP.agency,
118
+ description: `The tool "${t.v}" is dispatched to ${prim.name} (${prim.what}, line ${prim.line}). A model that can choose this tool can be steered into it by injected text; the tool needs a narrow, validated interface or a human confirmation.`,
119
+ remediation: 'Replace the generic tool with narrow, parameterised operations, validate every argument against an allow-list, and require confirmation for anything destructive.',
120
+ language: 'haskell', capability: 'sast', analysisKind: 'application', evidenceKind: 'source', confidence: 0.6,
121
+ modelNote: 'detected from the tool name and its dispatch target in this file; whether a model actually selects the tool depends on the tool list it is given',
122
+ chain: [{ file, line: lineOf(t.start), label: `tool "${t.v}"`, kind: 'source' }, { file, line: prim.line, label: prim.name, kind: 'sink' }],
123
+ });
124
+ }
125
+ void declared;
126
+ }
127
+ return out;
128
+ }