@clear-capabilities/agentic-security-scanner 0.176.0 → 0.177.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 (33) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bin/agentic-security.js +9 -2
  3. package/dist/1919.index.js +1 -1
  4. package/dist/2066.index.js +2 -1
  5. package/dist/2584.index.js +19 -1
  6. package/dist/{4978bec8fdf5c733dc85.json → 37d64df006576248d0d8.json} +5 -2
  7. package/dist/5456.index.js +291 -0
  8. package/dist/5620.index.js +147 -23
  9. package/dist/5937.index.js +672 -0
  10. package/dist/7838.index.js +70 -25
  11. package/dist/agentic-security.mjs +2 -2
  12. package/dist/agentic-security.mjs.sha256 +1 -1
  13. package/dist/{df1e8683e7eda7b2b79c.js → b4289e728659450b5e19.js} +4 -1
  14. package/package.json +5 -2
  15. package/src/posture/custom-rules.js +54 -8
  16. package/src/posture/dnr/exploit-correlation.js +25 -0
  17. package/src/posture/evaluation/lab/dataset.js +1 -1
  18. package/src/posture/evaluation/lab/report.js +19 -1
  19. package/src/posture/fleet/cli.js +22 -10
  20. package/src/posture/fleet/enterprise-graph.js +44 -3
  21. package/src/posture/fleet/job-runner.js +7 -7
  22. package/src/posture/fleet/job-store.js +3 -1
  23. package/src/posture/fleet/lineage-summary.js +55 -0
  24. package/src/posture/fleet/scan-child.js +4 -1
  25. package/src/posture/proof-fabric/generate.js +2 -1
  26. package/src/posture/proof-fabric/runner.js +24 -2
  27. package/src/posture/rule-compiler/cli.js +40 -6
  28. package/src/posture/rule-compiler/corpus.js +3 -7
  29. package/src/posture/rule-compiler/ir.js +52 -9
  30. package/src/posture/rule-compiler/proposal.js +14 -9
  31. package/src/posture/rule-compiler/regression.js +95 -0
  32. package/src/posture/rule-compiler/store.js +29 -1
  33. package/src/posture/rule-pack-signing.js +4 -2
@@ -5,7 +5,7 @@ export const modules = {
5
5
  /***/ 53948:
6
6
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
7
7
 
8
- module.exports = __webpack_require__.p + "df1e8683e7eda7b2b79c.js";
8
+ module.exports = __webpack_require__.p + "b4289e728659450b5e19.js";
9
9
 
10
10
  /***/ }),
11
11
 
@@ -609,7 +609,7 @@ const DB_FILE = node_path__WEBPACK_IMPORTED_MODULE_1__.join('fleet', 'fleet.db')
609
609
  const ROLLUP_FILE = 'fleet-rollup.json';
610
610
  const sha = (s) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_2__.createHash)('sha256').update(s).digest('hex');
611
611
 
612
- const usage = () => { console.error('usage: fleet init | edges [--undeclared] | scan <manifest.yaml> [--workers N] [--retries N] [--timeout-seconds N | --timeout-ms N] [--no-cache] | status [run-id] | resume <run-id|manifest.yaml> [--retry-failed] | query (--depends-on <id> | --blast-radius <component> [--version v] | --internet-to <data-class> [--involving-vulnerable auth|<repo>] | "<question>") | explore [--out file.html]'); return 4; };
612
+ const usage = () => { console.error('usage: fleet init | edges [--undeclared] | scan <manifest.yaml> [--workers N] [--retries N] [--timeout-seconds N | --timeout-ms N] [--no-cache] [--no-discover] [--lineage] | status [run-id] | resume <run-id|manifest.yaml> [--retry-failed] | query (--depends-on <id> | --blast-radius <component> [--version v] | --internet-to <data-class> [--involving-vulnerable auth|<repo>] | "<question>") | explore [--out file.html]'); return 4; };
613
613
  const isManifestArg = (s) => /\.ya?ml$/i.test(String(s ?? ''));
614
614
  const manifestArg = (args) => { const m = args._[2]; return m && !String(m).startsWith('--') ? m : 'fleet.yaml'; };
615
615
  const dbPath = (root) => (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, DB_FILE);
