@clear-capabilities/agentic-security-scanner 0.142.0 → 0.144.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/bin/agentic-security.js +530 -54
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +108 -17
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +1 -1
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/384.index.js +1 -1
  10. package/dist/435.index.js +165 -52
  11. package/dist/526.index.js +108 -17
  12. package/dist/552.index.js +97 -0
  13. package/dist/637.index.js +1 -1
  14. package/dist/730.index.js +311 -0
  15. package/dist/736.index.js +301 -0
  16. package/dist/824.index.js +7 -0
  17. package/dist/905.index.js +88 -22
  18. package/dist/920.index.js +491 -0
  19. package/dist/970.index.js +109 -0
  20. package/dist/agentic-security.mjs +13 -13
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/dist/calibration-seed.json +2 -0
  23. package/package.json +19 -11
  24. package/src/dataflow/index.js +18 -0
  25. package/src/dataflow/privacy-catalog.js +290 -0
  26. package/src/dataflow/privacy-deep-walker.js +515 -0
  27. package/src/dataflow/privacy-governance.js +126 -0
  28. package/src/dataflow/privacy-inventory.js +154 -0
  29. package/src/dataflow/privacy-sink-policy.js +125 -0
  30. package/src/dataflow/privacy-taint.js +115 -54
  31. package/src/dataflow/privacy-taxonomy.js +233 -0
  32. package/src/discovery/disprove.js +7 -3
  33. package/src/discovery/hunter.js +9 -5
  34. package/src/discovery/index.js +2 -2
  35. package/src/discovery/llm-invoke.js +69 -13
  36. package/src/egress/audit.js +147 -0
  37. package/src/egress/policy.js +313 -0
  38. package/src/egress/redact.js +180 -0
  39. package/src/engine.js +575 -288
  40. package/src/fix/apply-fix-service.js +403 -0
  41. package/src/fix/approver-registry.js +157 -0
  42. package/src/llm-validator/index.js +86 -9
  43. package/src/llm-validator/model-status.js +66 -0
  44. package/src/mcp/tools.js +157 -50
  45. package/src/pipeline/analyzer-supervisor.js +93 -0
  46. package/src/pipeline/analyzer-worker.js +26 -0
  47. package/src/pipeline/annotator-runner.js +33 -0
  48. package/src/pipeline/assurance-mode.js +91 -0
  49. package/src/pipeline/cascade-worker-pool.js +172 -0
  50. package/src/pipeline/cascade-worker.js +43 -0
  51. package/src/pipeline/coverage-ledger.js +0 -0
  52. package/src/pipeline/detector-runner.js +51 -0
  53. package/src/pipeline/enrichment-completion.js +58 -0
  54. package/src/pipeline/evidence-provenance.js +91 -0
  55. package/src/pipeline/finding-schema.js +101 -0
  56. package/src/pipeline/legacy-compat.js +101 -0
  57. package/src/pipeline/producer-collector.js +48 -0
  58. package/src/pipeline/producer-registry.js +112 -0
  59. package/src/pipeline/scan-health.js +144 -0
  60. package/src/posture/CLAUDE.md +2 -0
  61. package/src/posture/accuracy-scorecard.js +96 -1
  62. package/src/posture/adversary-agent.js +15 -3
  63. package/src/posture/artifact-registry.js +217 -0
  64. package/src/posture/auditor-walkthrough.js +70 -8
  65. package/src/posture/calibration-feedback.js +201 -0
  66. package/src/posture/calibration-seed.json +2 -0
  67. package/src/posture/calibration.js +25 -0
  68. package/src/posture/compliance-evidence-signing.js +131 -0
  69. package/src/posture/compliance-policy.js +314 -17
  70. package/src/posture/custom-rules.js +36 -0
  71. package/src/posture/deterministic.js +8 -1
  72. package/src/posture/encryption-provider.js +205 -0
  73. package/src/posture/evidence-grade-wording.js +71 -0
  74. package/src/posture/fix-history.js +113 -19
  75. package/src/posture/fix-honesty-gate.js +47 -6
  76. package/src/posture/fix-verify.js +56 -7
  77. package/src/posture/fleet.js +0 -0
  78. package/src/posture/flow-narration.js +7 -2
  79. package/src/posture/legal-hold.js +140 -0
  80. package/src/posture/llm-redteam.js +10 -1
  81. package/src/posture/material-change.js +90 -0
  82. package/src/posture/policy-bundle.js +274 -0
  83. package/src/posture/privacy-framework.js +33 -6
  84. package/src/posture/production-feedback.js +179 -0
  85. package/src/posture/retention-policy.js +132 -0
  86. package/src/posture/risk-dollars.js +216 -26
  87. package/src/posture/scan-checkpoint.js +176 -31
  88. package/src/posture/state-dir.js +36 -1
  89. package/src/posture/state-lifecycle-report.js +77 -0
  90. package/src/posture/suppressions.js +59 -3
  91. package/src/privacy/ir-adapter.js +380 -0
  92. package/src/report/index.js +83 -18
  93. package/src/report/oscal.js +635 -0
  94. package/src/sast/cpp.js +3 -14
  95. package/src/sca/llm-function-extract.js +6 -0
