@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,430 @@
|
|
|
1
|
+
// Lightweight intra-procedural data-flow detectors for C/C++.
|
|
2
|
+
//
|
|
3
|
+
// Goal: catch real-world CVE patterns that the banned-API rules in cpp.js
|
|
4
|
+
// miss — bugs that don't have a single "bad function call" but show up as
|
|
5
|
+
// a sequence of operations on the same variable within a function body.
|
|
6
|
+
//
|
|
7
|
+
// Five detectors:
|
|
8
|
+
// 1. use-after-free — free(p) ... deref/use of p downstream in same function
|
|
9
|
+
// 2. double-free — free(p) ... free(p) again on the same path
|
|
10
|
+
// 3. missing-null-check — p = malloc(); ... *p / p->x without `if (p)` guard
|
|
11
|
+
// 4. alloc-size-overflow — malloc(n * sizeof(T)) where n is not bounds-checked
|
|
12
|
+
// and comes from an untrusted source (param/recv/read)
|
|
13
|
+
// 5. off-by-one-loop — for (i=0; i <= len; i++) access arr[i] when arr is
|
|
14
|
+
// sized [len] (inclusive bound on size-typed length)
|
|
15
|
+
//
|
|
16
|
+
// Architecture:
|
|
17
|
+
// - parseFunctions(): naive brace-balanced split of a translation unit into
|
|
18
|
+
// function bodies + their parameter lists. No tree-sitter; we use the
|
|
19
|
+
// same brace-counting approach as findCppMethodSpans in the bench.
|
|
20
|
+
// - Per-function: tokenise into a line stream and track interesting
|
|
21
|
+
// "events" per variable (declared, assigned-from-alloc, freed, deref'd,
|
|
22
|
+
// null-checked, used-in-arithmetic). Emit a finding when the sequence
|
|
23
|
+
// matches one of the bug shapes.
|
|
24
|
+
//
|
|
25
|
+
// Gating: every detector requires multiple correlated events. A standalone
|
|
26
|
+
// `free(p)` is never enough — we need free + later use. This keeps FPs low
|
|
27
|
+
// on production code where free() is everywhere but UAF is rare.
|
|
28
|
+
|
|
29
|
+
const CPP_EXT_RE = /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/i;
|
|
30
|
+
|
|
31
|
+
// Parse-error counter — exposed via /status so silent failures are observable.
|
|
32
|
+
// Never resets between calls; intentionally process-lifetime so the counter
|
|
33
|
+
// accumulates across all files in a scan session.
|
|
34
|
+
export const _parseErrorCount = { value: 0 };
|
|
35
|
+
|
|
36
|
+
// Escape a string for safe interpolation into a RegExp source pattern.
|
|
37
|
+
// varNames come from C/C++ identifiers matched by [\w.->]+ — they can
|
|
38
|
+
// contain dots (struct member a.b) and arrow (a->b). Escape metacharacters
|
|
39
|
+
// so the caller's dynamic regex cannot catastrophically backtrack or mismatch.
|
|
40
|
+
function _esc(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
41
|
+
|
|
42
|
+
// Function-finder uses brace counting, NOT regex — the previous catch-all
|
|
43
|
+
// regex with nested `(?:...)+` quantifiers exhibited catastrophic backtracking
|
|
44
|
+
// on real-world C/C++ headers. See _findFunctions below.
|
|
45
|
+
//
|
|
46
|
+
// Keyword set that looks like a function call but isn't a definition.
|
|
47
|
+
const _NON_FN_KEYWORDS = new Set([
|
|
48
|
+
'if', 'while', 'for', 'switch', 'sizeof', 'return', 'else', 'catch',
|
|
49
|
+
'typedef', 'do', 'case', 'goto', 'throw', 'static_cast', 'dynamic_cast',
|
|
50
|
+
'reinterpret_cast', 'const_cast', '__attribute__', '__asm__', 'asm',
|
|
51
|
+
'decltype', 'alignof', 'alignas', 'noexcept', 'static_assert',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// Tokens we recognise inside function bodies. Comments are pre-stripped.
|
|
55
|
+
const _FREE_CALL_RE = /\b(?:free|kfree|vfree|g_free|av_free|av_freep|sk_free)\s*\(\s*([\w.->]+)\s*\)/g;
|
|
56
|
+
const _ALLOC_ASSIGN_RE = /\b([A-Za-z_]\w*)\s*=\s*(?:\(\s*[\w\s*]+\s*\)\s*)?(?:malloc|calloc|realloc|kmalloc|kzalloc|kcalloc|g_malloc|g_malloc0|av_malloc|av_mallocz|xmalloc|strdup|strndup|memdup|fopen)\s*\(([^)]*)\)/g;
|
|
57
|
+
const _DEREF_RE = /(?:\*\s*([A-Za-z_]\w*)\b|\b([A-Za-z_]\w*)\s*->|\b([A-Za-z_]\w*)\s*\[)/g;
|
|
58
|
+
const _NULL_CHECK_RE = /\bif\s*\(\s*(?:!\s*([A-Za-z_]\w*)\b|([A-Za-z_]\w*)\s*(?:==|!=)\s*NULL|([A-Za-z_]\w*)\s*(?:==|!=)\s*0\b|([A-Za-z_]\w*)\s*(?:==|!=)\s*nullptr)/g;
|
|
59
|
+
const _ARRAY_ACCESS_RE = /\b([A-Za-z_]\w*)\s*\[\s*([A-Za-z_]\w*)\s*\]/g;
|
|
60
|
+
const _LOOP_FOR_RE = /\bfor\s*\(\s*(?:[\w\s*]+?\s+)?([A-Za-z_]\w*)\s*=\s*0\s*;\s*\1\s*(<=|<)\s*([A-Za-z_]\w*)\s*;\s*(?:\+\+\1|\1\s*\+\+|\1\s*\+=\s*1)\s*\)/g;
|
|
61
|
+
const _SIZEOF_INT_RE = /\b(?:malloc|calloc|kmalloc|kzalloc|g_malloc|g_malloc0|xmalloc)\s*\(\s*([A-Za-z_]\w*)\s*\*\s*sizeof\s*\(/g;
|
|
62
|
+
// Tainted-source identifiers — values coming from outside the function are
|
|
63
|
+
// suspect for size-overflow detection.
|
|
64
|
+
const _TAINT_SOURCE_RE = /\b(?:recv|recvfrom|read|readv|fread|fgets|getline|getenv|scanf|sscanf|fscanf|atoi|atol|strtol|strtoul|ntohl|ntohs|be32toh|le32toh)\b/;
|
|
65
|
+
|
|
66
|
+
// Strip C/C++ comments AND string/char literals. Replace each with same-length
|
|
67
|
+
// whitespace so line numbers and offsets stay correct.
|
|
68
|
+
function _blank(content) {
|
|
69
|
+
let out = '', i = 0, n = content.length;
|
|
70
|
+
while (i < n) {
|
|
71
|
+
const ch = content[i];
|
|
72
|
+
if (ch === '/' && content[i + 1] === '/') {
|
|
73
|
+
while (i < n && content[i] !== '\n') { out += ' '; i++; }
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === '/' && content[i + 1] === '*') {
|
|
77
|
+
out += ' '; i += 2;
|
|
78
|
+
while (i < n - 1 && !(content[i] === '*' && content[i + 1] === '/')) {
|
|
79
|
+
out += content[i] === '\n' ? '\n' : ' '; i++;
|
|
80
|
+
}
|
|
81
|
+
out += i < n ? ' ' : ''; i += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '"' || ch === "'") {
|
|
85
|
+
const q = ch; out += q; i++;
|
|
86
|
+
while (i < n && content[i] !== q) {
|
|
87
|
+
if (content[i] === '\\' && i + 1 < n) { out += ' '; i += 2; continue; }
|
|
88
|
+
out += content[i] === '\n' ? '\n' : ' '; i++;
|
|
89
|
+
}
|
|
90
|
+
if (i < n) { out += q; i++; }
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out += ch; i++;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Find each top-level function definition via brace counting.
|
|
99
|
+
// Walks the file char-by-char, tracking `{` depth. At each `{` that takes
|
|
100
|
+
// depth from 0→1, we look backwards from that `{` for the most recent
|
|
101
|
+
// `<name>(<args>)` pair that looks like a function declaration. Then we
|
|
102
|
+
// find the matching `}` and extract the body span.
|
|
103
|
+
//
|
|
104
|
+
// Bounds: hard-cap at 5000 functions per file to avoid pathological cases.
|
|
105
|
+
function _findFunctions(content) {
|
|
106
|
+
const fns = [];
|
|
107
|
+
const n = content.length;
|
|
108
|
+
let depth = 0;
|
|
109
|
+
let i = 0;
|
|
110
|
+
const PARSE_LIMIT = 5000;
|
|
111
|
+
while (i < n && fns.length < PARSE_LIMIT) {
|
|
112
|
+
const ch = content[i];
|
|
113
|
+
if (ch === '{') {
|
|
114
|
+
if (depth === 0) {
|
|
115
|
+
// depth 0→1 transition. Look backwards from i for the function
|
|
116
|
+
// signature: skip whitespace, then expect `)`, scan back to matching
|
|
117
|
+
// `(`, then identify the function name before `(`.
|
|
118
|
+
let j = i - 1;
|
|
119
|
+
while (j >= 0 && /\s/.test(content[j])) j--;
|
|
120
|
+
// Optional const/noexcept/throw(...) before `{` (skip them).
|
|
121
|
+
// Scan back up to ~200 chars for the closing `)`.
|
|
122
|
+
const lookbackStart = Math.max(0, i - 200);
|
|
123
|
+
const tail = content.substring(lookbackStart, i);
|
|
124
|
+
const rparenIdx = tail.lastIndexOf(')');
|
|
125
|
+
if (rparenIdx > 0) {
|
|
126
|
+
// Find matching `(` by counting parens.
|
|
127
|
+
let pdepth = 1, k = rparenIdx - 1;
|
|
128
|
+
while (k >= 0 && pdepth > 0) {
|
|
129
|
+
if (tail[k] === ')') pdepth++;
|
|
130
|
+
else if (tail[k] === '(') pdepth--;
|
|
131
|
+
if (pdepth === 0) break;
|
|
132
|
+
k--;
|
|
133
|
+
}
|
|
134
|
+
if (k >= 0) {
|
|
135
|
+
// Identifier just before `(` (after trimming whitespace).
|
|
136
|
+
let nameEnd = k - 1;
|
|
137
|
+
while (nameEnd >= 0 && /\s/.test(tail[nameEnd])) nameEnd--;
|
|
138
|
+
let nameStart = nameEnd;
|
|
139
|
+
while (nameStart >= 0 && /[A-Za-z0-9_]/.test(tail[nameStart])) nameStart--;
|
|
140
|
+
nameStart++;
|
|
141
|
+
const name = tail.substring(nameStart, nameEnd + 1);
|
|
142
|
+
if (name && /^[A-Za-z_]\w*$/.test(name) && !_NON_FN_KEYWORDS.has(name)) {
|
|
143
|
+
// Find matching `}` for this function body via brace counting.
|
|
144
|
+
let bdepth = 1, bi = i + 1;
|
|
145
|
+
while (bi < n && bdepth > 0) {
|
|
146
|
+
const bc = content[bi];
|
|
147
|
+
if (bc === '{') bdepth++;
|
|
148
|
+
else if (bc === '}') bdepth--;
|
|
149
|
+
bi++;
|
|
150
|
+
}
|
|
151
|
+
if (bdepth === 0) {
|
|
152
|
+
const startLine = content.substring(0, i).split('\n').length + 1;
|
|
153
|
+
const body = content.substring(i + 1, bi - 1);
|
|
154
|
+
fns.push({ name, startLine, body });
|
|
155
|
+
// Jump past this function body so we don't re-enter on nested `{`.
|
|
156
|
+
depth = 0;
|
|
157
|
+
i = bi;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Not a recognised function signature; track depth normally.
|
|
164
|
+
depth++;
|
|
165
|
+
} else {
|
|
166
|
+
depth++;
|
|
167
|
+
}
|
|
168
|
+
} else if (ch === '}') {
|
|
169
|
+
if (depth > 0) depth--;
|
|
170
|
+
}
|
|
171
|
+
i++;
|
|
172
|
+
}
|
|
173
|
+
return fns;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Position → line within a function body (1-based, relative to body start).
|
|
177
|
+
function _lineOfOffset(body, off) {
|
|
178
|
+
let n = 1;
|
|
179
|
+
for (let i = 0; i < off && i < body.length; i++) if (body[i] === '\n') n++;
|
|
180
|
+
return n;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Detector 1: use-after-free ─────────────────────────────────────────────
|
|
184
|
+
// Pattern: free(p) appears at line L1; later in the SAME function, p is
|
|
185
|
+
// dereferenced (*p, p->x, p[i]) or passed to another function — AND there is
|
|
186
|
+
// no intervening reassignment of p (e.g. p = NULL; or p = malloc()).
|
|
187
|
+
function _detectUseAfterFree(fnBody, fnStart) {
|
|
188
|
+
const findings = [];
|
|
189
|
+
// Collect every free(p) site.
|
|
190
|
+
_FREE_CALL_RE.lastIndex = 0;
|
|
191
|
+
let m;
|
|
192
|
+
const frees = [];
|
|
193
|
+
while ((m = _FREE_CALL_RE.exec(fnBody))) {
|
|
194
|
+
frees.push({ varName: m[1], off: m.index, end: m.index + m[0].length, line: _lineOfOffset(fnBody, m.index) });
|
|
195
|
+
}
|
|
196
|
+
if (!frees.length) return findings;
|
|
197
|
+
for (const f of frees) {
|
|
198
|
+
// Track from end of free() call — starting 1 char in left `ree(p)` which
|
|
199
|
+
// matched the "use" pattern. Use the stored end offset instead.
|
|
200
|
+
const after = fnBody.substring(f.end);
|
|
201
|
+
// Reassignment kills the dangling pointer.
|
|
202
|
+
const ev = _esc(f.varName);
|
|
203
|
+
const reassignRe = new RegExp(`\\b${ev}\\s*=\\s*(?!=)`);
|
|
204
|
+
const reassignMatch = reassignRe.exec(after);
|
|
205
|
+
const stopOff = reassignMatch ? reassignMatch.index : after.length;
|
|
206
|
+
const scanRegion = after.substring(0, stopOff);
|
|
207
|
+
// Look for any deref or call passing the var.
|
|
208
|
+
const usePat = new RegExp(
|
|
209
|
+
`(?:\\*\\s*${ev}\\b|\\b${ev}\\s*->|\\b${ev}\\s*\\[|[\\w]+\\s*\\(\\s*${ev}\\s*[,)])`,
|
|
210
|
+
);
|
|
211
|
+
const useMatch = usePat.exec(scanRegion);
|
|
212
|
+
if (!useMatch) continue;
|
|
213
|
+
// The free() at line L; the use is on line L_use. Emit on L_use.
|
|
214
|
+
const useLineRel = _lineOfOffset(after, useMatch.index);
|
|
215
|
+
const freeLineRel = _lineOfOffset(fnBody, f.off);
|
|
216
|
+
const useLine = fnStart + freeLineRel + useLineRel - 1;
|
|
217
|
+
findings.push({
|
|
218
|
+
id: `cpp-flow:uaf:${useLine}`,
|
|
219
|
+
severity: 'high',
|
|
220
|
+
cwe: 'CWE-416',
|
|
221
|
+
family: 'mem-unsafe',
|
|
222
|
+
line: useLine,
|
|
223
|
+
vuln: 'Use-after-free — pointer used after free() in same function',
|
|
224
|
+
remediation: `Set the pointer to NULL after free() to fail loudly on later use: \`free(${f.varName}); ${f.varName} = NULL;\` or restructure so the use never occurs.`,
|
|
225
|
+
_parser: 'CPP_DATAFLOW',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return findings;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Detector 2: double-free ───────────────────────────────────────────────
|
|
232
|
+
function _detectDoubleFree(fnBody, fnStart) {
|
|
233
|
+
const findings = [];
|
|
234
|
+
_FREE_CALL_RE.lastIndex = 0;
|
|
235
|
+
const frees = [];
|
|
236
|
+
let m;
|
|
237
|
+
while ((m = _FREE_CALL_RE.exec(fnBody))) {
|
|
238
|
+
frees.push({ varName: m[1], off: m.index, end: m.index + m[0].length, line: _lineOfOffset(fnBody, m.index) });
|
|
239
|
+
}
|
|
240
|
+
if (frees.length < 2) return findings;
|
|
241
|
+
// Group by varName, look for two frees with no intervening reassignment.
|
|
242
|
+
const byVar = new Map();
|
|
243
|
+
for (const f of frees) {
|
|
244
|
+
if (!byVar.has(f.varName)) byVar.set(f.varName, []);
|
|
245
|
+
byVar.get(f.varName).push(f);
|
|
246
|
+
}
|
|
247
|
+
for (const [varName, list] of byVar) {
|
|
248
|
+
if (list.length < 2) continue;
|
|
249
|
+
for (let i = 0; i < list.length - 1; i++) {
|
|
250
|
+
const a = list[i], b = list[i + 1];
|
|
251
|
+
const between = fnBody.substring(a.off + 1, b.off);
|
|
252
|
+
const reassignRe = new RegExp(`\\b${_esc(varName)}\\s*=\\s*(?!=)`);
|
|
253
|
+
if (reassignRe.test(between)) continue;
|
|
254
|
+
const line = fnStart + b.line - 1;
|
|
255
|
+
findings.push({
|
|
256
|
+
id: `cpp-flow:double-free:${line}`,
|
|
257
|
+
severity: 'high',
|
|
258
|
+
cwe: 'CWE-415',
|
|
259
|
+
family: 'mem-unsafe',
|
|
260
|
+
line,
|
|
261
|
+
vuln: 'Double-free — same pointer freed twice without reassignment',
|
|
262
|
+
remediation: `Set the pointer to NULL after the first free(): \`free(${varName}); ${varName} = NULL;\`. free(NULL) is a no-op, so the second free becomes safe.`,
|
|
263
|
+
_parser: 'CPP_DATAFLOW',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return findings;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Detector 3: missing-null-check before deref of allocator return ───────
|
|
271
|
+
// Pattern: p = malloc(...); ... *p or p->x or p[i] before any
|
|
272
|
+
// if (p), if (!p), if (p == NULL), if (p != NULL).
|
|
273
|
+
function _detectMissingNullCheck(fnBody, fnStart) {
|
|
274
|
+
const findings = [];
|
|
275
|
+
_ALLOC_ASSIGN_RE.lastIndex = 0;
|
|
276
|
+
let m;
|
|
277
|
+
while ((m = _ALLOC_ASSIGN_RE.exec(fnBody))) {
|
|
278
|
+
const varName = m[1];
|
|
279
|
+
// Tail of function from after this allocation.
|
|
280
|
+
const after = fnBody.substring(m.index + m[0].length);
|
|
281
|
+
// Find first null-check OR deref, whichever comes first.
|
|
282
|
+
const checkPat = new RegExp(
|
|
283
|
+
`\\bif\\s*\\(\\s*(?:!\\s*${varName}\\b|${varName}\\s*(?:==|!=)\\s*(?:NULL|nullptr|0)\\b|\\(\\s*${varName}\\s*\\)\\s*(?:==|!=))`,
|
|
284
|
+
);
|
|
285
|
+
const derefPat = new RegExp(
|
|
286
|
+
`(?:\\*\\s*${varName}\\b|\\b${varName}\\s*->|\\b${varName}\\s*\\[)`,
|
|
287
|
+
);
|
|
288
|
+
const checkMatch = checkPat.exec(after);
|
|
289
|
+
const derefMatch = derefPat.exec(after);
|
|
290
|
+
if (!derefMatch) continue; // never derefed → no risk visible
|
|
291
|
+
if (checkMatch && checkMatch.index < derefMatch.index) continue; // checked first → ok
|
|
292
|
+
const lineRel = _lineOfOffset(after, derefMatch.index);
|
|
293
|
+
const allocLineRel = _lineOfOffset(fnBody, m.index);
|
|
294
|
+
const line = fnStart + allocLineRel + lineRel - 1;
|
|
295
|
+
findings.push({
|
|
296
|
+
id: `cpp-flow:no-null-check:${line}`,
|
|
297
|
+
severity: 'medium',
|
|
298
|
+
cwe: 'CWE-476',
|
|
299
|
+
family: 'mem-unsafe',
|
|
300
|
+
line,
|
|
301
|
+
vuln: 'Missing NULL check — allocator return dereferenced without verification',
|
|
302
|
+
remediation: `Check ${varName} before use: \`if (!${varName}) return -1;\`. Allocators can return NULL on OOM; dereferencing yields a SIGSEGV on Linux/macOS and an exploitable crash on some kernels.`,
|
|
303
|
+
_parser: 'CPP_DATAFLOW',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return findings;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Detector 4: allocation size overflow ──────────────────────────────────
|
|
310
|
+
// Pattern: malloc(n * sizeof(T)) where n appears to come from an untrusted
|
|
311
|
+
// source AND there is no `if (n > MAX)` bounds check before the malloc.
|
|
312
|
+
function _detectAllocSizeOverflow(fnBody, fnStart) {
|
|
313
|
+
const findings = [];
|
|
314
|
+
_SIZEOF_INT_RE.lastIndex = 0;
|
|
315
|
+
let m;
|
|
316
|
+
while ((m = _SIZEOF_INT_RE.exec(fnBody))) {
|
|
317
|
+
const sizeVar = m[1];
|
|
318
|
+
// Skip integer literals (handled by other rules).
|
|
319
|
+
if (/^\d+$/.test(sizeVar)) continue;
|
|
320
|
+
const before = fnBody.substring(0, m.index);
|
|
321
|
+
const esv = _esc(sizeVar);
|
|
322
|
+
// Heuristic 1: the size variable was assigned from a known tainted source
|
|
323
|
+
// earlier in the function.
|
|
324
|
+
const taintAssignRe = new RegExp(
|
|
325
|
+
`\\b${esv}\\s*=\\s*[^;]*${_TAINT_SOURCE_RE.source}`,
|
|
326
|
+
);
|
|
327
|
+
if (!taintAssignRe.test(before)) continue;
|
|
328
|
+
// Heuristic 2: there is no bound check on sizeVar before the malloc.
|
|
329
|
+
const boundCheckRe = new RegExp(
|
|
330
|
+
`\\bif\\s*\\(\\s*${esv}\\s*(?:>=?|<=?)\\s*\\w+`,
|
|
331
|
+
);
|
|
332
|
+
if (boundCheckRe.test(before)) continue;
|
|
333
|
+
const lineRel = _lineOfOffset(fnBody, m.index);
|
|
334
|
+
const line = fnStart + lineRel - 1;
|
|
335
|
+
findings.push({
|
|
336
|
+
id: `cpp-flow:alloc-size-overflow:${line}`,
|
|
337
|
+
severity: 'high',
|
|
338
|
+
cwe: 'CWE-190',
|
|
339
|
+
family: 'buffer-overflow',
|
|
340
|
+
line,
|
|
341
|
+
vuln: 'Allocation size overflow — externally-derived count without bounds check',
|
|
342
|
+
remediation: `Validate ${sizeVar} before allocation: \`if (${sizeVar} > MAX_COUNT) return -1;\`. A multiplied count from a 32-bit input can overflow size_t on 32-bit systems and wrap on 64-bit, returning a tiny buffer that downstream writes overflow.`,
|
|
343
|
+
_parser: 'CPP_DATAFLOW',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return findings;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Detector 5: off-by-one loop bound on length-sized array ───────────────
|
|
350
|
+
// Pattern: for (i = 0; i <= len; i++) followed by access to arr[i]
|
|
351
|
+
// where arr is declared with size [len] or similar. Inclusive `<=` on a
|
|
352
|
+
// size-typed bound iterates one element past the end.
|
|
353
|
+
function _detectOffByOne(fnBody, fnStart) {
|
|
354
|
+
const findings = [];
|
|
355
|
+
_LOOP_FOR_RE.lastIndex = 0;
|
|
356
|
+
let m;
|
|
357
|
+
while ((m = _LOOP_FOR_RE.exec(fnBody))) {
|
|
358
|
+
const cmp = m[2];
|
|
359
|
+
if (cmp !== '<=') continue; // only `<=` is the bug
|
|
360
|
+
const idxVar = m[1], boundVar = m[3];
|
|
361
|
+
// Find the loop body and check for `arr[idxVar]` access where the array
|
|
362
|
+
// dim is `boundVar` or where `boundVar` looks like a length.
|
|
363
|
+
// We don't track the declared array size precisely — instead, require the
|
|
364
|
+
// bound var to be named *len, *count, *size, etc. (length-typed).
|
|
365
|
+
if (!/^\w*(?:len|Len|count|Count|size|Size|length|Length|n)$/.test(boundVar)) continue;
|
|
366
|
+
// Inclusive iteration over [0..len] is one-past-end on a [len]-sized
|
|
367
|
+
// array. Emit if the loop body actually indexes some array with idxVar.
|
|
368
|
+
const loopStart = m.index + m[0].length;
|
|
369
|
+
// Bound the loop body to the next 1500 chars (conservative window).
|
|
370
|
+
const tail = fnBody.substring(loopStart, Math.min(fnBody.length, loopStart + 1500));
|
|
371
|
+
const idxAccessRe = new RegExp(`\\b\\w+\\s*\\[\\s*${_esc(idxVar)}\\s*\\]`);
|
|
372
|
+
if (!idxAccessRe.test(tail)) continue;
|
|
373
|
+
const lineRel = _lineOfOffset(fnBody, m.index);
|
|
374
|
+
const line = fnStart + lineRel - 1;
|
|
375
|
+
findings.push({
|
|
376
|
+
id: `cpp-flow:off-by-one:${line}`,
|
|
377
|
+
severity: 'medium',
|
|
378
|
+
cwe: 'CWE-193',
|
|
379
|
+
family: 'buffer-overflow',
|
|
380
|
+
line,
|
|
381
|
+
vuln: 'Off-by-one in loop bound — inclusive comparison against length-typed variable',
|
|
382
|
+
remediation: `Use \`<\` instead of \`<=\` when iterating up to an array length: \`for (int ${idxVar} = 0; ${idxVar} < ${boundVar}; ${idxVar}++)\`. The inclusive form iterates one past the last valid index.`,
|
|
383
|
+
_parser: 'CPP_DATAFLOW',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return findings;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function scanCppDataflow(file, raw) {
|
|
390
|
+
// Feature flag: off by default until scanner/test/cpp-dataflow.test.js
|
|
391
|
+
// passes and fixture pairs exist under scanner/test/fixtures/cpp-dataflow/.
|
|
392
|
+
// Enable with: AGENTIC_SECURITY_CPP_DATAFLOW=1
|
|
393
|
+
if (!process.env.AGENTIC_SECURITY_CPP_DATAFLOW) return [];
|
|
394
|
+
if (!CPP_EXT_RE.test(file) || !raw) return [];
|
|
395
|
+
if (raw.length > 500_000) return []; // very large files: skip
|
|
396
|
+
const stripped = _blank(raw);
|
|
397
|
+
const fns = _findFunctions(stripped);
|
|
398
|
+
if (!fns.length) {
|
|
399
|
+
// No detected function bodies — apply detectors over the whole file as
|
|
400
|
+
// one pseudo-function so we don't miss BigVul-style fragmented patches.
|
|
401
|
+
fns.push({ name: '__file__', startLine: 1, body: stripped });
|
|
402
|
+
}
|
|
403
|
+
const all = [];
|
|
404
|
+
for (const fn of fns) {
|
|
405
|
+
try {
|
|
406
|
+
all.push(..._detectUseAfterFree(fn.body, fn.startLine));
|
|
407
|
+
all.push(..._detectDoubleFree(fn.body, fn.startLine));
|
|
408
|
+
all.push(..._detectMissingNullCheck(fn.body, fn.startLine));
|
|
409
|
+
all.push(..._detectAllocSizeOverflow(fn.body, fn.startLine));
|
|
410
|
+
all.push(..._detectOffByOne(fn.body, fn.startLine));
|
|
411
|
+
} catch (err) {
|
|
412
|
+
// Count parse failures so /status can surface them; never rethrow.
|
|
413
|
+
_parseErrorCount.value++;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const f of all) {
|
|
417
|
+
f.file = file;
|
|
418
|
+
f.kind = 'sast';
|
|
419
|
+
if (!f.snippet) {
|
|
420
|
+
const lines = raw.split('\n');
|
|
421
|
+
f.snippet = (lines[f.line - 1] || '').trim().slice(0, 200);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return all;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export const _internals = {
|
|
428
|
+
_blank, _esc, _findFunctions, _detectUseAfterFree, _detectDoubleFree,
|
|
429
|
+
_detectMissingNullCheck, _detectAllocSizeOverflow, _detectOffByOne,
|
|
430
|
+
};
|