@clear-capabilities/agentic-security-scanner 0.143.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 +304 -0
  2. package/bin/agentic-security.js +477 -47
  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 +51 -2
  93. package/src/report/oscal.js +7 -2
  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;
@@ -1284,17 +1317,33 @@ export function toShipVerdict(scan, options = {}) {
1284
1317
  const confirmedCount = findings.filter(f => f.validated === true || f.confirmed === true).length;
1285
1318
  const cats = categoryScores(findings);
1286
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
+
1287
1329
  const lines = [];
1288
1330
  const bar = '─────────────────────────────────────────';
1289
1331
  // Patch the mascot reacts to the result — APPROVE if clean, ALERT if findings.
1290
- lines.push(actionable.length === 0 ? approveFace({ color }) : alertFace({ color }));
1332
+ const clean = actionable.length === 0 && !scanIncomplete;
1333
+ lines.push(clean ? approveFace({ color }) : alertFace({ color }));
1291
1334
  lines.push(bar);
1292
- if (actionable.length === 0) {
1335
+ if (clean) {
1293
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));
1294
1339
  } else {
1295
1340
  lines.push(c(' ❌ Not safe to deploy', SEV_COLOR.critical + BOLD));
1296
1341
  }
1297
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
+ }
1298
1347
  lines.push(` • ${sev.critical} critical · ${sev.high} high · ${advisoryCount} advisory`);
1299
1348
  // Per-category 0..100 score bars — Secrets / Permissions / Hooks / MCP / Agents.
1300
1349
  // Only render when at least one category has been contributed to (skip the
@@ -50,6 +50,7 @@
50
50
  import crypto from 'node:crypto';
51
51
  import { isDeterministic, SCANNER_VERSION } from '../posture/deterministic.js';
52
52
  import { normalizeFindings, TOOL_CAVEATS } from './index.js';
53
+ import { EVIDENCE_GRADE_DISCLAIMER } from '../posture/evidence-grade-wording.js';
53
54
 
54
55
  // The OSCAL release these documents declare conformance to. Bump deliberately:
55
56
  // `oscal-version` is a claim a validator checks the rest of the document
@@ -620,9 +621,13 @@ export function toOSCALCompliance(framework, rows, meta = {}) {
620
621
  observations,
621
622
  ...(findings.length ? { findings } : {}),
622
623
  ...(risks.length ? { risks } : {}),
624
+ // FR-507: names all three assurance tiers explicitly, not just "not
625
+ // certified" — see evidence-grade-wording.js for why that distinction
626
+ // matters (a reader who has never heard "attestation" used correctly
627
+ // has no way to know a real one is a different, valid artifact they
628
+ // might separately need).
623
629
  remarks:
624
- 'This document organizes automated scanner evidence against a control set. It is not an attestation '
625
- + 'of compliance and no licensed assessor reviewed it. '
630
+ `${EVIDENCE_GRADE_DISCLAIMER} No licensed assessor reviewed this document. `
626
631
  + TOOL_CAVEATS.map(c => `${c.id}: ${c.shortDescription}`).join(' | '),
627
632
  }],
628
633
  'back-matter': { resources: [frameworkResource, ...caveats] },
package/src/sast/cpp.js CHANGED
@@ -16,6 +16,7 @@
16
16
  // - hardcoded hardcoded user/password in fopen / connect calls
17
17
 
18
18
  import { blankComments } from './_comment-strip.js';
19
+ import { classifySecretCandidate as _entropyClassifySecret } from './_secret-entropy.js';
19
20
 
20
21
  // ── context detectors ───────────────────────────────────────────────────────
21
22
 
@@ -279,14 +280,8 @@ const FINDINGS = [
279
280
  const val = m && m[2];
280
281
  if (!val) return false;
281
282
  try {
282
- // Lazy import — avoids a circular dep on the entropy module being
283
- // present in older snapshots.
284
- // eslint-disable-next-line no-unused-vars
285
- const { classifySecretCandidate } = _entropyMod || {};
286
- if (classifySecretCandidate) {
287
- const r = classifySecretCandidate(val);
288
- if (r.skip) return false;
289
- }
283
+ const r = _entropyClassifySecret(val);
284
+ if (r.skip) return false;
290
285
  } catch { /* fail open */ }
291
286
  return true;
292
287
  },
@@ -319,12 +314,6 @@ const FINDINGS = [
319
314
  },
320
315
  ];
321
316
 
322
- // Late-bound entropy module — imported via a dynamic require shim so the
323
- // rule table can lazy-call it from inside gate functions without creating
324
- // a circular import at module load time.
325
- let _entropyMod = null;
326
- import('./_secret-entropy.js').then(m => { _entropyMod = m; }).catch(() => { _entropyMod = null; });
327
-
328
317
  function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
329
318
 
330
319
  export function scanCpp(fp, raw) {
@@ -9,6 +9,7 @@
9
9
  import * as crypto from 'node:crypto';
10
10
  import * as fs from 'node:fs';
11
11
  import * as path from 'node:path';
12
+ import { evaluateEgress } from '../egress/policy.js';
12
13
 
13
14
  const CACHE_DIR = process.env.XDG_CONFIG_HOME
14
15
  ? path.join(process.env.XDG_CONFIG_HOME, 'agentic-security', 'llm-sca-cache')
@@ -63,6 +64,11 @@ export async function extractVulnFunctionsViaLLM(supplyChain, opts = {}) {
63
64
  if (!isLlmScaEnabled()) return [];
64
65
  const config = _endpointConfig();
65
66
  if (!config) return [];
67
+ // FR-601: one decision for the whole batch (same endpoint for every CVE in
68
+ // this run) — evaluated before any of the per-candidate prompts below are
69
+ // built.
70
+ const egressDecision = evaluateEgress({ scanRoot: opts.scanRoot, purpose: 'sca-llm-function-extract', endpoint: config.endpoint });
71
+ if (!egressDecision.allowed) return [];
66
72
 
67
73
  const enriched = [];
68
74
  const candidates = (supplyChain || []).filter(sc =>