sentinelayer-cli 0.8.10 → 0.8.12

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/src/legacy-cli.js CHANGED
@@ -1237,7 +1237,10 @@ async function runLocalOmarGateCommand(args) {
1237
1237
  deterministic: {
1238
1238
  summary: detSummary,
1239
1239
  findings: detFindings,
1240
+ scope: deterministic.scope || {},
1241
+ layers: deterministic.layers || {},
1240
1242
  metadata: deterministic.metadata || {},
1243
+ artifacts: deterministic.artifacts || {},
1241
1244
  },
1242
1245
  onEvent: streamHandler,
1243
1246
  includeOnly: includeOnly.length > 0 ? includeOnly : null,
@@ -1331,6 +1334,7 @@ async function runLocalOmarGateCommand(args) {
1331
1334
  const combinedP1 = combinedSummary.P1 || 0;
1332
1335
  const combinedP2 = combinedSummary.P2 || 0;
1333
1336
  const combinedP3 = combinedSummary.P3 || 0;
1337
+ const omargateRunId = orchestratorResult?.runId || deterministic.runId;
1334
1338
 
1335
1339
  // Write per-phase artifacts alongside REVIEW_DETERMINISTIC so post-mortems
1336
1340
  // can inspect exactly what each layer contributed.
@@ -1376,6 +1380,7 @@ async function runLocalOmarGateCommand(args) {
1376
1380
  const report = `# Local Omar Gate Deep Scan
1377
1381
 
1378
1382
  Generated: ${nowIso()}
1383
+ Run ID: ${omargateRunId}
1379
1384
  Target: ${targetPath}
1380
1385
  Elapsed: ${totalElapsed}
1381
1386
 
@@ -1395,12 +1400,24 @@ ${formatFindingsMarkdown(allFindings)}
1395
1400
  const reportPath = await writeLocalCommandReport(targetPath, "omargate-deep", report, {
1396
1401
  outputDir: outputDirArg,
1397
1402
  });
1403
+ const { writeOmarGateDeterministicCache } = await import("./review/omargate-cache.js");
1404
+ const deterministicCache = await writeOmarGateDeterministicCache({
1405
+ targetPath,
1406
+ outputDir: outputDirArg,
1407
+ runId: omargateRunId,
1408
+ deterministic,
1409
+ reportPath,
1410
+ });
1411
+ artifactPaths.deterministicCache = deterministicCache.artifactPath;
1412
+ artifactPaths.latestOmarGate = deterministicCache.latestPath;
1413
+
1398
1414
  if (asJson) {
1399
1415
  console.log(
1400
1416
  JSON.stringify(
1401
1417
  {
1402
1418
  command: "/omargate deep",
1403
1419
  targetPath,
1420
+ runId: omargateRunId,
1404
1421
  reportPath,
1405
1422
  scannedFiles,
1406
1423
  p0: combinedP0,
@@ -1457,6 +1474,7 @@ async function runLocalAuditCommand(args) {
1457
1474
  const asJson = hasCommandOption(args, "--json");
1458
1475
  const pathArg = getCommandOptionValue(args, "--path") || ".";
1459
1476
  const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
1477
+ const reuseOmarGate = getCommandOptionValue(args, "--reuse-omargate") || "";
1460
1478
  const targetPath = path.resolve(process.cwd(), pathArg);
1461
1479
  if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
1462
1480
  throw new Error(`Invalid --path target: ${targetPath}`);
@@ -1467,6 +1485,16 @@ async function runLocalAuditCommand(args) {
1467
1485
  printInfo(`Target: ${targetPath}`);
1468
1486
  }
1469
1487
 
1488
+ const buildScanFromOmarGateCache = (cache) => {
1489
+ const findings = Array.isArray(cache?.findings) ? cache.findings : [];
1490
+ return {
1491
+ scannedFiles: Number(cache?.scope?.scannedFiles || findings.length || 0),
1492
+ findings,
1493
+ p1: findings.filter((item) => item.severity === "P1").length,
1494
+ p2: findings.filter((item) => item.severity === "P2").length,
1495
+ };
1496
+ };
1497
+
1470
1498
  const requiredChecks = [
1471
1499
  {
1472
1500
  key: ".github/workflows/omar-gate.yml",
@@ -1488,7 +1516,44 @@ async function runLocalAuditCommand(args) {
1488
1516
  },
1489
1517
  ];
1490
1518
 
1491
- const scan = await runCredentialScan(targetPath);
1519
+ let omargateReuse = {
1520
+ requested: reuseOmarGate || "",
1521
+ used: false,
1522
+ runId: "",
1523
+ artifactPath: "",
1524
+ reason: reuseOmarGate ? "not_found" : "not_requested",
1525
+ };
1526
+ let scan = null;
1527
+ if (reuseOmarGate) {
1528
+ const { loadOmarGateDeterministicCache } = await import("./review/omargate-cache.js");
1529
+ const reused = await loadOmarGateDeterministicCache({
1530
+ targetPath,
1531
+ outputDir: outputDirArg,
1532
+ runIdOrLatest: reuseOmarGate,
1533
+ });
1534
+ if (reused.found) {
1535
+ scan = buildScanFromOmarGateCache(reused.cache);
1536
+ omargateReuse = {
1537
+ requested: reuseOmarGate,
1538
+ used: true,
1539
+ runId: reused.runId,
1540
+ deterministicRunId: reused.cache?.deterministicRunId || "",
1541
+ artifactPath: reused.artifactPath,
1542
+ reason: "",
1543
+ };
1544
+ } else {
1545
+ omargateReuse = {
1546
+ requested: reuseOmarGate,
1547
+ used: false,
1548
+ runId: "",
1549
+ artifactPath: "",
1550
+ reason: reused.reason || "not_found",
1551
+ };
1552
+ }
1553
+ }
1554
+ if (!scan) {
1555
+ scan = await runCredentialScan(targetPath);
1556
+ }
1492
1557
  const failedP1Checks = requiredChecks.filter((item) => !item.ok && item.severity === "P1").length;
1493
1558
  const failedP2Checks = requiredChecks.filter((item) => !item.ok && item.severity === "P2").length;
1494
1559
  const totalP1 = scan.p1 + failedP1Checks;
@@ -1506,11 +1571,13 @@ async function runLocalAuditCommand(args) {
1506
1571
  Generated: ${nowIso()}
1507
1572
  Target: ${targetPath}
1508
1573
  Overall status: ${overallStatus}
1574
+ OmarGate reuse: ${omargateReuse.used ? `yes (${omargateReuse.runId})` : omargateReuse.requested ? `requested ${omargateReuse.requested} (${omargateReuse.reason})` : "no"}
1509
1575
 
1510
1576
  Readiness checks:
1511
1577
  ${checkText}
1512
1578
 
1513
1579
  Scan summary:
1580
+ - Reused OmarGate run: ${omargateReuse.used ? omargateReuse.runId : "n/a"}
1514
1581
  - Files scanned: ${scan.scannedFiles}
1515
1582
  - P1 findings: ${scan.p1}
1516
1583
  - P2 findings: ${scan.p2}
@@ -1536,6 +1603,9 @@ ${formatFindingsMarkdown(scan.findings)}
1536
1603
  p1Total: totalP1,
1537
1604
  p2Total: totalP2,
1538
1605
  blocking: totalP1 > 0,
1606
+ omargateReuse,
1607
+ reusedOmarGateRunId: omargateReuse.used ? omargateReuse.runId : "",
1608
+ reusedOmarGateDeterministicPath: omargateReuse.used ? omargateReuse.artifactPath : "",
1539
1609
  },
1540
1610
  null,
1541
1611
  2
@@ -1543,6 +1613,11 @@ ${formatFindingsMarkdown(scan.findings)}
1543
1613
  );
1544
1614
  } else {
1545
1615
  console.log(pc.cyan(`Report: ${reportPath}`));
1616
+ if (omargateReuse.used) {
1617
+ console.log(pc.gray(`Reused OmarGate run: ${omargateReuse.runId}`));
1618
+ } else if (omargateReuse.requested) {
1619
+ console.log(pc.gray(`OmarGate reuse unavailable: ${omargateReuse.reason}`));
1620
+ }
1546
1621
  console.log(`Overall status: ${overallStatus}`);
1547
1622
  console.log(`P1 total: ${totalP1}`);
1548
1623
  console.log(`P2 total: ${totalP2}`);
@@ -0,0 +1,285 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { resolveOutputRoot } from "../config/service.js";
5
+
6
+ const CACHE_SCHEMA_VERSION = "1.0.0";
7
+ const CACHE_KIND = "omargate-deterministic-cache";
8
+ const LATEST_INDEX_NAME = "latest-omargate.json";
9
+
10
+ function normalizeString(value) {
11
+ return String(value || "").trim();
12
+ }
13
+
14
+ function normalizeTargetPath(value) {
15
+ return path.resolve(String(value || "."));
16
+ }
17
+
18
+ function normalizeSummary(value = {}) {
19
+ const summary = value && typeof value === "object" ? value : {};
20
+ const P0 = Math.max(0, Math.floor(Number(summary.P0 || 0)));
21
+ const P1 = Math.max(0, Math.floor(Number(summary.P1 || 0)));
22
+ const P2 = Math.max(0, Math.floor(Number(summary.P2 || 0)));
23
+ const P3 = Math.max(0, Math.floor(Number(summary.P3 || 0)));
24
+ return {
25
+ P0,
26
+ P1,
27
+ P2,
28
+ P3,
29
+ blocking: summary.blocking === undefined ? P0 > 0 || P1 > 0 : Boolean(summary.blocking),
30
+ };
31
+ }
32
+
33
+ function sanitizeRunId(value) {
34
+ const normalized = normalizeString(value);
35
+ if (!normalized) {
36
+ return "";
37
+ }
38
+ return normalized.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
39
+ }
40
+
41
+ function isSafeRequestedRunId(requested, sanitized) {
42
+ return Boolean(requested) && requested === sanitized && sanitized !== "." && sanitized !== "..";
43
+ }
44
+
45
+ function deterministicCachePath(outputRoot, runId) {
46
+ return path.join(outputRoot, "runs", runId, "deterministic.json");
47
+ }
48
+
49
+ async function readJsonFile(filePath) {
50
+ const content = await fsp.readFile(filePath, "utf-8");
51
+ return JSON.parse(content);
52
+ }
53
+
54
+ function isCacheForTarget(cache, normalizedTargetPath) {
55
+ const cacheTarget = normalizeString(cache?.targetPath);
56
+ if (!cacheTarget) {
57
+ return false;
58
+ }
59
+ return path.resolve(cacheTarget) === normalizedTargetPath;
60
+ }
61
+
62
+ function buildMissingResult({ outputRoot, requested = "latest", reason = "not_found" } = {}) {
63
+ return {
64
+ found: false,
65
+ requested,
66
+ reason,
67
+ outputRoot,
68
+ };
69
+ }
70
+
71
+ async function loadCacheFile({ filePath, outputRoot, requested, normalizedTargetPath }) {
72
+ let cache;
73
+ try {
74
+ cache = await readJsonFile(filePath);
75
+ } catch {
76
+ return buildMissingResult({
77
+ outputRoot,
78
+ requested,
79
+ reason: "malformed_or_missing_cache",
80
+ });
81
+ }
82
+
83
+ if (cache?.kind !== CACHE_KIND) {
84
+ return buildMissingResult({
85
+ outputRoot,
86
+ requested,
87
+ reason: "invalid_cache_kind",
88
+ });
89
+ }
90
+ if (!isCacheForTarget(cache, normalizedTargetPath)) {
91
+ return buildMissingResult({
92
+ outputRoot,
93
+ requested,
94
+ reason: "target_mismatch",
95
+ });
96
+ }
97
+
98
+ return {
99
+ found: true,
100
+ requested,
101
+ runId: normalizeString(cache.runId),
102
+ artifactPath: filePath,
103
+ outputRoot,
104
+ cache,
105
+ };
106
+ }
107
+
108
+ async function loadLatestFromIndex({ outputRoot, normalizedTargetPath }) {
109
+ const latestPath = path.join(outputRoot, "runs", LATEST_INDEX_NAME);
110
+ let index;
111
+ try {
112
+ index = await readJsonFile(latestPath);
113
+ } catch {
114
+ return null;
115
+ }
116
+
117
+ const runId = sanitizeRunId(index?.runId);
118
+ if (!runId || !isCacheForTarget(index, normalizedTargetPath)) {
119
+ return null;
120
+ }
121
+ const artifactPath = normalizeString(index.artifactPath) || deterministicCachePath(outputRoot, runId);
122
+ const loaded = await loadCacheFile({
123
+ filePath: artifactPath,
124
+ outputRoot,
125
+ requested: "latest",
126
+ normalizedTargetPath,
127
+ });
128
+ return loaded.found ? loaded : null;
129
+ }
130
+
131
+ async function loadLatestByScanning({ outputRoot, normalizedTargetPath }) {
132
+ const runsDir = path.join(outputRoot, "runs");
133
+ let entries = [];
134
+ try {
135
+ entries = await fsp.readdir(runsDir, { withFileTypes: true });
136
+ } catch {
137
+ return null;
138
+ }
139
+
140
+ const candidates = [];
141
+ for (const entry of entries) {
142
+ if (!entry.isDirectory()) {
143
+ continue;
144
+ }
145
+ const runId = sanitizeRunId(entry.name);
146
+ if (!isSafeRequestedRunId(entry.name, runId)) {
147
+ continue;
148
+ }
149
+ const artifactPath = deterministicCachePath(outputRoot, runId);
150
+ try {
151
+ const stat = await fsp.stat(artifactPath);
152
+ candidates.push({ runId, artifactPath, mtimeMs: Number(stat.mtimeMs || 0) });
153
+ } catch {
154
+ // Ignore incomplete run directories.
155
+ }
156
+ }
157
+
158
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
159
+ for (const candidate of candidates) {
160
+ const loaded = await loadCacheFile({
161
+ filePath: candidate.artifactPath,
162
+ outputRoot,
163
+ requested: "latest",
164
+ normalizedTargetPath,
165
+ });
166
+ if (loaded.found) {
167
+ return loaded;
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+
173
+ export async function writeOmarGateDeterministicCache({
174
+ targetPath,
175
+ outputDir = "",
176
+ runId,
177
+ deterministic = {},
178
+ reportPath = "",
179
+ } = {}) {
180
+ const normalizedTargetPath = normalizeTargetPath(targetPath);
181
+ const outputRoot = await resolveOutputRoot({
182
+ cwd: normalizedTargetPath,
183
+ outputDirOverride: outputDir,
184
+ env: process.env,
185
+ });
186
+ const normalizedRunId = sanitizeRunId(runId || deterministic?.runId);
187
+ if (!normalizedRunId) {
188
+ throw new Error("OmarGate deterministic cache requires a runId.");
189
+ }
190
+
191
+ const runDirectory = path.join(outputRoot, "runs", normalizedRunId);
192
+ const artifactPath = path.join(runDirectory, "deterministic.json");
193
+ const latestPath = path.join(outputRoot, "runs", LATEST_INDEX_NAME);
194
+ await fsp.mkdir(runDirectory, { recursive: true });
195
+
196
+ const cache = {
197
+ schemaVersion: CACHE_SCHEMA_VERSION,
198
+ kind: CACHE_KIND,
199
+ runId: normalizedRunId,
200
+ targetPath: normalizedTargetPath,
201
+ generatedAt: new Date().toISOString(),
202
+ deterministicRunId: normalizeString(deterministic?.runId),
203
+ mode: normalizeString(deterministic?.mode) || "full",
204
+ summary: normalizeSummary(deterministic?.summary),
205
+ findings: Array.isArray(deterministic?.findings) ? deterministic.findings : [],
206
+ scope: deterministic?.scope && typeof deterministic.scope === "object" ? deterministic.scope : {},
207
+ layers: deterministic?.layers && typeof deterministic.layers === "object" ? deterministic.layers : {},
208
+ metadata: deterministic?.metadata && typeof deterministic.metadata === "object" ? deterministic.metadata : {},
209
+ artifacts: deterministic?.artifacts && typeof deterministic.artifacts === "object" ? deterministic.artifacts : {},
210
+ source: {
211
+ command: "/omargate deep",
212
+ reportPath: normalizeString(reportPath),
213
+ },
214
+ };
215
+ await fsp.writeFile(artifactPath, `${JSON.stringify(cache, null, 2)}\n`, "utf-8");
216
+ await fsp.writeFile(
217
+ latestPath,
218
+ `${JSON.stringify(
219
+ {
220
+ schemaVersion: CACHE_SCHEMA_VERSION,
221
+ kind: "omargate-latest-index",
222
+ runId: normalizedRunId,
223
+ targetPath: normalizedTargetPath,
224
+ artifactPath,
225
+ updatedAt: cache.generatedAt,
226
+ },
227
+ null,
228
+ 2
229
+ )}\n`,
230
+ "utf-8"
231
+ );
232
+
233
+ return {
234
+ runId: normalizedRunId,
235
+ outputRoot,
236
+ runDirectory,
237
+ artifactPath,
238
+ latestPath,
239
+ cache,
240
+ };
241
+ }
242
+
243
+ export async function loadOmarGateDeterministicCache({
244
+ targetPath,
245
+ outputDir = "",
246
+ runIdOrLatest = "latest",
247
+ } = {}) {
248
+ const normalizedTargetPath = normalizeTargetPath(targetPath);
249
+ const outputRoot = await resolveOutputRoot({
250
+ cwd: normalizedTargetPath,
251
+ outputDirOverride: outputDir,
252
+ env: process.env,
253
+ });
254
+ const requested = normalizeString(runIdOrLatest) || "latest";
255
+
256
+ if (requested.toLowerCase() === "latest") {
257
+ const latestFromIndex = await loadLatestFromIndex({
258
+ outputRoot,
259
+ normalizedTargetPath,
260
+ });
261
+ if (latestFromIndex) {
262
+ return latestFromIndex;
263
+ }
264
+ const latestFromScan = await loadLatestByScanning({
265
+ outputRoot,
266
+ normalizedTargetPath,
267
+ });
268
+ return latestFromScan || buildMissingResult({ outputRoot, requested, reason: "not_found" });
269
+ }
270
+
271
+ const runId = sanitizeRunId(requested);
272
+ if (!isSafeRequestedRunId(requested, runId)) {
273
+ return buildMissingResult({
274
+ outputRoot,
275
+ requested,
276
+ reason: "invalid_run_id",
277
+ });
278
+ }
279
+ return loadCacheFile({
280
+ filePath: deterministicCachePath(outputRoot, runId),
281
+ outputRoot,
282
+ requested,
283
+ normalizedTargetPath,
284
+ });
285
+ }