@@ -635,7 +635,7 @@ async function cmdEdges(args, root) {
635
635
  if (args.flags.json) { console.log(JSON.stringify({ discovery: g.discovery ?? null, edges }, null, 2)); return 0; }
636
636
  if (!edges.length) { console.log(args.flags.undeclared ? 'No edge was found in code that the manifest does not declare.' : 'The graph has no edges.'); }
637
637
  for (const e of edges) {
638
- const where = e.evidence.filter((x) => x.source === 'discovered-code-literal').map((x) => `${x.file}:${x.line}`);
638
+ const where = e.evidence.filter((x) => x.source === 'discovered-code-literal' || x.source === 'code-boundaries').map((x) => (x.file ? `${x.file}:${x.line}` : `code boundaries: ${x.host ?? x.topic ?? x.name}`));
639
639
  console.log(`${e.from} -[${e.type}]-> ${e.to} ${e.discovered ? 'FOUND ONLY IN CODE' : where.length ? 'declared, and seen in code' : 'declared'}${where.length ? ` (${where.join(', ')})` : ''}`);
640
640
  }
641
641
  if (g.discovery?.limitations) console.log(`\nLimits: ${g.discovery.limitations.join(' ')}`);
@@ -665,6 +665,12 @@ function entriesOf(artifacts, manifest) {
665
665
  return manifest.repositories.filter((r) => artifacts.has(r.id)).map((r) => ({ ...artifacts.get(r.id).entry, repo: r.id }));
666
666
  }
667
667
 
668
+ /** The PRD names its commands `fleet status JOB-ID` and `fleet resume JOB-ID`; a run is a set of jobs, so a job id names the run it belongs to. */
669
+ function runOfJob(store, id) {
670
+ const job = store.getJob(String(id));
671
+ return job ? store.getRun(job.runId) : null;
672
+ }
673
+
668
674
  async function finishRun({ args, root, store, runId, manifest, manifestFile, interrupted }) {
669
675
  const { artifacts, problems } = (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .loadArtifacts */ .kZ)({ store, runId, root });
670
676
  const jobs = store.listJobs(runId);
@@ -684,14 +690,16 @@ async function finishRun({ args, root, store, runId, manifest, manifestFile, int
684
690
  let discovered = null; let discoveryNote = args.flags['no-discover'] ? 'skipped (--no-discover)' : null;
685
691
  if (!args.flags['no-discover']) { try { discovered = (0,_discover_edges_js__WEBPACK_IMPORTED_MODULE_10__/* .discoverEdges */ .U)({ manifest }); } catch (e) { discoveryNote = `failed (${String(e.message).slice(0, 80)}); the graph holds declared edges only`; } }
686
692
  const split = discovered ? (0,_discover_edges_js__WEBPACK_IMPORTED_MODULE_10__/* .splitDeclared */ .Z)(discovered, manifest) : null;
687
- const graph = (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .finalizeGraph */ .Th)((0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .buildEnterpriseGraph */ .Ws)({ manifest, dependencyGraph: dep, discovered }));
693
+ const lineageByRepo = new Map([...artifacts].filter(([, a]) => a.lineage).map(([id, a]) => [id, a.lineage]));
694
+ const graph = (0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .finalizeGraph */ .Th)((0,_enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .buildEnterpriseGraph */ .Ws)({ manifest, dependencyGraph: dep, discovered, lineage: lineageByRepo.size ? lineageByRepo : null }));
688
695
  const findingsByRepo = Object.fromEntries([...artifacts].map(([id, a]) => [id, a.findings ?? []]));
689
- (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.safeWriteState)((0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ENTERPRISE_FILE), JSON.stringify({ ...graph, runId, manifestFile, findingsByRepo, discovery: discovered ? { scanned: discovered.scanned, limitations: discovered.limitations, undeclared: split.undeclared.length, confirmed: split.confirmed.length } : { note: discoveryNote }, builtAt: new Date().toISOString() }));
696
+ (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.safeWriteState)((0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ENTERPRISE_FILE), JSON.stringify({ ...graph, runId, manifestFile, findingsByRepo, codeBoundaries: lineageByRepo.size ? { repositories: lineageByRepo.size, undeclaredDataClasses: Object.fromEntries(graph.nodes.filter((n) => n.discoveredDataClasses?.length).map((n) => [n.id, n.discoveredDataClasses])) } : { note: args.flags.lineage ? 'no repository returned a Code Boundaries graph' : 'not requested (--lineage)' }, discovery: discovered ? { scanned: discovered.scanned, limitations: discovered.limitations, undeclared: split.undeclared.length, confirmed: split.confirmed.length } : { note: discoveryNote }, builtAt: new Date().toISOString() }));
690
697
 
691
698
  if (args.flags.json) console.log(JSON.stringify({ runId, health, rollup }, null, 2));
692
699
  else {
693
700
  console.log((0,_fleet_js__WEBPACK_IMPORTED_MODULE_5__/* .renderFleetSummary */ .LL)(rollup));
694
701
  console.log(`\nRun: ${runId}\n${(0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .renderOrgHealth */ .Vp)(health)}`);
702
+ if (lineageByRepo.size) { const und = graph.nodes.filter((n) => n.discoveredDataClasses?.length); console.log(`\nCode Boundaries: ${lineageByRepo.size} repositor${lineageByRepo.size === 1 ? 'y' : 'ies'} contributed a data flow graph.${und.length ? ` Data classes found in code but not declared: ${und.map((n) => `${n.id} (${n.discoveredDataClasses.join(', ')})`).join('; ')}.` : ' No undeclared data class was found.'}`); }
695
703
  if (split) console.log(`\nEdges found in code: ${split.undeclared.length} not in the manifest, ${split.confirmed.length} matching a declared edge. ${split.undeclared.length ? 'List them with: agentic-security fleet edges --undeclared' : ''}`.trimEnd());
696
704
  else if (discoveryNote) console.log(`\nEdge discovery ${discoveryNote}.`);
697
705
  if (interrupted) console.log(`\nInterrupted. Continue with: agentic-security fleet resume ${runId}`);
@@ -703,7 +711,9 @@ async function finishRun({ args, root, store, runId, manifest, manifestFile, int
703
711
  async function runAll({ args, root, store, runId, manifest, manifestFile }) {
704
712
  const ac = new AbortController(); const onSig = () => ac.abort(); process.once('SIGINT', onSig); process.once('SIGTERM', onSig);
705
713
  try {
706
- await (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .runJobs */ .Ag)({ manifest, store, runId, root, scan: _job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .childScan */ .IT, signal: ac.signal, useCache: !args.flags['no-cache'], onEvent: (e) => {
714
+ // Code Boundaries are requested at scan time and remembered on the run, so a resume does the same thing the scan would have
715
+ const lineage = !!args.flags.lineage || !!store.getRun(runId)?.meta?.lineage;
716
+ await (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .runJobs */ .Ag)({ manifest, store, runId, root, scan: _job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .childScan */ .IT, scanOptions: { lineage }, signal: ac.signal, useCache: !args.flags['no-cache'], onEvent: (e) => {
707
717
  if (args.flags.json) return;
708
718
  if (e.type === 'done') console.error(`[fleet] done: ${e.repo}${e.cached ? ' (cached)' : ''}${e.status === 'partial' ? ' (partial)' : ''}`);
709
719
  else if (e.type === 'failed') console.error(`[fleet] ${e.timedOut ? 'TIMED OUT' : 'FAILED'}: ${e.repo} (${e.error})${e.next === 'queued' ? ', will retry' : ''}`);
@@ -725,7 +735,7 @@ async function cmdScan(args, root) {
725
735
  try {
726
736
  const manifestHash = sha(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(manifestFile));
727
737
  console.error(`[fleet] planning ${manifest.repositories.length} repository(ies) for ${manifest.organization} (${manifest.workers} worker(s), ${manifest.retries} retr${manifest.retries === 1 ? 'y' : 'ies'}, ${Math.round(manifest.perRepoTimeoutMs / 1000)}s per repository)...`);
728
- const plan = (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .planRun */ .nY)({ manifest, manifestHash, store, meta: { manifestFile } });
738
+ const plan = (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .planRun */ .nY)({ manifest, manifestHash, store, config: args.flags.lineage ? { lineage: true } : {}, meta: { manifestFile, lineage: !!args.flags.lineage } });
729
739
  return await runAll({ args, root, store, runId: plan.runId, manifest, manifestFile });
730
740
  } finally { store.close(); }
731
741
  }
@@ -742,7 +752,7 @@ async function cmdResume(args, root) {
742
752
  const hash = sha(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, target)));
743
753
  run = store.listRuns().find((r) => r.manifestHash === hash && store.listJobs(r.runId).some((j) => ['queued', 'leased'].includes(j.status)));
744
754
  if (!run) { console.error('Nothing to resume for that manifest: no run of it has unfinished work. Use `fleet resume <run-id> --retry-failed` to retry failed repositories.'); return 4; }
745
- } else run = store.getRun(target);
755
+ } else run = store.getRun(target) ?? runOfJob(store, target);
746
756
  if (!run) { console.error(`No run ${target}. Runs: ${store.listRuns().slice(0, 5).map((r) => r.runId).join(', ') || 'none'}`); return 4; }
747
757
  const full = store.getRun(run.runId);
748
758
  const manifestFile = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, args.flags.manifest ?? full.meta?.manifestFile ?? (isManifestArg(target) ? target : 'fleet.yaml'));
@@ -774,14 +784,15 @@ async function cmdStatus(args, root) {
774
784
  const store = await (0,_job_store_js__WEBPACK_IMPORTED_MODULE_7__/* .createSqliteJobStore */ .J6)({ file: dbFile });
775
785
  try {
776
786
  const arg = args._[2];
777
- const run = arg && !isManifestArg(arg) ? store.getRun(String(arg)) : store.listRuns()[0] && store.getRun(store.listRuns()[0].runId);
778
- if (!run) { console.error(arg && !isManifestArg(arg) ? `No run ${arg}.` : 'No fleet run exists yet.'); return 4; }
787
+ const run = arg && !isManifestArg(arg) ? (store.getRun(String(arg)) ?? runOfJob(store, String(arg))) : store.listRuns()[0] && store.getRun(store.listRuns()[0].runId);
788
+ if (!run) { console.error(arg && !isManifestArg(arg) ? `No run or job ${arg}.` : 'No fleet run exists yet.'); return 4; }
789
+ if (arg && !isManifestArg(arg) && String(arg) !== run.runId) console.error(`Job ${arg} belongs to run ${run.runId}.`);
779
790
  const meta = run.meta ?? {};
780
791
  const manifest = (0,_fleet_manifest_js__WEBPACK_IMPORTED_MODULE_3__/* .loadFleetManifest */ .Cb)(node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(root, meta.manifestFile ?? 'fleet.yaml'));
781
792
  const jobs = store.listJobs(run.runId);
782
793
  const m = manifest.ok ? manifest.manifest : { repositories: jobs.map((j) => ({ id: j.repoId, required: j.required })) };
783
794
  const health = (0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .computeOrgHealth */ .WB)({ manifest: m, jobs });
784
- if (args.flags.json) { console.log(JSON.stringify({ runId: run.runId, health, jobs: jobs.map((j) => ({ repo: j.repoId, status: j.status, attempts: j.attempts, worker: j.worker, leaseExpiresAt: j.leaseExpiresAt, error: j.error })) }, null, 2)); return 0; }
795
+ if (args.flags.json) { console.log(JSON.stringify({ runId: run.runId, health, jobs: jobs.map((j) => ({ jobId: j.jobId, repo: j.repoId, status: j.status, attempts: j.attempts, worker: j.worker, leaseExpiresAt: j.leaseExpiresAt, error: j.error })) }, null, 2)); return 0; }
785
796
  console.log(`Run: ${run.runId} (${run.organization})\n${(0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .renderOrgHealth */ .Vp)(health)}`);
786
797
  const live = jobs.filter((j) => j.status === 'leased');
787
798
  if (live.length) console.log(`\nIn progress: ${live.map((j) => `${j.repoId} (attempt ${j.attempts}, lease until ${new Date(j.leaseExpiresAt).toISOString()})`).join(', ')}`);
@@ -850,6 +861,7 @@ async function cmdQuery(args, root) {
850
861
  for (const p of res.paths) {
851
862
  console.log(p.nodes.map((n) => (n === _enterprise_graph_js__WEBPACK_IMPORTED_MODULE_11__/* .INTERNET */ .aw ? 'Internet' : n)).join(' -> '));
852
863
  if (p.discoveredEdges?.length) console.log(` rests on edge(s) found only in code, not declared: ${p.discoveredEdges.join(', ')}`);
864
+ if (p.discoveredDataClassNodes?.length) console.log(` reaches ${toClass} data only because the code shows it, not the manifest: ${p.discoveredDataClassNodes.join(', ')}`);
853
865
  if (p.involving.length) console.log(` involves vulnerable code: ${p.involving.map((n) => `${n} (${involving.why.get(n)})`).join(', ')}`);
854
866
  }
855
867
  if (res.paths.length) console.log(`\n${res.paths.length} path(s). Reach edges are as declared in the fleet manifest, plus any marked above as found only in code.${res.truncated ? ' The search bound was reached; more may exist.' : ''}`);
@@ -1064,7 +1076,7 @@ const AUTH_CWES = new Set(['CWE-287', 'CWE-306', 'CWE-285', 'CWE-862', 'CWE-863'
1064
1076
  const edgeId = (type, from, to) => `edge:${type}:${from}:${to}`;
1065
1077
  const hash = (v) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash)('sha256').update(JSON.stringify(v)).digest('hex');
1066
1078
 
1067
- function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null }) {
1079
+ function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null, lineage = null }) {
1068
1080
  const nodes = new Map();
1069
1081
  const edges = new Map();
1070
1082
  const addNode = (n) => nodes.set(n.id, n);
@@ -1092,7 +1104,48 @@ function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null })
1092
1104
  // Edges found in the code. They share an id with a declared edge of the same type and endpoints, so the evidence list shows both; an
1093
1105
  // edge with ONLY discovered evidence is marked `discovered: true` so a query can say it rests on a code literal, not on a declaration.
1094
1106
  for (const d of discovered?.edges ?? []) for (const evidence of d.evidence) addEdge(d.type, d.from, d.to, evidence, true);
1095
- for (const e of edges.values()) e.discovered = e.evidence.every((x) => x.source === 'discovered-code-literal');
1107
+ // Code Boundaries (each repository's own data flow graph, summarized): data classes the code handles, and destinations it sends to.
1108
+ // These add to what the manifest declared and never replace it; a class the manifest did not declare is listed on the node so a path
1109
+ // that exists only because of it can say so.
1110
+ const aliasesOf = new Map([...manifest.repositories, ...(manifest.resources ?? [])].map((n) => [n.id, [n.id, ...(n.aliases ?? [])].map((a) => String(a).toLowerCase())]));
1111
+ for (const [repoId, sum] of lineage ?? []) {
1112
+ const node = nodes.get(repoId);
1113
+ if (!node || !sum) continue;
1114
+ const declared = new Set(node.dataClasses);
1115
+ const found = (sum.dataClasses ?? []).filter((c) => !declared.has(c));
1116
+ node.dataClasses = [...declared, ...found];
1117
+ node.discoveredDataClasses = found;
1118
+ const cb = (extra) => ({ source: 'code-boundaries', repo: repoId, declared: false, file: null, ...extra });
1119
+ for (const d of sum.externalDestinations ?? []) {
1120
+ if (!d.host) continue;
1121
+ const first = d.host.split('.')[0];
1122
+ for (const [target, names] of aliasesOf) {
1123
+ if (target === repoId || !(names.includes(d.host) || names.includes(first))) continue;
1124
+ const tn = nodes.get(target);
1125
+ addEdge(tn?.type === 'external-provider' ? 'uses-provider' : 'calls', repoId, target, cb({ host: d.host, nodeKind: 'external' }), true);
1126
+ }
1127
+ }
1128
+ for (const q of sum.queues ?? []) {
1129
+ for (const [target, names] of aliasesOf) {
1130
+ const tn = nodes.get(target);
1131
+ if (tn?.type !== 'queue' || !names.includes(String(q.topic).toLowerCase())) continue;
1132
+ if (q.operation === 'consume') addEdge('consumes', target, repoId, cb({ topic: q.topic }), true); else addEdge('produces', repoId, target, cb({ topic: q.topic }), true);
1133
+ }
1134
+ }
1135
+ for (const st of sum.stores ?? []) {
1136
+ for (const key of [st.host, st.database]) {
1137
+ if (!key) continue;
1138
+ const k = String(key).toLowerCase();
1139
+ for (const [target, names] of aliasesOf) {
1140
+ const tn = nodes.get(target);
1141
+ if (tn?.type !== 'database' || !(names.includes(k) || names.includes(k.split('.')[0]))) continue;
1142
+ addEdge('connects-to', repoId, target, cb({ store: st.subtype, name: key }), true);
1143
+ }
1144
+ }
1145
+ }
1146
+ }
1147
+ const FOUND_IN_CODE = new Set(['discovered-code-literal', 'code-boundaries']);
1148
+ for (const e of edges.values()) e.discovered = e.evidence.every((x) => FOUND_IN_CODE.has(x.source));
1096
1149
  return { schema: ENTERPRISE_GRAPH_SCHEMA, version: 1, organization: manifest.organization, nodes: [...nodes.values()], edges: [...edges.values()], digest: null };
1097
1150
  }
1098
1151
 
@@ -1139,7 +1192,7 @@ function attackPaths({ graph, toDataClass, from = INTERNET, involving = null, ma
1139
1192
  if (results.length >= maxPaths) { truncated = true; return; }
1140
1193
  if (targets.has(id) && nodePath.length > 1) {
1141
1194
  const inv = involving ? nodePath.filter((n) => involving.all.has(n)) : [];
1142
- if (!involving || inv.length) results.push({ nodes: [...nodePath], edges: edgePath.map((e) => e.id), involving: inv, discoveredEdges: edgePath.filter((e) => e.discovered).map((e) => e.id), evidence: edgePath.flatMap((e) => e.evidence.map((x) => ({ edge: e.id, ...x }))) });
1195
+ if (!involving || inv.length) results.push({ nodes: [...nodePath], edges: edgePath.map((e) => e.id), involving: inv, discoveredEdges: edgePath.filter((e) => e.discovered).map((e) => e.id), discoveredDataClassNodes: nodePath.filter((n) => (nodeOf(graph, n)?.discoveredDataClasses ?? []).includes(toDataClass)), evidence: edgePath.flatMap((e) => e.evidence.map((x) => ({ edge: e.id, ...x }))) });
1143
1196
  }
1144
1197
  if (nodePath.length > maxDepth) { truncated = true; return; }
1145
1198
  for (const e of out.get(id) ?? []) if (!nodePath.includes(e.to)) walk(e.to, [...nodePath, e.to], [...edgePath, e]);
@@ -1540,8 +1593,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1540
1593
  const DEFAULTS = Object.freeze({ leaseMs: 30_000, heartbeatMs: 10_000, retryDelayMs: 2_000, pollMs: 100 });
1541
1594
 
1542
1595
  /** The default scan: one supervised child process per repository. Throws `{code: 'TIMEOUT'|'ERROR', message}`. */
1543
- async function childScan(repoPath, { timeoutMs, signal }) {
1544
- const r = await (0,_sandbox_supervise_js__WEBPACK_IMPORTED_MODULE_5__/* .superviseSpawn */ .u)(process.execPath, [SCAN_CHILD, repoPath], { cwd: repoPath, env: { ...process.env, AGENTIC_SECURITY_NO_STATE: '1' }, timeoutMs, signal, maxOutputBytes: 96 * 1024 * 1024 });
1596
+ async function childScan(repoPath, { timeoutMs, signal, lineage = false }) {
1597
+ const r = await (0,_sandbox_supervise_js__WEBPACK_IMPORTED_MODULE_5__/* .superviseSpawn */ .u)(process.execPath, [SCAN_CHILD, repoPath], { cwd: repoPath, env: { ...process.env, AGENTIC_SECURITY_NO_STATE: '1', ...(lineage ? { AGENTIC_SECURITY_LINEAGE_DEEP: '1' } : {}) }, timeoutMs, signal, maxOutputBytes: 96 * 1024 * 1024 });
1545
1598
  const fail = (code, message) => Object.assign(new Error(message), { code });
1546
1599
  if (r.timedOut) throw fail('TIMEOUT', `no result within ${Math.round(timeoutMs / 1000)}s`);
1547
1600
  if (r.cancelled) throw fail('CANCELLED', 'cancelled');
@@ -1584,7 +1637,7 @@ function planRun({ manifest, manifestHash, store, runId = null, config = {}, now
1584
1637
  * Run (or continue) a planned run until no job is queued or leased by a live worker. Returns when the work is done, the signal
1585
1638
  * aborts, or `maxWaitMs` passes with only other workers' leases outstanding.
1586
1639
  */
1587
- async function runJobs({ manifest, store, runId, root, scan = childScan, workers = manifest.workers, leaseMs = DEFAULTS.leaseMs, heartbeatMs = DEFAULTS.heartbeatMs, retryDelayMs = DEFAULTS.retryDelayMs, pollMs = DEFAULTS.pollMs, maxWaitMs = null, signal, now = () => Date.now(), onEvent = () => {}, cacheDir = null, artifactDir = null, useCache = true }) {
1640
+ async function runJobs({ manifest, store, runId, root, scan = childScan, scanOptions = {}, workers = manifest.workers, leaseMs = DEFAULTS.leaseMs, heartbeatMs = DEFAULTS.heartbeatMs, retryDelayMs = DEFAULTS.retryDelayMs, pollMs = DEFAULTS.pollMs, maxWaitMs = null, signal, now = () => Date.now(), onEvent = () => {}, cacheDir = null, artifactDir = null, useCache = true }) {
1588
1641
  const repoById = new Map(manifest.repositories.map((r) => [r.id, r]));
1589
1642
  const cacheD = cacheDir ?? (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, 'fleet', 'cache');
1590
1643
  const artD = artifactDir ?? (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, 'fleet', 'artifacts', runId);
@@ -1617,7 +1670,7 @@ async function runJobs({ manifest, store, runId, root, scan = childScan, workers
1617
1670
  onEvent({ type: 'start', repo: job.repoId, attempt: job.attempts });
1618
1671
  const cached = useCache ? (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .readCache */ .xe)(cacheD, job.cacheKey) : null;
1619
1672
  if (cached) {
1620
- const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, cached: true });
1673
+ const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, lineage: cached.lineage ?? null, cached: true });
1621
1674
  const status = cached.entry?.scanHealth && cached.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
1622
1675
  if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: cached.entry?.total ?? null, cached: true, artifact: node_path__WEBPACK_IMPORTED_MODULE_2__.basename(a.file) }, now: now() })) onEvent({ type: 'done', repo: job.repoId, cached: true, status });
1623
1676
  continue;
@@ -1628,15 +1681,15 @@ async function runJobs({ manifest, store, runId, root, scan = childScan, workers
1628
1681
  let lost = false;
1629
1682
  const hb = setInterval(() => { if (!store.heartbeat(job.jobId, job.epoch, { now: now(), leaseMs })) { lost = true; ac.abort(); } }, heartbeatMs);
1630
1683
  try {
1631
- const out = await scan(repo.path, { timeoutMs: job.timeoutMs, signal: ac.signal });
1684
+ const out = await scan(repo.path, { timeoutMs: job.timeoutMs, signal: ac.signal, ...scanOptions });
1632
1685
  clearInterval(hb);
1633
1686
  if (lost) { onEvent({ type: 'fenced', repo: job.repoId }); continue; }
1634
1687
  if (!out?.entry) throw Object.assign(new Error('the scan returned no entry'), { code: 'ERROR' });
1635
1688
  if (out.entry.ok === false) throw Object.assign(new Error(out.entry.error || 'the scan failed'), { code: 'ERROR' });
1636
- const a = writeArtifact(job, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
1689
+ const a = writeArtifact(job, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated, lineage: out.lineage ?? null });
1637
1690
  const status = out.entry.scanHealth && out.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
1638
1691
  if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: out.entry.total ?? null, cached: false, artifact: node_path__WEBPACK_IMPORTED_MODULE_2__.basename(a.file) }, now: now() })) {
1639
- if (useCache && status === 'succeeded') (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .writeCache */ .aR)(cacheD, job.cacheKey, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
1692
+ if (useCache && status === 'succeeded') (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .writeCache */ .aR)(cacheD, job.cacheKey, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated, lineage: out.lineage ?? null });
1640
1693
  onEvent({ type: 'done', repo: job.repoId, cached: false, status });
1641
1694
  } else onEvent({ type: 'fenced', repo: job.repoId });
1642
1695
  } catch (e) {
@@ -1773,7 +1826,9 @@ async function createSqliteJobStore({ file }) {
1773
1826
  const { DatabaseSync } = await Promise.resolve(/* import() */).then(__webpack_require__.t.bind(__webpack_require__, 80099, 19));
1774
1827
  if (file !== ':memory:') node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_1__.dirname(file), { recursive: true, mode: 0o700 });
1775
1828
  const db = new DatabaseSync(file);
1776
- db.exec('PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;');
1829
+ // the busy timeout comes FIRST: the very next statements (switching the journal mode, creating tables) take locks, and another process
1830
+ // opening the same file at the same instant must wait for them instead of failing with "database is locked"
1831
+ db.exec('PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');
1777
1832
  db.exec(SCHEMA);
1778
1833
  const tx = (fn) => { db.exec('BEGIN IMMEDIATE'); try { const r = fn(); db.exec('COMMIT'); return r; } catch (e) { try { db.exec('ROLLBACK'); } catch { /* already rolled back */ } throw e; } };
1779
1834
  const q = (sql, ...p) => db.prepare(sql).all(...p);
@@ -1820,6 +1875,71 @@ async function createSqliteJobStore({ file }) {
1820
1875
  }
1821
1876
 
1822
1877
 
1878
+ /***/ }),
1879
+
1880
+ /***/ 81010:
1881
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1882
+
1883
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1884
+ /* harmony export */ h: () => (/* binding */ summarizeLineage)
1885
+ /* harmony export */ });
1886
+ // A bounded summary of one repository's Code Boundaries graph (the Data Flow Explorer's `DataFlowGraph v1`), for the enterprise graph
1887
+ // (PRD 4: "federate Code Boundaries across repositories"). The whole graph is not copied: a fleet of hundreds of repositories does not
1888
+ // need each node, only what crosses a repository boundary or changes what the organization knows about it:
1889
+ // * the data classes that appear in the code (so a repository holding payment data is known to, even if the manifest forgot to say);
1890
+ // * the external destinations the code sends data to, by host;
1891
+ // * the queue topics it publishes to or consumes from;
1892
+ // * the stores it touches, by host/database/table name;
1893
+ // * counts, including flows that carry sensitive classes across a trust boundary.
1894
+ // Pure; hostile input (the graph came out of a scan of someone's code), so every string is bounded and shape-checked.
1895
+ const MAX_ITEMS = 64;
1896
+ const SENSITIVE = new Set(['pii', 'phi', 'pci', 'fin', 'credentials']);
1897
+ const arr = (v) => (Array.isArray(v) ? v : []);
1898
+ const clean = (v, max = 120) => (typeof v === 'string' && v.length > 0 && v.length <= max && !/[\s\u0000-\u001f]/.test(v) ? v : null);
1899
+
1900
+ function hostOf(literal) {
1901
+ if (typeof literal !== 'string') return null;
1902
+ try { const h = new URL(literal).hostname.toLowerCase(); return clean(h, 253); } catch { /* not a URL */ }
1903
+ const bare = literal.toLowerCase().replace(/:\d+$/, '');
1904
+ return /^[a-z0-9][a-z0-9.-]{0,252}$/.test(bare) ? bare : null;
1905
+ }
1906
+
1907
+ function summarizeLineage(graph) {
1908
+ if (!graph || typeof graph !== 'object') return null;
1909
+ const classes = new Set();
1910
+ for (const d of arr(graph.dataElements)) for (const c of arr(d?.dataClasses)) { if (typeof c !== 'string') continue; const l = clean(c.toLowerCase(), 31); if (l && /^[a-z][a-z0-9-]*$/.test(l)) classes.add(l); }
1911
+ const ext = new Map(); const queues = new Map(); const stores = new Map();
1912
+ for (const n of arr(graph.nodes)) {
1913
+ if (!n || typeof n !== 'object') continue;
1914
+ if (n.kind === 'external') {
1915
+ const host = hostOf(n.destination?.literalValue);
1916
+ const key = `${n.subtype ?? 'external'}|${host ?? ''}`;
1917
+ if (!ext.has(key) && ext.size < MAX_ITEMS) ext.set(key, { subtype: clean(n.subtype, 60) ?? 'external', host });
1918
+ } else if (n.kind === 'queue') {
1919
+ const topic = clean(n.queueDetail?.topic, 120); const op = n.queueDetail?.operation === 'consume' || n.queueDetail?.operation === 'receive' ? 'consume' : 'publish';
1920
+ if (topic && queues.size < MAX_ITEMS) queues.set(`${topic}|${op}`, { topic, operation: op });
1921
+ } else if (n.kind === 'store') {
1922
+ const d = n.storeDetail ?? {};
1923
+ const item = { subtype: clean(n.subtype, 60) ?? 'store', host: hostOf(d.host), database: clean(d.database, 120), table: clean(d.table, 120) };
1924
+ const key = JSON.stringify(item);
1925
+ if (stores.size < MAX_ITEMS) stores.set(key, item);
1926
+ }
1927
+ }
1928
+ const edges = arr(graph.edges);
1929
+ const crossing = new Set(edges.filter((e) => arr(e?.boundaryCrossings).length).map((e) => e.id));
1930
+ const dataById = new Map(arr(graph.dataElements).map((d) => [d?.id, d]));
1931
+ const sensitiveCrossing = arr(graph.flows).filter((f) => arr(f?.edgeIds).some((id) => crossing.has(id)) && arr(f?.dataElementIds).some((id) => arr(dataById.get(id)?.dataClasses).some((c) => SENSITIVE.has(String(c).toLowerCase())))).length;
1932
+ return {
1933
+ schema: 'agentic-security/fleet-lineage-summary', version: 1,
1934
+ dataClasses: [...classes].sort().slice(0, MAX_ITEMS),
1935
+ externalDestinations: [...ext.values()],
1936
+ queues: [...queues.values()],
1937
+ stores: [...stores.values()],
1938
+ counts: { nodes: arr(graph.nodes).length, edges: edges.length, flows: arr(graph.flows).length, boundaryCrossingEdges: crossing.size, sensitiveFlowsCrossingBoundary: sensitiveCrossing },
1939
+ };
1940
+ }
1941
+
1942
+
1823
1943
  /***/ }),
1824
1944
 
1825
1945
  /***/ 75435:
@@ -2033,6 +2153,7 @@ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependen
2033
2153
  /* harmony export */ });
2034
2154
  /* harmony import */ var _fleet_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(80313);
2035
2155
  /* harmony import */ var _runScan_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(45950);
2156
+ /* harmony import */ var _lineage_summary_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(81010);
2036
2157
  var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_runScan_js__WEBPACK_IMPORTED_MODULE_1__]);
2037
2158
  _runScan_js__WEBPACK_IMPORTED_MODULE_1__ = (__webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__)[0];
2038
2159
  // Child-process entry for ONE repository's scan inside a fleet run (PRD 4, requirement 4: one hung repository must not stop the
@@ -2046,6 +2167,7 @@ _runScan_js__WEBPACK_IMPORTED_MODULE_1__ = (__webpack_async_dependencies__.then
2046
2167
 
2047
2168
 
2048
2169
 
2170
+
2049
2171
  const BEGIN = '@@FLEET-SCAN-BEGIN@@';
2050
2172
  const END = '@@FLEET-SCAN-END@@';
2051
2173
  const MAX_FINDINGS = 5000;
@@ -2058,7 +2180,9 @@ async function main(repoPath) {
2058
2180
  const entry = res.results?.[0];
2059
2181
  if (!entry) throw new Error('the scan produced no entry');
2060
2182
  const findings = (captured?.findings ?? []).slice(0, MAX_FINDINGS).map(slim);
2061
- process.stdout.write(`${BEGIN}${JSON.stringify({ entry, findings, findingsTruncated: (captured?.findings?.length ?? 0) > MAX_FINDINGS })}${END}`);
2183
+ // the Code Boundaries graph exists only when the controller asked for it (AGENTIC_SECURITY_LINEAGE_DEEP=1); it is summarized, never copied whole
2184
+ const lineage = process.env.AGENTIC_SECURITY_LINEAGE_DEEP === '1' ? (0,_lineage_summary_js__WEBPACK_IMPORTED_MODULE_2__/* .summarizeLineage */ .h)(captured?.lineageGraph) : null;
2185
+ process.stdout.write(`${BEGIN}${JSON.stringify({ entry, findings, findingsTruncated: (captured?.findings?.length ?? 0) > MAX_FINDINGS, lineage })}${END}`);
2062
2186
  }
2063
2187
 
2064
2188
  if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {