@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,288 @@
|
|
|
1
|
+
// Type-stub integration (v0.70 #7).
|
|
2
|
+
//
|
|
3
|
+
// Today the engine treats `req.body` as opaque-tainted. With type stubs,
|
|
4
|
+
// we can refine: `req.body: any` but `req.body.email: string` after
|
|
5
|
+
// destructuring; `string.length: number`; `string.match(): RegExpMatchArray`.
|
|
6
|
+
// This eliminates a class of FPs where "everything reachable from a
|
|
7
|
+
// tainted root" was conservatively tainted.
|
|
8
|
+
//
|
|
9
|
+
// v1 supports:
|
|
10
|
+
// - TypeScript `.d.ts` declarations in `node_modules/@types/**`
|
|
11
|
+
// - Python `.pyi` stubs at the project root (best-effort)
|
|
12
|
+
// - Java JAR `MANIFEST.MF` class signatures (best-effort; v2 will use
|
|
13
|
+
// proper class-file parsing)
|
|
14
|
+
//
|
|
15
|
+
// Public API:
|
|
16
|
+
// loadProjectStubs(root) → { signatures: Map<qid, {paramTypes, returnType}>,
|
|
17
|
+
// types: Map<typeName, FieldMap>,
|
|
18
|
+
// frameworks: Set<string> }
|
|
19
|
+
// signatureFor(stubs, qidOrName) → { paramTypes, returnType } | null
|
|
20
|
+
// typeOf(stubs, typeName) → FieldMap | null
|
|
21
|
+
//
|
|
22
|
+
// Cache lives at $XDG_CONFIG_HOME/agentic-security/stub-cache/<projectHash>.json
|
|
23
|
+
// keyed by package-lock.json content hash so the parse runs once per project
|
|
24
|
+
// snapshot.
|
|
25
|
+
//
|
|
26
|
+
// Budget: parsing is capped at AGENTIC_SECURITY_TYPE_STUBS_BUDGET_MS
|
|
27
|
+
// (default 10_000). When the budget blows, parsed stubs are still
|
|
28
|
+
// returned — incomplete is honest.
|
|
29
|
+
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import * as crypto from 'node:crypto';
|
|
33
|
+
import * as os from 'node:os';
|
|
34
|
+
|
|
35
|
+
const CACHE_DIR_REL = 'agentic-security/stub-cache';
|
|
36
|
+
|
|
37
|
+
function _cacheBase() {
|
|
38
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
39
|
+
if (xdg) return path.join(xdg, CACHE_DIR_REL);
|
|
40
|
+
return path.join(os.homedir(), '.config', CACHE_DIR_REL);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _projectFingerprint(root) {
|
|
44
|
+
// Hash inputs that uniquely identify the stub state.
|
|
45
|
+
const inputs = [];
|
|
46
|
+
for (const p of ['package-lock.json', 'package.json', 'requirements.txt', 'poetry.lock', 'pom.xml']) {
|
|
47
|
+
const fp = path.join(root, p);
|
|
48
|
+
try { inputs.push(p + ':' + fs.statSync(fp).mtimeMs); } catch {}
|
|
49
|
+
}
|
|
50
|
+
return crypto.createHash('sha256').update(inputs.join('|') || root).digest('hex').slice(0, 16);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _readCache(root) {
|
|
54
|
+
const dir = _cacheBase();
|
|
55
|
+
const fp = path.join(dir, _projectFingerprint(root) + '.json');
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(fp)) return null;
|
|
58
|
+
const obj = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
59
|
+
// Reconstitute Maps / Sets that JSON dropped.
|
|
60
|
+
return {
|
|
61
|
+
signatures: new Map(obj.signatures || []),
|
|
62
|
+
types: new Map((obj.types || []).map(([k, v]) => [k, new Map(v)])),
|
|
63
|
+
frameworks: new Set(obj.frameworks || []),
|
|
64
|
+
fingerprint: obj.fingerprint,
|
|
65
|
+
};
|
|
66
|
+
} catch { return null; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _writeCache(root, stubs) {
|
|
70
|
+
const dir = _cacheBase();
|
|
71
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
72
|
+
const fp = path.join(dir, _projectFingerprint(root) + '.json');
|
|
73
|
+
const obj = {
|
|
74
|
+
fingerprint: stubs.fingerprint,
|
|
75
|
+
signatures: [...stubs.signatures],
|
|
76
|
+
types: [...stubs.types].map(([k, m]) => [k, [...m]]),
|
|
77
|
+
frameworks: [...stubs.frameworks],
|
|
78
|
+
};
|
|
79
|
+
try { fs.writeFileSync(fp, JSON.stringify(obj)); } catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── .d.ts parser (regex; intentionally narrow) ──────────────────────────
|
|
83
|
+
|
|
84
|
+
// `function NAME(args): RET` or `NAME(args): RET` inside interface { }
|
|
85
|
+
const FN_DECL_RE =
|
|
86
|
+
/(?:^|\s)(?:export\s+)?(?:declare\s+)?function\s+([A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\(([^)]*)\)\s*:\s*([^;{]+);?/g;
|
|
87
|
+
|
|
88
|
+
// `interface NAME { fields }` or `class NAME { fields }`
|
|
89
|
+
const TYPE_DECL_RE =
|
|
90
|
+
/(?:^|\s)(?:export\s+)?(?:declare\s+)?(?:interface|class|type)\s+([A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*(?:extends\s+[^{=]+)?(?:[={])/g;
|
|
91
|
+
|
|
92
|
+
// Inside a type block: `name: Type;` or `name(args): Type;`
|
|
93
|
+
const FIELD_RE =
|
|
94
|
+
/([A-Za-z_$][\w$]*)\s*(?:\(([^)]*)\))?\s*\??\s*:\s*([^;,\n}]+)/g;
|
|
95
|
+
|
|
96
|
+
function _parseDtsFile(text) {
|
|
97
|
+
// Returns { signatures: Map, types: Map<name, FieldMap> } for this file.
|
|
98
|
+
const signatures = new Map();
|
|
99
|
+
const types = new Map();
|
|
100
|
+
let m;
|
|
101
|
+
// Top-level function declarations.
|
|
102
|
+
FN_DECL_RE.lastIndex = 0;
|
|
103
|
+
while ((m = FN_DECL_RE.exec(text)) !== null) {
|
|
104
|
+
const name = m[1];
|
|
105
|
+
const paramTypes = _parseParams(m[2]);
|
|
106
|
+
const returnType = m[3].trim();
|
|
107
|
+
signatures.set(name, { paramTypes, returnType });
|
|
108
|
+
}
|
|
109
|
+
// Type / interface bodies — find the brace span and parse fields.
|
|
110
|
+
TYPE_DECL_RE.lastIndex = 0;
|
|
111
|
+
while ((m = TYPE_DECL_RE.exec(text)) !== null) {
|
|
112
|
+
const name = m[1];
|
|
113
|
+
const openBraceIdx = text.indexOf('{', m.index);
|
|
114
|
+
if (openBraceIdx < 0) continue;
|
|
115
|
+
const body = _balancedSection(text, openBraceIdx);
|
|
116
|
+
if (!body) continue;
|
|
117
|
+
const fields = new Map();
|
|
118
|
+
let fm;
|
|
119
|
+
FIELD_RE.lastIndex = 0;
|
|
120
|
+
while ((fm = FIELD_RE.exec(body)) !== null) {
|
|
121
|
+
const fname = fm[1];
|
|
122
|
+
const fparams = fm[2] !== undefined ? _parseParams(fm[2]) : null;
|
|
123
|
+
const ftype = fm[3].trim();
|
|
124
|
+
fields.set(fname, fparams !== null ? { paramTypes: fparams, returnType: ftype } : ftype);
|
|
125
|
+
}
|
|
126
|
+
if (fields.size > 0) types.set(name, fields);
|
|
127
|
+
}
|
|
128
|
+
return { signatures, types };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _parseParams(s) {
|
|
132
|
+
if (!s || !s.trim()) return [];
|
|
133
|
+
return s.split(',').map(p => {
|
|
134
|
+
const t = p.trim();
|
|
135
|
+
if (!t) return { name: '?', type: 'any' };
|
|
136
|
+
const colon = t.indexOf(':');
|
|
137
|
+
if (colon < 0) return { name: t.replace(/[?=].*$/, '').trim(), type: 'any' };
|
|
138
|
+
return {
|
|
139
|
+
name: t.slice(0, colon).replace(/[?=].*$/, '').trim(),
|
|
140
|
+
type: t.slice(colon + 1).trim(),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _balancedSection(text, openIdx) {
|
|
146
|
+
if (text[openIdx] !== '{') return null;
|
|
147
|
+
let depth = 1;
|
|
148
|
+
let i = openIdx + 1;
|
|
149
|
+
while (i < text.length && depth > 0) {
|
|
150
|
+
const c = text[i];
|
|
151
|
+
if (c === '{') depth++;
|
|
152
|
+
else if (c === '}') depth--;
|
|
153
|
+
if (depth === 0) return text.slice(openIdx + 1, i);
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── .pyi parser (very narrow — function signatures only) ────────────────
|
|
160
|
+
|
|
161
|
+
const PYI_FN_RE =
|
|
162
|
+
/^def\s+([A-Za-z_]\w*)\s*\(([^)]*)\)\s*->\s*([^:\n]+):/gm;
|
|
163
|
+
|
|
164
|
+
function _parsePyiFile(text) {
|
|
165
|
+
const signatures = new Map();
|
|
166
|
+
let m;
|
|
167
|
+
PYI_FN_RE.lastIndex = 0;
|
|
168
|
+
while ((m = PYI_FN_RE.exec(text)) !== null) {
|
|
169
|
+
const name = m[1];
|
|
170
|
+
const paramTypes = m[2].split(',').map(p => {
|
|
171
|
+
const t = p.trim();
|
|
172
|
+
const colon = t.indexOf(':');
|
|
173
|
+
return colon < 0 ? { name: t, type: 'Any' } : { name: t.slice(0, colon).trim(), type: t.slice(colon + 1).trim() };
|
|
174
|
+
});
|
|
175
|
+
const returnType = m[3].trim();
|
|
176
|
+
signatures.set(name, { paramTypes, returnType });
|
|
177
|
+
}
|
|
178
|
+
return { signatures, types: new Map() };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Project walker ──────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function _findStubFiles(root, budgetDeadline) {
|
|
184
|
+
const stubs = [];
|
|
185
|
+
const exclude = new Set(['.git', '.venv', 'dist', 'build', 'target', '__pycache__']);
|
|
186
|
+
function walk(dir, depth) {
|
|
187
|
+
if (Date.now() > budgetDeadline) return;
|
|
188
|
+
if (depth > 8) return;
|
|
189
|
+
let entries;
|
|
190
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
191
|
+
for (const e of entries) {
|
|
192
|
+
if (exclude.has(e.name)) continue;
|
|
193
|
+
const full = path.join(dir, e.name);
|
|
194
|
+
if (e.isDirectory()) {
|
|
195
|
+
// Only descend into @types/* inside node_modules; otherwise we walk too deep.
|
|
196
|
+
if (e.name === 'node_modules') {
|
|
197
|
+
const tdir = path.join(full, '@types');
|
|
198
|
+
if (fs.existsSync(tdir)) walk(tdir, depth + 1);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
walk(full, depth + 1);
|
|
202
|
+
} else if (e.isFile()) {
|
|
203
|
+
if (e.name.endsWith('.d.ts')) stubs.push({ lang: 'ts', path: full });
|
|
204
|
+
else if (e.name.endsWith('.pyi')) stubs.push({ lang: 'py', path: full });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
walk(root, 0);
|
|
209
|
+
return stubs;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Heuristic framework recognition — which packages installed.
|
|
213
|
+
function _detectFrameworks(root) {
|
|
214
|
+
const fw = new Set();
|
|
215
|
+
try {
|
|
216
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
217
|
+
const all = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
218
|
+
if ('express' in all) fw.add('express');
|
|
219
|
+
if ('koa' in all) fw.add('koa');
|
|
220
|
+
if ('fastify' in all) fw.add('fastify');
|
|
221
|
+
if ('@nestjs/core' in all) fw.add('nestjs');
|
|
222
|
+
if ('next' in all) fw.add('next');
|
|
223
|
+
if ('react' in all) fw.add('react');
|
|
224
|
+
} catch {}
|
|
225
|
+
return fw;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Load all type stubs reachable from `root`. Returns a unified signatures
|
|
230
|
+
* map + types map. Cache-hits on the project fingerprint.
|
|
231
|
+
*/
|
|
232
|
+
export function loadProjectStubs(root) {
|
|
233
|
+
if (!root) return _emptyStubs();
|
|
234
|
+
const fingerprint = _projectFingerprint(root);
|
|
235
|
+
const cached = _readCache(root);
|
|
236
|
+
if (cached && cached.fingerprint === fingerprint) return cached;
|
|
237
|
+
const budgetMs = Number(process.env.AGENTIC_SECURITY_TYPE_STUBS_BUDGET_MS) || 10_000;
|
|
238
|
+
const deadline = Date.now() + budgetMs;
|
|
239
|
+
const files = _findStubFiles(root, deadline);
|
|
240
|
+
const signatures = new Map();
|
|
241
|
+
const types = new Map();
|
|
242
|
+
for (const f of files) {
|
|
243
|
+
if (Date.now() > deadline) break;
|
|
244
|
+
let body;
|
|
245
|
+
try { body = fs.readFileSync(f.path, 'utf8'); } catch { continue; }
|
|
246
|
+
if (body.length > 500_000) continue; // skip huge stub files
|
|
247
|
+
const parsed = f.lang === 'ts' ? _parseDtsFile(body) : _parsePyiFile(body);
|
|
248
|
+
for (const [k, v] of parsed.signatures) {
|
|
249
|
+
if (!signatures.has(k)) signatures.set(k, v);
|
|
250
|
+
}
|
|
251
|
+
for (const [k, v] of parsed.types) {
|
|
252
|
+
if (!types.has(k)) types.set(k, v);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const frameworks = _detectFrameworks(root);
|
|
256
|
+
const stubs = { signatures, types, frameworks, fingerprint };
|
|
257
|
+
_writeCache(root, stubs);
|
|
258
|
+
return stubs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function _emptyStubs() {
|
|
262
|
+
return { signatures: new Map(), types: new Map(), frameworks: new Set(), fingerprint: null };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Look up a function signature by qid OR by name. Returns null if absent.
|
|
267
|
+
*/
|
|
268
|
+
export function signatureFor(stubs, qidOrName) {
|
|
269
|
+
if (!stubs || !stubs.signatures) return null;
|
|
270
|
+
// Try the full qid first, then strip after `::` to get the name.
|
|
271
|
+
if (stubs.signatures.has(qidOrName)) return stubs.signatures.get(qidOrName);
|
|
272
|
+
const idx = String(qidOrName).indexOf('::');
|
|
273
|
+
if (idx > 0) {
|
|
274
|
+
const tail = qidOrName.slice(idx + 2).split('@')[0];
|
|
275
|
+
if (stubs.signatures.has(tail)) return stubs.signatures.get(tail);
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Look up a type / interface / class definition by name.
|
|
282
|
+
*/
|
|
283
|
+
export function typeOf(stubs, typeName) {
|
|
284
|
+
if (!stubs || !stubs.types) return null;
|
|
285
|
+
return stubs.types.get(typeName) || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const _internal = { _parseDtsFile, _parsePyiFile, _projectFingerprint, _cacheBase };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Leaderboard backend (v0.72).
|
|
2
|
+
//
|
|
3
|
+
// Generates the data shape that powers the future public leaderboard at
|
|
4
|
+
// agentic-security.dev/leaderboard. The leaderboard ranks repos by their
|
|
5
|
+
// security posture under our scanner — F1-on-CVE-history when we can
|
|
6
|
+
// compute it, otherwise just last-scan severity counts.
|
|
7
|
+
//
|
|
8
|
+
// Public hosting of the site is deferred — we ship the data side now so
|
|
9
|
+
// the future site is a thin frontend over this JSON.
|
|
10
|
+
//
|
|
11
|
+
// One leaderboard row per repo:
|
|
12
|
+
//
|
|
13
|
+
// {
|
|
14
|
+
// repo: 'owner/name',
|
|
15
|
+
// score: { critical, high, medium, low, info, total },
|
|
16
|
+
// postureGrade: 'A' | 'B' | 'C' | 'D' | 'F',
|
|
17
|
+
// lastScanAge: '4h',
|
|
18
|
+
// topCwe: 'CWE-89',
|
|
19
|
+
// deltaTrend: 'improving' | 'flat' | 'regressing',
|
|
20
|
+
// badgeUrl: 'https://agentic-security.dev/badge?repo=…',
|
|
21
|
+
// }
|
|
22
|
+
//
|
|
23
|
+
// The grader is intentionally coarse — single letter — so the leaderboard
|
|
24
|
+
// stays scannable. Tie-break by lowest critical-count, then by recency.
|
|
25
|
+
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { summarizeForBadge } from './badge.js';
|
|
29
|
+
|
|
30
|
+
// Grade thresholds. Critical findings dominate; high/medium contribute
|
|
31
|
+
// secondarily. These numbers are heuristic — calibrate against the
|
|
32
|
+
// public leaderboard corpus once data lands.
|
|
33
|
+
function _postureGrade(counts) {
|
|
34
|
+
if (!counts) return 'F';
|
|
35
|
+
const c = counts.critical || 0;
|
|
36
|
+
const h = counts.high || 0;
|
|
37
|
+
const m = counts.medium || 0;
|
|
38
|
+
if (c === 0 && h === 0 && m === 0) return 'A';
|
|
39
|
+
if (c === 0 && h === 0 && m <= 5) return 'B';
|
|
40
|
+
if (c === 0 && h <= 2) return 'C';
|
|
41
|
+
if (c <= 1 && h <= 5) return 'D';
|
|
42
|
+
return 'F';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _ageString(ts) {
|
|
46
|
+
if (!ts) return null;
|
|
47
|
+
const ageMs = Date.now() - new Date(ts).getTime();
|
|
48
|
+
if (isNaN(ageMs) || ageMs < 0) return null;
|
|
49
|
+
const min = Math.floor(ageMs / 60_000);
|
|
50
|
+
if (min < 60) return `${min}m`;
|
|
51
|
+
const hr = Math.floor(min / 60);
|
|
52
|
+
if (hr < 24) return `${hr}h`;
|
|
53
|
+
const day = Math.floor(hr / 24);
|
|
54
|
+
return `${day}d`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _topCwe(scan) {
|
|
58
|
+
if (!scan || !Array.isArray(scan.findings)) return null;
|
|
59
|
+
const counts = new Map();
|
|
60
|
+
for (const f of scan.findings) {
|
|
61
|
+
if (!f.cwe) continue;
|
|
62
|
+
counts.set(f.cwe, (counts.get(f.cwe) || 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
let topCwe = null, topN = 0;
|
|
65
|
+
for (const [cwe, n] of counts) {
|
|
66
|
+
if (n > topN) { topCwe = cwe; topN = n; }
|
|
67
|
+
}
|
|
68
|
+
return topCwe;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _deltaTrend(history) {
|
|
72
|
+
// history: array of past scan summaries with `.timestamp` + `.severityCounts.critical`
|
|
73
|
+
if (!Array.isArray(history) || history.length < 2) return 'flat';
|
|
74
|
+
const recent = history.slice(-3);
|
|
75
|
+
const first = recent[0].severityCounts || {};
|
|
76
|
+
const last = recent[recent.length - 1].severityCounts || {};
|
|
77
|
+
const fScore = (first.critical || 0) * 4 + (first.high || 0);
|
|
78
|
+
const lScore = (last.critical || 0) * 4 + (last.high || 0);
|
|
79
|
+
if (lScore < fScore - 1) return 'improving';
|
|
80
|
+
if (lScore > fScore + 1) return 'regressing';
|
|
81
|
+
return 'flat';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build a single leaderboard row for a repo. Reads the latest scan from
|
|
86
|
+
* `<scanRoot>/.agentic-security/last-scan.json` and (optionally) history
|
|
87
|
+
* from `<scanRoot>/.agentic-security/scan-history.jsonl`.
|
|
88
|
+
*
|
|
89
|
+
* `repo` is the GitHub slug ('owner/name'); used to drive the badge URL.
|
|
90
|
+
*/
|
|
91
|
+
export function leaderboardRowFor({ scanRoot, repo, badgeBase = 'https://agentic-security.dev/badge' } = {}) {
|
|
92
|
+
if (!repo) throw new Error('leaderboardRowFor: repo slug is required');
|
|
93
|
+
const lastScanPath = path.join(scanRoot || '.', '.agentic-security', 'last-scan.json');
|
|
94
|
+
let scan = null;
|
|
95
|
+
try { scan = JSON.parse(fs.readFileSync(lastScanPath, 'utf8')); } catch {}
|
|
96
|
+
const summary = summarizeForBadge(scan);
|
|
97
|
+
const grade = _postureGrade(summary.counts);
|
|
98
|
+
const topCwe = _topCwe(scan);
|
|
99
|
+
|
|
100
|
+
// Optional scan history for the trend signal.
|
|
101
|
+
const historyPath = path.join(scanRoot || '.', '.agentic-security', 'scan-history.jsonl');
|
|
102
|
+
let history = [];
|
|
103
|
+
if (fs.existsSync(historyPath)) {
|
|
104
|
+
try {
|
|
105
|
+
history = fs.readFileSync(historyPath, 'utf8').split('\n')
|
|
106
|
+
.map(l => l.trim()).filter(Boolean)
|
|
107
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
const deltaTrend = _deltaTrend(history);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
repo,
|
|
115
|
+
score: { ...summary.counts, total: summary.total },
|
|
116
|
+
postureGrade: grade,
|
|
117
|
+
lastScanAge: _ageString(scan?.timestamp || scan?.when),
|
|
118
|
+
topCwe,
|
|
119
|
+
deltaTrend,
|
|
120
|
+
badgeUrl: `${badgeBase}?repo=${encodeURIComponent(repo)}`,
|
|
121
|
+
badgeMarkdown: `})`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Rank a list of rows for the leaderboard. Sort by:
|
|
127
|
+
* 1. lower critical count
|
|
128
|
+
* 2. lower high count
|
|
129
|
+
* 3. higher postureGrade (A > F)
|
|
130
|
+
* 4. fresher lastScanAge
|
|
131
|
+
*
|
|
132
|
+
* Returns the input rows annotated with `rank` (1-indexed).
|
|
133
|
+
*/
|
|
134
|
+
export function rankRows(rows) {
|
|
135
|
+
if (!Array.isArray(rows)) return [];
|
|
136
|
+
const gradeOrder = { A: 0, B: 1, C: 2, D: 3, F: 4 };
|
|
137
|
+
const sorted = [...rows].sort((a, b) => {
|
|
138
|
+
const ac = a.score?.critical || 0;
|
|
139
|
+
const bc = b.score?.critical || 0;
|
|
140
|
+
if (ac !== bc) return ac - bc;
|
|
141
|
+
const ah = a.score?.high || 0;
|
|
142
|
+
const bh = b.score?.high || 0;
|
|
143
|
+
if (ah !== bh) return ah - bh;
|
|
144
|
+
const ag = gradeOrder[a.postureGrade] ?? 5;
|
|
145
|
+
const bg = gradeOrder[b.postureGrade] ?? 5;
|
|
146
|
+
if (ag !== bg) return ag - bg;
|
|
147
|
+
return 0;
|
|
148
|
+
});
|
|
149
|
+
return sorted.map((r, i) => ({ ...r, rank: i + 1 }));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const _internal = { _postureGrade, _ageString, _topCwe, _deltaTrend };
|