@clear-capabilities/agentic-security-scanner 0.195.27 → 0.195.29

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 (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/bin/agentic-security.js +41 -4
  3. package/dist/5856.index.js +634 -0
  4. package/dist/7033.index.js +74 -14
  5. package/dist/9220.index.js +24 -1
  6. package/dist/agentic-security.mjs +9 -9
  7. package/dist/agentic-security.mjs.sha256 +1 -1
  8. package/dist/{90ebba38d8860ec77376.json → c06a1b4c20155c06fb48.json} +4 -3
  9. package/package.json +4 -3
  10. package/src/dataflow/catalog.js +42 -1
  11. package/src/engine.js +91 -11
  12. package/src/language/CLAUDE.md +7 -1
  13. package/src/language/assurance.js +53 -2
  14. package/src/language/haskell-budget-state.js +85 -0
  15. package/src/language/haskell-coverage.js +230 -0
  16. package/src/language/haskell-guards.js +26 -5
  17. package/src/language/haskell-project-models.js +107 -0
  18. package/src/language/nix-eval-isolation.js +74 -14
  19. package/src/llm-validator/bounded-context.js +193 -0
  20. package/src/llm-validator/claim-groups.js +105 -0
  21. package/src/llm-validator/disposition.js +264 -0
  22. package/src/llm-validator/index.js +52 -26
  23. package/src/pipeline/evidence-record.js +112 -0
  24. package/src/pipeline/scan-health.js +12 -1
  25. package/src/posture/artifact-registry.js +2 -0
  26. package/src/posture/drift.js +19 -1
  27. package/src/posture/reachability-filter.js +3 -3
  28. package/src/posture/suppressions.js +21 -0
  29. package/src/posture/watch-mode.js +22 -1
  30. package/src/report/disposition-view.js +90 -0
  31. package/src/report/index.js +47 -0
  32. package/src/runScan.js +60 -6
  33. package/src/sast/oidc-trust.js +74 -0
  34. package/src/sast/pipeline.js +14 -1
  35. package/src/sast/workflow-flow.js +272 -0
  36. package/src/sca/basis.js +88 -0
  37. package/src/scope/flags.js +55 -0
  38. package/src/scope/inventory.js +241 -0
package/CHANGELOG.md CHANGED
@@ -9,6 +9,30 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.195.29 - `scan --watch` watches, the validator collects callers and configuration, and pilot tooling for the measured targets
13
+
14
+ **Measured, 0.195.28:** `TIMINGS preflight=3s push=6s pr=1s checks=430s merge=6s verify=6s tag=2s release=131s npm=450s total=1035s release-clock=145s`, shipped from the merge to the published release in 145 s; the run stopped once on a git index lock held by the post-commit gate warm-up, and `ship --resume` continued after it.
15
+
16
+ - **`scan --watch` exited after its first scan** and never watched anything: the watcher does not block, and the scan command's dispatch exits with whatever the command returns. It now stays running until Ctrl-C or SIGTERM, and says when the first scan is done. A new end-to-end test runs the real CLI with a scope flag: a change to a Git-ignored file does not rescan, and a change to a tracked file under `dist/` (which the default watcher filter skips) does.
17
+ - **The model validator collects callers and configuration** (PRD FR 05). Callers: the nearest function definition above the finding, then up to three call sites of that name in scanned files of the same language, labelled as matched by name rather than resolved. Configuration: environment keys read near the finding, then the scanned configuration files that set them; a real `.env` file is never read, and the value of a secret-like key is masked before redaction. What could not be collected is still stated in the prompt. Prompt version `v2.2-callers-config`, so cached verdicts are not reused.
18
+ - **A Haskell request whose whole destination is a literal is refuted** when the client's text reaches only the query, headers, body, method or authentication: a `constant-input` refutation tied to the file's content hash, kept in the refuted record. A client-chosen path, a concatenation, a custom manager or an unknown record field still only lowers the finding to `info`.
19
+ - **Pilot and benchmark tooling** for the targets that need data the project does not hold. `bench/haskell-pilot/pilot.mjs capture` records what M0 asks for and measures the 95% parse and 90% semantic rates with failures in the denominator, with frozen exclusions; `compare` diffs two reports of one snapshot and says it is not recall. `bench/haskell-core-families/adjudicate.mjs` imports blind two-reviewer labels, reports agreement and Cohen's kappa, adds only unanimous cases, and freezes the manifest and definitions; the runner refuses the holdout without a matching freeze and logs every holdout run, and reports 95% Wilson intervals. No independent case and no pilot snapshot exists yet; every family still reports `insufficient-cases`.
20
+ - **CI**: the cabal store's cache key names the runner's OS release, so packages built on one Ubuntu release are not restored on another.
21
+
22
+ ## 0.195.28 - Git-aware scope, Haskell coverage accounting, and findings that keep their evidence
23
+
24
+ **Measured, 0.195.27:** `TIMINGS preflight=4s push=8s pr=4s checks=15s merge=6s verify=6s tag=2s release=131s npm=268s total=444s release-clock=145s`, shipped from the merge to the published release in 145 s.
25
+
26
+ The buildable part of the Haskell coverage and reliable findings plan (FR 01 to FR 03 and FR 05 to FR 11). FR 04's independently labelled benchmark and the pilot measurements are not in this release: every Haskell family still reports `insufficient-cases`. Nothing moved a maturity. Three defaults change, listed first.
27
+
28
+ - **Default changes.** A dependency finding whose reachability is unknown (`manifest-only`, `transitive-only`) keeps its severity; only an `unreachable` function or a `build-only` dependency lowers it. A model `reject` no longer removes a finding: each finding carries a disposition (`confirmed`, `needs-review`, `refuted`, `hardening`, `unvalidated`), and only evidence or a reviewed suppression refutes. Findings from different detectors about the same weakness at the same line are grouped under one primary with the others listed in `claims`, and the report reconciles raw, grouped, refuted and disposition counts (`candidateReconciliation`); drift compares grouped members too, so grouping never shows a finding as fixed.
29
+ - **Git-aware scope (FR 01).** The scan inventory follows Git: an ignored file is never opened (an instrumented test proves it), a tracked file under a default-skipped directory such as `dist/` or `vendor/` stays eligible, submodules and nested repositories are reported as unscanned units, and `--explain-scope` says why each path is in or out. A finding outside a narrower scan's coverage is out of scope, never fixed. `scan --watch` keeps the scope flags on every rescan and asks the same inventory, rebuilt when `.gitignore` changes.
30
+ - **Haskell coverage (FR 02, FR 03).** Coverage is accounted by stage, package and rule family, listing completed and remaining files. `AGENTIC_SECURITY_HASKELL_PARSE_BUDGETS` raises named parse bounds (unknown names are refused and reported), and `.agentic-security/haskell-budget.json` records the work left so the next scan states what completed, what will hit the same bound again and what changed. There is no resume: every scan re-parses every file. Reviewed project models describe wrappers, and are matched only by the scan of their own root.
31
+ - **Model validation (FR 05).** The model sees numbered spans (the finding, source, sink, path steps, related Haskell and Nix files), each redacted under its own path, plus a stated list of what was not collected (callers, configuration). It must cite span lines: a citation outside what it was shown voids the reply (`escalate`), no citation is recorded as missing evidence, and valid citations are evidence that never confirms on its own. Prompt version `v2.1-cited`, so cached verdicts are not reused. Accepted suppressions are bound to a content fingerprint and reopen when the code changes.
32
+ - **Workflows (FR 06).** GitHub Actions files are parsed as YAML: composite actions, `github-script`, inputs to unknown actions, `workflow_run` artifacts, agent actions with write authority on untrusted triggers, and OIDC trust policy evidence (`restrictive`, `permissive`, `mismatched`, `absent`) with this repository's name taken from its `origin` remote. Reference: `docs/reference/workflow-analysis.md`.
33
+ - **Linux isolated Nix evaluation, tested for the first time.** The hosted runner image moved to Ubuntu 26.04, which ships `bwrap`, so the NIX-011 suite exercised the Linux sandbox instead of reporting `unsupported`, and found three defects. Namespaces did not stop process creation: the probe spawned `/bin/echo`, so evaluation was correctly refused as `blocked`. A seccomp filter now denies fork, vfork and non-thread clone (threads still work) on x86_64 and arm64; another architecture gets no filter, and the probe refuses it. `--clearenv` dropped the clean environment, so the evaluator ran with no `HOME`, `PATH` or `LANG`; it is now set inside the sandbox. The memory ceiling summed the process group, which `--new-session` leaves, so a memory balloon was never counted; it now sums the process tree. On 26.04 the GHC 9.4.8 bindist still asks for the `gold` linker (in its settings and in the `hsc2hs` wrapper), which binutils no longer ships; CI now removes the flag from the GHC install when gold is absent.
34
+ - **Noise, evidence and views (FR 07 to FR 10).** Comment-only developer notes and missing-control findings are classified (`hardening`); dependency findings record their basis and merge advisory aliases with every occurrence kept; JSON output carries a scan record, finding record and disposition; `--exclude-kind` and view filters change what is shown, never the stored evidence.
35
+
12
36
  ## 0.195.27 - GA programme, release G: provenance adverse histories with a frozen contract and an absolute latency budget, promoted-rule regressions that run in CI
13
37
 
14
38
  **Measured, 0.195.26:** `TIMINGS preflight=3s push=7s pr=4s checks=393s merge=6s verify=5s tag=2s release=161s npm=636s total=1217s release-clock=174s`, shipped from the merge to the published release in 174 s; the npm listing lagged the publish and `ship --resume` confirmed it.
@@ -11,6 +11,9 @@ const PKG_VERSION = __require('../package.json').version;
11
11
  import { signLastScan as _signLastScan, verifyLastScan as _verifyLastScanShared } from '../src/posture/integrity.js';
12
12
  import { isProvenanceHealthy, sanitizeForTerminal } from '../src/posture/provenance/schema.js';
13
13
  import { runScan } from '../src/runScan.js';
14
+ import { parseScopeFlags, renderScopePreview } from '../src/scope/flags.js';
15
+ import { buildDispositionView, parseExcludeKinds } from '../src/report/disposition-view.js';
16
+ import { buildScanInventory } from '../src/scope/inventory.js';
14
17
  import { persistGraphSnapshot, loadSnapshots, loadSnapshot, mostRecentPriorSnapshot } from '../src/lineage/graph-snapshot.js';
15
18
 
16
19
  // Every command is dispatched as `process.exit(await cmdX(args))`, and
@@ -339,6 +342,9 @@ Options:
339
342
  --pr [ref] Diff-aware: scan only files changed since ref (auto-detects PR base)
340
343
  --deterministic Reproducible scan: stable sort, no-network, lockfile-checked
341
344
  --incremental Reuse taint summaries from prior scans (speeds up deep mode in CI)
345
+ --explain-scope Preview included/skipped paths and ignore reasons, then exit (also --include-path, --exclude-path, --include-ignored-path, --ignore-source all|repository, --no-respect-gitignore)
346
+ --exclude-kind <k[,k]> Hide sca, generated, secrets or logic findings from the report view only (still stored; not marked assessed)
347
+ --view-category/-language/-package/-path/-disposition <v>, --view-min-confidence <n> Filter the report view
342
348
  --set-baseline Save current findings as baseline (suppresses pre-existing issues)
343
349
  --since-baseline Only show findings NOT in the saved baseline
344
350
  --hide-proven-safe Drop findings discharged by a flow proof (provably safe)
@@ -551,6 +557,7 @@ async function writeMachineOutput(targetAbs, scan, meta, profile, args) {
551
557
  }
552
558
  }