@@ -4,6 +4,7 @@ import { _isCustomSuppressed } from '../engine.js';
4
4
  import { alertFace, approveFace } from './mascot.js';
5
5
  import { SCANNER_VERSION } from '../posture/version.js';
6
6
  import { proofBlock } from '../posture/proof-artifact.js';
7
+ import { applyLegacyCompat, legacyFieldDeprecationNotice } from '../pipeline/legacy-compat.js';
7
8
 
8
9
  const SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
9
10
  const SEV_TO_SARIF = { critical: 'error', high: 'error', medium: 'warning', low: 'note', info: 'none' };
@@ -426,6 +427,12 @@ export function normalizeFindings(scan){
426
427
  toxicityLabel: sc.toxicityLabel || null,
427
428
  });
428
429
  }
430
+ // FR-108: backfill deprecated field names from their current replacements
431
+ // before this shape reaches ANY consumer (JSON/SARIF/HTML/CSV/JUnit/MCP
432
+ // all derive from this one function) — one place, covers every output
433
+ // format, matches this function's own role as "the canonical shape".
434
+ for (const f of out) applyLegacyCompat(f);
435
+
429
436
  // Sort by severity tier, then within a tier by EPSS percentile (desc) so that
430
437
  // CVEs with active in-the-wild abuse float above theoretical CVEs.
