@clear-capabilities/agentic-security-scanner 0.195.14 → 0.195.15
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 +15 -0
- package/dist/{4fe5c024ca0626112826.json → 9699866704e8a4a5839f.json} +2 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/language/CLAUDE.md +1 -0
- package/src/language/haskell-ir.js +8 -1
- package/src/language/haskell-security-rules.js +41 -2
- package/src/language/haskell-syntax.js +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3d395cf95358af8a372a685bf6119f46dac36e3502237a525812949a53776c3c agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.195.
|
|
3
|
+
"version": "0.195.15",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"test:lineage": "node --test test/lineage/analysis-completion.test.js test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js test/lineage/runtime-assurance.test.js test/lineage/runtime-edge-states.test.js test/lineage/runtime-explorer-view.test.js test/cli/runtime-explorer-cli.test.js test/cli/runtime-identity-cli.test.js test/cli/runtime-twin-cli.test.js test/lineage/cut-planner.test.js test/lineage/scc.test.js test/lineage/scc-solver.test.js test/graph/graph-views.test.js test/lineage/dag07-completion.test.js test/lineage/owned-build.test.js test/dataflow/boundary-propagation.test.js test/lineage/boundary-contract.test.js test/lineage/correlation-v2.test.js test/lineage/path-control-evidence.test.js test/lineage/smt-z3-backend.test.js test/cli/dag-commands.test.js",
|
|
94
94
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js test/proof-corpus-lib.test.js test/proof-corpus-runner.test.js",
|
|
95
95
|
"test:server": "node --test test/server/graph-loader.test.js test/server/security.test.js test/server/routes.test.js test/server/http-server.test.js test/server/cmd-explore.test.js test/server/cmd-dataflow-export.test.js test/server/static-assets.test.js test/server/dataflow-scenario-cli.test.js test/server/dataflow-impact-cli.test.js",
|
|
96
|
-
"test:haskell": "node --test test/haskell/haskell-reachability.test.js test/haskell/haskell-real-code-shapes.test.js test/haskell/haskell-unseen-v5-shapes.test.js test/haskell/haskell-unseen-v4-shapes.test.js test/haskell/haskell-alias-taint.test.js test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-remediation-0190.test.js test/haskell/haskell-remediation-kdf.test.js test/haskell/haskell-remediation-html-text.test.js test/haskell/haskell-remediation-scotty-body.test.js test/haskell/haskell-taint-shapes.test.js test/haskell/haskell-real-advisory-shapes.test.js test/haskell/haskell-discovery-shapes.test.js test/haskell/haskell-quasiquote-interp.test.js test/haskell/haskell-record-dot.test.js test/haskell/haskell-global-cell.test.js test/haskell/haskell-syntax-extensions.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js test/haskell/haskell-boundary-propagation.test.js test/haskell/haskell-generalization.test.js test/haskell/haskell-trust-boundary.test.js test/haskell/haskell-comment-invariance.test.js test/haskell/haskell-remediation-classes.test.js test/haskell/haskell-trust-boundary-frameworks.test.js test/haskell/haskell-generic-detectors.test.js",
|
|
96
|
+
"test:haskell": "node --test test/haskell/haskell-reachability.test.js test/haskell/haskell-real-code-shapes.test.js test/haskell/haskell-unseen-v5-shapes.test.js test/haskell/haskell-unseen-v4-shapes.test.js test/haskell/haskell-alias-taint.test.js test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-remediation-0190.test.js test/haskell/haskell-remediation-kdf.test.js test/haskell/haskell-remediation-html-text.test.js test/haskell/haskell-remediation-scotty-body.test.js test/haskell/haskell-taint-shapes.test.js test/haskell/haskell-real-advisory-shapes.test.js test/haskell/haskell-discovery-shapes.test.js test/haskell/haskell-quasiquote-interp.test.js test/haskell/haskell-record-dot.test.js test/haskell/haskell-global-cell.test.js test/haskell/haskell-syntax-extensions.test.js test/haskell/haskell-rule-precision.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js test/haskell/haskell-boundary-propagation.test.js test/haskell/haskell-generalization.test.js test/haskell/haskell-trust-boundary.test.js test/haskell/haskell-comment-invariance.test.js test/haskell/haskell-remediation-classes.test.js test/haskell/haskell-trust-boundary-frameworks.test.js test/haskell/haskell-generic-detectors.test.js",
|
|
97
97
|
"test:nix": "node --test test/nix/nix-option-types.test.js test/nix/nix-development-shapes.test.js test/nix/nixos-hardening-daemon.test.js test/nix/nixos-hardening-extended.test.js test/nix/nix-build-trust.test.js test/nix/nix-eval-isolation.test.js test/nix/nix-input-inventory.test.js test/nix/nix-parser-ir.test.js test/nix/nix-remediation.test.js test/nix/nix-resolved-closure.test.js test/nix/nix-sca-patches.test.js test/nix/nix-sca-severity.test.js test/nix/nix-haskell-closure-names.test.js test/nix/nix-script-taint.test.js test/nix/nix-secrets-store.test.js test/nix/nixos-hardening.test.js test/nix/nixos-module-resolution.test.js test/nix/nix-fix-multifile.test.js test/nix/nixos-evaluator-forms.test.js test/nix/nix-advisory-feed.test.js test/nix/nix-fix-prewrite.test.js test/nix/nix-boundary-scope.test.js",
|
|
98
98
|
"test:language": "node --test test/bench-haskell-precision.test.js test/bench-independent-haskell-panel.test.js test/language/capability-ledger.test.js test/language/language-accuracy-regression.test.js test/language/language-aibom.test.js test/language/language-assurance.test.js test/language/language-bom.test.js test/language/language-compliance.test.js test/language/language-contracts.test.js test/language/language-corpora-integrity.test.js test/language/language-discovery.test.js test/language/language-doc-coverage.test.js test/language/language-evidence-posture.test.js test/language/language-explorer.test.js test/language/frozen-splits.test.js test/language/language-gap-fixes.test.js test/language/language-gap-fixes-2.test.js test/language/language-governance.test.js test/language/language-integrations.test.js test/language/language-llm-agent.test.js test/language/language-local-ai.test.js test/language/language-parity-haskell.test.js test/language/language-polyglot-bridges.test.js test/language/language-privacy-lineage.test.js test/language/language-proof-remediation.test.js test/language/language-report-formats.test.js test/language/language-secrets.test.js test/language/language-trust-boundary.test.js",
|
|
99
99
|
"test:language-stress": "node --test --test-concurrency=1 test/language/language-stress-offline.test.js",
|
package/src/language/CLAUDE.md
CHANGED
|
@@ -66,6 +66,7 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
|
|
|
66
66
|
- **Guards are recognised by what they do.** `haskell-guards.js`: a project function is an HTML escaper when it (or its local helpers) spells out the entities for `<`, `>` and `&` (`htmlEscapers`; never by name), and it neutralises a CWE-79 flow only when EVERY use of the value in the sink argument is under such a call (`allUsesEscaped`); a host allow-list may be a named constant and may sit in an `&&`; `sh -c SCRIPT arg0 arg1` is shell injection only when the SCRIPT is built from the value, and tainted positional parameters are data when the constant script expands them only inside double quotes after `--` (`quotedPositionals`, which decodes the Haskell string escapes the IR keeps raw).
|
|
67
67
|
- **Handlers named by the framework's convention are routes.** `haskell-web.js` adds `getFooR`/`postFooR`/`handleFooR` functions whose signature mentions `Handler` in a module importing Yesod when no route table in an analysed file names them (the table usually lives in `config/routes`). Scotty's rejection order is judged by `inlineOrder` as well as by line: a `case` lists the success branch first, so `status 40x >> finish` in the LAST alternative guards it (a bare `status` with no halting call does not).
|
|
68
68
|
- **A project definition of a modelled name keeps the registry's meaning (since 0.195.11).** `haskell-ir.js` `applyVar`/`lowerVarValue`: a resolution to a project `def` whose module-qualified name `isKnownApi` (the project IS the framework, or vendors it) is lowered as the LIBRARY call (`hs.status: 'external'`, `hs.modelledDefinition: qid`) and the definition is still referenced (`noteRef(..., 'modelled-definition')`, so it is never an uncalled entry point). Before this, IHP's own `IHP.Controller.Param.param` shadowed the catalog when IHP scanned itself, and a request parameter reaching `Process.system` through a helper was not reported (four lost true positives on the fresh set). A test stub named like a catalog module gets the same treatment, which is why the html-text remediation test keeps its scotty stub out of the scan. Pinned both ways in `test/haskell/haskell-taint-shapes.test.js`, with the three shapes below.
|
|
69
|
+
- **Rule precision from semantics (since 0.195.15; `test/haskell/haskell-rule-precision.test.js`).** `haskell-security-rules.js`: a partial list function (`NONEMPTY_TOTAL`) on a list that is a cons, a non-empty literal, or a concatenation with one of those (`nonEmptyList`) is total and not reported; the conditions a function tests include the first argument of `when`/`unless`/`guard` (`COND_COMBINATOR`), not only `if` nodes; a comparison of a list's LENGTH with a number (`case length xs of 3`, `length xs == 2`; `LENGTH_CALL`, `lengthVars` for the scratch variable the IR puts a case scrutinee in) tests the list. `hash x :: Digest MD5` / `hashlazy x :: Digest SHA1`: `haskell-syntax.js` keeps a `Digest <algo>` annotation as a `typed` node (`DIGEST_TYPE`), `haskell-ir.js` returns the inner call stamped `hs.digest`, and `hs-weak-hash` treats it like `hashWith MD5`. `hs-unsafe-io-boundary` is already lowered to `info` for every site by `engine.js` (`originalSeverity` kept), so no shape rule was added for it.
|
|
69
70
|
- **Record dot, labels, global cells, lambda-form functions, more syntax (since 0.195.14).** `haskell-syntax.js` reads `r.field` / `(e).field` / `(.field)` as a field selection (`sel` token, `RECORD_DOT_PRAGMA`; the IR lowers `field r` to a member read of a known record field) and `#field` as a label value (`LABELS_PRAGMA`; the IR lowers it to a literal, so IHP's `get #f r` / `set #f v r` are library calls carrying the record's and the value's taint, not a field selection: a stated limit); both only under the LANGUAGE pragma. `\cases` (`lamcases`), `mdo` (a `do` under `RECDO_PRAGMA`; `rec` blocks are not read), GADT `data T where` constructors (names, record fields, arity by arrows) and `pattern P ..` (a `patsyn` declaration, not a function named `pattern`) parse. `haskell-ir.js` `lambdaBodied` rewrites `f = \x -> ..`, `f = \case ..`, `f = \cases ..` into `f x = ..` at the top level and in `let`/`where` (a call then resolves to the function instead of applying a closure value). `cellBodyOf` recognises a module-level cell (`name = unsafePerformIO (newIORef ..)`, MVar/TVar/STRef alike) and `lowerCellOp` names it `$cell:<qid>` for the whole project (a read is that ident with `hs.globalCell`, a write an assignment to it); `dataflow/engine.js` `_cellSeeds` records a cell any function assigned tainted data to and `_unionAnnotationTaint` seeds it at the entry of every Haskell function, the fixed-point pre-pass re-running when the set grows (flow-insensitive; the root is not carried across, so the trust verdict of the reader is `unattributed`). `aliasBodyOf` accepts a partial application of a library function to names or literals (`remember = writeIORef lastName`), with the argument names checked for shadowing. `haskell-models.js`: typed-process `proc` (CWE-78 program, CWE-88 arguments), esqueleto `rawSql`/`rawExecute`, optparse-applicative `execParser`/`customExecParser` as a `cli` source, versions recorded in `HS_PACKAGES`. Pinned in `test/haskell/haskell-{record-dot,global-cell,syntax-extensions}.test.js`.
|
|
70
71
|
- **String-interpolating quasi-quoters (since 0.195.14; `test/haskell/haskell-quasiquote-interp.test.js`).** `haskell-parser.js` `INTERP_QUOTERS` maps a module to its quoters and interpolation syntax (`hash` `#{e}`: Data.String.Interpolate, Data.String.Interpolate.IsString, Text.Shakespeare.Text; `dollar` `$name`/`${e}`: NeatInterpolation; `brace` `{e}`/`{e:spec}`: PyF); `interpolationsOf` reads them (escapes `$$`, `{{`; a format spec after a top-level `:` is dropped). An accepted quote is rewritten in place, same length and newlines, as `(iQ (e1) (e2))` (kind `quasiquote-interp`, non-opaque, assumption `upstream-quoter`); a literal-only body stays `quasiquote-inert`; an unimported module or an unknown quoter stays `quasiquote` (opaque). `haskell-ir.js` `applyVar` lowers an UNBOUND `iQ` application to a concatenation (`binary +`, `hs.concat`, `hs.quasiquote`), so the taint engine follows it and a sanitizer inside an interpolation is on the path. The template text is not modelled (no markup literal for `htmlSkeleton` sinks).
|
|
71
72
|
- **The discovery set and two layout rules (since 0.195.13; `bench/dag/discovery-haskell`, pinned in `test/haskell/haskell-discovery-shapes.test.js`).** Six projects the engine had never been run on (hoogle, the wai and servant monorepos, mighttpd2, postgres-websockets, the yesod scaffold), scanned once and every flagged site read and labelled (`labels.json`, `USE-LOG.md`): a DEVELOPMENT set, not the precision gate's fresh set and not the sealed set. Hoogle's request handler was dropped by two layout rules, fixed in BOTH parsers: a `where` that starts a line at an implicit block's own indent closes the block (Haskell's parse-error(t) rule; `haskell-parser.js` `openBlock`, as `haskell-syntax.js` `blockAfter` already did), and a `;` separates the items of an implicit block only on a line that starts at the block's indent and opened no nested block (`do`, `of`, `let`, `where`, `\case`) earlier on that line (`splitItems` in both; `s <- act pay; pure s` on one line of a nested `do`, `remod t = do (a,_) <- f t; pure a` in a `where`). The route-contract detector (`posture/api-contract.js` `contractScope`) now scopes a contract file to the package that holds it, which is what turned a servant monorepo's two example `swagger.json` files into 78 Haskell route findings.
|
|
@@ -599,7 +599,14 @@ class FnLowerer {
|
|
|
599
599
|
// `read s :: Int`: the value is a number, so it is a clean constant for the taint engine. The lowered inner expression rides on `hs.inner`, which the rule and web
|
|
600
600
|
// analyses read (an unsafe coercion in it, the id it names), exactly as they read `hs.selector`. (A sanitizer call would be wrong here: the engine's sanitizer label is per expression, so a
|
|
601
601
|
// number in `"kill " ++ show n ++ name` would mark the tainted `name` sanitized too.)
|
|
602
|
-
case 'typed':
|
|
602
|
+
case 'typed': {
|
|
603
|
+
if (e.digest) { // `hash x :: Digest MD5`: the call itself, stamped with the algorithm the annotation selects
|
|
604
|
+
const inner = this.lowerExpr(e.e, ln);
|
|
605
|
+
if (inner && inner.kind === 'call') inner.hs = { ...(inner.hs || {}), digest: e.digest };
|
|
606
|
+
return inner;
|
|
607
|
+
}
|
|
608
|
+
return { kind: 'literal', value: 0, hs: { numeric: e.type, inner: this.lowerExpr(e.e, ln) } };
|
|
609
|
+
}
|
|
603
610
|
case 'tuple': return { kind: 'array', elements: e.items.map((x) => this.lowerExpr(x, ln)), hs: { tuple: true } };
|
|
604
611
|
case 'list': return { kind: 'array', elements: e.items.map((x) => this.lowerExpr(x, ln)) };
|
|
605
612
|
case 'range': return { kind: 'array', elements: e.items.map((x) => this.lowerExpr(x, ln)), hs: { range: true } };
|
|
@@ -49,6 +49,10 @@ export const BOUNDARIES = Object.freeze({
|
|
|
49
49
|
|
|
50
50
|
const WEAK_HASH_CALLS = new Set(['Crypto.Hash.MD5.hash', 'Crypto.Hash.MD5.hashlazy', 'Crypto.Hash.SHA1.hash', 'Crypto.Hash.SHA1.hashlazy', 'Data.Digest.Pure.MD5.md5', 'Data.Digest.Pure.MD5.md5DigestBytes', 'Data.Digest.Pure.SHA.sha1', 'Data.Digest.Pure.SHA.sha1Incremental']);
|
|
51
51
|
const WEAK_ALGO_CONS = new Set(['MD5', 'MD4', 'MD2', 'SHA1']);
|
|
52
|
+
// Partial list functions that are total on a list with at least one element (see nonEmptyList), and the length calls a size test may name.
|
|
53
|
+
const NONEMPTY_TOTAL = /(?:^|\.)(?:head|last|init|tail|maximum|minimum|foldr1|foldl1|maximumBy|minimumBy|cycle)$/;
|
|
54
|
+
const LENGTH_CALL = /(?:^|\.)(?:length|genericLength|size)$/;
|
|
55
|
+
const COND_COMBINATOR = /(?:^|\.)(?:when|unless|guard)$/;
|
|
52
56
|
const ANY_HASH_CALLS = new Set([...WEAK_HASH_CALLS, 'Crypto.Hash.hash', 'Crypto.Hash.hashlazy', 'Crypto.Hash.hashWith', 'Crypto.Hash.SHA256.hash', 'Crypto.Hash.SHA256.hashlazy', 'Crypto.Hash.SHA512.hash', 'Crypto.Hash.SHA512.hashlazy', 'Data.Digest.Pure.SHA.sha256', 'Data.Digest.Pure.SHA.sha512']);
|
|
53
57
|
const WEAK_CIPHER_MODULES = new Set(['Crypto.Cipher.DES', 'Crypto.Cipher.RC4', 'Crypto.Cipher.RC2', 'Crypto.Cipher.Blowfish']);
|
|
54
58
|
const ECB_CALLS = new Set(['Crypto.Cipher.Types.ecbEncrypt', 'Crypto.Cipher.Types.ecbDecrypt']);
|
|
@@ -547,7 +551,40 @@ export function analyzeHaskellRules(files, opts = {}) {
|
|
|
547
551
|
if (x.kind === 'binary' && ['==', '/=', '!='].includes(x.op) && [x.left, x.right].some((side) => side && side.kind === 'array' && side.elements.length === 0)) for (const nm of directNames(x)) testedVars.add(nm);
|
|
548
552
|
}
|
|
549
553
|
}
|
|
554
|
+
// `case length xs of 3 -> ..`, `if length xs == 3`: a comparison of the list's LENGTH with a number is a test of the list (the IR lowers the
|
|
555
|
+
// case alternative to an equality on the scrutinee variable, so the length call sits behind an assignment).
|
|
556
|
+
const lengthVars = new Set();
|
|
557
|
+
for (const n of fnNodes(fn)) if (n.kind === 'assign' && n.target && n.source && n.source.kind === 'call' && typeof n.source.callee === 'string' && LENGTH_CALL.test(n.source.callee)) lengthVars.add(n.target);
|
|
558
|
+
// The conditions this function tests: every `if`, plus the first argument of `when` / `unless` / `guard` wherever the call sits
|
|
559
|
+
// (`when (length xs > 2) (putStrLn (xs !! 2))` is a guard written as a combinator, not an `if` node).
|
|
560
|
+
const condExprs = [];
|
|
561
|
+
for (const n of fnNodes(fn)) {
|
|
562
|
+
if (n.kind === 'if' && n.cond) condExprs.push(n.cond);
|
|
563
|
+
for (const ex of [n.value, n.source, ...(n.args || [])]) if (ex) for (const x of walk(ex)) if (x.kind === 'call' && typeof x.callee === 'string' && COND_COMBINATOR.test(x.callee) && x.args && x.args[0]) condExprs.push(x.args[0]);
|
|
564
|
+
}
|
|
565
|
+
for (const cond of condExprs) {
|
|
566
|
+
for (const x of walk(cond)) {
|
|
567
|
+
if (x.kind === 'call' && typeof x.callee === 'string' && EMPTINESS_CHECK.test(x.callee)) {
|
|
568
|
+
for (const nm of directNames(x)) testedVars.add(nm);
|
|
569
|
+
for (const s of splitsIn(x)) testedSplits.add(s);
|
|
570
|
+
// `case length xs of 3 -> ..`: the match is on a scratch variable holding the length; the list behind it is what was tested
|
|
571
|
+
for (const a of x.args || []) if (a && a.kind === 'ident' && lengthVars.has(a.name)) for (const nm of assignSources.get(a.name) || []) testedVars.add(nm);
|
|
572
|
+
}
|
|
573
|
+
if (x.kind !== 'binary' || !['==', '/=', '!='].includes(x.op)) continue;
|
|
574
|
+
for (const side of [x.left, x.right]) {
|
|
575
|
+
if (side && side.kind === 'call' && typeof side.callee === 'string' && LENGTH_CALL.test(side.callee)) for (const nm of directNames(side)) testedVars.add(nm);
|
|
576
|
+
if (side && side.kind === 'ident' && lengthVars.has(side.name)) for (const nm of assignSources.get(side.name) || []) testedVars.add(nm);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
550
580
|
const testedAll = withOrigins(testedVars);
|
|
581
|
+
// `head (x : xs)`, `last (xs ++ [x])`, `head [a, b]`: the list has at least one element, so the call is total (real code: xmonad's `last (["Root"] ++ ..)`).
|
|
582
|
+
const nonEmptyList = (e) => {
|
|
583
|
+
if (!e || typeof e !== 'object') return false;
|
|
584
|
+
if (e.kind === 'array') return !!(e.hs && e.hs.cons) || (!(e.hs && (e.hs.tuple || e.hs.con || e.hs.range)) && (e.elements || []).length > 0);
|
|
585
|
+
if (e.kind === 'binary' && e.hs && e.hs.concat) return nonEmptyList(e.left) || nonEmptyList(e.right);
|
|
586
|
+
return false;
|
|
587
|
+
};
|
|
551
588
|
const isTested = (args) => {
|
|
552
589
|
if (args.some((a) => splitsIn(a).some((s) => testedSplits.has(s)))) return true;
|
|
553
590
|
const mine = withOrigins([...args.flatMap((a) => [...directNames(a)])]);
|
|
@@ -686,7 +723,9 @@ export function analyzeHaskellRules(files, opts = {}) {
|
|
|
686
723
|
// a signing/encryption secret that is a constant in the source
|
|
687
724
|
if (SECRET_KEY_CALLS.has(c) && args[0] && isLiteralLike(args[0]) && !(args[0].kind === 'literal' && String(args[0].value).length < 8)) emit('hs-hardcoded-secret', file, line, { detail: `${c} given a constant`, confidence: 0.85, evidence: { call: c } });
|
|
688
725
|
// weak hash
|
|
689
|
-
|
|
726
|
+
// `hash x :: Digest MD5` / `hashlazy x :: Digest SHA1`: the digest is chosen by the type annotation (haskell-syntax.js keeps a
|
|
727
|
+
// `Digest <algo>` annotation as a `typed` node; haskell-ir.js stamps `hs.digest` on the call).
|
|
728
|
+
if (WEAK_HASH_CALLS.has(c) || (c === 'Crypto.Hash.hashWith' && args[0] && args[0].kind === 'literal' && WEAK_ALGO_CONS.has(args[0].value)) || (ANY_HASH_CALLS.has(c) && e.hs && e.hs.digest && WEAK_ALGO_CONS.has(e.hs.digest))) {
|
|
690
729
|
const pw = args.some((a) => [...names(a)].some((n) => PASSWORD_VAR.test(n)));
|
|
691
730
|
// MD5 and SHA-1 are broken for collision and preimage resistance, which only matters where an attacker gains from a
|
|
692
731
|
// collision: a digest that is VERIFIED (compared with an expected value), that authenticates or signs, that hashes a
|
|
@@ -732,7 +771,7 @@ export function analyzeHaskellRules(files, opts = {}) {
|
|
|
732
771
|
emit('hs-unbounded-allocation', file, line, { detail: `${c} sized by parsed caller text`, evidence: { allocator: c } });
|
|
733
772
|
}
|
|
734
773
|
// a partial function applied to caller text: one malformed value raises an uncaught exception
|
|
735
|
-
if (PARTIALS.has(c) && args.some((a) => mentionsText(a)) && !isTested(args)) emit('hs-partial-function-on-input', file, line, { detail: `${c} on caller-supplied input`, evidence: { partial: c }, inlineCtx: e.hs && e.hs.inlineCtx });
|
|
774
|
+
if (PARTIALS.has(c) && !(NONEMPTY_TOTAL.test(c) && nonEmptyList(args[0])) && args.some((a) => mentionsText(a)) && !isTested(args)) emit('hs-partial-function-on-input', file, line, { detail: `${c} on caller-supplied input`, evidence: { partial: c }, inlineCtx: e.hs && e.hs.inlineCtx });
|
|
736
775
|
// a session/token cookie assembled without HttpOnly or Secure
|
|
737
776
|
if (c === 'Web.Cookie.defaultSetCookie') { /* handled on the record update below */ }
|
|
738
777
|
// unbounded body
|
|
@@ -23,6 +23,7 @@ const TH_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bTemplateHaskell\b/;
|
|
|
23
23
|
const RECORD_DOT_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bOverloadedRecordDot\b/;
|
|
24
24
|
const LABELS_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bOverloadedLabels\b/;
|
|
25
25
|
const RECDO_PRAGMA = /\{-#\s*LANGUAGE\b[^#]*\bRecursiveDo\b/;
|
|
26
|
+
const DIGEST_TYPE = /^\(?\s*Digest\s+([A-Za-z][A-Za-z0-9_]*)\s*\)?$/;
|
|
26
27
|
const RESERVED_OPS = new Set(['=', '|', '->', '<-', '::', '=>', '\\', '@', '~', '..']);
|
|
27
28
|
const SYM = new Set('!#$%&*+./<=>?@\\^|-~:'.split(''));
|
|
28
29
|
const OPEN = { '(': ')', '[': ']', '{': '}' };
|
|
@@ -459,6 +460,8 @@ export function parseSyntax(src, opts = {}) {
|
|
|
459
460
|
// lowers to a constant (no caller text survives such a type). Every other annotation is dropped as before.
|
|
460
461
|
const typeText = T.slice(from, pos).map((x) => x.v).join(' ');
|
|
461
462
|
if (NUMERIC_TYPE.test(typeText)) e = { t: 'typed', e, type: typeText, line: e && e.line };
|
|
463
|
+
// `hash x :: Digest MD5`: the annotation chooses the digest algorithm (type-directed selection); kept for the weak-hash rule.
|
|
464
|
+
else { const m = DIGEST_TYPE.exec(typeText); if (m) e = { t: 'typed', e, type: typeText, digest: m[1], line: e && e.line }; }
|
|
462
465
|
}
|
|
463
466
|
return e;
|
|
464
467
|
} finally { depth--; }
|