@clear-capabilities/agentic-security-scanner 0.153.1 → 0.154.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 +67 -0
- package/bin/agentic-security.js +123 -19
- package/dist/1444.index.js +64 -19
- package/dist/1905.index.js +46 -3
- package/dist/2862.index.js +325 -0
- package/dist/3499.index.js +5 -1
- package/dist/3531.index.js +261 -0
- package/dist/3596.index.js +103 -0
- package/dist/4113.index.js +15 -3
- package/dist/4171.index.js +319 -0
- package/dist/5637.index.js +10 -4
- package/dist/6359.index.js +433 -0
- package/dist/7838.index.js +15 -3
- package/dist/8551.index.js +1164 -0
- package/dist/8587.index.js +410 -0
- package/dist/8714.index.js +98 -0
- package/dist/8798.index.js +218 -0
- package/dist/9220.index.js +35 -3
- package/dist/9560.index.js +26 -3
- package/dist/agentic-security.mjs +51 -51
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +18 -3
- package/src/dataflow/CLAUDE.md +9 -0
- package/src/dataflow/catalog-haskell.js +37 -0
- package/src/dataflow/catalog.js +4 -0
- package/src/dataflow/engine.js +33 -3
- package/src/dataflow/privacy-catalog-haskell.js +50 -0
- package/src/dataflow/privacy-catalog.js +5 -1
- package/src/discovery/CLAUDE.md +6 -0
- package/src/discovery/index.js +12 -3
- package/src/discovery/lenses.js +4 -1
- package/src/discovery/partition.js +28 -0
- package/src/egress/redact.js +8 -0
- package/src/engine.js +140 -19
- package/src/ir/index.js +39 -2
- package/src/language/CLAUDE.md +74 -0
- package/src/language/accuracy.js +90 -0
- package/src/language/aibom.js +307 -0
- package/src/language/assurance.js +193 -0
- package/src/language/bom-validate.js +227 -0
- package/src/language/bom.js +236 -0
- package/src/language/bridges.js +392 -0
- package/src/language/compliance-map.js +61 -0
- package/src/language/context.js +199 -0
- package/src/language/contracts.js +339 -0
- package/src/language/discovery.js +359 -0
- package/src/language/engine-pass.js +120 -0
- package/src/language/fix-lifecycle.js +120 -0
- package/src/language/haskell-adapter.js +51 -0
- package/src/language/haskell-disclosure.js +170 -0
- package/src/language/haskell-findings.js +44 -0
- package/src/language/haskell-fix.js +409 -0
- package/src/language/haskell-grammar.js +78 -0
- package/src/language/haskell-guards.js +382 -0
- package/src/language/haskell-ir.js +1377 -0
- package/src/language/haskell-llm.js +128 -0
- package/src/language/haskell-manifests.js +1290 -0
- package/src/language/haskell-models.js +278 -0
- package/src/language/haskell-parser.js +1073 -0
- package/src/language/haskell-resolved-graph.js +414 -0
- package/src/language/haskell-sca.js +387 -0
- package/src/language/haskell-security-rules.js +332 -0
- package/src/language/haskell-supply.js +140 -0
- package/src/language/haskell-sweep.js +85 -0
- package/src/language/haskell-syntax.js +895 -0
- package/src/language/haskell-web.js +749 -0
- package/src/language/nix-adapter.js +42 -0
- package/src/language/nix-agents.js +155 -0
- package/src/language/nix-build-trust.js +790 -0
- package/src/language/nix-closure.js +331 -0
- package/src/language/nix-eval-isolation.js +274 -0
- package/src/language/nix-fix.js +268 -0
- package/src/language/nix-grammar.js +97 -0
- package/src/language/nix-inventory.js +379 -0
- package/src/language/nix-ir.js +642 -0
- package/src/language/nix-parser.js +908 -0
- package/src/language/nix-privacy.js +114 -0
- package/src/language/nix-sca.js +358 -0
- package/src/language/nix-script-taint.js +885 -0
- package/src/language/nix-secrets.js +396 -0
- package/src/language/nixos-hardening.js +564 -0
- package/src/language/nixos-module-resolver.js +595 -0
- package/src/language/nixos-option-catalog.js +96 -0
- package/src/language/pragma.js +150 -0
- package/src/language/resolved-pass.js +146 -0
- package/src/language/secrets.js +323 -0
- package/src/language/state-artifacts.js +78 -0
- package/src/language/support-registry.js +224 -0
- package/src/language/witness.js +185 -0
- package/src/lineage/CLAUDE.md +8 -0
- package/src/lineage/coverage.js +2 -0
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/graph-builder.js +5 -1
- package/src/lineage/haskell-view.js +319 -0
- package/src/lineage/index.js +8 -1
- package/src/lineage/nix-view.js +198 -0
- package/src/lineage/sink-registry.js +10 -1
- package/src/lineage/source-registry.js +3 -0
- package/src/lineage/transform-catalog.js +12 -0
- package/src/llm-validator/index.js +16 -2
- package/src/lsp/server.js +69 -9
- package/src/mcp/CLAUDE.md +7 -0
- package/src/mcp/tools.js +26 -3
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/accuracy-scorecard.js +51 -0
- package/src/posture/aibom.js +61 -3
- package/src/posture/artifact-registry.js +12 -0
- package/src/posture/auditor-walkthrough.js +41 -0
- package/src/posture/clustering.js +2 -2
- package/src/posture/evidence-bundle.js +7 -1
- package/src/posture/execution-proof.js +15 -0
- package/src/posture/falsification.js +7 -0
- package/src/posture/family-resolve.js +7 -0
- package/src/posture/fix-history.js +40 -0
- package/src/posture/fix-verify.js +15 -3
- package/src/posture/provenance/git-evidence.js +24 -0
- package/src/posture/provenance/origin-resolver.js +14 -2
- package/src/posture/provenance/predicate-replay.js +11 -2
- package/src/posture/provenance/sca-origin.js +20 -3
- package/src/posture/sbom-diff.js +7 -6
- package/src/posture/sbom.js +85 -32
- package/src/posture/sca-policy.js +10 -1
- package/src/posture/sca-verdict.js +8 -1
- package/src/posture/secret-history.js +62 -18
- package/src/posture/stable-id.js +21 -2
- package/src/posture/state-dir.js +21 -0
- package/src/posture/threat-model-auto.js +4 -4
- package/src/posture/triage.js +18 -1
- package/src/posture/watch-mode.js +34 -3
- package/src/pr-delta.js +9 -4
- package/src/report/index.js +35 -9
- package/src/runScan.js +56 -5
- package/src/sast/code-injection-multilang.js +15 -0
- package/src/sast/go-extended.js +11 -0
- package/src/sast/pipeline.js +34 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Haskell library/API model registry (HS-003, shared by HS-004..HS-006).
|
|
2
|
+
//
|
|
3
|
+
// One declarative table of the library surfaces the Haskell analysers understand: where untrusted
|
|
4
|
+
// data comes from, which calls are dangerous in which CWE family, and which calls legitimately
|
|
5
|
+
// neutralise which family. The taint catalog (`dataflow/catalog-haskell.js`) is GENERATED from this
|
|
6
|
+
// table, so there is no second list to drift.
|
|
7
|
+
//
|
|
8
|
+
// Identity, not names. Every entry is keyed by the defining MODULE plus the function name
|
|
9
|
+
// (`System.Process.callCommand`). The Haskell IR resolves a call through the file's imports to the
|
|
10
|
+
// qualified name before the catalog sees it, so a user's own `execute` becomes `Main.execute` and
|
|
11
|
+
// never matches `Database.PostgreSQL.Simple.execute`. A wildcard import that the registry itself
|
|
12
|
+
// proves is the only provider of a name is resolved through `qualifyAmbiguous` and recorded as
|
|
13
|
+
// uncertain, never silently trusted.
|
|
14
|
+
//
|
|
15
|
+
// Version honesty. `tested` lists the package versions the model was written against (the exact
|
|
16
|
+
// library versions in each model fixture). A model applies to every version of the package unless
|
|
17
|
+
// `since`/`until` narrow it; an unrecognised version is reported by `modelStatus`, not assumed safe.
|
|
18
|
+
//
|
|
19
|
+
// Not modelled on purpose: type correctness, `Text`/`ByteString`/`newtype` wrappers, JSON decoding
|
|
20
|
+
// and `Safe Haskell` never sanitize (nothing here treats them as a sanitizer), and nothing here
|
|
21
|
+
// claims a vulnerability class for an API whose behaviour is not described.
|
|
22
|
+
|
|
23
|
+
export const HS_MODEL_VERSION = 'haskell-models/1';
|
|
24
|
+
|
|
25
|
+
const E = (module, names) => names.map((name) => ({ module, name }));
|
|
26
|
+
const q = (module, name) => `${module}.${name}`;
|
|
27
|
+
|
|
28
|
+
// ── packages ────────────────────────────────────────────────────────────────
|
|
29
|
+
export const HS_PACKAGES = Object.freeze({
|
|
30
|
+
base: { modules: ['Prelude', 'System.IO', 'System.Environment', 'System.Exit', 'Data.List', 'Data.Char', 'Data.Maybe', 'Control.Monad'], tested: ['4.18', '4.19'] },
|
|
31
|
+
process: { modules: ['System.Process', 'System.Process.Typed'], tested: ['1.6.17', '1.6.18'] },
|
|
32
|
+
directory: { modules: ['System.Directory'], tested: ['1.3.8'] },
|
|
33
|
+
filepath: { modules: ['System.FilePath', 'System.FilePath.Posix'], tested: ['1.4.100'] },
|
|
34
|
+
'postgresql-simple': { modules: ['Database.PostgreSQL.Simple', 'Database.PostgreSQL.Simple.Types', 'Database.PostgreSQL.Simple.ToField'], tested: ['0.6.5', '0.7.0'] },
|
|
35
|
+
'mysql-simple': { modules: ['Database.MySQL.Simple'], tested: ['0.4.9'] },
|
|
36
|
+
'sqlite-simple': { modules: ['Database.SQLite.Simple'], tested: ['0.4.18'] },
|
|
37
|
+
persistent: { modules: ['Database.Persist.Sql'], tested: ['2.14'] },
|
|
38
|
+
scotty: { modules: ['Web.Scotty', 'Web.Scotty.Trans'], tested: ['0.12', '0.20'] },
|
|
39
|
+
wai: { modules: ['Network.Wai'], tested: ['3.2'] },
|
|
40
|
+
yesod: { modules: ['Yesod.Core', 'Yesod'], tested: ['1.6'] },
|
|
41
|
+
'http-client': { modules: ['Network.HTTP.Client', 'Network.HTTP.Client.TLS'], tested: ['0.7'] },
|
|
42
|
+
'http-conduit': { modules: ['Network.HTTP.Simple', 'Network.HTTP.Conduit'], tested: ['2.3'] },
|
|
43
|
+
wreq: { modules: ['Network.Wreq'], tested: ['0.5'] },
|
|
44
|
+
'blaze-html': { modules: ['Text.Blaze', 'Text.Blaze.Html', 'Text.Blaze.Html5', 'Text.Blaze.Internal'], tested: ['0.9'] },
|
|
45
|
+
lucid: { modules: ['Lucid', 'Lucid.Base'], tested: ['2.11'] },
|
|
46
|
+
shakespeare: { modules: ['Text.Hamlet', 'Text.Shakespeare'], tested: ['2.0'] },
|
|
47
|
+
text: { modules: ['Data.Text', 'Data.Text.IO', 'Data.Text.Lazy', 'Data.Text.Lazy.IO', 'Data.Text.Encoding'], tested: ['2.0'] },
|
|
48
|
+
cryptonite: { modules: ['Crypto.Hash', 'Crypto.Cipher.Types', 'Crypto.Random', 'Crypto.KDF.PBKDF2', 'Crypto.Hash.MD5', 'Crypto.Hash.SHA1', 'Crypto.Hash.SHA256', 'Crypto.Hash.SHA512', 'Crypto.Cipher.ChaChaPoly1305'], tested: ['0.30'] },
|
|
49
|
+
random: { modules: ['System.Random'], tested: ['1.2'] },
|
|
50
|
+
hxt: { modules: ['Text.XML.HXT.Core'], tested: ['9.3'] },
|
|
51
|
+
bytestring: { modules: ['Data.ByteString', 'Data.ByteString.Char8', 'Data.ByteString.Lazy'], tested: ['0.11', '0.12'] },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── sources ─────────────────────────────────────────────────────────────────
|
|
55
|
+
// provenance follows the existing taxonomy: http-body | url-param | path-param | header | cookie | env | cli | file-read | stdin | network
|
|
56
|
+
const SRC = [];
|
|
57
|
+
const src = (module, names, provenance, label, extra = {}) => { for (const e of E(module, names)) SRC.push({ ...e, provenance, label: `${label}${names.length > 1 ? '' : ''}`, package: extra.package, framework: extra.framework || null }); };
|
|
58
|
+
|
|
59
|
+
src('Prelude', ['getLine', 'getContents', 'readLn', 'interact'], 'stdin', 'standard input', { package: 'base' });
|
|
60
|
+
src('System.IO', ['getLine', 'getContents', 'hGetLine', 'hGetContents', 'hGetChar'], 'stdin', 'handle input', { package: 'base' });
|
|
61
|
+
src('Data.Text.IO', ['getLine', 'getContents', 'hGetLine', 'hGetContents', 'interact'], 'stdin', 'text input', { package: 'text' });
|
|
62
|
+
src('Data.ByteString', ['getLine', 'getContents', 'hGetLine', 'hGetContents'], 'stdin', 'bytes input', { package: 'bytestring' });
|
|
63
|
+
src('Data.ByteString.Char8', ['getLine', 'getContents', 'hGetLine', 'hGetContents'], 'stdin', 'bytes input', { package: 'bytestring' });
|
|
64
|
+
src('System.Environment', ['getArgs'], 'cli', 'command-line argument', { package: 'base' });
|
|
65
|
+
src('System.Environment', ['getEnv', 'lookupEnv', 'getEnvironment'], 'env', 'environment variable', { package: 'base' });
|
|
66
|
+
src('Prelude', ['readFile'], 'file-read', 'file content', { package: 'base' });
|
|
67
|
+
src('System.IO', ['readFile'], 'file-read', 'file content', { package: 'base' });
|
|
68
|
+
src('Data.Text.IO', ['readFile'], 'file-read', 'file content', { package: 'text' });
|
|
69
|
+
src('Data.ByteString', ['readFile'], 'file-read', 'file content', { package: 'bytestring' });
|
|
70
|
+
src('Data.ByteString.Lazy', ['readFile'], 'file-read', 'file content', { package: 'bytestring' });
|
|
71
|
+
// HTTP: scotty
|
|
72
|
+
src('Web.Scotty', ['param', 'params', 'queryParam', 'queryParams', 'captureParam', 'captureParams', 'formParam', 'formParams', 'pathParam'], 'url-param', 'scotty request parameter', { package: 'scotty', framework: 'scotty' });
|
|
73
|
+
src('Web.Scotty', ['header', 'headers'], 'header', 'scotty request header', { package: 'scotty', framework: 'scotty' });
|
|
74
|
+
src('Web.Scotty', ['body', 'jsonData', 'files'], 'http-body', 'scotty request body', { package: 'scotty', framework: 'scotty' });
|
|
75
|
+
src('Web.Scotty.Trans', ['param', 'params', 'queryParam', 'queryParams', 'captureParam', 'captureParams', 'formParam', 'formParams', 'pathParam'], 'url-param', 'scotty request parameter', { package: 'scotty', framework: 'scotty' });
|
|
76
|
+
src('Web.Scotty.Trans', ['header', 'headers'], 'header', 'scotty request header', { package: 'scotty', framework: 'scotty' });
|
|
77
|
+
src('Web.Scotty.Trans', ['body', 'jsonData', 'files'], 'http-body', 'scotty request body', { package: 'scotty', framework: 'scotty' });
|
|
78
|
+
// HTTP: WAI
|
|
79
|
+
src('Network.Wai', ['queryString', 'rawQueryString', 'rawPathInfo', 'pathInfo'], 'url-param', 'WAI request target', { package: 'wai', framework: 'wai' });
|
|
80
|
+
src('Network.Wai', ['requestHeaders', 'requestHeaderHost', 'requestHeaderReferer', 'requestHeaderUserAgent'], 'header', 'WAI request header', { package: 'wai', framework: 'wai' });
|
|
81
|
+
src('Network.Wai', ['getRequestBodyChunk', 'strictRequestBody', 'lazyRequestBody', 'requestBody'], 'http-body', 'WAI request body', { package: 'wai', framework: 'wai' });
|
|
82
|
+
// HTTP: yesod
|
|
83
|
+
src('Yesod.Core', ['lookupGetParam', 'lookupGetParams', 'lookupPostParam', 'lookupPostParams', 'runInputGet', 'runInputPost', 'lookupHeader', 'lookupCookie', 'lookupBearerAuth'], 'url-param', 'yesod request input', { package: 'yesod', framework: 'yesod' });
|
|
84
|
+
src('Yesod', ['lookupGetParam', 'lookupGetParams', 'lookupPostParam', 'lookupPostParams', 'runInputGet', 'runInputPost', 'lookupHeader', 'lookupCookie', 'lookupBearerAuth'], 'url-param', 'yesod request input', { package: 'yesod', framework: 'yesod' });
|
|
85
|
+
// HTTP responses are untrusted data too when they come back from a remote service the app calls
|
|
86
|
+
src('Network.HTTP.Simple', ['getResponseBody'], 'network', 'remote HTTP response', { package: 'http-conduit' });
|
|
87
|
+
src('Network.HTTP.Client', ['responseBody'], 'network', 'remote HTTP response', { package: 'http-client' });
|
|
88
|
+
|
|
89
|
+
// ── sinks ───────────────────────────────────────────────────────────────────
|
|
90
|
+
// family: the taint family the existing engine already uses (cmd | sql | path | url | xss)
|
|
91
|
+
const SINK = [];
|
|
92
|
+
const sink = (module, names, def) => { for (const e of E(module, names)) SINK.push({ ...e, ...def }); };
|
|
93
|
+
|
|
94
|
+
// CWE-78. A shell string is parsed by /bin/sh; an executable path is not, but a tainted one is still arbitrary execution.
|
|
95
|
+
sink('System.Process', ['callCommand', 'system', 'rawSystem'], { family: 'cmd', cwe: 'CWE-78', severity: 'critical', argIndex: 0, shell: true, package: 'process', vuln: 'OS Command Injection (shell string)', remediation: 'Do not build a shell string from input. Use `callProcess "prog" [args]` (no shell) with a fixed program, and validate each argument.' });
|
|
96
|
+
sink('System.Process', ['shell'], { family: 'cmd', cwe: 'CWE-78', severity: 'critical', argIndex: 0, shell: true, package: 'process', vuln: 'OS Command Injection (shell command spec)', remediation: 'Use `proc` with a fixed program and an argument list instead of `shell`.' });
|
|
97
|
+
sink('System.Process', ['spawnCommand', 'readCreateProcess'], { family: 'cmd', cwe: 'CWE-78', severity: 'critical', argIndex: 0, shell: true, package: 'process', vuln: 'OS Command Injection (spawnCommand)', remediation: 'Use `spawnProcess "prog" [args]` with a fixed program.' });
|
|
98
|
+
sink('System.Process', ['callProcess', 'spawnProcess', 'readProcess', 'readProcessWithExitCode', 'proc', 'createProcess'], { family: 'cmd', cwe: 'CWE-78', severity: 'high', argIndex: 0, shell: false, package: 'process', vuln: 'OS Command Injection (attacker-chosen executable)', remediation: 'The program path must be a fixed value or chosen from an allow-list.' });
|
|
99
|
+
// Argument/option injection: shell-free, so no shell parsing, but an attacker still steers the program (`--output=/etc/x`, `-e`).
|
|
100
|
+
sink('System.Process', ['callProcess', 'spawnProcess', 'proc'], { family: 'cmd', cwe: 'CWE-88', severity: 'medium', argIndex: 1, shell: false, argv: true, package: 'process', vuln: 'Argument Injection (process arguments)', remediation: 'Shell-free argument lists prevent shell parsing but not option injection: validate each argument against an allow-list and insert `--` before untrusted values.' });
|
|
101
|
+
sink('System.Process', ['readProcess', 'readProcessWithExitCode'], { family: 'cmd', cwe: 'CWE-88', severity: 'medium', argIndex: 1, shell: false, argv: true, package: 'process', vuln: 'Argument Injection (process arguments)', remediation: 'Validate each argument against an allow-list and insert `--` before untrusted values.' });
|
|
102
|
+
sink('System.Process.Typed', ['shell'], { family: 'cmd', cwe: 'CWE-78', severity: 'critical', argIndex: 0, shell: true, package: 'process', vuln: 'OS Command Injection (typed-process shell)', remediation: 'Use `proc` with a fixed program and an argument list.' });
|
|
103
|
+
|
|
104
|
+
// CWE-22. Path-taking operations.
|
|
105
|
+
const PATH_FN = ['readFile', 'writeFile', 'appendFile', 'openFile', 'withFile', 'readFile\'', 'openBinaryFile', 'withBinaryFile'];
|
|
106
|
+
sink('Prelude', ['readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'base', vuln: 'Path Traversal', remediation: 'Resolve the path with `canonicalizePath` and confirm it stays under an allowed base directory, or accept only a bare file name (`takeFileName p == p`).' });
|
|
107
|
+
sink('System.IO', ['openFile', 'withFile', 'openBinaryFile', 'withBinaryFile', 'readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'base', vuln: 'Path Traversal', remediation: 'Resolve the path with `canonicalizePath` and confirm it stays under an allowed base directory.' });
|
|
108
|
+
sink('Data.Text.IO', ['readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'text', vuln: 'Path Traversal', remediation: 'Confirm the canonical path stays under an allowed base directory.' });
|
|
109
|
+
sink('Data.ByteString', ['readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'bytestring', vuln: 'Path Traversal', remediation: 'Confirm the canonical path stays under an allowed base directory.' });
|
|
110
|
+
sink('Data.ByteString.Lazy', ['readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'bytestring', vuln: 'Path Traversal', remediation: 'Confirm the canonical path stays under an allowed base directory.' });
|
|
111
|
+
sink('System.Directory', ['removeFile', 'removeDirectory', 'removeDirectoryRecursive', 'removePathForcibly', 'createDirectory', 'createDirectoryIfMissing', 'listDirectory', 'getDirectoryContents', 'doesFileExist', 'doesDirectoryExist', 'getPermissions', 'setPermissions', 'setCurrentDirectory', 'withCurrentDirectory', 'getFileSize'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'directory', vuln: 'Path Traversal (filesystem operation)', remediation: 'Confirm the canonical path stays under an allowed base directory before using it.' });
|
|
112
|
+
sink('System.Directory', ['copyFile', 'renameFile', 'renamePath', 'renameDirectory', 'copyFileWithMetadata'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 'all', package: 'directory', vuln: 'Path Traversal (copy/rename)', remediation: 'Confirm both canonical paths stay under an allowed base directory.' });
|
|
113
|
+
void PATH_FN;
|
|
114
|
+
|
|
115
|
+
// CWE-89. The query TEXT is the sink; parameters bound through `Only`/`(a, b)` are the safe form.
|
|
116
|
+
const SQL = { family: 'sql', cwe: 'CWE-89', severity: 'critical', argIndex: 1, vuln: 'SQL Injection (query text)', remediation: 'Pass values as parameters: `query conn "SELECT … WHERE id = ?" (Only x)`. Never concatenate input into the query text.' };
|
|
117
|
+
sink('Database.PostgreSQL.Simple', ['query', 'execute', 'executeMany', 'returning', 'fold', 'forEach'], { ...SQL, package: 'postgresql-simple' });
|
|
118
|
+
sink('Database.PostgreSQL.Simple', ['query_', 'execute_', 'fold_', 'forEach_'], { ...SQL, package: 'postgresql-simple' });
|
|
119
|
+
sink('Database.PostgreSQL.Simple.Types', ['Query'], { ...SQL, argIndex: 0, package: 'postgresql-simple', vuln: 'SQL Injection (Query constructed from text)' });
|
|
120
|
+
sink('Database.MySQL.Simple', ['query', 'execute', 'executeMany', 'query_', 'execute_'], { ...SQL, package: 'mysql-simple' });
|
|
121
|
+
sink('Database.SQLite.Simple', ['query', 'execute', 'executeMany', 'query_', 'execute_', 'executeNamed', 'queryNamed', 'fold', 'fold_'], { ...SQL, package: 'sqlite-simple' });
|
|
122
|
+
sink('Database.Persist.Sql', ['rawSql', 'rawExecute', 'rawQuery'], { ...SQL, argIndex: 0, package: 'persistent', vuln: 'SQL Injection (raw SQL text)' });
|
|
123
|
+
|
|
124
|
+
// CWE-918. The URL (or request spec built from it) is the sink.
|
|
125
|
+
const SSRF = { family: 'url', cwe: 'CWE-918', severity: 'high', argIndex: 0, vuln: 'Server-Side Request Forgery', remediation: 'Parse the URL, require an allow-listed scheme and host, and reject private or link-local addresses after DNS resolution.' };
|
|
126
|
+
sink('Network.HTTP.Simple', ['httpBS', 'httpLBS', 'httpJSON', 'httpNoBody', 'httpSink', 'parseRequest', 'parseRequest_'], { ...SSRF, package: 'http-conduit' });
|
|
127
|
+
sink('Network.HTTP.Conduit', ['simpleHttp', 'parseRequest', 'parseUrlThrow', 'parseRequest_', 'httpLbs'], { ...SSRF, package: 'http-conduit' });
|
|
128
|
+
sink('Network.HTTP.Client', ['parseRequest', 'parseUrlThrow', 'parseRequest_', 'requestFromURI'], { ...SSRF, package: 'http-client' });
|
|
129
|
+
sink('Network.HTTP.Client', ['httpLbs', 'httpNoBody', 'withResponse', 'responseOpen'], { ...SSRF, argIndex: 0, package: 'http-client' });
|
|
130
|
+
sink('Network.Wreq', ['get', 'post', 'put', 'delete', 'head', 'getWith', 'postWith', 'putWith'], { ...SSRF, argIndex: 0, package: 'wreq' });
|
|
131
|
+
|
|
132
|
+
// CWE-1427. Untrusted text placed in a model request body. The call is an ordinary HTTP body setter, so the
|
|
133
|
+
// finding is kept only for a file that shows AI evidence (an AI endpoint, model literal or SDK import):
|
|
134
|
+
// haskell-llm.js drops the same flow anywhere else, because there it is just an HTTP body.
|
|
135
|
+
const LLM = { family: 'llm-prompt', cwe: 'CWE-1427', severity: 'high', argIndex: 0, vuln: 'Prompt Injection (untrusted text in a model request)', remediation: 'Keep untrusted text out of the system/instruction part of the request, constrain what the model may do with it, and treat the response as untrusted too. An allow-list or length/charset check on the value is a real control; a delimiter in the prompt is not.' };
|
|
136
|
+
sink('Network.HTTP.Simple', ['setRequestBodyJSON', 'setRequestBodyLBS', 'setRequestBodyURLEncoded'], { ...LLM, package: 'http-conduit' });
|
|
137
|
+
sink('Network.HTTP.Conduit', ['setRequestBodyJSON', 'setRequestBodyLBS'], { ...LLM, package: 'http-conduit' });
|
|
138
|
+
|
|
139
|
+
// CWE-79. Raw (already-escaped-by-contract) HTML sinks, and Scotty's html response.
|
|
140
|
+
const XSS = { family: 'xss', cwe: 'CWE-79', severity: 'high', argIndex: 0, vuln: 'Cross-Site Scripting (unescaped HTML)', remediation: 'Render with the escaping combinator (`toHtml`) instead of the raw/pre-escaped one, or sanitize with an HTML allow-list.' };
|
|
141
|
+
sink('Text.Blaze.Html', ['preEscapedToHtml'], { ...XSS, package: 'blaze-html' });
|
|
142
|
+
sink('Text.Blaze', ['preEscapedText', 'preEscapedString', 'preEscapedLazyText', 'unsafeByteString', 'unsafeLazyByteString'], { ...XSS, package: 'blaze-html' });
|
|
143
|
+
sink('Text.Blaze.Internal', ['preEscapedText', 'preEscapedString', 'preEscapedLazyText', 'unsafeByteString', 'unsafeLazyByteString'], { ...XSS, package: 'blaze-html' });
|
|
144
|
+
sink('Text.Blaze.Html5', ['preEscapedToHtml'], { ...XSS, package: 'blaze-html' });
|
|
145
|
+
sink('Lucid', ['toHtmlRaw'], { ...XSS, package: 'lucid' });
|
|
146
|
+
sink('Lucid.Base', ['toHtmlRaw'], { ...XSS, package: 'lucid' });
|
|
147
|
+
sink('Text.Hamlet', ['preEscapedToMarkup'], { ...XSS, package: 'shakespeare' });
|
|
148
|
+
sink('Web.Scotty', ['html'], { ...XSS, package: 'scotty', framework: 'scotty', vuln: 'Cross-Site Scripting (HTML response from text)' });
|
|
149
|
+
sink('Web.Scotty.Trans', ['html'], { ...XSS, package: 'scotty', framework: 'scotty', vuln: 'Cross-Site Scripting (HTML response from text)' });
|
|
150
|
+
|
|
151
|
+
// HTML assembled by concatenation and written out as text: only when the string being built contains markup (a literal
|
|
152
|
+
// "<tag"), because `putStrLn` on its own is not an HTML sink. Medium: the output channel is not proven to be a browser.
|
|
153
|
+
const XSS_TEXT = { ...XSS, severity: 'medium', htmlSkeleton: true, vuln: 'Cross-Site Scripting (HTML built by string concatenation)' };
|
|
154
|
+
sink('Prelude', ['putStrLn', 'putStr'], XSS_TEXT);
|
|
155
|
+
sink('System.IO', ['putStrLn', 'putStr'], XSS_TEXT);
|
|
156
|
+
sink('Data.Text.IO', ['putStrLn', 'putStr'], { ...XSS_TEXT, package: 'text' });
|
|
157
|
+
|
|
158
|
+
// ── sanitizers (context-specific; `appliesTo` is the family each one actually neutralises) ──
|
|
159
|
+
const SAN = [];
|
|
160
|
+
const san = (module, names, appliesTo, note, extra = {}) => { for (const e of E(module, names)) SAN.push({ ...e, appliesTo, note, ...extra }); };
|
|
161
|
+
san('Text.Blaze.Html', ['toHtml', 'toMarkup', 'text', 'string', 'lazyText'], ['xss'], 'escapes &, <, >, quotes for HTML text/attribute context');
|
|
162
|
+
san('Text.Blaze', ['toMarkup', 'toValue', 'text', 'string', 'lazyText'], ['xss'], 'escapes for HTML context');
|
|
163
|
+
san('Lucid', ['toHtml', 'toHtmlRaw_'], ['xss'], 'Lucid.toHtml escapes');
|
|
164
|
+
san('System.FilePath', ['takeFileName', 'takeBaseName'], ['path'], 'drops every directory component');
|
|
165
|
+
san('System.FilePath.Posix', ['takeFileName', 'takeBaseName'], ['path'], 'drops every directory component');
|
|
166
|
+
san('Database.PostgreSQL.Simple', ['Only'], [], 'binds a parameter; it is a safe argument position, not a query-text sanitizer');
|
|
167
|
+
|
|
168
|
+
export const HS_SOURCES = Object.freeze(SRC);
|
|
169
|
+
export const HS_SINKS = Object.freeze(SINK);
|
|
170
|
+
export const HS_SANITIZERS = Object.freeze(SAN);
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// ── logging sinks (HS-004 sensitive-data logging) ─────────────────────────────
|
|
174
|
+
// argIndex is the position of the MESSAGE (not the handle / level).
|
|
175
|
+
export const HS_LOG_SINKS = Object.freeze([
|
|
176
|
+
...E('Prelude', ['putStrLn', 'putStr', 'print']).map((e) => ({ ...e, argIndex: 0, channel: 'stdout' })),
|
|
177
|
+
...E('System.IO', ['putStrLn', 'putStr', 'print']).map((e) => ({ ...e, argIndex: 0, channel: 'stdout' })),
|
|
178
|
+
...E('System.IO', ['hPutStrLn', 'hPutStr', 'hPrint']).map((e) => ({ ...e, argIndex: 1, channel: 'handle' })),
|
|
179
|
+
...E('Data.Text.IO', ['putStrLn', 'putStr', 'hPutStrLn', 'hPutStr']).map((e) => ({ ...e, argIndex: e.name.startsWith('h') ? 1 : 0, channel: 'stdout' })),
|
|
180
|
+
...E('Prelude', ['appendFile']).map((e) => ({ ...e, argIndex: 1, channel: 'file' })),
|
|
181
|
+
...E('System.IO', ['appendFile']).map((e) => ({ ...e, argIndex: 1, channel: 'file' })),
|
|
182
|
+
...E('Debug.Trace', ['trace', 'traceShow', 'traceM', 'traceShowM', 'traceShowId', 'traceId']).map((e) => ({ ...e, argIndex: 0, channel: 'trace' })),
|
|
183
|
+
...E('Control.Monad.Logger', ['logDebugN', 'logInfoN', 'logWarnN', 'logErrorN', 'logOtherN']).map((e) => ({ ...e, argIndex: 0, channel: 'logger' })),
|
|
184
|
+
...E('Katip', ['logFM', 'logTM', 'logItem']).map((e) => ({ ...e, argIndex: 1, channel: 'logger' })),
|
|
185
|
+
...E('System.Log.Logger', ['debugM', 'infoM', 'noticeM', 'warningM', 'errorM', 'criticalM']).map((e) => ({ ...e, argIndex: 1, channel: 'logger' })),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
// Library surfaces the structural rules (haskell-security-rules.js) look for; registering them here makes
|
|
189
|
+
// the IR qualify these names through imports exactly as it does for the taint models.
|
|
190
|
+
export const HS_RULE_APIS = Object.freeze([
|
|
191
|
+
...E('Crypto.Hash', ['hash', 'hashWith', 'hashlazy']),
|
|
192
|
+
...E('Crypto.Hash.MD5', ['hash', 'hashlazy']), ...E('Crypto.Hash.SHA1', ['hash', 'hashlazy']),
|
|
193
|
+
...E('Crypto.Hash.SHA256', ['hash', 'hashlazy']), ...E('Crypto.Hash.SHA512', ['hash', 'hashlazy']),
|
|
194
|
+
...E('Data.Digest.Pure.MD5', ['md5']), ...E('Data.Digest.Pure.SHA', ['sha1', 'sha256', 'sha512']),
|
|
195
|
+
...E('Crypto.Cipher.Types', ['makeIV', 'ecbEncrypt', 'ecbDecrypt', 'nullIV', 'cbcEncrypt', 'cbcDecrypt']),
|
|
196
|
+
...E('Crypto.Cipher.ChaChaPoly1305', ['nonce12', 'nonce8']),
|
|
197
|
+
...E('Crypto.KDF.PBKDF2', ['generate', 'fastPBKDF2_SHA1', 'fastPBKDF2_SHA256', 'fastPBKDF2_SHA512']),
|
|
198
|
+
...E('System.Random', ['randomRIO', 'randomIO', 'newStdGen', 'getStdGen', 'mkStdGen', 'randomR', 'random', 'randoms', 'randomRs']),
|
|
199
|
+
...E('Crypto.Random', ['getRandomBytes']), ...E('System.Entropy', ['getEntropy']),
|
|
200
|
+
...E('Network.Wai', ['strictRequestBody', 'lazyRequestBody']),
|
|
201
|
+
...E('Data.Time.Clock.POSIX', ['getPOSIXTime']), ...E('Data.Time.Clock', ['getCurrentTime']), ...E('System.CPUTime', ['getCPUTime']),
|
|
202
|
+
...E('System.IO', ['getContents', 'hGetContents', 'appendFile']),
|
|
203
|
+
...E('Data.ByteString', ['getContents', 'hGetContents', 'replicate']), ...E('Data.ByteString.Char8', ['getContents', 'hGetContents', 'replicate']),
|
|
204
|
+
...E('Data.ByteString.Lazy', ['getContents', 'hGetContents', 'take', 'replicate']), ...E('Data.ByteString.Lazy.Char8', ['getContents', 'hGetContents', 'take']),
|
|
205
|
+
...E('Data.Text.IO', ['getContents', 'hGetContents']), ...E('Data.Text.Lazy.IO', ['getContents', 'hGetContents']),
|
|
206
|
+
...E('Data.Text', ['replicate', 'take']), ...E('Data.Vector', ['replicate']),
|
|
207
|
+
...E('Web.Cookie', ['defaultSetCookie']),
|
|
208
|
+
...E('Text.XML.HXT.Core', ['withSubstDTDEntities']),
|
|
209
|
+
...E('System.IO.Unsafe', ['unsafePerformIO', 'unsafeInterleaveIO', 'unsafeDupablePerformIO']),
|
|
210
|
+
...E('Unsafe.Coerce', ['unsafeCoerce']),
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// ── web framework surface (HS-006) ─────────────────────────────────────────────
|
|
215
|
+
// Route registration, middleware and the credential/rejection primitives the web analyser keys on.
|
|
216
|
+
export const HS_WEB_MODEL_VERSION = 'haskell-web-models/1';
|
|
217
|
+
export const HS_WEB_FRAMEWORKS = Object.freeze({
|
|
218
|
+
scotty: { modules: ['Web.Scotty', 'Web.Scotty.Trans'], package: 'scotty', tested: ['0.12', '0.20'], routeFns: ['get', 'post', 'put', 'delete', 'patch', 'options', 'addroute', 'matchAny'] },
|
|
219
|
+
wai: { modules: ['Network.Wai', 'Network.Wai.Handler.Warp'], package: 'wai', tested: ['3.2'] },
|
|
220
|
+
servant: { modules: ['Servant', 'Servant.API', 'Servant.Server', 'Servant.Auth', 'Servant.Auth.Server'], package: 'servant-server', tested: ['0.19', '0.20'] },
|
|
221
|
+
yesod: { modules: ['Yesod', 'Yesod.Core', 'Yesod.Auth'], package: 'yesod', tested: ['1.6'] },
|
|
222
|
+
});
|
|
223
|
+
export const HS_WEB_APIS = Object.freeze([
|
|
224
|
+
...E('Web.Scotty', ['get', 'post', 'put', 'delete', 'patch', 'options', 'addroute', 'matchAny', 'middleware', 'scotty', 'scottyApp', 'status', 'finish', 'raise', 'redirect', 'text', 'json', 'html', 'file', 'setHeader', 'header', 'headers', 'param', 'params', 'captureParam', 'queryParam', 'formParam', 'body', 'jsonData', 'files', 'rescue', 'liftIO', 'notFound']),
|
|
225
|
+
...E('Web.Scotty.Trans', ['get', 'post', 'put', 'delete', 'patch', 'middleware', 'scottyT', 'status', 'finish', 'raise', 'redirect', 'text', 'json', 'html', 'header', 'param', 'captureParam', 'queryParam', 'formParam', 'body', 'jsonData']),
|
|
226
|
+
...E('Network.Wai', ['pathInfo', 'requestMethod', 'requestHeaders', 'responseLBS', 'queryString', 'rawPathInfo']),
|
|
227
|
+
...E('Network.Wai.Handler.Warp', ['run', 'runSettings']),
|
|
228
|
+
...E('Network.HTTP.Types', ['status200', 'status201', 'status400', 'status401', 'status403', 'status404', 'unauthorized401', 'forbidden403', 'methodGet', 'methodPost', 'methodPut', 'methodDelete', 'hAuthorization', 'hCookie']),
|
|
229
|
+
...E('Network.Wai.Middleware.HttpAuth', ['basicAuth', 'extractBasicAuth']),
|
|
230
|
+
...E('Yesod.Core', ['requireAuthId', 'requireAuth', 'maybeAuthId', 'maybeAuth', 'permissionDenied', 'notAuthenticated', 'lookupGetParam', 'lookupPostParam', 'runInputPost', 'runInputGet', 'getYesod', 'redirect']),
|
|
231
|
+
...E('Yesod.Auth', ['requireAuthId', 'requireAuth', 'maybeAuthId', 'maybeAuth', 'isAdmin']),
|
|
232
|
+
...E('Servant', ['throwError', 'err401', 'err403', 'err404']),
|
|
233
|
+
...E('Servant.Server', ['throwError', 'err401', 'err403', 'err404', 'serve']),
|
|
234
|
+
...E('Web.JWT', ['decodeAndVerifySignature', 'decode', 'verify']),
|
|
235
|
+
...E('Web.Cookie', ['parseCookies']),
|
|
236
|
+
...E('Control.Monad.Except', ['throwError']),
|
|
237
|
+
...E('Control.Exception', ['throwIO', 'throw']),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const API_INDEX = new Map();
|
|
241
|
+
for (const s of [...SRC, ...SINK, ...SAN, ...HS_LOG_SINKS, ...HS_RULE_APIS, ...HS_WEB_APIS]) { const k = q(s.module, s.name); if (!API_INDEX.has(k)) API_INDEX.set(k, []); API_INDEX.get(k).push(s); }
|
|
242
|
+
const NAME_TO_MODULES = new Map();
|
|
243
|
+
for (const s of [...SRC, ...SINK, ...SAN, ...HS_LOG_SINKS, ...HS_RULE_APIS, ...HS_WEB_APIS]) { if (!NAME_TO_MODULES.has(s.name)) NAME_TO_MODULES.set(s.name, new Set()); NAME_TO_MODULES.get(s.name).add(s.module); }
|
|
244
|
+
|
|
245
|
+
export const isKnownApi = (module, name) => API_INDEX.has(q(module, name));
|
|
246
|
+
const SOURCE_QNAMES = new Set(SRC.map((e) => q(e.module, e.name)));
|
|
247
|
+
export const isSourceApi = (qualified) => SOURCE_QNAMES.has(qualified);
|
|
248
|
+
const SOURCE_INFO = new Map(SRC.map((e) => [q(e.module, e.name), { label: e.label, provenance: e.provenance }]));
|
|
249
|
+
export const sourceInfo = (qualified) => SOURCE_INFO.get(qualified) || null;
|
|
250
|
+
export const modelsFor = (qualified) => API_INDEX.get(qualified) || [];
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A call whose import is a wildcard among several (`import Web.Scotty` + `import Data.Text`) cannot be
|
|
254
|
+
* attributed to one module by the IR alone. If EXACTLY ONE candidate module is one this registry
|
|
255
|
+
* models for that name, use it, and report that the choice rests on the registry, not on the import list.
|
|
256
|
+
* Zero or several modelled candidates stay unresolved (returns null): the name is never guessed.
|
|
257
|
+
*/
|
|
258
|
+
export function qualifyAmbiguous(name, candidateModules) {
|
|
259
|
+
const hits = (candidateModules || []).filter((m) => isKnownApi(m, name));
|
|
260
|
+
return hits.length === 1 ? { module: hits[0], viaRegistry: true } : null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Which package a module belongs to, with the versions the models were written against. */
|
|
264
|
+
export function packageOfModule(module) {
|
|
265
|
+
for (const [pkg, def] of Object.entries(HS_PACKAGES)) if (def.modules.includes(module)) return { package: pkg, tested: def.tested };
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Tested-version status for a package. Absent version data is `unknown`, never `ok`.
|
|
271
|
+
* @returns {'tested'|'untested-version'|'unknown-version'|'unmodelled-package'}
|
|
272
|
+
*/
|
|
273
|
+
export function modelStatus(pkg, version) {
|
|
274
|
+
const def = HS_PACKAGES[pkg];
|
|
275
|
+
if (!def) return 'unmodelled-package';
|
|
276
|
+
if (!version) return 'unknown-version';
|
|
277
|
+
return def.tested.some((t) => String(version) === t || String(version).startsWith(`${t}.`)) ? 'tested' : 'untested-version';
|
|
278
|
+
}
|