431
438
  return out.sort((a, b) => {
@@ -516,6 +523,13 @@ export function toJSON(scan, meta={}, opts={}){
516
523
  // threw and were skipped. The findings still ship; downstream consumers
517
524
  // see the gap.
518
525
  annotatorErrors: Array.isArray(scan.annotatorErrors) ? scan.annotatorErrors : [],
526
+ // FR-201 (assurance-hardening PRD, E2): per-file SAST/logic/secrets
527
+ // detector errors, captured by pipeline/detector-runner.js's runDetector()
528
+ // isolation wrapper. Empty array means clean; entries here mean one or
529
+ // more detectors threw on a specific file and were skipped -- other
530
+ // detectors and other files still ran (that isolation is the acceptance
531
+ // criterion itself, not just this field's presence).
532
+ detectorErrors: Array.isArray(scan.detectorErrors) ? scan.detectorErrors : [],
519
533
  // R4 — run attestation: a stable, order-independent digest over this
520
534
  // finding set bound to the engine/ruleset/bundle that produced it.
521
535
  // Attached by the CLI (posture/attestation.js); null when not computed.
@@ -529,6 +543,10 @@ export function toJSON(scan, meta={}, opts={}){
529
543
  // render an empty one. An AUTHORISED suppression is reported too — the
530
544
  // signature proves who asked for it, not that the results are absent.
531
545
  suppressedRules: scan.suppressedRules || null,
546
+ // FR-108: null in the (eventual, intended) steady state where no finding
547
+ // used a deprecated field name; present only when normalizeFindings()
548
+ // actually backfilled a legacy alias, naming exactly which and why.
549
+ legacyFieldNotice: legacyFieldDeprecationNotice(findings),
532
550
  _scanMeta: scan._scanMeta || null,
533
551
  // S7: engine.js computes these on every scan with components, and their
534
552
  // own findings already flow into `findings` above — but the structured
@@ -546,6 +564,21 @@ export function toJSON(scan, meta={}, opts={}){
546
564
  rootCauseSweep: scan.rootCauseSweep || null,
547
565
  attackTaxonomy: scan.attackTaxonomy || null,
548
566
  privacyFramework: scan.privacyFramework || null,
567
+ // FR-405: `false` is a real, meaningful value here (privacy analysis
568
+ // ran but was not IR-backed) — must not collapse to null via `||`,
569
+ // which would make it indistinguishable from "never ran at all".
570
+ privacyIrBacked: scan.privacyIrBacked ?? null,
571
+ // FR-402: which privacy data-classification taxonomy version actually
572
+ // classified this scan (built-in, or built-in+custom when an operator's
573
+ // .agentic-security/privacy-taxonomy.json extended it) — see
574
+ // dataflow/privacy-taxonomy.js. Null means the annotator never ran.
575
+ privacyTaxonomyVersion: scan.privacyTaxonomyVersion ?? null,
576
+ // Assurance-hardening PRD FR-206 (Milestone 0): additive scan-health
577
+ // summary (pipeline/scan-health.js) — separates "no findings" from
578
+ // "analysis complete". Null when the engine did not compute one (e.g. a
579
+ // hand-built `scan` object in a test), same convention as the S7 fields
580
+ // above; a real scan always computes it.
581
+ scanHealth: scan.scanHealth || null,
549
582
  };
550
583
  if (opts.includeSuppressed) out.suppressed = scan.suppressions||[];
551
584
  return out;
@@ -736,6 +769,26 @@ export function toMarkdown(scan, meta={}){
736
769
  return lines.join('\n');
737
770
  }
738
771
 
772
+ // The load-bearing caveats every machine-readable artifact must carry, in one
773
+ // place. A consumer that ingests our output without seeing these will read an
774
+ // ordinal priority score as a probability, and will read a benchmark-tuned F1
775
+ // as a general-purpose quality claim. Both are wrong, and both are the kind of
776
+ // wrong that only shows up in somebody else's risk-acceptance decision.
777
+ export const TOOL_CAVEATS = Object.freeze([
778
+ {
779
+ id: 'scores-are-ordinal',
780
+ name: 'ScoresAreOrdinal',
781
+ shortDescription: 'priority/exploitability scores are ordinal, not calibrated probabilities',
782
+ fullDescription: 'The properties.exploitability and properties.confidence fields on each result are ORDINAL priority scores used to rank findings within a scan. They are NOT calibrated probabilities; do not render them as percentages or feed them into pricing / risk-acceptance decisions. Use the tier labels (critical/high/medium/low) for coarse bucketing. See bench/README.md for the open calibration work.',
783
+ },
784
+ {
785
+ id: 'owasp-benchmark-tuning',
786
+ name: 'OwaspBenchmarkTuning',
787
+ shortDescription: 'engine ships OWASP-Benchmark-shape precision lifters; F1 numbers do not generalize',
788
+ fullDescription: 'The engine includes precision lifters (sast/primary-cwe-java.js, sast/java-constant-fold.js) whose heuristics are tuned to OWASP Benchmark v1.2 file shape (servlet-style files <=300 LoC, canonical variable names). F1 numbers cited against OWASP Benchmark do NOT generalize to arbitrary Java code. Expect higher FP rates on real-world codebases until per-customer tuning lands. See bench/README.md.',
789
+ },
790
+ ]);
791
+
739
792
  export function toSARIF(scan, meta={}){
740
793
  const findings = normalizeFindings(scan);
741
794
  const ruleMap = new Map();
@@ -758,22 +811,18 @@ export function toSARIF(scan, meta={}){
758
811
  // SARIF run itself so machine consumers see them. Without these, a CI that
759
812
  // ingests SARIF treats "confidence: 0.9" as a probability and the
760
813
  // benchmark-tuned 0.907 number as quality evidence.
761
- const SARIF_NOTIFICATIONS = [
762
- {
763
- id: 'scores-are-ordinal',
764
- name: 'ScoresAreOrdinal',
765
- shortDescription: { text: 'priority/exploitability scores are ordinal, not calibrated probabilities' },
766
- defaultConfiguration: { level: 'note' },
767
- fullDescription: { text: 'The properties.exploitability and properties.confidence fields on each result are ORDINAL priority scores used to rank findings within a scan. They are NOT calibrated probabilities; do not render them as percentages or feed them into pricing / risk-acceptance decisions. Use the tier labels (critical/high/medium/low) for coarse bucketing. See bench/README.md for the open calibration work.' },
768
- },
769
- {
770
- id: 'owasp-benchmark-tuning',
771
- name: 'OwaspBenchmarkTuning',
772
- shortDescription: { text: 'engine ships OWASP-Benchmark-shape precision lifters; F1 numbers do not generalize' },
773
- defaultConfiguration: { level: 'note' },
774
- fullDescription: { text: 'The engine includes precision lifters (sast/primary-cwe-java.js, sast/java-constant-fold.js) whose heuristics are tuned to OWASP Benchmark v1.2 file shape (servlet-style files <=300 LoC, canonical variable names). F1 numbers cited against OWASP Benchmark do NOT generalize to arbitrary Java code. Expect higher FP rates on real-world codebases until per-customer tuning lands. See bench/README.md.' },
775
- },
776
- ];
814
+ //
815
+ // The texts live in TOOL_CAVEATS (module scope) because SARIF is no longer
816
+ // the only machine format that has to carry them: report/oscal.js emits the
817
+ // same list as back-matter resources. Two copies of a caveat is one copy
818
+ // that goes stale, and the stale one is always the one somebody reads.
819
+ const SARIF_NOTIFICATIONS = TOOL_CAVEATS.map(c => ({
820
+ id: c.id,
821
+ name: c.name,
822
+ shortDescription: { text: c.shortDescription },
823
+ defaultConfiguration: { level: 'note' },
824
+ fullDescription: { text: c.fullDescription },
825
+ }));
777
826
  return {
778
827
  $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
779
828
  version: '2.1.0',
@@ -1268,17 +1317,33 @@ export function toShipVerdict(scan, options = {}) {
1268
1317
  const confirmedCount = findings.filter(f => f.validated === true || f.confirmed === true).length;
1269
1318
  const cats = categoryScores(findings);
1270
1319
 
1320
+ // FR-206: "a partial zero-finding scan never says clean." `scanHealth`
1321
+ // (computeScanHealth, scan-health.js) was already computed correctly —
1322
+ // an annotator error, a file timeout, or a skipped analyzer already
1323
+ // demoted `status` to 'partial' — but nothing in this, the actual
1324
+ // human-facing one-screen verdict, ever consulted it. Zero actionable
1325
+ // findings on a scan that didn't finish cleanly is "we don't know," not
1326
+ // "safe."
1327
+ const scanIncomplete = scan.scanHealth && scan.scanHealth.status && scan.scanHealth.status !== 'complete';
1328
+
1271
1329
  const lines = [];
1272
1330
  const bar = '─────────────────────────────────────────';
1273
1331
  // Patch the mascot reacts to the result — APPROVE if clean, ALERT if findings.
1274
- lines.push(actionable.length === 0 ? approveFace({ color }) : alertFace({ color }));
1332
+ const clean = actionable.length === 0 && !scanIncomplete;
1333
+ lines.push(clean ? approveFace({ color }) : alertFace({ color }));
1275
1334
  lines.push(bar);
1276
- if (actionable.length === 0) {
1335
+ if (clean) {
1277
1336
  lines.push(c(' ✅ Safe to deploy', SEV_COLOR.low + BOLD));
1337
+ } else if (actionable.length === 0) {
1338
+ lines.push(c(' ⚠️ Scan incomplete — cannot confirm safe to deploy', SEV_COLOR.high + BOLD));
1278
1339
  } else {
1279
1340
  lines.push(c(' ❌ Not safe to deploy', SEV_COLOR.critical + BOLD));
1280
1341
  }
1281
1342
  lines.push(bar);
1343
+ if (scanIncomplete) {
1344
+ const conditions = Array.isArray(scan.scanHealth.conditions) ? scan.scanHealth.conditions : [];
1345
+ lines.push(c(` ${scan.scanHealth.status}: ${conditions[0] || 'analysis did not complete cleanly'}${conditions.length > 1 ? ` (+${conditions.length - 1} more)` : ''}`, SEV_COLOR.high));
1346
+ }
1282
1347
  lines.push(` • ${sev.critical} critical · ${sev.high} high · ${advisoryCount} advisory`);
1283
1348
  // Per-category 0..100 score bars — Secrets / Permissions / Hooks / MCP / Agents.
1284
1349
  // Only render when at least one category has been contributed to (skip the