553
559
 
560
+ const SCOPE_BOOLEAN_FLAGS = new Set(['explain-scope', 'respect-gitignore', 'no-respect-gitignore']);
554
561
  function parseArgs(argv) {
555
562
  const args = { _: [], flags: {} };
556
563
  for (let i = 0; i < argv.length; i++) {
@@ -559,6 +566,8 @@ function parseArgs(argv) {
559
566
  const [k, v] = a.slice(2).split('=', 2);
560
567
  if (v !== undefined) { args.flags[k] = v; continue; }
561
568
  const next = argv[i + 1];
569
+ // Scope switches never take a value, so `--explain-scope .` keeps `.` as the target.
570
+ if (SCOPE_BOOLEAN_FLAGS.has(k)) { args.flags[k] = true; continue; }
562
571
  if (next && !next.startsWith('--')) { args.flags[k] = next; i++; }
563
572
  else args.flags[k] = true;
564
573
  } else {
@@ -688,6 +697,14 @@ async function cmdScan(args) {
688
697
  console.error(`agentic-security scan: "${target}" does not exist (resolved to ${targetAbs}); nothing was scanned.`);
689
698
  process.exit(4);
690
699
  }
700
+ // FR 01: Git-aware scope flags. Any of them opts this run into the inventory-based scope;
701
+ // --explain-scope previews it and exits before any analysis or file read.
702
+ const scopeFlags = parseScopeFlags(process.argv.slice(2));
703
+ if (scopeFlags.errors.length) { for (const e of scopeFlags.errors) process.stderr.write(`agentic-security scan: ${e}\n`); return 2; }
704
+ if (scopeFlags.explain) {
705
+ process.stdout.write(renderScopePreview(buildScanInventory(targetAbs, scopeFlags.scope)));
706
+ return 0;
707
+ }
691
708
  // Load persona profile (R1). Persona-aware defaults flow from here.
692
709
  const profile = loadPersonaProfile(targetAbs, args);
693
710
  const format = args.flags.format || (profile.profile === 'pro' ? 'cli' : 'ship');
@@ -722,14 +739,19 @@ async function cmdScan(args) {
722
739
  // normal one-shot scan.
723
740
  if (args.flags['watch']) {
724
741
  process.env.AGENTIC_SECURITY_INCREMENTAL = '1';
725
- const { watchProject, computeDelta, persistStatus, renderStatusLine } = await import('../src/posture/watch-mode.js');
742
+ const { watchProject, computeDelta, persistStatus, renderStatusLine, createScopeFilter } = await import('../src/posture/watch-mode.js');
726
743
  process.stderr.write(`[watch] scanning ${targetAbs} on change — Ctrl-C to stop. Status → .agentic-security/watch-status.md\n`);
727
- const seed = await runScan(targetAbs, { onProgress: scanProgressReporter() });
744
+ // The scope flags apply to the first scan, every rescan and the watcher itself (PRD FR 01): the watcher asks the same
745
+ // inventory, so an ignored file never triggers a rescan and an eligible one always does.
746
+ const _watchScoped = scopeFlags.active || process.env.AGENTIC_SECURITY_GIT_SCOPE === '1';
747
+ const _watchScanOpts = scopeFlags.active ? { gitScope: true, scope: scopeFlags.scope } : {};
748
+ const seed = await runScan(targetAbs, { onProgress: scanProgressReporter(), ..._watchScanOpts });
728
749
  clearScanProgressLine();
729
750
  let prevFindings = seed.scan.findings || [];
751
+ process.stderr.write(`[watch] first scan done (${prevFindings.length} finding(s)); watching for changes${_watchScoped ? ' in the scan scope' : ''}\n`);
730
752
  await watchProject(targetAbs, async () => {
731
753
  try {
732
- const { scan } = await runScan(targetAbs, { onProgress: scanProgressReporter() });
754
+ const { scan } = await runScan(targetAbs, { onProgress: scanProgressReporter(), ..._watchScanOpts });
733
755
  clearScanProgressLine();
734
756
  const curr = scan.findings || [];
735
757
  const delta = computeDelta(prevFindings, curr);
@@ -739,8 +761,13 @@ async function cmdScan(args) {
739
761
  } catch (e) {
740
762
  process.stderr.write(`[watch] rescan failed: ${e.message}\n`);
741
763
  }
764
+ }, _watchScoped ? { isEligible: createScopeFilter(targetAbs, scopeFlags.active ? scopeFlags.scope : {}) } : {});
765
+ // watchProject does NOT block: it starts its loop and returns. The dispatch is `process.exit(await cmdScan(args))`, so returning here
766
+ // exited right after the first scan and `scan --watch` never watched anything. Stay pending until Ctrl-C or SIGTERM.
767
+ return await new Promise((resolve) => {
768
+ process.once('SIGINT', () => resolve(130));
769
+ process.once('SIGTERM', () => resolve(143));
742
770
  });
743
- return 0; // watchProject blocks until aborted
744
771
  }
745
772
 
746
773
  // --incremental : reuse taint summaries from prior scans for faster deep mode.
@@ -797,7 +824,10 @@ async function cmdScan(args) {
797
824
 
798
825
  const { scan, meta } = await runScan(target, {
799
826
  changedSince,
827
+ invocation: process.argv.slice(2),
800
828
  onProgress: scanProgressReporter(),
829
+ ...(scopeFlags.active ? { gitScope: true, scope: scopeFlags.scope } : {}),
830
+ ...(args.flags['haskell-source-map'] ? { haskellSourceMap: String(args.flags['haskell-source-map']) } : {}),
801
831
  });
802
832
  // --require-provenance: flag (never fail) any finding whose provenance
803
833
  // isn't resolved, via scanHealth — deliberately independent of the
@@ -962,6 +992,13 @@ async function cmdScan(args) {
962
992
  if (_line) process.stderr.write(`⚠️ agentic-security: ${_line}\n`);
963
993
  }
964
994
  } catch { /* reporting must never fail a scan */ }
995
+ // PRD FR 09: disposition view. Display only; stored buckets are not touched.
996
+ { const ek = parseExcludeKinds(args.flags['exclude-kind']);
997
+ if (ek.errors.length) { for (const e of ek.errors) process.stderr.write(`agentic-security scan: ${e}\n`); return 2; }
998
+ const flt = {};
999
+ for (const k of ['category', 'language', 'package', 'path', 'disposition']) if (args.flags[`view-${k}`]) flt[k] = String(args.flags[`view-${k}`]);
1000
+ if (args.flags['view-min-confidence'] !== undefined) flt.minConfidence = Number(args.flags['view-min-confidence']);
1001
+ scan.reportView = buildDispositionView(scan, { excludeKinds: ek.kinds, filters: flt }); }
965
1002
 
966
1003
  // Curated rule packs: --pack <name> (repeatable). Narrows findings to the
967
1004
  // CWEs covered by the requested pack(s).