@clear-capabilities/agentic-security-scanner 0.162.0 → 0.163.1
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 +34 -0
- package/bin/agentic-security.js +87 -0
- package/dist/1340.index.js +1871 -0
- package/dist/3477.index.js +727 -0
- package/dist/3499.index.js +23 -3
- package/dist/4113.index.js +1 -1
- package/dist/5014.index.js +833 -0
- package/dist/5756.index.js +2 -2
- package/dist/6257.index.js +1 -1
- package/dist/6907.index.js +248 -0
- package/dist/6994.index.js +1 -1
- package/dist/7033.index.js +33 -5
- package/dist/7439.index.js +1822 -0
- package/dist/7838.index.js +1 -1
- package/dist/9560.index.js +2318 -23
- package/dist/957.index.js +1 -1
- package/dist/9999.index.js +43 -7
- package/dist/agentic-security.mjs +12 -12
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +33 -6
- package/src/capabilities/CLAUDE.md +114 -0
- package/src/capabilities/address.js +73 -0
- package/src/capabilities/attack-coverage.js +93 -0
- package/src/capabilities/decide.js +256 -0
- package/src/capabilities/delegate.js +69 -0
- package/src/capabilities/egress.js +20 -0
- package/src/capabilities/hook-advice.js +50 -0
- package/src/capabilities/index.js +32 -0
- package/src/capabilities/manifest.js +344 -0
- package/src/capabilities/outbound.js +189 -0
- package/src/capabilities/paths.js +57 -0
- package/src/capabilities/probes.js +211 -0
- package/src/capabilities/proxy.js +182 -0
- package/src/capabilities/reasons.js +67 -0
- package/src/capabilities/receipts.js +356 -0
- package/src/capabilities/records.js +58 -0
- package/src/capabilities/recovery.js +367 -0
- package/src/capabilities/report.js +81 -0
- package/src/capabilities/runner.js +236 -0
- package/src/capabilities/secrets.js +26 -0
- package/src/capabilities/tool-gate.js +119 -0
- package/src/capabilities/tool-registry.js +60 -0
- package/src/dataflow/CLAUDE.md +30 -0
- package/src/dataflow/catalog.js +42 -1
- package/src/dataflow/engine.js +67 -2
- package/src/dataflow/guard-dominance.js +352 -0
- package/src/discovery/CLAUDE.md +9 -0
- package/src/discovery/index.js +15 -1
- package/src/discovery/lenses.js +18 -0
- package/src/engine.js +80 -15
- package/src/integrations/index.js +5 -2
- package/src/ir/CLAUDE.md +14 -0
- package/src/ir/implicit-handler-params.js +74 -0
- package/src/ir/parser-cs.js +20 -0
- package/src/ir/parser-go.js +30 -13
- package/src/ir/parser-java.js +11 -0
- package/src/language/nix-eval-isolation.js +33 -5
- package/src/lineage/CLAUDE.md +31 -0
- package/src/lineage/deployment/adapter-cloud.js +202 -0
- package/src/lineage/deployment/adapter-compose.js +90 -0
- package/src/lineage/deployment/adapter-kit.js +142 -0
- package/src/lineage/deployment/adapter-kubernetes.js +247 -0
- package/src/lineage/deployment/attack-paths.js +443 -0
- package/src/lineage/deployment/boundaries-run.js +223 -0
- package/src/lineage/deployment/boundary-graph.js +567 -0
- package/src/lineage/deployment/cross-repo.js +253 -0
- package/src/lineage/deployment/drift.js +383 -0
- package/src/lineage/deployment/ingest.js +221 -0
- package/src/lineage/deployment/projection.js +244 -0
- package/src/lineage/deployment/trace-correlation.js +541 -0
- package/src/lineage/source-registry.js +19 -0
- package/src/mcp/CLAUDE.md +5 -2
- package/src/mcp/invariant-tools.js +64 -0
- package/src/mcp/portfolio-tools.js +79 -0
- package/src/mcp/redact.js +15 -0
- package/src/mcp/server.js +24 -3
- package/src/mcp/tools.js +13 -1
- package/src/posture/CLAUDE.md +332 -1
- package/src/posture/accuracy-scorecard.js +95 -4
- package/src/posture/assurance/rollback.js +214 -0
- package/src/posture/assurance/rollout.js +200 -0
- package/src/posture/business-logic.js +26 -4
- package/src/posture/evaluation/bakeoff.js +148 -0
- package/src/posture/evaluation/custody.js +200 -0
- package/src/posture/evaluation/deployment-ablation.js +416 -0
- package/src/posture/evaluation/dev-cases.js +65 -0
- package/src/posture/evaluation/economics.js +85 -0
- package/src/posture/evaluation/gates.js +161 -0
- package/src/posture/evaluation/grouping.js +101 -0
- package/src/posture/evaluation/interval.js +132 -0
- package/src/posture/evaluation/invariant-ablation.js +325 -0
- package/src/posture/evaluation/labels.js +200 -0
- package/src/posture/evaluation/protocol.js +308 -0
- package/src/posture/evaluation/release-gate.js +209 -0
- package/src/posture/evaluation/report.js +158 -0
- package/src/posture/evaluation/runner.js +273 -0
- package/src/posture/evaluation/scan-child.js +80 -0
- package/src/posture/evaluation/score.js +174 -0
- package/src/posture/evaluation/synthetic.js +90 -0
- package/src/posture/evaluation/why-missed.js +219 -0
- package/src/posture/fleet.js +11 -2
- package/src/posture/invariants/cli.js +95 -0
- package/src/posture/invariants/coverage.js +164 -0
- package/src/posture/invariants/export.js +178 -0
- package/src/posture/invariants/expressions.js +46 -0
- package/src/posture/invariants/lifecycle.js +289 -0
- package/src/posture/invariants/project-input.js +55 -0
- package/src/posture/invariants/repair.js +340 -0
- package/src/posture/invariants/run.js +45 -0
- package/src/posture/invariants/scenarios.js +311 -0
- package/src/posture/invariants/schema.js +167 -0
- package/src/posture/invariants/shrink.js +407 -0
- package/src/posture/invariants/state-assertions.js +165 -0
- package/src/posture/logic-claims.js +12 -0
- package/src/posture/model-routing.js +40 -0
- package/src/posture/oracles/adapters.js +172 -1
- package/src/posture/oracles/conformance.js +3 -2
- package/src/posture/oracles/oracle.js +23 -3
- package/src/posture/oracles/registry.js +1 -1
- package/src/posture/portfolio/backend.js +279 -0
- package/src/posture/portfolio/bundle.js +314 -0
- package/src/posture/portfolio/cli.js +142 -0
- package/src/posture/portfolio/manifest.js +263 -0
- package/src/posture/portfolio/progress.js +267 -0
- package/src/posture/portfolio/resume.js +130 -0
- package/src/posture/portfolio/retention.js +207 -0
- package/src/posture/portfolio/scheduler.js +488 -0
- package/src/posture/portfolio/signing.js +155 -0
- package/src/posture/portfolio/wording.js +59 -0
- package/src/posture/portfolio/work-units.js +405 -0
- package/src/posture/replay/replay.js +1 -0
- package/src/posture/routing/adapter.js +140 -0
- package/src/posture/routing/calibration.js +278 -0
- package/src/posture/routing/canary.js +141 -0
- package/src/posture/routing/control.js +35 -0
- package/src/posture/routing/decide.js +211 -0
- package/src/posture/routing/drift.js +137 -0
- package/src/posture/routing/economics.js +185 -0
- package/src/posture/routing/feedback.js +207 -0
- package/src/posture/routing/index.js +31 -0
- package/src/posture/routing/outcomes.js +281 -0
- package/src/posture/routing/promotion.js +291 -0
- package/src/posture/routing/receipts.js +59 -0
- package/src/posture/routing/report.js +142 -0
- package/src/posture/routing/shadow.js +53 -0
- package/src/posture/specification-mining.js +26 -0
- package/src/posture/verification/patch-negative.js +16 -4
- package/src/posture/verification/patch-promotion.js +10 -2
- package/src/report/index.js +51 -1
- package/src/sandbox/CLAUDE.md +10 -0
- package/src/sandbox/backend-userspace.js +91 -13
- package/dist/5613.index.js +0 -500
- package/dist/6239.index.js +0 -469
package/src/engine.js
CHANGED
|
@@ -243,6 +243,7 @@ import { applyLearnedCalibration } from './posture/triage-learning.js';
|
|
|
243
243
|
import { annotateFormalVerification } from './dataflow/formal-verify.js';
|
|
244
244
|
import { annotateProofGate } from './dataflow/proof-gate.js';
|
|
245
245
|
import { applySanitizerGate } from './dataflow/sanitizer-gate.js';
|
|
246
|
+
import { guardDominatesSink } from './dataflow/guard-dominance.js';
|
|
246
247
|
import { annotateFalsification } from './posture/falsification.js';
|
|
247
248
|
import { routeModelForFinding } from './posture/model-routing.js';
|
|
248
249
|
import { buildEntrypointInventory } from './posture/entrypoint-inventory.js';
|
|
@@ -1468,10 +1469,38 @@ function _guardWindow(ctx, before = 25, after = 5) {
|
|
|
1468
1469
|
const line = ctx.line || 1;
|
|
1469
1470
|
const lo = Math.max(0, line - before);
|
|
1470
1471
|
const hi = Math.min(ctx.lines.length, line + after);
|
|
1471
|
-
return ctx.lines.slice(lo, hi).join('\n')
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1472
|
+
return _stripCommentsKeepLines(ctx.lines.slice(lo, hi).join('\n'));
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Comment removal that is aware of string literals and keeps every newline, so a line index in the result is a line index in the file.
|
|
1476
|
+
// The previous regex chain treated the `//` in `"https://"` as the start of a comment and erased the rest of the line, which is where the
|
|
1477
|
+
// allow-list check usually is (`in_array("https://" . $host, $allowed)`): a guard written on that line disappeared from the window and a
|
|
1478
|
+
// neighbouring declaration happened to stand in for it. Strings never span lines here; a stray apostrophe therefore costs at most one line.
|
|
1479
|
+
function _stripCommentsKeepLines(text) {
|
|
1480
|
+
let out = '';
|
|
1481
|
+
let i = 0; const n = text.length;
|
|
1482
|
+
let quote = null;
|
|
1483
|
+
while (i < n) {
|
|
1484
|
+
const c = text[i]; const d = text[i + 1];
|
|
1485
|
+
if (c === '\n') { quote = null; out += c; i++; continue; }
|
|
1486
|
+
if (quote) {
|
|
1487
|
+
out += c;
|
|
1488
|
+
if (c === '\\' && i + 1 < n && text[i + 1] !== '\n') { out += text[i + 1]; i += 2; continue; }
|
|
1489
|
+
if (c === quote) quote = null;
|
|
1490
|
+
i++; continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (c === '"' || c === "'" || c === '`') { quote = c; out += c; i++; continue; }
|
|
1493
|
+
if (c === '/' && d === '*') {
|
|
1494
|
+
const end = text.indexOf('*/', i + 2);
|
|
1495
|
+
const body = text.slice(i, end < 0 ? n : end + 2);
|
|
1496
|
+
out += body.replace(/[^\n]/g, ' ');
|
|
1497
|
+
i += body.length; continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (c === '/' && d === '/') { while (i < n && text[i] !== '\n') { out += ' '; i++; } continue; }
|
|
1500
|
+
if (c === '#' && (i === 0 || !/[\w'"`]/.test(text[i - 1]))) { while (i < n && text[i] !== '\n') { out += ' '; i++; } continue; }
|
|
1501
|
+
out += c; i++;
|
|
1502
|
+
}
|
|
1503
|
+
return out;
|
|
1475
1504
|
}
|
|
1476
1505
|
|
|
1477
1506
|
// PRD R15: a guard-shaped token anywhere in the -25/+5 window used to be
|
|
@@ -1524,7 +1553,20 @@ function _guardMatchNearSinkIdentifier(ctx, guardRe, span = 2) {
|
|
|
1524
1553
|
const w = _guardWindow(ctx);
|
|
1525
1554
|
const re = new RegExp(guardRe.source, guardRe.flags.includes('g') ? guardRe.flags : guardRe.flags + 'g');
|
|
1526
1555
|
const ids = _sinkLineIdentifiers(ctx);
|
|
1527
|
-
|
|
1556
|
+
// QA-005.AC02: text that LOOKS like a guard is not enough; it must dominate the sink. `_guardWindow` starts at `winLo` in the file.
|
|
1557
|
+
const winLo = Math.max(0, (ctx.line || 1) - 25);
|
|
1558
|
+
const sinkIdx = (ctx.line || 1) - 1;
|
|
1559
|
+
const dominates = (windowLineIdx) => {
|
|
1560
|
+
const v = guardDominatesSink({ lines: ctx.lines, guardIdx: winLo + windowLineIdx, sinkIdx, file: ctx.file });
|
|
1561
|
+
if (v.dominates) ctx._guardEvidence = { guardLine: winLo + windowLineIdx + 1, dominance: v.reason };
|
|
1562
|
+
else if (_stageEvidenceOn()) _stageEvidence.guard.push({ file: ctx.file || null, sinkLine: sinkIdx + 1, guardLine: winLo + windowLineIdx + 1, rejectedBecause: v.reason });
|
|
1563
|
+
return v.dominates;
|
|
1564
|
+
};
|
|
1565
|
+
if (!ids.size) { // can't correlate by identifier: shape-only, but the shape must still dominate
|
|
1566
|
+
let nm; const probe = new RegExp(re.source, re.flags);
|
|
1567
|
+
while ((nm = probe.exec(w))) { if (dominates(w.slice(0, nm.index).split('\n').length - 1)) return true; if (nm.index === probe.lastIndex) probe.lastIndex++; }
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1528
1570
|
const wLines = w.split('\n');
|
|
1529
1571
|
const sinkLineText = (ctx.lines && ctx.lines[(ctx.line || 1) - 1]) || '';
|
|
1530
1572
|
let m;
|
|
@@ -1547,7 +1589,7 @@ function _guardMatchNearSinkIdentifier(ctx, guardRe, span = 2) {
|
|
|
1547
1589
|
const hi = Math.min(wLines.length, guardLineIdx + span + 1);
|
|
1548
1590
|
const local = wLines.slice(lo, hi).filter((l) => l !== sinkLineText).join('\n');
|
|
1549
1591
|
for (const id of ids) {
|
|
1550
|
-
if (new RegExp(`\\b${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(local)) return true;
|
|
1592
|
+
if (new RegExp(`\\b${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(local) && dominates(guardLineIdx)) return true;
|
|
1551
1593
|
}
|
|
1552
1594
|
}
|
|
1553
1595
|
return false;
|
|
@@ -1555,6 +1597,16 @@ function _guardMatchNearSinkIdentifier(ctx, guardRe, span = 2) {
|
|
|
1555
1597
|
|
|
1556
1598
|
function _hasSsrfHostGuard(ctx) { return _guardMatchNearSinkIdentifier(ctx, _SSRF_HOST_GUARD_RE); }
|
|
1557
1599
|
|
|
1600
|
+
// An open-redirect (CWE-601) target guard: a framework's local-URL / safe-redirect predicate, a relative-path check (`startsWith("/")`),
|
|
1601
|
+
// or a host allow-list. QA-006: once the taint layer sees a handler's implicit request parameters, a redirect that is validated
|
|
1602
|
+
// by the framework's own predicate (`LocalRedirect(Url.IsLocalUrl(next) ? next : "/")`) reaches this pass like any other guarded
|
|
1603
|
+
// sink, and is cleared by the same rule as SSRF and path guards: the guard must name the sink's identifier AND dominate the sink.
|
|
1604
|
+
const _REDIRECT_GUARD_RE = new RegExp(
|
|
1605
|
+
String.raw`\b(?:IsLocalUrl|is_safe_url|url_has_allowed_host_and_scheme|safe_redirect|safeRedirect|isSafeRedirect|isRelativeUrl|is_relative_url|validateRedirect|validate_redirect|SafeRedirect)\b`
|
|
1606
|
+
+ String.raw`|\.\s*(?:startsWith|startswith|StartsWith|HasPrefix)\s*\(\s*['"]/['"]\s*\)|\bstr_starts_with\s*\(|\bstrpos\s*\(\s*\$?\w+\s*,\s*['"]/['"]\s*\)`
|
|
1607
|
+
+ '|' + _SSRF_HOST_GUARD_RE.source, _SSRF_HOST_GUARD_RE.flags);
|
|
1608
|
+
function _hasRedirectGuard(ctx) { return _guardMatchNearSinkIdentifier(ctx, _REDIRECT_GUARD_RE); }
|
|
1609
|
+
|
|
1558
1610
|
// A path-traversal containment guard near the file sink: a basename/strip
|
|
1559
1611
|
// helper that removes directory components, a framework safe-join, or a
|
|
1560
1612
|
// canonicalize-then-startsWith containment check.
|
|
@@ -1609,7 +1661,7 @@ function _hasXssOutputEncoding(ctx) {
|
|
|
1609
1661
|
// detector produced them, using the same comment-stripped window guards.
|
|
1610
1662
|
// Recall-safe: a genuinely-unguarded sink has no guard in its window (the
|
|
1611
1663
|
// cve-replay vulnerable-tier TP count is the invariant that proves this).
|
|
1612
|
-
export function dropGuardedFindings(findings, fileContents) {
|
|
1664
|
+
export function dropGuardedFindings(findings, fileContents, onDrop = null) {
|
|
1613
1665
|
if (!Array.isArray(findings)) return findings;
|
|
1614
1666
|
const cache = new Map();
|
|
1615
1667
|
const linesOf = (file) => {
|
|
@@ -1641,7 +1693,7 @@ export function dropGuardedFindings(findings, fileContents) {
|
|
|
1641
1693
|
if (f.family === 'sibling-guard-omission') return true;
|
|
1642
1694
|
const cwe = f.cwe || '';
|
|
1643
1695
|
const isXss = cwe === 'CWE-79' && /reflected xss/i.test(f.vuln || '');
|
|
1644
|
-
if (cwe !== 'CWE-918' && cwe !== 'CWE-22' && !isXss) return true;
|
|
1696
|
+
if (cwe !== 'CWE-918' && cwe !== 'CWE-22' && cwe !== 'CWE-601' && !isXss) return true;
|
|
1645
1697
|
// Flow-pair findings (REGEX parser) carry the line on sink/source, not at
|
|
1646
1698
|
// the top level; fall back to those before parsing it out of the id.
|
|
1647
1699
|
let line = f.line;
|
|
@@ -1655,10 +1707,12 @@ export function dropGuardedFindings(findings, fileContents) {
|
|
|
1655
1707
|
if (typeof line !== 'number') return true;
|
|
1656
1708
|
const lines = linesOf(f.file);
|
|
1657
1709
|
if (!lines) return true;
|
|
1658
|
-
const ctx = { lines, line };
|
|
1659
|
-
if (
|
|
1660
|
-
if (cwe === 'CWE-
|
|
1661
|
-
if (
|
|
1710
|
+
const ctx = { lines, line, file: f.file };
|
|
1711
|
+
const dropped = (kind) => { if (onDrop) onDrop(f, { kind, line, evidence: ctx._guardEvidence || null }); return false; };
|
|
1712
|
+
if (cwe === 'CWE-918' && _hasSsrfHostGuard(ctx)) return dropped('ssrf-host-guard');
|
|
1713
|
+
if (cwe === 'CWE-22' && _hasPathGuard(ctx)) return dropped('path-containment-guard');
|
|
1714
|
+
if (cwe === 'CWE-601' && _hasRedirectGuard(ctx)) return dropped('redirect-target-guard');
|
|
1715
|
+
if (isXss && _hasXssOutputEncoding(ctx)) return dropped('xss-output-encoding');
|
|
1662
1716
|
return true;
|
|
1663
1717
|
});
|
|
1664
1718
|
}
|
|
@@ -2693,7 +2747,14 @@ const _PROTOCOL_CONSTANT_VAL_RE = new RegExp('^(?:'
|
|
|
2693
2747
|
}
|
|
2694
2748
|
// Module-level suppression log; cleared at the start of each runFullScan invocation.
|
|
2695
2749
|
const _suppressionLog = [];
|
|
2696
|
-
function _resetSuppressions(){ _suppressionLog.length = 0; }
|
|
2750
|
+
function _resetSuppressions(){ _suppressionLog.length = 0; _stageEvidence.dedupe.length = 0; _stageEvidence.guard.length = 0; }
|
|
2751
|
+
// QA-005: stage-level diagnostics for the evaluation's why-missed analysis. DIAGNOSTIC ONLY: it records what the filter and
|
|
2752
|
+
// dedupe stages did and never changes a finding (test/posture-level pin: findings are identical with it on and off). Collected
|
|
2753
|
+
// only when AGENTIC_SECURITY_STAGE_EVIDENCE=1, because a normal scan has no reader for it.
|
|
2754
|
+
// guard: guard-shaped text the recognizer found but that did NOT dominate its sink, so did not clear the finding
|
|
2755
|
+
// dedupe: findings collapsed into a winner, with the loser's own location kept (matchable identity)
|
|
2756
|
+
const _stageEvidence = { dedupe: [], guard: [] };
|
|
2757
|
+
const _stageEvidenceOn = () => process.env.AGENTIC_SECURITY_STAGE_EVIDENCE === '1';
|
|
2697
2758
|
// R8: the per-file taint result minus the four arrays that are also appended
|
|
2698
2759
|
// wholesale to the aggregates. Resume rebuilds those from the aggregate slices
|
|
2699
2760
|
// so pfr[p] and the aggregates share object identity, exactly as in a normal run.
|
|
@@ -6587,6 +6648,7 @@ function dedupeFindingsWithEvidence(findings){
|
|
|
6587
6648
|
: (SEV_RANK[f.severity]??9) < (SEV_RANK[kept.severity]??9);
|
|
6588
6649
|
const winner = keepNew ? f : kept;
|
|
6589
6650
|
const loser = keepNew ? kept : f;
|
|
6651
|
+
if(_stageEvidenceOn())_stageEvidence.dedupe.push({file,family:fam,winner:{id:winner.id||null,file:winner.file||null,line:winner.line??null,parser:winner.parser||null},loser:{id:loser.id||null,file:loser.file||null,line:loser.line??null,parser:loser.parser||null}});
|
|
6590
6652
|
if(!winner.evidence)winner.evidence=[winner.parser||"UNKNOWN"];
|
|
6591
6653
|
if(loser.parser&&!winner.evidence.includes(loser.parser))winner.evidence.push(loser.parser);
|
|
6592
6654
|
if(loser.vuln&&loser.vuln!==winner.vuln){
|
|
@@ -9602,7 +9664,10 @@ function _deterministicFileTimings(timings) {
|
|
|
9602
9664
|
// #1 — centralized SSRF/path guard recognition: drop CWE-918/CWE-22 findings
|
|
9603
9665
|
// on code hardened by a host allow/deny check or a path containment guard,
|
|
9604
9666
|
// regardless of which detector emitted them. Opt out: AGENTIC_SECURITY_NO_GUARD_RECOGNITION=1.
|
|
9605
|
-
if(process.env.AGENTIC_SECURITY_NO_GUARD_RECOGNITION!=='1'){try{finalFindings=dropGuardedFindings(finalFindings,fc
|
|
9667
|
+
if(process.env.AGENTIC_SECURITY_NO_GUARD_RECOGNITION!=='1'){try{finalFindings=dropGuardedFindings(finalFindings,fc,(f,why)=>{
|
|
9668
|
+
// QA-005.AC01: a guard-recognized drop is a suppression and goes in the ledger with the identity a later analysis needs to match it.
|
|
9669
|
+
_suppressionLog.push({vuln:f.vuln,file:f.file,line:why.line,snippet:f.snippet||'',reason:'guard-recognized:'+why.kind+(why.evidence?':dominates@'+why.evidence.guardLine+':'+why.evidence.dominance:''),id:f.id||null,cwe:f.cwe||null,family:f.family||null});
|
|
9670
|
+
});}catch(_){}}
|
|
9606
9671
|
// 0.34.6: filter out Java FPs where a sanitizer pattern (argv-form ProcessBuilder,
|
|
9607
9672
|
// parameterized prepareStatement, constant-folded dead-branch) is present.
|
|
9608
9673
|
// applyJavaBenchSuppressions is a no-op on non-.java files.
|
|
@@ -11098,7 +11163,7 @@ function _deterministicFileTimings(timings) {
|
|
|
11098
11163
|
compliance: _complianceReport ? { stale: _complianceReport.summary?.stale || 0 } : null,
|
|
11099
11164
|
});
|
|
11100
11165
|
} // end if (!skipAnnotators) — FR-PROV-029
|
|
11101
|
-
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,proofCoverage:_proofCoverage,kevCatalog:kevCatalogMeta(),routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,linesScanned:Object.values(fc).reduce((_n,_c)=>_n+(typeof _c==='string'?_c.split("\n").length:0),0),crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,depFileContents,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors,detectorErrors:_detectorErrors,executionProof:_executionProofSummary,logicClaims:_logicClaims,vulnHistory:_vulnHistory,threatModel:_threatModel,privacyFramework:_privacyFramework,privacyIrBacked:_privacyIrBacked,privacyTaxonomyVersion:_privacyTaxonomyVersion,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary,scanHealth:_scanHealth,coverageLedger:_coverageLedger,lineageGraph:_lineageGraph,lineageStatus:_lineageStatus,languageBridges:_languageBridges,languageBom:_languageBom,aiAssistance:_aiAssistance};}
|
|
11166
|
+
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,proofCoverage:_proofCoverage,kevCatalog:kevCatalogMeta(),routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,linesScanned:Object.values(fc).reduce((_n,_c)=>_n+(typeof _c==='string'?_c.split("\n").length:0),0),crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,depFileContents,suppressions:_getSuppressions(),...(_stageEvidenceOn()?{stageEvidence:{dedupe:[..._stageEvidence.dedupe],guard:[..._stageEvidence.guard]}}:{}),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors,detectorErrors:_detectorErrors,executionProof:_executionProofSummary,logicClaims:_logicClaims,vulnHistory:_vulnHistory,threatModel:_threatModel,privacyFramework:_privacyFramework,privacyIrBacked:_privacyIrBacked,privacyTaxonomyVersion:_privacyTaxonomyVersion,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary,scanHealth:_scanHealth,coverageLedger:_coverageLedger,lineageGraph:_lineageGraph,lineageStatus:_lineageStatus,languageBridges:_languageBridges,languageBom:_languageBom,aiAssistance:_aiAssistance};}
|
|
11102
11167
|
|
|
11103
11168
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
11104
11169
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
|
@@ -14,6 +14,7 @@ import * as fs from 'node:fs';
|
|
|
14
14
|
import * as path from 'node:path';
|
|
15
15
|
import * as yaml from '../util/yaml.js';
|
|
16
16
|
import { statePath } from '../posture/state-dir.js';
|
|
17
|
+
import { portfolioAssuranceEnabled, HEADLINE, BLOCKED_HEADLINE } from '../posture/portfolio/wording.js';
|
|
17
18
|
|
|
18
19
|
function _configPath(scanRoot) {
|
|
19
20
|
return statePath(scanRoot, 'integrations.yml');
|
|
@@ -36,12 +37,14 @@ export function buildSlackDigest(findings, summary, options = {}) {
|
|
|
36
37
|
const project = options.project || 'project';
|
|
37
38
|
const status = (summary.critical || 0) === 0 && (summary.high || 0) === 0
|
|
38
39
|
? '✅ safe to deploy' : '❌ not safe to deploy';
|
|
40
|
+
const statusLine = portfolioAssuranceEnabled({ option: options.portfolioAssurance })
|
|
41
|
+
? (status.startsWith('✅') ? `✅ ${HEADLINE}` : `❌ ${BLOCKED_HEADLINE}`) + ' (completed supported checks only)' : status;
|
|
39
42
|
const top = (findings || []).slice(0, 3);
|
|
40
43
|
|
|
41
44
|
const lines = [
|
|
42
45
|
`*🛡 agentic-security daily — ${new Date().toISOString().slice(0,10)}*`,
|
|
43
46
|
`Project: \`${project}\``,
|
|
44
|
-
`Status: ${
|
|
47
|
+
`Status: ${statusLine}`,
|
|
45
48
|
'',
|
|
46
49
|
`*Findings:* ${summary.critical || 0} critical · ${summary.high || 0} high · ${summary.medium || 0} medium`,
|
|
47
50
|
];
|
|
@@ -66,7 +69,7 @@ export function buildDiscordDigest(findings, summary, options = {}) {
|
|
|
66
69
|
embeds: [{
|
|
67
70
|
title: `🛡 agentic-security — ${project}`,
|
|
68
71
|
color: safe ? 0x2ecc71 : 0xe74c3c,
|
|
69
|
-
description: safe ? '✅ safe to deploy' : '❌ not safe to deploy',
|
|
72
|
+
description: portfolioAssuranceEnabled({ option: options.portfolioAssurance }) ? (safe ? `✅ ${HEADLINE} (completed supported checks only)` : `❌ ${BLOCKED_HEADLINE}`) : (safe ? '✅ safe to deploy' : '❌ not safe to deploy'),
|
|
70
73
|
fields: [
|
|
71
74
|
{ name: 'Critical', value: String(summary.critical || 0), inline: true },
|
|
72
75
|
{ name: 'High', value: String(summary.high || 0), inline: true },
|
package/src/ir/CLAUDE.md
CHANGED
|
@@ -34,6 +34,20 @@ consumed by `scanner/src/dataflow/` for taint analysis.
|
|
|
34
34
|
| Rust | `parser-rust.js` | Hand-rolled, the `parser-go.js`/`parser-cs.js` pattern (preprocess: comments blanked, `::` paths flattened to dots, turbofish/`?`/`.await` stripped; then statement-split + recursive CFG build). **SARD 80% F1 push, first-class from day one — no prior IR existed for Rust at all** (only the opt-in, WASM-based `tree-sitter-loader.js` long-tail path below, which nothing in the taint engine consumes). Models: `fn` items (free/impl/trait, qualifiers, generics, `impl Type` methods named `Type.method`), `let`/destructuring/compound-assign, method chains (args accumulated innermost-first, so `Command::new("sh").arg("-c").arg(x)` has the literal program name at arg 0 regardless of the chain's terminal method), macros (`format!`/`write!`/etc. lower to a template with inline `{ident}` and positional args; `vec!` to an array; every other macro keeps its own `name!` callee so `sqlx::query!` — the compile-checked, parameterized macro — never collides with the `query` sink); `if`/`if let`/`while`/`while let`/`loop`/`for`/`match` (pattern bindings assigned from the scrutinee) including expression-position control flow (`let x = match … {}` yields a `union` of the branch tails); trailing closures inlined with their parameter bound to the receiver; extractor-typed parameters (`Query(q): Query<P>`) and route-attributed handlers (`#[get("/<x>")]`) emit `fn.paramAnnotations`. **`.unwrap()`/`.expect(msg)` are popped from the chain, not just taint-transparent** — leaving them in shifts the terminal segment away from the real sink name (`sqlx::query(&sql).fetch_all(pool).await.unwrap()` would otherwise end in `.unwrap`, never `.fetch_all`, and no catalog entry could ever match it), the same "terminal segment shift" defect class documented for Go/Java/Kotlin above; a macro-only statement whose sole continuation is a popped `.unwrap()` (`write!(w, "…", x).unwrap();`) still counts as a call, tracked via a separate `_baseIsCall` flag so it doesn't fall through to "no call happened here" once the passthrough is gone. Three self-scan-flagged regexes (`(?:#\[[^\]]*\]\s*)+`, the attribute-skip pattern, appearing at three call sites) match the "optional/quantified group flanked by `\s*`" ReDoS heuristic shape but are linear by construction — each iteration is bounded by the mandatory literal `#[`…`]`, no shared character class exists for the engine to backtrack across — confirmed by direct timing (50 000 back-to-back attributes, 200 000 non-matching whitespace chars: both single-digit milliseconds), not merely asserted; see the comment at `_parseParams` in `parser-rust.js`. Guarded by `test/parser-rust.test.js` (CFG shape) and `test/rust-taint-flow.test.js` (end-to-end taint flow + precision cases). **Measured `bench/layer-recall` impact: 0 → 5/5 (100%) whole-corpus, 0 → 3/3 (100%) deep-tier** on first measurement — the corpus is small and every entry is annotation-sourced (axum/actix/rocket extractor parameters route directly into `paramAnnotations`), so 100% reflects the corpus's own composition, not general Rust ecosystem coverage; see `docs/METRICS.md`'s own disclosure. |
|
|
35
35
|
| Long-tail (solidity/go/swift/dart) | `tree-sitter-loader.js` | **Optional** `web-tree-sitter` + `tree-sitter-wasms` (ABI-pinned 0.20.8 ↔ 0.1.13), lazy + degrades when absent. Powers `sast/tree-sitter-sinks.js` (opt-in via `AGENTIC_SECURITY_TREE_SITTER=1`). Marked `--external` in the build so the committed bundle never embeds WASM. Rust's own tree-sitter grammar remains loadable here too (unaffected by `parser-rust.js` existing), but the hand-rolled parser above is what feeds the taint engine — this path is now redundant for Rust specifically, kept only because removing a language from `tree-sitter-sinks.js`'s opt-in surface is out of scope for this change. |
|
|
36
36
|
|
|
37
|
+
## Implicit handler parameters and Go line attribution (QA-006)
|
|
38
|
+
|
|
39
|
+
`implicit-handler-params.js` is the pure decision "is this parameter request-bound without any annotation". `parser-cs.js` emits a
|
|
40
|
+
`paramAnnotations` entry with decorator `ImplicitMvcActionParam` for a string-shaped parameter of a public, non-static, non-`[NonAction]` method of a
|
|
41
|
+
controller class (derived from Controller/ControllerBase/ApiController, or named `*Controller`) unless it carries `[FromServices]` or an explicit
|
|
42
|
+
`[From*]`; `parser-java.js` emits `ImplicitSpringMappedParam` for a plain `String` parameter of a method annotated `@GetMapping`/`@PostMapping`/
|
|
43
|
+
`@RequestMapping`/... The catalog turns those two names into sources; the IR states the fact and the catalog decides what is a source. Nothing in the
|
|
44
|
+
module reads a path, label or environment variable.
|
|
45
|
+
|
|
46
|
+
`parser-go.js` line attribution: `FUNC_RE`'s leading `\s*` swallows blank lines, so the function used to be placed at the first newline of the run above
|
|
47
|
+
it, and `_splitStatements` dropped blank and comment lines so every later statement was pulled upward by their count. Statement lines are now start-offset
|
|
48
|
+
based (`_splitStatements(...).lineOffsets`, `_buildCfg` takes the line the body BEGINS on), a `//` comment no longer swallows the newline that ends its
|
|
49
|
+
line, and nested if/for/closure bodies take their base from the real brace position. Tests: `test/evaluation/engine-mechanisms.test.js`.
|
|
50
|
+
|
|
37
51
|
## Python parser — dual-path with auto fallback
|
|
38
52
|
|
|
39
53
|
The Python pipeline has two implementations. The dispatcher in `index.js`
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Implicit request-bound handler parameters (QA-006, mechanism "framework entry points").
|
|
2
|
+
//
|
|
3
|
+
// A web framework binds the parameters of a request-handler method from the
|
|
4
|
+
// request WITHOUT any annotation on the parameter itself:
|
|
5
|
+
// - ASP.NET MVC / Core: a public action method of a controller binds `string next`
|
|
6
|
+
// from the route, query string or form unless it carries `[FromServices]`.
|
|
7
|
+
// - Spring MVC: a method carrying `@GetMapping` / `@RequestMapping` / ... binds a plain
|
|
8
|
+
// `String q` parameter from the request parameters (`@RequestParam` is optional).
|
|
9
|
+
//
|
|
10
|
+
// The taint engine's annotation channel (`fn.paramAnnotations`, matched by the catalog's
|
|
11
|
+
// `type: 'annotation'` sources) only ever saw the EXPLICIT forms (`[FromQuery]`,
|
|
12
|
+
// `@RequestParam`), so a handler written in the equally common implicit form had no
|
|
13
|
+
// source at all and the flow was invisible to the taint layer. This module is the pure
|
|
14
|
+
// decision of "is this parameter implicitly request-bound", shared by the C# and Java
|
|
15
|
+
// frontends; each frontend turns a positive answer into a `paramAnnotations` entry whose
|
|
16
|
+
// decorator is one of the two names below, which the catalog binds to a source.
|
|
17
|
+
//
|
|
18
|
+
// Scope, stated so nothing is over-read:
|
|
19
|
+
// - ONLY string-shaped parameters. A numeric or Guid parameter cannot carry an injection
|
|
20
|
+
// payload, and a complex model parameter's field-level taint is out of scope here.
|
|
21
|
+
// - ONLY inside a class the framework would treat as a handler (a Controller-derived
|
|
22
|
+
// class, or any class whose name ends in `Controller`; for Spring, the MAPPING
|
|
23
|
+
// annotation on the method is what makes it a handler, not the class).
|
|
24
|
+
// - A DI-injected parameter is not request-bound: `[FromServices]` opts a parameter out.
|
|
25
|
+
// - Nothing here looks at a file name, path, comment or label.
|
|
26
|
+
|
|
27
|
+
export const IMPLICIT_MVC_PARAM = 'ImplicitMvcActionParam';
|
|
28
|
+
export const IMPLICIT_SPRING_PARAM = 'ImplicitSpringMappedParam';
|
|
29
|
+
|
|
30
|
+
const CS_CONTROLLER_BASE = /^(?:Controller|ControllerBase|ApiController|AsyncController)$/;
|
|
31
|
+
const CS_STRINGISH = /^(?:(?:System\.)?[Ss]tring\??(?:\[\])?|(?:List|IList|IEnumerable|ICollection|IReadOnlyList)<\s*(?:System\.)?[Ss]tring\??\s*>)$/;
|
|
32
|
+
const CS_DI_OPT_OUT = new Set(['FromServices', 'FromKeyedServices']);
|
|
33
|
+
|
|
34
|
+
/** True when a C# class record (`{ name, bases }`) is one the framework treats as a controller. */
|
|
35
|
+
function isCsControllerClass(range) {
|
|
36
|
+
if (!range) return false;
|
|
37
|
+
if (/Controller$/.test(range.name || '')) return true;
|
|
38
|
+
return (range.bases || []).some((b) => CS_CONTROLLER_BASE.test(b));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Should a C# parameter be treated as implicitly request-bound?
|
|
43
|
+
* @param {object} o
|
|
44
|
+
* @param {string} o.modifiers the method's modifier text (the declaration prefix), e.g. 'public async'
|
|
45
|
+
* @param {string} o.rawType the parameter's declared type text, e.g. 'string', 'List<string>'
|
|
46
|
+
* @param {string[]} o.decorators the parameter's attribute names
|
|
47
|
+
* @param {boolean} o.nonAction the method carries [NonAction]
|
|
48
|
+
* @param {object} o.classRange the enclosing class record
|
|
49
|
+
*/
|
|
50
|
+
export function isImplicitCsActionParam({ modifiers = '', rawType = '', decorators = [], nonAction = false, classRange = null }) {
|
|
51
|
+
if (nonAction) return false;
|
|
52
|
+
if (!isCsControllerClass(classRange)) return false;
|
|
53
|
+
if (!/\bpublic\b/.test(modifiers) || /\bstatic\b/.test(modifiers)) return false;
|
|
54
|
+
if (decorators.some((d) => CS_DI_OPT_OUT.has(d))) return false;
|
|
55
|
+
// An explicit binding attribute is handled by the explicit annotation sources.
|
|
56
|
+
if (decorators.some((d) => /^From/.test(d))) return false;
|
|
57
|
+
return CS_STRINGISH.test(String(rawType).trim());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SPRING_MAPPING = /^(?:RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)$/;
|
|
61
|
+
const SPRING_DI_OR_BINDING = /^(?:RequestParam|PathVariable|RequestBody|RequestHeader|CookieValue|ModelAttribute|RequestAttribute|SessionAttribute|RequestPart|Value|Autowired)$/;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Should a Java parameter be treated as implicitly request-bound?
|
|
65
|
+
* @param {string[]} o.methodAnnotations annotation simple names on the method
|
|
66
|
+
* @param {string} o.paramType the parameter's simple type name ('String')
|
|
67
|
+
* @param {string[]} o.decorators annotation names already on the parameter
|
|
68
|
+
*/
|
|
69
|
+
export function isImplicitSpringMappedParam({ methodAnnotations = [], paramType = '', decorators = [] }) {
|
|
70
|
+
if (!methodAnnotations.some((a) => SPRING_MAPPING.test(a))) return false;
|
|
71
|
+
if (paramType !== 'String') return false;
|
|
72
|
+
if (decorators.some((d) => SPRING_DI_OR_BINDING.test(d))) return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
package/src/ir/parser-cs.js
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
// This is a v1. Promoted to a Roslyn-backed CST parser (analogous to
|
|
46
46
|
// parser-py-cst.js) once we have a dotnet capability probe.
|
|
47
47
|
|
|
48
|
+
import { IMPLICIT_MVC_PARAM, isImplicitCsActionParam } from './implicit-handler-params.js';
|
|
48
49
|
import * as crypto from 'node:crypto';
|
|
49
50
|
import { callSitesFromCfg } from './call-sites.js';
|
|
50
51
|
import { matchBalancedCall } from './balanced-call.js';
|
|
@@ -270,6 +271,16 @@ function _extractClassFields(code, range) {
|
|
|
270
271
|
// Innermost class whose range contains `pos` (a method declaration's match
|
|
271
272
|
// offset), or null when the method sits outside any class (top-level
|
|
272
273
|
// statements — legal but rare).
|
|
274
|
+
function _enclosingClassRange(ranges, pos) {
|
|
275
|
+
let best = null;
|
|
276
|
+
for (const r of ranges) {
|
|
277
|
+
if (pos >= r.start && pos < r.end) {
|
|
278
|
+
if (!best || (r.end - r.start) < (best.end - best.start)) best = r;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return best;
|
|
282
|
+
}
|
|
283
|
+
|
|
273
284
|
function _enclosingClassName(ranges, pos) {
|
|
274
285
|
let best = null;
|
|
275
286
|
for (const r of ranges) {
|
|
@@ -1537,7 +1548,12 @@ export function parseCSharpFile(file, code) {
|
|
|
1537
1548
|
while ((m = METHOD_RE.exec(code)) !== null) {
|
|
1538
1549
|
const name = m[2];
|
|
1539
1550
|
const className = _enclosingClassName(classRanges, m.index);
|
|
1551
|
+
const classRange = _enclosingClassRange(classRanges, m.index);
|
|
1540
1552
|
const paramsText = m[3] || '';
|
|
1553
|
+
// [NonAction] sits in the attribute region just before the declaration.
|
|
1554
|
+
// Only the attribute region of THIS member counts: cut at the previous statement or block end.
|
|
1555
|
+
const attrRegion = code.slice(Math.max(0, m.index - 300), m.index + 1);
|
|
1556
|
+
const nonAction = /\bNonAction\b/.test(attrRegion.slice(Math.max(attrRegion.lastIndexOf('}'), attrRegion.lastIndexOf(';')) + 1));
|
|
1541
1557
|
const paramAnnotations = [];
|
|
1542
1558
|
const paramTypes = {};
|
|
1543
1559
|
// `keptIdx` tracks the parameter's position in the FILTERED array — the
|
|
@@ -1616,6 +1632,10 @@ export function parseCSharpFile(file, code) {
|
|
|
1616
1632
|
paramAnnotations.push({ index: keptIdx, name: paramName, decorator });
|
|
1617
1633
|
}
|
|
1618
1634
|
if (paramType) paramTypes[paramName] = paramType;
|
|
1635
|
+
// QA-006: a controller action's plain string parameter is bound from the request with no attribute at all.
|
|
1636
|
+
if (isImplicitCsActionParam({ modifiers: m[0], rawType: nameTokens.filter(w => !/^(?:ref|out|params|in|this)$/.test(w)).join(' '), decorators, nonAction, classRange })) {
|
|
1637
|
+
paramAnnotations.push({ index: keptIdx, name: paramName, decorator: IMPLICIT_MVC_PARAM });
|
|
1638
|
+
}
|
|
1619
1639
|
keptIdx++;
|
|
1620
1640
|
}
|
|
1621
1641
|
return paramName;
|
package/src/ir/parser-go.js
CHANGED
|
@@ -34,6 +34,10 @@ const FUNC_RE = new RegExp(
|
|
|
34
34
|
|
|
35
35
|
function _splitStatements(body) {
|
|
36
36
|
const out = [];
|
|
37
|
+
// Index (in `body`) of each statement's first character, so a caller can recover its real line. Blank lines and comment-only
|
|
38
|
+
// lines yield no statement, so counting statements undercounts lines wherever a function body has either.
|
|
39
|
+
const starts = [];
|
|
40
|
+
let bufStart = 0;
|
|
37
41
|
let buf = '';
|
|
38
42
|
let depth = 0;
|
|
39
43
|
let inStr = null;
|
|
@@ -57,19 +61,24 @@ function _splitStatements(body) {
|
|
|
57
61
|
if (c === '`') { inRaw = true; buf += c; continue; }
|
|
58
62
|
if (c === '/' && body[i + 1] === '/') {
|
|
59
63
|
while (i < body.length && body[i] !== '\n') i++;
|
|
64
|
+
// Leave the newline for the loop to see: swallowing it meant a comment line never ended its statement, so the next line was
|
|
65
|
+
// glued to whatever came before the comment and its line number was counted from the wrong place.
|
|
66
|
+
i--;
|
|
60
67
|
continue;
|
|
61
68
|
}
|
|
62
69
|
if (c === '{' || c === '(' || c === '[') depth++;
|
|
63
70
|
if (c === '}' || c === ')' || c === ']') depth--;
|
|
64
71
|
if ((c === '\n' || c === ';') && depth === 0) {
|
|
65
72
|
const t = buf.trim();
|
|
66
|
-
if (t) out.push(t);
|
|
73
|
+
if (t) { out.push(t); starts.push(bufStart + (buf.length - buf.trimStart().length)); }
|
|
67
74
|
buf = '';
|
|
75
|
+
bufStart = i + 1;
|
|
68
76
|
continue;
|
|
69
77
|
}
|
|
70
78
|
buf += c;
|
|
71
79
|
}
|
|
72
|
-
if (buf.trim()) out.push(buf.trim());
|
|
80
|
+
if (buf.trim()) { out.push(buf.trim()); starts.push(bufStart + (buf.length - buf.trimStart().length)); }
|
|
81
|
+
out.lineOffsets = starts.map((idx) => _countNewlinesUpTo(body, idx));
|
|
73
82
|
return out;
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -399,17 +408,21 @@ function _link(nodes, src, dst) {
|
|
|
399
408
|
if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
|
|
400
409
|
}
|
|
401
410
|
|
|
411
|
+
// `startLine` is the line on which `bodyText` BEGINS (the line of its opening brace); each statement's line is that plus the
|
|
412
|
+
// newlines before the statement, so blank lines and comments inside the body no longer pull later statements upward.
|
|
402
413
|
function _buildCfg(bodyText, nodes, prevId, startLine) {
|
|
403
414
|
const stmts = _splitStatements(bodyText);
|
|
404
415
|
let prev = prevId;
|
|
405
416
|
let line = startLine;
|
|
406
|
-
for (
|
|
417
|
+
for (let si = 0; si < stmts.length; si++) {
|
|
418
|
+
const stmt = stmts[si];
|
|
419
|
+
line = startLine + (stmts.lineOffsets ? stmts.lineOffsets[si] : 0);
|
|
407
420
|
const s = stmt.trim();
|
|
408
421
|
if (!s || s.startsWith('//')) { line++; continue; }
|
|
409
422
|
|
|
410
423
|
// if statement with brace body
|
|
411
|
-
const ifMatch = s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}(?:\s*else\s*\{([\s\S]*)\})?\s*$/
|
|
412
|
-
s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}\s*$/
|
|
424
|
+
const ifMatch = s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}(?:\s*else\s*\{([\s\S]*)\})?\s*$/sd) ||
|
|
425
|
+
s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}\s*$/sd);
|
|
413
426
|
if (ifMatch) {
|
|
414
427
|
const condText = ifMatch[1].replace(/;[^;]*$/, '').trim();
|
|
415
428
|
const thenBody = ifMatch[2];
|
|
@@ -417,10 +430,10 @@ function _buildCfg(bodyText, nodes, prevId, startLine) {
|
|
|
417
430
|
const ifNode = _addNode(nodes, { kind: 'if', cond: _lowerExpr(condText), line });
|
|
418
431
|
_link(nodes, prev, ifNode);
|
|
419
432
|
const join = _addNode(nodes, { kind: 'noop', line });
|
|
420
|
-
const thenTail = _buildCfg(thenBody, nodes, ifNode, line +
|
|
433
|
+
const thenTail = _buildCfg(thenBody, nodes, ifNode, line + _countNewlinesUpTo(s, ifMatch.indices[2][0]));
|
|
421
434
|
_link(nodes, thenTail, join);
|
|
422
435
|
if (elseBody) {
|
|
423
|
-
const elseTail = _buildCfg(elseBody, nodes, ifNode, line +
|
|
436
|
+
const elseTail = _buildCfg(elseBody, nodes, ifNode, line + _countNewlinesUpTo(s, ifMatch.indices[3][0]));
|
|
424
437
|
_link(nodes, elseTail, join);
|
|
425
438
|
} else {
|
|
426
439
|
_link(nodes, ifNode, join);
|
|
@@ -431,7 +444,7 @@ function _buildCfg(bodyText, nodes, prevId, startLine) {
|
|
|
431
444
|
}
|
|
432
445
|
|
|
433
446
|
// for loop with brace body
|
|
434
|
-
const forMatch = s.match(/^for\s+([\s\S]*?)\s*\{([\s\S]*)\}\s*$/
|
|
447
|
+
const forMatch = s.match(/^for\s+([\s\S]*?)\s*\{([\s\S]*)\}\s*$/sd);
|
|
435
448
|
if (forMatch) {
|
|
436
449
|
const header = _addNode(nodes, { kind: 'loop-header', line });
|
|
437
450
|
_link(nodes, prev, header);
|
|
@@ -446,7 +459,7 @@ function _buildCfg(bodyText, nodes, prevId, startLine) {
|
|
|
446
459
|
_link(nodes, header, assignId);
|
|
447
460
|
bodyPrev = assignId;
|
|
448
461
|
}
|
|
449
|
-
const bodyTail = _buildCfg(loopBody, nodes, bodyPrev, line +
|
|
462
|
+
const bodyTail = _buildCfg(loopBody, nodes, bodyPrev, line + _countNewlinesUpTo(s, forMatch.indices[2][0]));
|
|
450
463
|
_link(nodes, bodyTail, header);
|
|
451
464
|
const join = _addNode(nodes, { kind: 'noop', line });
|
|
452
465
|
_link(nodes, header, join);
|
|
@@ -476,8 +489,9 @@ function _buildCfg(bodyText, nodes, prevId, startLine) {
|
|
|
476
489
|
const callId = _addNode(nodes, { kind: 'call', line, callee: closureCall.callee, args: outerArgs });
|
|
477
490
|
_link(nodes, prev, callId);
|
|
478
491
|
const closureOffset = s.indexOf(closure.closureText);
|
|
479
|
-
const
|
|
480
|
-
|
|
492
|
+
const closureBodyIdx = closureOffset >= 0 ? s.indexOf(closure.body, closureOffset) : -1;
|
|
493
|
+
const bodyStartLine = closureBodyIdx >= 0
|
|
494
|
+
? line + _countNewlinesUpTo(s, closureBodyIdx)
|
|
481
495
|
: line;
|
|
482
496
|
prev = _buildCfg(closure.body, nodes, callId, bodyStartLine);
|
|
483
497
|
line += (s.match(/\n/g) || []).length + 1;
|
|
@@ -517,11 +531,14 @@ export function parseGoFile(file, code) {
|
|
|
517
531
|
if (braceIdx < 0) continue;
|
|
518
532
|
const extracted = _extractBody(code, braceIdx);
|
|
519
533
|
if (!extracted) continue;
|
|
520
|
-
|
|
534
|
+
// FUNC_RE's leading boundary is a newline and its `\s*` then swallows every blank line before `func`, so m.index can sit
|
|
535
|
+
// several lines ABOVE the declaration (a blank line before a function is the gofmt norm). Every line below, the function's
|
|
536
|
+
// own and each statement's, is counted from here, so the keyword's offset is what must be measured.
|
|
537
|
+
const startLine = _lineAt(code, m.index + Math.max(0, m[0].indexOf('func')));
|
|
521
538
|
const nodes = {};
|
|
522
539
|
const entry = _addNode(nodes, { kind: 'entry', line: startLine });
|
|
523
540
|
const exit = _addNode(nodes, { kind: 'exit', line: startLine });
|
|
524
|
-
const tail = _buildCfg(extracted.body, nodes, entry,
|
|
541
|
+
const tail = _buildCfg(extracted.body, nodes, entry, _lineAt(code, braceIdx + 1));
|
|
525
542
|
_link(nodes, tail, exit);
|
|
526
543
|
const cfg = { entry, exit, nodes };
|
|
527
544
|
functions.push({
|
package/src/ir/parser-java.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { blankComments } from '../sast/_comment-strip.js';
|
|
25
25
|
import { callSitesFromCfg } from './call-sites.js';
|
|
26
|
+
import { IMPLICIT_SPRING_PARAM, isImplicitSpringMappedParam } from './implicit-handler-params.js';
|
|
26
27
|
|
|
27
28
|
let _nodeIdSeq = 0;
|
|
28
29
|
function nextNodeId() { return 'jn' + (++_nodeIdSeq); }
|
|
@@ -1082,6 +1083,11 @@ export async function parseJavaFile(file, raw) {
|
|
|
1082
1083
|
|| md.children?.methodDeclarator?.[0]?.children?.formalParameterList?.[0];
|
|
1083
1084
|
const paramAnnotations = [];
|
|
1084
1085
|
const paramTypes = {};
|
|
1086
|
+
// QA-006: annotation simple names on the METHOD itself (`@GetMapping`), read from its modifiers.
|
|
1087
|
+
const methodAnnotations = (md.children?.methodModifier || []).map((mm) => {
|
|
1088
|
+
const ids = mm.children?.annotation?.[0]?.children?.typeName?.[0]?.children?.Identifier;
|
|
1089
|
+
return ids?.length ? ids[ids.length - 1]?.image : null;
|
|
1090
|
+
}).filter(Boolean);
|
|
1085
1091
|
const params = (fpl?.children?.formalParameter || []).map((fp, idx) => {
|
|
1086
1092
|
// Regular parameters nest under `variableParaRegularParameter`.
|
|
1087
1093
|
// Varargs (`String... args`) instead use a distinct
|
|
@@ -1119,6 +1125,7 @@ export async function parseJavaFile(file, raw) {
|
|
|
1119
1125
|
// the first, or a stacked annotation is silently dropped (the
|
|
1120
1126
|
// same class of bug Task 3's C# regex needed a fix round for).
|
|
1121
1127
|
const variableModifiers = vp?.children?.variableModifier || [];
|
|
1128
|
+
const ownDecorators = [];
|
|
1122
1129
|
for (const vm of variableModifiers) {
|
|
1123
1130
|
const ann = vm.children?.annotation?.[0];
|
|
1124
1131
|
// `typeName.children.Identifier` is an array of ALL
|
|
@@ -1133,9 +1140,13 @@ export async function parseJavaFile(file, raw) {
|
|
|
1133
1140
|
const identifiers = ann?.children?.typeName?.[0]?.children?.Identifier;
|
|
1134
1141
|
const decoratorName = identifiers?.length ? identifiers[identifiers.length - 1]?.image : undefined;
|
|
1135
1142
|
if (decoratorName && paramName) {
|
|
1143
|
+
ownDecorators.push(decoratorName);
|
|
1136
1144
|
paramAnnotations.push({ index: idx, name: paramName, decorator: decoratorName });
|
|
1137
1145
|
}
|
|
1138
1146
|
}
|
|
1147
|
+
if (paramName && isImplicitSpringMappedParam({ methodAnnotations, paramType, decorators: ownDecorators })) {
|
|
1148
|
+
paramAnnotations.push({ index: idx, name: paramName, decorator: IMPLICIT_SPRING_PARAM });
|
|
1149
|
+
}
|
|
1139
1150
|
return paramName;
|
|
1140
1151
|
}).filter(Boolean);
|
|
1141
1152
|
const body = md.children?.methodBody?.[0]?.children?.block?.[0];
|
|
@@ -148,6 +148,15 @@ const un=net.connect(sock);un.on('connect',()=>{r.daemonSocket='CONNECTED';un.de
|
|
|
148
148
|
setTimeout(()=>{r.timeout=true;console.log(JSON.stringify(r));process.exit(0)},8000);
|
|
149
149
|
`;
|
|
150
150
|
|
|
151
|
+
/** server.listen that settles: it resolves when listening and rejects on the server's error (a bare callback never fires on failure). */
|
|
152
|
+
function listenOrReject(server, ...args) {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const onError = (e) => reject(e);
|
|
155
|
+
server.once('error', onError);
|
|
156
|
+
server.listen(...args, () => { server.off('error', onError); resolve(); });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
/**
|
|
152
161
|
* Prove the isolation of a backend with real attempts, independent of any evaluator flag. The result is
|
|
153
162
|
* `verified` only if the one permitted read worked, the scratch write worked and every other attempt was refused.
|
|
@@ -160,12 +169,31 @@ export async function probeSandbox(sb, opts = {}) {
|
|
|
160
169
|
for (const d of [allowed, scratch, outside]) mkdirSync(d);
|
|
161
170
|
writeFileSync(join(allowed, 'a.txt'), 'allowed'); writeFileSync(join(outside, 'secret.txt'), 'secret');
|
|
162
171
|
const script = join(base, 'probe.cjs'); writeFileSync(script, PROBE_SOURCE);
|
|
163
|
-
|
|
172
|
+
let sockPath = join(base, 'daemon.sock');
|
|
173
|
+
let sockDir = null;
|
|
164
174
|
let connections = 0;
|
|
165
175
|
const tcp = netServer((s) => { connections++; s.destroy(); });
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
let uds = netServer((s) => { connections++; s.destroy(); });
|
|
177
|
+
try {
|
|
178
|
+
await listenOrReject(tcp, 0, '127.0.0.1');
|
|
179
|
+
try {
|
|
180
|
+
await listenOrReject(uds, sockPath);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// A unix socket path is limited to about 104 bytes, and a temporary directory can easily be longer than that (macOS, CI runners).
|
|
183
|
+
// Without an error handler the listen call never completed and the whole evaluation hung. Retry in a short directory; the probe
|
|
184
|
+
// proves that the sandboxed process is refused, which does not depend on where the socket lives.
|
|
185
|
+
if (!['EINVAL', 'ENAMETOOLONG'].includes(e && e.code) || !existsSync('/tmp')) throw e;
|
|
186
|
+
uds.close();
|
|
187
|
+
uds = netServer((s) => { connections++; s.destroy(); });
|
|
188
|
+
sockDir = mkdtempSync('/tmp/nep-');
|
|
189
|
+
sockPath = join(sockDir, 'd.sock');
|
|
190
|
+
await listenOrReject(uds, sockPath);
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
tcp.close(); uds.close(); rmSync(base, { recursive: true, force: true });
|
|
194
|
+
if (sockDir) rmSync(sockDir, { recursive: true, force: true });
|
|
195
|
+
return { verified: false, backend: sb.backend, reason: `the probe could not create its listeners (${(e && e.code) || 'error'}): isolation is not verified`, attempts: {} };
|
|
196
|
+
}
|
|
169
197
|
const port = tcp.address().port;
|
|
170
198
|
try {
|
|
171
199
|
const exe = opts.nodePath || process.execPath;
|
|
@@ -186,7 +214,7 @@ export async function probeSandbox(sb, opts = {}) {
|
|
|
186
214
|
if (attempts.credentialEnv && attempts.credentialEnv !== 'absent') insufficient.push('a credential environment variable was visible');
|
|
187
215
|
if (attempts.timeout) insufficient.push('the probe timed out');
|
|
188
216
|
return { verified: insufficient.length === 0 && connections === 0, backend: sb.backend, reason: insufficient.length ? insufficient.join('; ') : (connections ? 'the probe listeners saw a connection' : null), attempts, listenerConnections: connections, insufficient };
|
|
189
|
-
} finally { tcp.close(); uds.close(); rmSync(base, { recursive: true, force: true }); }
|
|
217
|
+
} finally { tcp.close(); uds.close(); rmSync(base, { recursive: true, force: true }); if (sockDir) rmSync(sockDir, { recursive: true, force: true }); }
|
|
190
218
|
}
|
|
191
219
|
|
|
192
220
|
// ── 3. evaluator feature detection ───────────────────────────────────────────
|