@clear-capabilities/agentic-security-scanner 0.74.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 +1580 -0
- package/bin/.agentic-security/findings.json +1577 -0
- package/bin/.agentic-security/last-scan.json +1577 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +465 -0
- package/bin/.agentic-security/streak.json +25 -0
- package/bin/agentic-security-audit.js +198 -0
- package/bin/agentic-security-consistency.js +80 -0
- package/bin/agentic-security-diff.js +136 -0
- package/bin/agentic-security-lsp.js +12 -0
- package/bin/agentic-security-mcp.js +40 -0
- package/bin/agentic-security-rule.js +153 -0
- package/bin/agentic-security.js +1683 -0
- package/dist/117.index.js +207 -0
- package/dist/178.index.js +250 -0
- package/dist/218.index.js +793 -0
- package/dist/227.index.js +192 -0
- package/dist/301.index.js +167 -0
- package/dist/384.index.js +18 -0
- package/dist/476.index.js +126 -0
- package/dist/513.index.js +373 -0
- package/dist/520.index.js +13 -0
- package/dist/601.index.js +1038 -0
- package/dist/634.index.js +1892 -0
- package/dist/637.index.js +216 -0
- package/dist/660.index.js +131 -0
- package/dist/675.index.js +451 -0
- package/dist/826.index.js +188 -0
- package/dist/830.index.js +133 -0
- package/dist/agentic-security.mjs +272 -0
- package/dist/agentic-security.mjs.sha256 +1 -0
- package/dist/calibration-seed.json +27 -0
- package/package.json +77 -0
- package/src/.agentic-security/findings.json +80844 -0
- package/src/.agentic-security/last-scan.json +80844 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +8408 -0
- package/src/.agentic-security/streak.json +26 -0
- package/src/badge.js +188 -0
- package/src/compare.js +203 -0
- package/src/dataflow/.agentic-security/findings.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +735 -0
- package/src/dataflow/.agentic-security/streak.json +24 -0
- package/src/dataflow/CLAUDE.md +38 -0
- package/src/dataflow/access-paths.js +172 -0
- package/src/dataflow/async-sequencing.js +177 -0
- package/src/dataflow/backward.js +201 -0
- package/src/dataflow/catalog-expanded.js +485 -0
- package/src/dataflow/catalog.js +659 -0
- package/src/dataflow/cross-repo.js +219 -0
- package/src/dataflow/engine.js +588 -0
- package/src/dataflow/exception-flow.js +116 -0
- package/src/dataflow/exploit-prover.js +187 -0
- package/src/dataflow/higher-order.js +221 -0
- package/src/dataflow/ifds.js +347 -0
- package/src/dataflow/implicit-flow.js +129 -0
- package/src/dataflow/incremental.js +229 -0
- package/src/dataflow/index.js +181 -0
- package/src/dataflow/numeric-domain.js +192 -0
- package/src/dataflow/path-feasibility.js +114 -0
- package/src/dataflow/points-to.js +337 -0
- package/src/dataflow/polyglot.js +190 -0
- package/src/dataflow/proven-clean.js +159 -0
- package/src/dataflow/receiver-context.js +76 -0
- package/src/dataflow/sanitizer-proof.js +154 -0
- package/src/dataflow/soft-taint.js +140 -0
- package/src/dataflow/string-domain.js +234 -0
- package/src/dataflow/stub-aware-filter.js +100 -0
- package/src/dataflow/summaries.js +132 -0
- package/src/dataflow/symbolic-exec.js +238 -0
- package/src/dataflow/tabulation.js +135 -0
- package/src/engine.js +7763 -0
- package/src/history-scan.js +229 -0
- package/src/index.js +3 -0
- package/src/integrations/.agentic-security/findings.json +1504 -0
- package/src/integrations/.agentic-security/last-scan.json +1504 -0
- package/src/integrations/.agentic-security/scan-history.json +40 -0
- package/src/integrations/.agentic-security/streak.json +21 -0
- package/src/integrations/index.js +321 -0
- package/src/integrations/tickets.js +200 -0
- package/src/ir/.agentic-security/findings.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +364 -0
- package/src/ir/.agentic-security/streak.json +23 -0
- package/src/ir/CLAUDE.md +172 -0
- package/src/ir/callgraph.js +73 -0
- package/src/ir/class-hierarchy.js +195 -0
- package/src/ir/index.js +152 -0
- package/src/ir/parser-cs.js +260 -0
- package/src/ir/parser-java.js +286 -0
- package/src/ir/parser-js.js +413 -0
- package/src/ir/parser-kt.js +258 -0
- package/src/ir/parser-py-cst.js +136 -0
- package/src/ir/parser-py.helper.py +501 -0
- package/src/ir/parser-py.js +312 -0
- package/src/ir/ssa.js +315 -0
- package/src/ir/type-stubs.js +288 -0
- package/src/leaderboard.js +152 -0
- package/src/llm-validator/.agentic-security/findings.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
- package/src/llm-validator/.agentic-security/scan-history.json +168 -0
- package/src/llm-validator/.agentic-security/streak.json +20 -0
- package/src/llm-validator/consistency.js +141 -0
- package/src/llm-validator/index.js +437 -0
- package/src/lsp/.agentic-security/findings.json +28 -0
- package/src/lsp/.agentic-security/last-scan.json +28 -0
- package/src/lsp/.agentic-security/scan-history.json +79 -0
- package/src/lsp/.agentic-security/streak.json +22 -0
- package/src/lsp/server.js +275 -0
- package/src/mcp/.agentic-security/findings.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +1125 -0
- package/src/mcp/.agentic-security/streak.json +22 -0
- package/src/mcp/CLAUDE.md +54 -0
- package/src/mcp/audit.js +136 -0
- package/src/mcp/redact.js +75 -0
- package/src/mcp/server.js +158 -0
- package/src/mcp/stdio.js +83 -0
- package/src/mcp/tools.js +940 -0
- package/src/mcp/validate.js +49 -0
- package/src/personality.js +164 -0
- package/src/poc-video.js +239 -0
- package/src/posture/.agentic-security/findings.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +5557 -0
- package/src/posture/.agentic-security/streak.json +24 -0
- package/src/posture/CLAUDE.md +42 -0
- package/src/posture/adversarial-self-test.js +114 -0
- package/src/posture/adversary-agent.js +204 -0
- package/src/posture/agents-memory.js +135 -0
- package/src/posture/ai-code-fingerprint.js +171 -0
- package/src/posture/aibom.js +284 -0
- package/src/posture/api-inventory.js +96 -0
- package/src/posture/attack-playbooks.js +305 -0
- package/src/posture/auditor-agent.js +115 -0
- package/src/posture/auth-posture-import.js +135 -0
- package/src/posture/baseline-compare.js +114 -0
- package/src/posture/blast-radius.js +836 -0
- package/src/posture/bounty-prediction.js +141 -0
- package/src/posture/business-logic.js +239 -0
- package/src/posture/calibration-drift.js +93 -0
- package/src/posture/calibration-seed.json +27 -0
- package/src/posture/calibration.js +204 -0
- package/src/posture/clustering.js +75 -0
- package/src/posture/concurrency-checker.js +265 -0
- package/src/posture/confidence.js +65 -0
- package/src/posture/container-runtime.js +149 -0
- package/src/posture/counterfactual.js +109 -0
- package/src/posture/cross-lang-graphql.js +165 -0
- package/src/posture/cross-lang-grpc.js +166 -0
- package/src/posture/cross-lang-meta.js +101 -0
- package/src/posture/cross-lang-openapi.js +187 -0
- package/src/posture/cross-lang-orm.js +153 -0
- package/src/posture/cross-lang-queues.js +210 -0
- package/src/posture/crown-jewels.js +110 -0
- package/src/posture/custom-rules.js +361 -0
- package/src/posture/cve-alert-daemon.js +433 -0
- package/src/posture/cve-lookup.js +129 -0
- package/src/posture/dead-code.js +430 -0
- package/src/posture/defender-agent.js +158 -0
- package/src/posture/deploy-platform.js +204 -0
- package/src/posture/detector-fuzz.js +61 -0
- package/src/posture/deterministic.js +99 -0
- package/src/posture/drift.js +165 -0
- package/src/posture/epss.js +156 -0
- package/src/posture/exploitability-probability.js +212 -0
- package/src/posture/exploitability.js +121 -0
- package/src/posture/feature-flags.js +110 -0
- package/src/posture/finding-defaults.js +132 -0
- package/src/posture/fix-history.js +411 -0
- package/src/posture/fix-plan.js +121 -0
- package/src/posture/fix-verify-loop.js +157 -0
- package/src/posture/fix-verify.js +130 -0
- package/src/posture/flow-narration.js +105 -0
- package/src/posture/grader-calibration.js +156 -0
- package/src/posture/harness-discovery.js +113 -0
- package/src/posture/holdout-eval.js +144 -0
- package/src/posture/iac-reachability.js +163 -0
- package/src/posture/iam-policy.js +128 -0
- package/src/posture/integrity.js +97 -0
- package/src/posture/learning.js +166 -0
- package/src/posture/license-policy.js +109 -0
- package/src/posture/llm-redteam-prompts.js +418 -0
- package/src/posture/llm-redteam.js +303 -0
- package/src/posture/material-change.js +163 -0
- package/src/posture/mitigation-composite.js +55 -0
- package/src/posture/mttr.js +91 -0
- package/src/posture/network-policy-import.js +126 -0
- package/src/posture/path-predicates.js +99 -0
- package/src/posture/persona-prioritization.js +153 -0
- package/src/posture/poc-cwe-map.js +51 -0
- package/src/posture/poc-generator.js +500 -0
- package/src/posture/policy-gate.js +174 -0
- package/src/posture/pre-incident-archaeology.js +110 -0
- package/src/posture/profile.js +93 -0
- package/src/posture/reachability-filter.js +42 -0
- package/src/posture/regression-test-gen.js +200 -0
- package/src/posture/reverse-blast-radius.js +110 -0
- package/src/posture/router.js +109 -0
- package/src/posture/rule-overrides.js +198 -0
- package/src/posture/rule-pack-signing.js +209 -0
- package/src/posture/rule-packs.js +143 -0
- package/src/posture/rule-synthesis.js +108 -0
- package/src/posture/ruleset-version.js +71 -0
- package/src/posture/sbom.js +129 -0
- package/src/posture/schema-aware-bridge.js +207 -0
- package/src/posture/security-trend.js +87 -0
- package/src/posture/semantic-clone.js +114 -0
- package/src/posture/specification-mining.js +170 -0
- package/src/posture/stable-id.js +75 -0
- package/src/posture/stack-playbook.js +229 -0
- package/src/posture/streak.js +249 -0
- package/src/posture/suppressions.js +135 -0
- package/src/posture/telemetry-ingest.js +112 -0
- package/src/posture/threat-model.js +145 -0
- package/src/posture/three-agent-pipeline.js +74 -0
- package/src/posture/triage.js +146 -0
- package/src/posture/trust-boundary-diagram.js +115 -0
- package/src/posture/type-narrowing.js +129 -0
- package/src/posture/validator-metrics.js +179 -0
- package/src/posture/verifier-ephemeral.js +118 -0
- package/src/posture/verifier-target.js +147 -0
- package/src/posture/verifier.js +257 -0
- package/src/posture/version.js +75 -0
- package/src/posture/waf-ingest.js +200 -0
- package/src/posture/why-fired.js +141 -0
- package/src/pr-comment.js +172 -0
- package/src/pr-delta.js +198 -0
- package/src/report/.agentic-security/findings.json +79 -0
- package/src/report/.agentic-security/last-scan.json +79 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +332 -0
- package/src/report/.agentic-security/streak.json +23 -0
- package/src/report/index.js +1136 -0
- package/src/report/mascot.js +42 -0
- package/src/runScan.js +141 -0
- package/src/sast/.agentic-security/findings.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +788 -0
- package/src/sast/.agentic-security/streak.json +23 -0
- package/src/sast/CLAUDE.md +39 -0
- package/src/sast/_comment-strip.js +46 -0
- package/src/sast/agent-tool-escalation.js +131 -0
- package/src/sast/auth-provider.js +171 -0
- package/src/sast/authz.js +236 -0
- package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
- package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
- package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
- package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
- package/src/sast/bench-shape/index.js +62 -0
- package/src/sast/claude-hook-injection.js +199 -0
- package/src/sast/claude-md-prompt-injection.js +170 -0
- package/src/sast/claude-settings.js +165 -0
- package/src/sast/client-side.js +149 -0
- package/src/sast/cpp-bench-extras.js +122 -0
- package/src/sast/cpp-dataflow.js +430 -0
- package/src/sast/cpp.js +248 -0
- package/src/sast/csharp.js +152 -0
- package/src/sast/csrf.js +82 -0
- package/src/sast/dart-flutter.js +173 -0
- package/src/sast/db-rls.js +147 -0
- package/src/sast/db-taint.js +215 -0
- package/src/sast/defi-deep.js +242 -0
- package/src/sast/deserialization-gadgets.js +113 -0
- package/src/sast/django-hardening.js +230 -0
- package/src/sast/env-hygiene.js +125 -0
- package/src/sast/fastapi-hardening.js +145 -0
- package/src/sast/go-extended.js +84 -0
- package/src/sast/host-header.js +106 -0
- package/src/sast/index.js +17 -0
- package/src/sast/java-ast-folding.js +561 -0
- package/src/sast/java-bench-extras.js +708 -0
- package/src/sast/java-collection-passthrough.js +178 -0
- package/src/sast/java-constant-fold.js +244 -0
- package/src/sast/java-deserialization.js +125 -0
- package/src/sast/jndi.js +104 -0
- package/src/sast/juliet-shape.js +324 -0
- package/src/sast/jwt-exp.js +104 -0
- package/src/sast/kotlin.js +82 -0
- package/src/sast/laravel-hardening.js +198 -0
- package/src/sast/ldap-injection.js +100 -0
- package/src/sast/llm-owasp.js +465 -0
- package/src/sast/llm-stored-prompt.js +103 -0
- package/src/sast/llm-trading-agent.js +161 -0
- package/src/sast/llm.js +308 -0
- package/src/sast/logic.js +140 -0
- package/src/sast/mass-assignment.js +101 -0
- package/src/sast/mcp-audit.js +242 -0
- package/src/sast/mobile-manifest.js +195 -0
- package/src/sast/model-load.js +164 -0
- package/src/sast/mutation-xss.js +87 -0
- package/src/sast/nosql-injection.js +82 -0
- package/src/sast/open-redirect.js +119 -0
- package/src/sast/php.js +91 -0
- package/src/sast/pipeline.js +122 -0
- package/src/sast/primary-cwe-java.js +155 -0
- package/src/sast/prompt-firewall.js +151 -0
- package/src/sast/prompt-template.js +157 -0
- package/src/sast/prototype-pollution.js +112 -0
- package/src/sast/python-sinks.js +195 -0
- package/src/sast/quarkus-hardening.js +102 -0
- package/src/sast/rag-poisoning.js +118 -0
- package/src/sast/rate-limit.js +128 -0
- package/src/sast/response-splitting.js +138 -0
- package/src/sast/ruby.js +108 -0
- package/src/sast/rust.js +105 -0
- package/src/sast/solidity.js +167 -0
- package/src/sast/springboot-hardening.js +186 -0
- package/src/sast/ssrf-cloud-metadata.js +80 -0
- package/src/sast/ssti.js +116 -0
- package/src/sast/swift.js +162 -0
- package/src/sast/toctou.js +95 -0
- package/src/sast/webhook.js +101 -0
- package/src/sast/xpath-injection.js +51 -0
- package/src/sast/xxe.js +140 -0
- package/src/sast/zip-slip.js +200 -0
- package/src/sca/base-images.json +45 -0
- package/src/sca/container.js +107 -0
- package/src/sca/dep-confusion.js +134 -0
- package/src/sca/index.js +6 -0
- package/src/sca/popular-packages.json +41 -0
- package/src/sca/sarif-ingest.js +187 -0
- package/src/sca/vuln-function-hints.json +89 -0
- package/src/secrets/index.js +4 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Differential / incremental taint (P4.3).
|
|
2
|
+
//
|
|
3
|
+
// Today every scan re-analyzes every file. On a 100k-LoC monorepo the
|
|
4
|
+
// deep-engine pass takes 4-8 minutes. PR-scoped re-analysis should be
|
|
5
|
+
// 10-50× faster.
|
|
6
|
+
//
|
|
7
|
+
// Strategy:
|
|
8
|
+
// 1. Persist a SHA-256 of each file's POST-COMMENT-STRIP source under
|
|
9
|
+
// `.agentic-security/incremental/files.json`.
|
|
10
|
+
// 2. Persist each function's SUMMARY (returnTainted / mutatedParams /
|
|
11
|
+
// taintedGlobals) keyed by `qid` under
|
|
12
|
+
// `.agentic-security/incremental/summaries.json`.
|
|
13
|
+
// 3. On the next scan, diff the file-hash map. For unchanged files,
|
|
14
|
+
// seed the SummaryCache with the persisted summaries.
|
|
15
|
+
// 4. For CHANGED files, invalidate their qids' summaries AND the
|
|
16
|
+
// summaries of any function that previously called into them
|
|
17
|
+
// (back-pointer set persisted alongside summaries).
|
|
18
|
+
//
|
|
19
|
+
// Safety:
|
|
20
|
+
// - The cache invalidates on rule-pack version change (any change to
|
|
21
|
+
// `catalog.js` bumps the rules.lock.json digest).
|
|
22
|
+
// - The cache invalidates on scanner version change.
|
|
23
|
+
// - On any inconsistency (truncated file, JSON parse error), the cache
|
|
24
|
+
// is dropped and we fall back to a full scan.
|
|
25
|
+
//
|
|
26
|
+
// This module is purely the persistence + invalidation layer. The engine
|
|
27
|
+
// is responsible for calling `seedSummaryCache` / `recordSummary` /
|
|
28
|
+
// `commitIncrementalState`.
|
|
29
|
+
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import * as crypto from 'node:crypto';
|
|
33
|
+
|
|
34
|
+
const STATE_DIR = '.agentic-security/incremental';
|
|
35
|
+
const FILES_PATH = 'files.json';
|
|
36
|
+
const SUMMARIES_PATH = 'summaries.json';
|
|
37
|
+
const VERSION_PATH = 'version.json';
|
|
38
|
+
const MAX_PERSISTED_SUMMARIES = 50000;
|
|
39
|
+
|
|
40
|
+
/** Compute the content hash used for file-equality detection. */
|
|
41
|
+
export function hashFileContent(stripped) {
|
|
42
|
+
return crypto.createHash('sha256').update(stripped || '').digest('hex');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read the persisted state. Returns a fresh empty state on any error. */
|
|
46
|
+
export function readIncrementalState(projectRoot) {
|
|
47
|
+
const dir = path.join(projectRoot, STATE_DIR);
|
|
48
|
+
try {
|
|
49
|
+
const versionFp = path.join(dir, VERSION_PATH);
|
|
50
|
+
if (!fs.existsSync(versionFp)) return _emptyState();
|
|
51
|
+
const v = JSON.parse(fs.readFileSync(versionFp, 'utf8'));
|
|
52
|
+
return {
|
|
53
|
+
version: v,
|
|
54
|
+
files: _readJsonOrEmpty(path.join(dir, FILES_PATH), {}),
|
|
55
|
+
summaries: _readJsonOrEmpty(path.join(dir, SUMMARIES_PATH), {}),
|
|
56
|
+
};
|
|
57
|
+
} catch (_e) {
|
|
58
|
+
return _emptyState();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _emptyState() {
|
|
63
|
+
return { version: null, files: {}, summaries: {} };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _readJsonOrEmpty(fp, fallback) {
|
|
67
|
+
try {
|
|
68
|
+
if (!fs.existsSync(fp)) return fallback;
|
|
69
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
70
|
+
} catch (_e) {
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate persisted state against the current rule-pack + scanner version.
|
|
77
|
+
* Returns `{ valid: boolean, reason?: string }`.
|
|
78
|
+
*/
|
|
79
|
+
export function validateIncrementalState(state, currentVersion) {
|
|
80
|
+
if (!state || !state.version) return { valid: false, reason: 'no prior state' };
|
|
81
|
+
if (!currentVersion) return { valid: false, reason: 'no current version' };
|
|
82
|
+
if (state.version.scanner !== currentVersion.scanner) return { valid: false, reason: 'scanner version changed' };
|
|
83
|
+
if (state.version.rules !== currentVersion.rules) return { valid: false, reason: 'rule-pack changed' };
|
|
84
|
+
return { valid: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Diff the previous file-hash map against the current scan's hashes.
|
|
89
|
+
* Returns:
|
|
90
|
+
* { unchanged: [filePath, ...], changed: [filePath, ...], added: [...], removed: [...] }
|
|
91
|
+
*/
|
|
92
|
+
export function diffFileHashes(prevFiles, currentHashes) {
|
|
93
|
+
const unchanged = [];
|
|
94
|
+
const changed = [];
|
|
95
|
+
const added = [];
|
|
96
|
+
const removed = [];
|
|
97
|
+
for (const [fp, h] of Object.entries(currentHashes)) {
|
|
98
|
+
if (!(fp in prevFiles)) added.push(fp);
|
|
99
|
+
else if (prevFiles[fp] === h) unchanged.push(fp);
|
|
100
|
+
else changed.push(fp);
|
|
101
|
+
}
|
|
102
|
+
for (const fp of Object.keys(prevFiles)) {
|
|
103
|
+
if (!(fp in currentHashes)) removed.push(fp);
|
|
104
|
+
}
|
|
105
|
+
return { unchanged, changed, added, removed };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decide which previously-persisted summaries are still safe to reuse.
|
|
110
|
+
*
|
|
111
|
+
* summaries: persisted summary map { qid: summary }
|
|
112
|
+
* callerOfQid: persisted reverse-call-graph { qid: [callerQid, ...] }
|
|
113
|
+
* changedQids: Set of qids whose source files changed
|
|
114
|
+
*
|
|
115
|
+
* Returns: { reusable: Set<qid>, invalidated: Set<qid> }
|
|
116
|
+
*/
|
|
117
|
+
export function pickReusableSummaries(summaries, callerOfQid, changedQids) {
|
|
118
|
+
const invalidated = new Set();
|
|
119
|
+
// Seed with directly-changed qids.
|
|
120
|
+
for (const q of changedQids) invalidated.add(q);
|
|
121
|
+
// BFS via reverse call graph — invalidate every transitive caller.
|
|
122
|
+
const stack = [...changedQids];
|
|
123
|
+
while (stack.length) {
|
|
124
|
+
const q = stack.pop();
|
|
125
|
+
const callers = callerOfQid?.[q] || [];
|
|
126
|
+
for (const c of callers) {
|
|
127
|
+
if (!invalidated.has(c)) {
|
|
128
|
+
invalidated.add(c);
|
|
129
|
+
stack.push(c);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const reusable = new Set();
|
|
134
|
+
for (const q of Object.keys(summaries || {})) {
|
|
135
|
+
if (!invalidated.has(q)) reusable.add(q);
|
|
136
|
+
}
|
|
137
|
+
return { reusable, invalidated };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Seed a SummaryCache instance from persisted summaries. */
|
|
141
|
+
export function seedSummaryCache(summaryCache, persisted, reusableQids) {
|
|
142
|
+
if (!summaryCache || !persisted) return 0;
|
|
143
|
+
let n = 0;
|
|
144
|
+
for (const qid of reusableQids) {
|
|
145
|
+
const s = persisted[qid];
|
|
146
|
+
if (!s) continue;
|
|
147
|
+
// Reconstitute Set fields (JSON dropped them).
|
|
148
|
+
const summary = {
|
|
149
|
+
returnTainted: !!s.returnTainted,
|
|
150
|
+
mutatedParams: new Set(s.mutatedParams || []),
|
|
151
|
+
taintedGlobals: new Set(s.taintedGlobals || []),
|
|
152
|
+
findings: Array.isArray(s.findings) ? s.findings : [],
|
|
153
|
+
};
|
|
154
|
+
// Use the bottom taint-state key — these are summaries that DON'T depend
|
|
155
|
+
// on entry taint state (e.g., pure functions). Higher-fidelity reuse
|
|
156
|
+
// would require persisting the entry-state hash too; deferred.
|
|
157
|
+
summaryCache.set(qid, new Set(), summary, null);
|
|
158
|
+
n++;
|
|
159
|
+
}
|
|
160
|
+
return n;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Serialize a SummaryCache for persistence. Only persists summaries with
|
|
165
|
+
* `_persistable: true` (set by the engine when the summary is independent
|
|
166
|
+
* of an entry taint-state — typically pure functions or terminal sinks).
|
|
167
|
+
*
|
|
168
|
+
* Returns a plain object `{ qid: summary }` safe for JSON.stringify.
|
|
169
|
+
*/
|
|
170
|
+
export function serializeSummaries(summaryCache) {
|
|
171
|
+
const out = {};
|
|
172
|
+
if (!summaryCache || !summaryCache._cache) return out;
|
|
173
|
+
let count = 0;
|
|
174
|
+
for (const [key, summary] of summaryCache._cache) {
|
|
175
|
+
if (count >= MAX_PERSISTED_SUMMARIES) break;
|
|
176
|
+
if (!summary || summary._budgetExceeded || summary._recursive) continue;
|
|
177
|
+
const qid = key.split('::')[0];
|
|
178
|
+
if (!qid) continue;
|
|
179
|
+
out[qid] = {
|
|
180
|
+
returnTainted: !!summary.returnTainted,
|
|
181
|
+
mutatedParams: [...(summary.mutatedParams || [])],
|
|
182
|
+
taintedGlobals: [...(summary.taintedGlobals || [])],
|
|
183
|
+
findings: Array.isArray(summary.findings) ? summary.findings.slice(0, 50) : [],
|
|
184
|
+
};
|
|
185
|
+
count++;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Commit incremental state to disk. Idempotent — safe to call from anywhere.
|
|
192
|
+
*
|
|
193
|
+
* state.files { filepath: sha256 }
|
|
194
|
+
* state.summaries { qid: summary } (output of serializeSummaries)
|
|
195
|
+
* state.callers { qid: [callerQid] } (reverse call-graph)
|
|
196
|
+
* currentVersion { scanner, rules }
|
|
197
|
+
*/
|
|
198
|
+
export function commitIncrementalState(projectRoot, state, currentVersion) {
|
|
199
|
+
if (!projectRoot) return false;
|
|
200
|
+
const dir = path.join(projectRoot, STATE_DIR);
|
|
201
|
+
try {
|
|
202
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
203
|
+
fs.writeFileSync(path.join(dir, VERSION_PATH), JSON.stringify(currentVersion, null, 2));
|
|
204
|
+
fs.writeFileSync(path.join(dir, FILES_PATH), JSON.stringify(state.files || {}, null, 2));
|
|
205
|
+
const payload = {
|
|
206
|
+
summaries: state.summaries || {},
|
|
207
|
+
callers: state.callers || {},
|
|
208
|
+
};
|
|
209
|
+
fs.writeFileSync(path.join(dir, SUMMARIES_PATH), JSON.stringify(payload));
|
|
210
|
+
return true;
|
|
211
|
+
} catch (_e) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Drop persisted state — used when a version mismatch is detected. */
|
|
217
|
+
export function dropIncrementalState(projectRoot) {
|
|
218
|
+
const dir = path.join(projectRoot, STATE_DIR);
|
|
219
|
+
try {
|
|
220
|
+
if (!fs.existsSync(dir)) return true;
|
|
221
|
+
for (const fn of [VERSION_PATH, FILES_PATH, SUMMARIES_PATH]) {
|
|
222
|
+
const fp = path.join(dir, fn);
|
|
223
|
+
if (fs.existsSync(fp)) fs.unlinkSync(fp);
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
} catch (_e) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Layer 2 entry point.
|
|
2
|
+
import { runTaintEngine } from './engine.js';
|
|
3
|
+
import { CATALOG, matchSource, matchSinkOrSanitizer, _catalogSize } from './catalog.js';
|
|
4
|
+
import { applyPathFeasibility } from './path-feasibility.js';
|
|
5
|
+
import { SummaryCache, entryStateFromCall } from './summaries.js';
|
|
6
|
+
import { rhsReachableFunctions, shouldAnalyzeUnderRhs } from './tabulation.js';
|
|
7
|
+
import { annotateBackwardSlices } from './backward.js';
|
|
8
|
+
import {
|
|
9
|
+
readIncrementalState, validateIncrementalState, diffFileHashes,
|
|
10
|
+
hashFileContent, pickReusableSummaries, seedSummaryCache,
|
|
11
|
+
serializeSummaries, commitIncrementalState,
|
|
12
|
+
} from './incremental.js';
|
|
13
|
+
import { buildPointsTo } from './points-to.js';
|
|
14
|
+
import { annotateSoftTaint } from './soft-taint.js';
|
|
15
|
+
import { runIfdsTaintEngine } from './ifds.js';
|
|
16
|
+
import { proveExploits } from './exploit-prover.js';
|
|
17
|
+
import { applyStubAwareFilter } from './stub-aware-filter.js';
|
|
18
|
+
import { loadProjectStubs } from '../ir/type-stubs.js';
|
|
19
|
+
|
|
20
|
+
export function runDeepAnalysis(perFileIR, callGraph, opts = {}) {
|
|
21
|
+
// Path-feasibility pass over every function before the taint walk.
|
|
22
|
+
let totalPruned = 0;
|
|
23
|
+
for (const fn of callGraph.functions.values()) {
|
|
24
|
+
const r = applyPathFeasibility(fn);
|
|
25
|
+
totalPruned += r.pruned;
|
|
26
|
+
}
|
|
27
|
+
// P2.1 — RHS-lite reachability slice. When AGENTIC_SECURITY_RHS=1 the
|
|
28
|
+
// engine narrows analysis to sink-reachable functions. Default OFF
|
|
29
|
+
// because it changes the finding-set composition.
|
|
30
|
+
if (process.env.AGENTIC_SECURITY_RHS === '1') {
|
|
31
|
+
const ctx = rhsReachableFunctions(perFileIR, callGraph);
|
|
32
|
+
if (ctx.reachable) {
|
|
33
|
+
opts = { ...opts, _rhsReachable: ctx.reachable, _rhsCheck: shouldAnalyzeUnderRhs };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// v0.69 — cross-scan incremental cache (AGENTIC_SECURITY_INCREMENTAL=1).
|
|
37
|
+
// Read persisted state, seed the SummaryCache with summaries from files
|
|
38
|
+
// whose content hasn't changed, then hand it to runTaintEngine. After,
|
|
39
|
+
// serialize the cache and commit to disk.
|
|
40
|
+
const incrementalEnabled = process.env.AGENTIC_SECURITY_INCREMENTAL === '1';
|
|
41
|
+
let preSeededCache = null;
|
|
42
|
+
let priorState = null;
|
|
43
|
+
let currentFileHashes = null;
|
|
44
|
+
if (incrementalEnabled && opts.scanRoot && opts.fileContents) {
|
|
45
|
+
priorState = readIncrementalState(opts.scanRoot);
|
|
46
|
+
const currentVersion = {
|
|
47
|
+
scanner: opts.scannerVersion || 'unknown',
|
|
48
|
+
rules: opts.rulesDigest || `catalog:${_catalogSize()}`,
|
|
49
|
+
};
|
|
50
|
+
const valid = validateIncrementalState(priorState, currentVersion);
|
|
51
|
+
if (valid.valid) {
|
|
52
|
+
currentFileHashes = {};
|
|
53
|
+
for (const [fp, content] of Object.entries(opts.fileContents)) {
|
|
54
|
+
currentFileHashes[fp] = hashFileContent(content);
|
|
55
|
+
}
|
|
56
|
+
const diff = diffFileHashes(priorState.files || {}, currentFileHashes);
|
|
57
|
+
const changedQids = new Set();
|
|
58
|
+
// Map a changed file to the qids it owns. perFileIR exposes file→fns.
|
|
59
|
+
for (const fp of [...diff.changed, ...diff.added, ...diff.removed]) {
|
|
60
|
+
const ir = perFileIR[fp];
|
|
61
|
+
if (!ir) continue;
|
|
62
|
+
for (const fn of (ir.functions || [])) changedQids.add(fn.qid);
|
|
63
|
+
}
|
|
64
|
+
const persistedPayload = (priorState.summaries && priorState.summaries.summaries) || priorState.summaries || {};
|
|
65
|
+
const callerOfQid = (priorState.summaries && priorState.summaries.callers) || {};
|
|
66
|
+
const { reusable } = pickReusableSummaries(persistedPayload, callerOfQid, changedQids);
|
|
67
|
+
preSeededCache = new SummaryCache();
|
|
68
|
+
const seededN = seedSummaryCache(preSeededCache, persistedPayload, reusable);
|
|
69
|
+
preSeededCache._incrementalSeeded = seededN;
|
|
70
|
+
preSeededCache._incrementalReusable = reusable.size;
|
|
71
|
+
} else {
|
|
72
|
+
// Stale → caller should drop; we just don't seed.
|
|
73
|
+
priorState = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// v0.70 #2 — Steensgaard points-to / alias analysis. Built once before
|
|
77
|
+
// the worklist, passed via opts so the engine can resolve aliased
|
|
78
|
+
// mutations (`let a = obj; a.x = tainted; sink(obj.x)`).
|
|
79
|
+
let pointsToGraph = null;
|
|
80
|
+
if (process.env.AGENTIC_SECURITY_POINTS_TO === '1') {
|
|
81
|
+
try { pointsToGraph = buildPointsTo(perFileIR, callGraph); }
|
|
82
|
+
catch { pointsToGraph = null; }
|
|
83
|
+
}
|
|
84
|
+
// v0.71 #3 — IFDS alternative analyzer (AGENTIC_SECURITY_IFDS=1).
|
|
85
|
+
// Runs the formal Reps-Horwitz-Sagiv tabulation in parallel with the
|
|
86
|
+
// worklist engine. We MERGE findings — the IFDS solver may catch
|
|
87
|
+
// context-sensitive flows the k=2 cache joined out. Deduped by sink+line.
|
|
88
|
+
let findings = runTaintEngine(perFileIR, callGraph, {
|
|
89
|
+
...opts,
|
|
90
|
+
summaryCache: preSeededCache || undefined,
|
|
91
|
+
_pointsTo: pointsToGraph || undefined,
|
|
92
|
+
});
|
|
93
|
+
if (process.env.AGENTIC_SECURITY_IFDS === '1') {
|
|
94
|
+
try {
|
|
95
|
+
const ifdsFindings = runIfdsTaintEngine(perFileIR, callGraph, opts);
|
|
96
|
+
const existing = new Set(findings.map(f => `${f.file}:${f.line}:${f.sink?.label || ''}`));
|
|
97
|
+
for (const f of ifdsFindings) {
|
|
98
|
+
const key = `${f.file}:${f.line}:${f.sink?.label || ''}`;
|
|
99
|
+
if (!existing.has(key)) findings.push(f);
|
|
100
|
+
}
|
|
101
|
+
} catch { /* IFDS failure should not fail the scan */ }
|
|
102
|
+
}
|
|
103
|
+
for (const f of findings) f._pathFeasibilityPruned = totalPruned;
|
|
104
|
+
if (preSeededCache) {
|
|
105
|
+
Object.defineProperty(findings, '_incrementalStats', {
|
|
106
|
+
value: {
|
|
107
|
+
seeded: preSeededCache._incrementalSeeded || 0,
|
|
108
|
+
reusable: preSeededCache._incrementalReusable || 0,
|
|
109
|
+
},
|
|
110
|
+
enumerable: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// P1.4 — backward slice (opt-in via AGENTIC_SECURITY_BACKWARD_SLICE=1).
|
|
114
|
+
if (process.env.AGENTIC_SECURITY_BACKWARD_SLICE === '1') {
|
|
115
|
+
findings = annotateBackwardSlices(findings, perFileIR, callGraph);
|
|
116
|
+
}
|
|
117
|
+
// v0.70 #6 — probabilistic / soft taint. Walks each finding's trace +
|
|
118
|
+
// chain, multiplies (1 - effectiveness) across sanitizers, demotes
|
|
119
|
+
// below-threshold findings to lower severity (never drops).
|
|
120
|
+
if (process.env.AGENTIC_SECURITY_SOFT_TAINT === '1') {
|
|
121
|
+
findings = annotateSoftTaint(findings);
|
|
122
|
+
}
|
|
123
|
+
// v0.73 — type-stub-aware filter. Consults the project's TS/.pyi/JAR
|
|
124
|
+
// stub signatures (loaded by ir/type-stubs.js when AGENTIC_SECURITY_TYPE_STUBS=1).
|
|
125
|
+
// If a finding's source type is provably non-stringy (number, boolean,
|
|
126
|
+
// Date, RegExp) AND the sink class can't be triggered by that type,
|
|
127
|
+
// demote the finding's severity.
|
|
128
|
+
if (process.env.AGENTIC_SECURITY_TYPE_STUBS === '1' && opts.scanRoot) {
|
|
129
|
+
try {
|
|
130
|
+
const stubs = loadProjectStubs(opts.scanRoot);
|
|
131
|
+
findings = applyStubAwareFilter(findings, stubs);
|
|
132
|
+
} catch { /* stub load failure must not fail the scan */ }
|
|
133
|
+
}
|
|
134
|
+
// v0.71 #9 — symbolic exploit proof. For each finding, run the SMT-lite
|
|
135
|
+
// infeasibility check (and optionally Z3 when AGENTIC_SECURITY_SYMEXEC_Z3=1
|
|
136
|
+
// AND z3-solver is installed). Attach _exploitInput / _provenUnreachable.
|
|
137
|
+
if (process.env.AGENTIC_SECURITY_SYMEXEC === '1') {
|
|
138
|
+
try {
|
|
139
|
+
const useZ3 = process.env.AGENTIC_SECURITY_SYMEXEC_Z3 === '1';
|
|
140
|
+
// proveExploits returns a Promise; we keep the deep pass synchronous
|
|
141
|
+
// by not awaiting — the prover runs eagerly with z3=null (sync path).
|
|
142
|
+
// For Z3 path, callers should use the async runDeepAnalysisAsync (TBD).
|
|
143
|
+
if (!useZ3) {
|
|
144
|
+
// Synchronous SMT-lite branch.
|
|
145
|
+
// proveExploits awaits z3 only when opts.useZ3=true; otherwise it
|
|
146
|
+
// returns synchronously through the same async function (Promise of
|
|
147
|
+
// sync result). We tolerate the Promise here since findings are
|
|
148
|
+
// mutated in place.
|
|
149
|
+
const p = proveExploits(findings, { useZ3: false });
|
|
150
|
+
if (p && typeof p.then === 'function') p.catch(() => {});
|
|
151
|
+
}
|
|
152
|
+
} catch { /* prover failure should not fail the scan */ }
|
|
153
|
+
}
|
|
154
|
+
// v0.69 — commit incremental state after a successful scan.
|
|
155
|
+
if (incrementalEnabled && opts.scanRoot && currentFileHashes) {
|
|
156
|
+
const cache = findings._summaryCache;
|
|
157
|
+
const summaries = cache ? serializeSummaries(cache) : {};
|
|
158
|
+
// Reverse call-graph (qid → callers) — derive from callGraph.
|
|
159
|
+
const callers = {};
|
|
160
|
+
if (callGraph && callGraph.functions) {
|
|
161
|
+
for (const fn of callGraph.functions.values()) {
|
|
162
|
+
if (!Array.isArray(fn.calls)) continue;
|
|
163
|
+
for (const callee of fn.calls) {
|
|
164
|
+
if (!callers[callee]) callers[callee] = [];
|
|
165
|
+
callers[callee].push(fn.qid);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
commitIncrementalState(opts.scanRoot, {
|
|
170
|
+
files: currentFileHashes,
|
|
171
|
+
summaries,
|
|
172
|
+
callers,
|
|
173
|
+
}, {
|
|
174
|
+
scanner: opts.scannerVersion || 'unknown',
|
|
175
|
+
rules: opts.rulesDigest || `catalog:${_catalogSize()}`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return findings;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { runTaintEngine, CATALOG, matchSource, matchSinkOrSanitizer, _catalogSize, applyPathFeasibility, SummaryCache, entryStateFromCall, rhsReachableFunctions, shouldAnalyzeUnderRhs, annotateBackwardSlices };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Numeric range / abstract integer domain (P3.2).
|
|
2
|
+
//
|
|
3
|
+
// Today's path-feasibility module (`path-feasibility.js`) prunes branches
|
|
4
|
+
// only when the condition folds to a literal constant. This module adds an
|
|
5
|
+
// abstract domain over INTEGER VALUES so we can reason about ranges:
|
|
6
|
+
//
|
|
7
|
+
// const idx = 5;
|
|
8
|
+
// if (idx < 0) return; // ← provably dead
|
|
9
|
+
// if (idx >= arr.length) return; // ← provably dead if arr.length ≥ 6
|
|
10
|
+
// arr[idx] // ← bounds-check-safe
|
|
11
|
+
//
|
|
12
|
+
// const idx = parseInt(req.query.i);
|
|
13
|
+
// if (idx < 0 || idx >= 100) return;
|
|
14
|
+
// table[idx] // ← idx narrowed to [0,99]
|
|
15
|
+
//
|
|
16
|
+
// The abstract domain is the classical interval lattice with TOP / BOTTOM:
|
|
17
|
+
//
|
|
18
|
+
// TOP ≡ (-∞, +∞) — no information
|
|
19
|
+
// range(lo, hi) — closed interval; lo ≤ hi; lo,hi ∈ ℤ ∪ {-∞, +∞}
|
|
20
|
+
// BOTTOM — unreachable (use after a contradiction)
|
|
21
|
+
//
|
|
22
|
+
// Operations: join (∪), meet (∩), narrow-after-conditional, arithmetic
|
|
23
|
+
// (+, -, *, /), and a `decide` predicate over a relational test
|
|
24
|
+
// (lhs op rhs) returning 'true' | 'false' | 'maybe'.
|
|
25
|
+
//
|
|
26
|
+
// This is intentionally light-weight: no widening, no congruences, no
|
|
27
|
+
// strided intervals — just the things you need to prune ~30% of false-
|
|
28
|
+
// positive bounds-related paths in real code.
|
|
29
|
+
|
|
30
|
+
const NEG_INF = -Infinity;
|
|
31
|
+
const POS_INF = +Infinity;
|
|
32
|
+
|
|
33
|
+
export const TOP = Object.freeze({ kind: 'range', lo: NEG_INF, hi: POS_INF });
|
|
34
|
+
export const BOTTOM = Object.freeze({ kind: 'bottom' });
|
|
35
|
+
|
|
36
|
+
/** Build a closed interval [lo, hi]. Order is normalized. */
|
|
37
|
+
export function range(lo, hi) {
|
|
38
|
+
if (lo === undefined || lo === null) lo = NEG_INF;
|
|
39
|
+
if (hi === undefined || hi === null) hi = POS_INF;
|
|
40
|
+
if (lo > hi) return BOTTOM;
|
|
41
|
+
return { kind: 'range', lo, hi };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Convenience constructor for a single literal value. */
|
|
45
|
+
export function constant(n) {
|
|
46
|
+
if (typeof n !== 'number' || Number.isNaN(n)) return TOP;
|
|
47
|
+
if (!Number.isFinite(n)) return TOP;
|
|
48
|
+
return range(n, n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isBottom(a) { return a && a.kind === 'bottom'; }
|
|
52
|
+
function isTop(a) { return a && a.kind === 'range' && a.lo === NEG_INF && a.hi === POS_INF; }
|
|
53
|
+
|
|
54
|
+
/** Lattice join (least upper bound) — used at if/loop joins. */
|
|
55
|
+
export function join(a, b) {
|
|
56
|
+
if (!a || isBottom(a)) return b;
|
|
57
|
+
if (!b || isBottom(b)) return a;
|
|
58
|
+
return range(Math.min(a.lo, b.lo), Math.max(a.hi, b.hi));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Lattice meet (greatest lower bound) — used to narrow after a guard. */
|
|
62
|
+
export function meet(a, b) {
|
|
63
|
+
if (!a || !b) return BOTTOM;
|
|
64
|
+
if (isBottom(a) || isBottom(b)) return BOTTOM;
|
|
65
|
+
const lo = Math.max(a.lo, b.lo);
|
|
66
|
+
const hi = Math.min(a.hi, b.hi);
|
|
67
|
+
if (lo > hi) return BOTTOM;
|
|
68
|
+
return range(lo, hi);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Arithmetic. */
|
|
72
|
+
export function add(a, b) {
|
|
73
|
+
if (!a || !b || isBottom(a) || isBottom(b)) return BOTTOM;
|
|
74
|
+
return range(a.lo + b.lo, a.hi + b.hi);
|
|
75
|
+
}
|
|
76
|
+
export function sub(a, b) {
|
|
77
|
+
if (!a || !b || isBottom(a) || isBottom(b)) return BOTTOM;
|
|
78
|
+
return range(a.lo - b.hi, a.hi - b.lo);
|
|
79
|
+
}
|
|
80
|
+
export function mul(a, b) {
|
|
81
|
+
if (!a || !b || isBottom(a) || isBottom(b)) return BOTTOM;
|
|
82
|
+
const candidates = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi];
|
|
83
|
+
return range(Math.min(...candidates), Math.max(...candidates));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Decide a relational test `a op b` where a, b are ranges.
|
|
88
|
+
* Returns 'true' iff every concrete pair in a×b satisfies the test.
|
|
89
|
+
* Returns 'false' iff every concrete pair fails it.
|
|
90
|
+
* Returns 'maybe' otherwise (overlap → undecidable).
|
|
91
|
+
*
|
|
92
|
+
* Supported ops: '<', '<=', '>', '>=', '==', '!=', '===', '!=='.
|
|
93
|
+
*/
|
|
94
|
+
export function decide(a, op, b) {
|
|
95
|
+
if (!a || !b || isBottom(a) || isBottom(b)) return 'maybe';
|
|
96
|
+
switch (op) {
|
|
97
|
+
case '<': return a.hi < b.lo ? 'true' : a.lo >= b.hi ? 'false' : 'maybe';
|
|
98
|
+
case '<=': return a.hi <= b.lo ? 'true' : a.lo > b.hi ? 'false' : 'maybe';
|
|
99
|
+
case '>': return a.lo > b.hi ? 'true' : a.hi <= b.lo ? 'false' : 'maybe';
|
|
100
|
+
case '>=': return a.lo >= b.hi ? 'true' : a.hi < b.lo ? 'false' : 'maybe';
|
|
101
|
+
case '==':
|
|
102
|
+
case '===': {
|
|
103
|
+
// True iff intervals reduce to the same singleton.
|
|
104
|
+
if (a.lo === a.hi && b.lo === b.hi && a.lo === b.lo) return 'true';
|
|
105
|
+
// False iff disjoint.
|
|
106
|
+
if (a.hi < b.lo || b.hi < a.lo) return 'false';
|
|
107
|
+
return 'maybe';
|
|
108
|
+
}
|
|
109
|
+
case '!=':
|
|
110
|
+
case '!==': {
|
|
111
|
+
if (a.hi < b.lo || b.hi < a.lo) return 'true';
|
|
112
|
+
if (a.lo === a.hi && b.lo === b.hi && a.lo === b.lo) return 'false';
|
|
113
|
+
return 'maybe';
|
|
114
|
+
}
|
|
115
|
+
default: return 'maybe';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Narrow `a` by the assertion `a op b` having been observed as true.
|
|
121
|
+
* e.g. narrow(TOP, '>=', constant(0)) → range(0, +∞)
|
|
122
|
+
* narrow(TOP, '<', constant(10)) → range(-∞, 9)
|
|
123
|
+
*
|
|
124
|
+
* Returns a refined range; BOTTOM if the assertion is incompatible.
|
|
125
|
+
*/
|
|
126
|
+
export function narrow(a, op, b) {
|
|
127
|
+
if (!a || !b) return a || TOP;
|
|
128
|
+
if (isBottom(a) || isBottom(b)) return BOTTOM;
|
|
129
|
+
switch (op) {
|
|
130
|
+
case '<': return meet(a, range(NEG_INF, b.hi - 1));
|
|
131
|
+
case '<=': return meet(a, range(NEG_INF, b.hi));
|
|
132
|
+
case '>': return meet(a, range(b.lo + 1, POS_INF));
|
|
133
|
+
case '>=': return meet(a, range(b.lo, POS_INF));
|
|
134
|
+
case '==':
|
|
135
|
+
case '===': return meet(a, b);
|
|
136
|
+
case '!=':
|
|
137
|
+
case '!==': {
|
|
138
|
+
// Only refine when b is a singleton matching a boundary of a.
|
|
139
|
+
if (b.lo === b.hi) {
|
|
140
|
+
if (a.lo === b.lo) return range(a.lo + 1, a.hi);
|
|
141
|
+
if (a.hi === b.lo) return range(a.lo, a.hi - 1);
|
|
142
|
+
}
|
|
143
|
+
return a;
|
|
144
|
+
}
|
|
145
|
+
default: return a;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Abstract an AST-ish expression into a range. Returns TOP for anything
|
|
151
|
+
* we can't fold. The parser shape mirrors what `path-feasibility.js`
|
|
152
|
+
* consumes: { kind: 'literal'|'ident'|'bin', ... }
|
|
153
|
+
*
|
|
154
|
+
* env: Map<varName, range> — the abstract store
|
|
155
|
+
*/
|
|
156
|
+
export function abstractEval(expr, env) {
|
|
157
|
+
if (!expr) return TOP;
|
|
158
|
+
if (expr.kind === 'literal' && typeof expr.value === 'number' && Number.isFinite(expr.value)) {
|
|
159
|
+
return constant(expr.value);
|
|
160
|
+
}
|
|
161
|
+
if (expr.kind === 'ident' && env instanceof Map) {
|
|
162
|
+
return env.get(expr.name) || TOP;
|
|
163
|
+
}
|
|
164
|
+
if (expr.kind === 'bin') {
|
|
165
|
+
const l = abstractEval(expr.left, env);
|
|
166
|
+
const r = abstractEval(expr.right, env);
|
|
167
|
+
switch (expr.op) {
|
|
168
|
+
case '+': return add(l, r);
|
|
169
|
+
case '-': return sub(l, r);
|
|
170
|
+
case '*': return mul(l, r);
|
|
171
|
+
default: return TOP;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return TOP;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Render an interval for debugging / finding evidence. */
|
|
178
|
+
export function render(a) {
|
|
179
|
+
if (!a) return '⊤';
|
|
180
|
+
if (isBottom(a)) return '⊥';
|
|
181
|
+
if (isTop(a)) return '(-∞, +∞)';
|
|
182
|
+
const lo = a.lo === NEG_INF ? '-∞' : a.lo;
|
|
183
|
+
const hi = a.hi === POS_INF ? '+∞' : a.hi;
|
|
184
|
+
return `[${lo}, ${hi}]`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** True iff `a ⊑ b` (a is at least as precise as b). */
|
|
188
|
+
export function leq(a, b) {
|
|
189
|
+
if (isBottom(a)) return true;
|
|
190
|
+
if (isBottom(b)) return false;
|
|
191
|
+
return a.lo >= b.lo && a.hi <= b.hi;
|
|
192
|
+
}
|