@mstar-harness/engine 3.2.6 → 3.4.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.
package/dist/audit.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/audit.ts
2
+ import { execFileSync } from "node:child_process";
2
3
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
3
4
  import { basename as basename2, join as join4, resolve as resolve4, sep as sep2 } from "node:path";
4
5
 
@@ -113,38 +114,6 @@ var CONFIG_KEYS = {
113
114
  [MSTARC_ENFORCEMENT_KEY]: "enforcement"
114
115
  };
115
116
 
116
- // src/path.ts
117
- function assertSafePathComponent(value, what) {
118
- if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
119
- throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
120
- }
121
- }
122
- var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
123
- # Principle: process stays local; results are shared with the team.
124
- # Default-ignore everything under .mstar/, then re-include the tracked results.
125
- .mstar/**
126
- !.mstar/AGENTS.md
127
- !.mstar/knowledge/
128
- !.mstar/knowledge/**
129
- !.mstar/specs/
130
- !.mstar/specs/**
131
- # .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
132
- .mstarc
133
- `;
134
- var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
135
- # Default-ignore everything under .agents/, then re-include the tracked results.
136
- .agents/**
137
- !.agents/AGENTS.md
138
- !.agents/knowledge/
139
- !.agents/knowledge/**
140
- !.agents/specs/
141
- !.agents/specs/**
142
- `;
143
- var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
144
- `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
145
- var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
146
- `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
147
-
148
117
  // src/status.ts
149
118
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, realpathSync } from "node:fs";
150
119
  import { dirname as dirname3, join as join3, resolve as resolve3, sep } from "node:path";
@@ -336,6 +305,38 @@ function registerWorkflowEntryLocked(statusPath, entry) {
336
305
  return doc;
337
306
  }
338
307
 
308
+ // src/path.ts
309
+ function assertSafePathComponent(value, what) {
310
+ if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
311
+ throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
312
+ }
313
+ }
314
+ var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
315
+ # Principle: process stays local; results are shared with the team.
316
+ # Default-ignore everything under .mstar/, then re-include the tracked results.
317
+ .mstar/**
318
+ !.mstar/AGENTS.md
319
+ !.mstar/knowledge/
320
+ !.mstar/knowledge/**
321
+ !.mstar/specs/
322
+ !.mstar/specs/**
323
+ # .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
324
+ .mstarc
325
+ `;
326
+ var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
327
+ # Default-ignore everything under .agents/, then re-include the tracked results.
328
+ .agents/**
329
+ !.agents/AGENTS.md
330
+ !.agents/knowledge/
331
+ !.agents/knowledge/**
332
+ !.agents/specs/
333
+ !.agents/specs/**
334
+ `;
335
+ var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
336
+ `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
337
+ var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
338
+ `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
339
+
339
340
  // src/audit.ts
340
341
  function violation2(severity, code, message, fix) {
341
342
  return { ok: false, severity, code, message, fix };
@@ -343,6 +344,7 @@ function violation2(severity, code, message, fix) {
343
344
  var AUDIT_PRIORITIES = ["P1", "P2", "P3"];
344
345
  var AUDIT_EFFORTS = ["XS", "S", "M", "L", "XL"];
345
346
  var AUDIT_RISKS = ["LOW", "MED", "HIGH"];
347
+ var AUDIT_CONFIDENCES = ["HIGH", "MED", "LOW"];
346
348
  var AUDIT_CATEGORIES = [
347
349
  "bug",
348
350
  "security",
@@ -409,9 +411,11 @@ function validateAuditStatusBlocks(planText) {
409
411
  return { ok: violations.length === 0, violations };
410
412
  }
411
413
  var WHOLE_MATCH_PATTERNS = [
412
- { type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
414
+ { type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g },
413
415
  { type: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
414
416
  { type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
417
+ { type: "github-pat", re: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
418
+ { type: "stripe-live-key", re: /\bsk_live_[A-Za-z0-9]{16,}\b/g },
415
419
  { type: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
416
420
  { type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\b/g },
417
421
  { type: "api-secret-key", re: /\bsk-[A-Za-z0-9-]{20,}\b/g }
@@ -422,6 +426,36 @@ var VALUE_PATTERNS = [
422
426
  re: /(["']?)\b(password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|secret|token)\b(["']?)(\s*[:=]\s*)("[^"\n]{8,}"|'[^'\n]{8,}'|[A-Za-z0-9_./+\-=]{16,})/gi
423
427
  }
424
428
  ];
429
+ var NEVER_COMMIT_FILENAMES = [
430
+ { type: "env-file", re: /^\.env/i },
431
+ { type: "private-key-file", re: /\.(?:pem|key)$/i },
432
+ { type: "ssh-private-key-file", re: /^id_(?:rsa|ed25519|ecdsa|dsa)$/ },
433
+ { type: "credentials-json", re: /^credentials\.json$/i },
434
+ { type: "service-account-json", re: /^service-account\.json$/i },
435
+ { type: "git-credentials", re: /^\.?git-credentials$/i }
436
+ ];
437
+ var CI_IAC_LEAK_SHAPES = [
438
+ {
439
+ kind: "actions-plaintext-env",
440
+ description: "GitHub Actions env assignment with plaintext literal",
441
+ re: /^\s*(?:-\s+)?env:\s*[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY)[A-Z0-9_]*\s*[:=]\s*["']?[A-Za-z0-9_/+=-]{8,}["']?\s*$/
442
+ },
443
+ {
444
+ kind: "actions-secret-echo",
445
+ description: "echo of a GitHub Actions secrets context value",
446
+ re: /\becho\b[^#\n]*\$\{\{\s*secrets\.[A-Za-z0-9_]+\s*\}\}/
447
+ },
448
+ {
449
+ kind: "dockerfile-credential-env",
450
+ description: "Dockerfile ENV/ARG with credential-looking name",
451
+ re: /^\s*(?:ENV|ARG)\s+[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|APIKEY|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*\b/i
452
+ },
453
+ {
454
+ kind: "terraform-hardcoded-password",
455
+ description: "Terraform hardcoded password attribute",
456
+ re: /^\s*password\s*=\s*"[^$\{][^"]*"\s*$/
457
+ }
458
+ ];
425
459
  function buildLineStarts(text) {
426
460
  const starts = [0];
427
461
  for (let i = 0;i < text.length; i++) {
@@ -443,17 +477,25 @@ function lineAt(starts, index) {
443
477
  }
444
478
  return lo + 1;
445
479
  }
480
+ function lineStartOf(text, index) {
481
+ return text.lastIndexOf(`
482
+ `, index - 1) + 1;
483
+ }
446
484
  function redactSecrets(text, filePath) {
447
485
  const starts = buildLineStarts(text);
448
486
  const marker = (type, index) => `[REDACTED ${type}@${lineAt(starts, index)}${filePath === undefined ? "" : ` in ${filePath}`}]`;
449
- const replacements = [];
450
- const findings = [];
487
+ const spans = [];
451
488
  for (const pattern of WHOLE_MATCH_PATTERNS) {
452
489
  for (const match of text.matchAll(pattern.re)) {
453
490
  if (match.index === undefined)
454
491
  continue;
455
- replacements.push({ index: match.index, length: match[0].length, text: marker(pattern.type, match.index) });
456
- findings.push({ line: lineAt(starts, match.index), type: pattern.type });
492
+ spans.push({
493
+ start: match.index,
494
+ end: match.index + match[0].length,
495
+ priority: 0,
496
+ text: marker(pattern.type, match.index),
497
+ type: pattern.type
498
+ });
457
499
  }
458
500
  }
459
501
  for (const pattern of VALUE_PATTERNS) {
@@ -462,20 +504,287 @@ function redactSecrets(text, filePath) {
462
504
  continue;
463
505
  const type = pattern.typeOf(match[2]);
464
506
  const replacement = `${match[1]}${match[2]}${match[3]}${match[4]}${marker(type, match.index)}`;
465
- replacements.push({ index: match.index, length: match[0].length, text: replacement });
466
- findings.push({ line: lineAt(starts, match.index), type });
507
+ spans.push({ start: match.index, end: match.index + match[0].length, priority: 1, text: replacement, type });
467
508
  }
468
509
  }
469
- replacements.sort((a, b) => b.index - a.index);
510
+ for (const shape of CI_IAC_LEAK_SHAPES) {
511
+ const lineScoped = new RegExp(shape.re.source, shape.re.ignoreCase ? "gim" : "gm");
512
+ for (const match of text.matchAll(lineScoped)) {
513
+ if (match.index === undefined)
514
+ continue;
515
+ const lineEnd = text.indexOf(`
516
+ `, match.index);
517
+ const end = lineEnd === -1 ? text.length : lineEnd;
518
+ spans.push({
519
+ start: match.index,
520
+ end,
521
+ priority: 2,
522
+ text: `${" ".repeat(match.index - lineStartOf(text, match.index))}${marker(shape.kind, match.index)}`,
523
+ type: shape.kind
524
+ });
525
+ }
526
+ }
527
+ spans.sort((a, b) => a.start - b.start || b.end - a.end || b.priority - a.priority);
528
+ const merged = [];
529
+ let groupMaxEnd = -1;
530
+ let best = null;
531
+ for (const span of spans) {
532
+ if (span.start < groupMaxEnd) {
533
+ if (groupMaxEnd < span.end)
534
+ groupMaxEnd = span.end;
535
+ if (span.end - span.start > best.end - best.start)
536
+ best = span;
537
+ } else {
538
+ if (best !== null)
539
+ merged.push(best);
540
+ groupMaxEnd = span.end;
541
+ best = span;
542
+ }
543
+ }
544
+ if (best !== null)
545
+ merged.push(best);
470
546
  let out = text;
471
- for (const r of replacements)
472
- out = out.slice(0, r.index) + r.text + out.slice(r.index + r.length);
547
+ for (let i = merged.length - 1;i >= 0; i--) {
548
+ const r = merged[i];
549
+ out = out.slice(0, r.start) + r.text + out.slice(r.end);
550
+ }
551
+ const findings = merged.map((r) => ({ line: lineAt(starts, r.start), type: r.type }));
473
552
  const deduped = new Map;
474
553
  for (const f of findings)
475
554
  deduped.set(`${f.line}:${f.type}`, f);
476
555
  const sorted = [...deduped.values()].sort((a, b) => a.line - b.line || a.type.localeCompare(b.type));
477
556
  return { text: out, findings: sorted };
478
557
  }
558
+ var SAFE_PLACEHOLDER_SHAPES = [
559
+ /\$\{[A-Za-z_][A-Za-z0-9_]*\}/g,
560
+ /process\.env\.[A-Za-z_][A-Za-z0-9_]*/g,
561
+ /os\.environ(?:\.get)?\(?\s*["']/g
562
+ ];
563
+ var SAFE_PLACEHOLDER_VALUES = ["your-api-key-here", "<your_api_key>", "<your-api-key>"];
564
+ function maskSafePlaceholders(line) {
565
+ let masked = line;
566
+ for (const shape of [...SAFE_PLACEHOLDER_VALUES, ...SAFE_PLACEHOLDER_SHAPES]) {
567
+ if (typeof shape === "string") {
568
+ let at = masked.toLowerCase().indexOf(shape);
569
+ while (at !== -1) {
570
+ let from = at;
571
+ let to = at + shape.length;
572
+ if (masked[from - 1] === '"' || masked[from - 1] === "'")
573
+ from--;
574
+ if (masked[to] === '"' || masked[to] === "'")
575
+ to++;
576
+ masked = masked.slice(0, from) + " ".repeat(to - from) + masked.slice(to);
577
+ at = masked.toLowerCase().indexOf(shape);
578
+ }
579
+ } else {
580
+ for (const match of masked.matchAll(shape)) {
581
+ if (match.index === undefined)
582
+ continue;
583
+ let from = match.index;
584
+ let to = match.index + match[0].length;
585
+ if (masked[from - 1] === '"' || masked[from - 1] === "'")
586
+ from--;
587
+ if (masked[to] === '"' || masked[to] === "'")
588
+ to++;
589
+ masked = masked.slice(0, from) + " ".repeat(to - from) + masked.slice(to);
590
+ }
591
+ }
592
+ }
593
+ return masked;
594
+ }
595
+ var ACTIONS_ENV_KEY = /[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY)[A-Z0-9_]*/;
596
+ function scanActionsEnvMap(lines) {
597
+ const out = [];
598
+ let inMap = false;
599
+ let mapIndent = 0;
600
+ for (let i = 0;i < lines.length; i++) {
601
+ const rawLine = lines[i] ?? "";
602
+ if (!rawLine.trim())
603
+ continue;
604
+ const indent = rawLine.length - rawLine.trimStart().length;
605
+ const line = maskSafePlaceholders(rawLine);
606
+ if (inMap) {
607
+ if (indent <= mapIndent) {
608
+ inMap = false;
609
+ } else if (line.trim() !== "") {
610
+ const child = /^\s*(?:["']?)([A-Za-z0-9_-]+)(?:["']?)\s*:\s*(.+?)\s*$/.exec(line);
611
+ const value = child?.[2] ?? "";
612
+ if (child !== null && ACTIONS_ENV_KEY.test(child[1]) && !/^\$\{\{[^}]*\}\}$/.test(value) && !/^\$\{[^}]*\}$/.test(value)) {
613
+ out.push(i + 1);
614
+ }
615
+ }
616
+ if (inMap)
617
+ continue;
618
+ }
619
+ if (/^\s*(?:-\s+)?env:\s*(?:#.*)?$/.test(line)) {
620
+ inMap = true;
621
+ mapIndent = indent;
622
+ }
623
+ }
624
+ return out;
625
+ }
626
+ function scanSecrets(files) {
627
+ const findings = [];
628
+ let unreadableFiles = 0;
629
+ const privateKeyRow = WHOLE_MATCH_PATTERNS.find((pattern) => pattern.type === "private-key");
630
+ if (privateKeyRow === undefined) {
631
+ throw new Error("scanSecrets: WHOLE_MATCH_PATTERNS is missing its private-key row (full-text PEM pass)");
632
+ }
633
+ for (const file of files) {
634
+ let text;
635
+ try {
636
+ text = readFileSync3(file, "utf8");
637
+ } catch {
638
+ unreadableFiles++;
639
+ continue;
640
+ }
641
+ const base = basename2(file);
642
+ for (const entry of NEVER_COMMIT_FILENAMES) {
643
+ if (entry.re.test(base))
644
+ findings.push({ file, line: 1, type: entry.type });
645
+ }
646
+ const starts = buildLineStarts(text);
647
+ for (const match of text.matchAll(privateKeyRow.re)) {
648
+ if (match.index === undefined)
649
+ continue;
650
+ findings.push({ file, line: lineAt(starts, match.index), type: privateKeyRow.type });
651
+ }
652
+ const lines = text.split(`
653
+ `);
654
+ for (const lineNo of scanActionsEnvMap(lines)) {
655
+ findings.push({ file, line: lineNo, type: "actions-plaintext-env" });
656
+ }
657
+ for (let i = 0;i < lines.length; i++) {
658
+ const line = maskSafePlaceholders(lines[i] ?? "");
659
+ for (const pattern of WHOLE_MATCH_PATTERNS) {
660
+ if (line.match(pattern.re) !== null)
661
+ findings.push({ file, line: i + 1, type: pattern.type });
662
+ }
663
+ for (const pattern of VALUE_PATTERNS) {
664
+ const match = pattern.re.exec(line);
665
+ pattern.re.lastIndex = 0;
666
+ if (match !== null && !/^\$\{[^}]*\}$/.test(match[5])) {
667
+ findings.push({ file, line: i + 1, type: pattern.typeOf(match[2]) });
668
+ }
669
+ }
670
+ for (const shape of CI_IAC_LEAK_SHAPES) {
671
+ if (shape.re.test(line))
672
+ findings.push({ file, line: i + 1, type: shape.kind });
673
+ }
674
+ }
675
+ }
676
+ return { findings, unreadableFiles };
677
+ }
678
+ var LOCKFILE_NAMES = [
679
+ "package-lock.json",
680
+ "pnpm-lock.yaml",
681
+ "yarn.lock",
682
+ "bun.lock",
683
+ "bun.lockb",
684
+ "Cargo.lock",
685
+ "poetry.lock",
686
+ "uv.lock",
687
+ "Gemfile.lock",
688
+ "composer.lock"
689
+ ];
690
+ function rootLockfiles(root) {
691
+ let entries;
692
+ try {
693
+ entries = readdirSync2(root, { withFileTypes: true });
694
+ } catch {
695
+ return [];
696
+ }
697
+ const names = new Set(LOCKFILE_NAMES);
698
+ const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join4(root, entry.name));
699
+ if (present.length === 0)
700
+ return [];
701
+ try {
702
+ const tracked = new Set(execFileSync("git", ["ls-files", "-z", "--", "."], {
703
+ cwd: root,
704
+ encoding: "utf8",
705
+ stdio: ["ignore", "pipe", "ignore"]
706
+ }).split("\x00").filter((f) => f !== ""));
707
+ return present.filter((p) => tracked.has(basename2(p)));
708
+ } catch {
709
+ return present;
710
+ }
711
+ }
712
+ function supplyChainChecks(repoRoot) {
713
+ const findings = [];
714
+ const violations = [];
715
+ const lockfiles = rootLockfiles(repoRoot);
716
+ if (lockfiles.length === 0) {
717
+ findings.push({ kind: "lockfile-missing", file: repoRoot });
718
+ violations.push(violation2("medium", "audit.supply.lockfile-missing", `no recognized lockfile at ${repoRoot}`, "commit a lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, …)"));
719
+ } else if (lockfiles.length > 1) {
720
+ findings.push({ kind: "lockfile-duplicate", file: lockfiles.map((f) => f.replace(`${repoRoot}/`, "")).join(", ") });
721
+ violations.push(violation2("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
722
+ }
723
+ const workflowsDir = join4(repoRoot, ".github", "workflows");
724
+ let wfEntries = [];
725
+ try {
726
+ wfEntries = readdirSync2(workflowsDir, { withFileTypes: true });
727
+ } catch {
728
+ wfEntries = [];
729
+ }
730
+ for (const entry of wfEntries) {
731
+ if (!entry.isFile() || !/\.(?:ya?ml)$/.test(entry.name))
732
+ continue;
733
+ const wfPath = join4(workflowsDir, entry.name);
734
+ const relPath = `.github/workflows/${entry.name}`;
735
+ let text;
736
+ try {
737
+ text = readFileSync3(wfPath, "utf8");
738
+ } catch {
739
+ continue;
740
+ }
741
+ const lines = text.split(`
742
+ `);
743
+ const hasPrt = /(?:^|\n)\s*(?:(?:-\s+)?pull_request_target\b|on:\s*(?:\[[^\]]*\s*)?pull_request_target\b)/.test(text);
744
+ const prtHeadSteps = new Set;
745
+ for (let i = 0;i < lines.length; i++) {
746
+ const line = lines[i] ?? "";
747
+ if (!/uses:\s*actions\/checkout\b/.test(line))
748
+ continue;
749
+ let stepIndent = line.length - line.trimStart().length;
750
+ for (let k = i - 1;k >= 0; k--) {
751
+ const up = lines[k] ?? "";
752
+ const ind = up.length - up.trimStart().length;
753
+ if (up.trimStart().startsWith("- ") && ind < stepIndent) {
754
+ stepIndent = ind;
755
+ break;
756
+ }
757
+ }
758
+ for (let j = i + 1;j < lines.length; j++) {
759
+ const l2 = lines[j] ?? "";
760
+ if (l2.trim() && l2.length - l2.trimStart().length <= stepIndent)
761
+ break;
762
+ if (/github\.event\.pull_request\.head\.(?:sha|ref)\b/.test(l2)) {
763
+ prtHeadSteps.add(i);
764
+ break;
765
+ }
766
+ }
767
+ }
768
+ for (let i = 0;i < lines.length; i++) {
769
+ const line = lines[i] ?? "";
770
+ const uses = /^\s*(?:-\s+)?uses:\s*(\S+)@(\S+)\s*(?:#.*)?$/.exec(line);
771
+ if (uses !== null) {
772
+ const ref = uses[2].replace(/^["']|["']$/g, "");
773
+ const shaLike = /^[0-9a-f]{40}$/.test(ref);
774
+ const versionLike = /^v\d+(?:\.\d+)*$/.test(ref);
775
+ if (!shaLike && !versionLike) {
776
+ findings.push({ kind: "action-unpinned", file: relPath, line: i + 1 });
777
+ violations.push(violation2("high", "audit.supply.action-unpinned", `${relPath}:${i + 1} uses \`${uses[1]}@${ref}\` — mutable ref`, "pin the action to a full commit SHA"));
778
+ }
779
+ }
780
+ if (hasPrt && prtHeadSteps.has(i)) {
781
+ findings.push({ kind: "pull_request_target-head", file: relPath, line: i + 1 });
782
+ violations.push(violation2("high", "audit.supply.pull_request_target-head", `${relPath}:${i + 1} checks out the PR head under pull_request_target`, "check out the base ref or use a pull_request trigger for untrusted code"));
783
+ }
784
+ }
785
+ }
786
+ return { ok: violations.length === 0, violations, findings };
787
+ }
479
788
  function slugify(title) {
480
789
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
481
790
  }
@@ -509,14 +818,36 @@ function renderPlanFile(finding, plannedAt) {
509
818
  `)}
510
819
  `;
511
820
  }
821
+ function redactText(text) {
822
+ return redactSecrets(text).text;
823
+ }
824
+ function redactFinding(finding) {
825
+ return {
826
+ ...finding,
827
+ title: redactText(finding.title),
828
+ impact: redactText(finding.impact),
829
+ evidence: finding.evidence.map(redactText),
830
+ ...finding.fixSketch !== undefined ? { fixSketch: redactText(finding.fixSketch) } : {},
831
+ ...finding.verification !== undefined ? { verification: redactText(finding.verification) } : {}
832
+ };
833
+ }
512
834
  function readPlanFileSummary(filePath) {
513
835
  const text = readFileSync3(filePath, "utf8");
514
836
  const title = (text.match(/^# (.+)$/m) ?? [])[1] ?? filePath;
515
837
  const blocks = parseStatusBlocks(text);
516
838
  return { title: title.trim(), fields: blocks.length > 0 ? blocks[0].fields : new Map };
517
839
  }
840
+ function extractSecurityDispositionSections(text) {
841
+ const grab = (heading) => {
842
+ const match = text.match(new RegExp(`(?:^|\\r?\\n)##[ \\t]+${heading}[ \\t]*\\r?\\n(?:[ \\t]*\\r?\\n)?([\\s\\S]*?)(?=\\r?\\n## |$)`, "i"));
843
+ if (!match)
844
+ return [];
845
+ return match[1].split(/\r?\n/).map((line) => line.replace(/\r$/, "")).filter((line) => line.startsWith("- "));
846
+ };
847
+ return { needsVerification: grab("Needs verification"), hardeningChecked: grab("Hardening & checked notes") };
848
+ }
518
849
  function renderIndex(params) {
519
- const { date, repoName, repoShortSha, rows, rejected } = params;
850
+ const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
520
851
  const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))} |`).join(`
521
852
  `);
522
853
  const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
@@ -537,6 +868,12 @@ function renderIndex(params) {
537
868
  if (directionRows !== "") {
538
869
  sections.push("", "## Direction", "", directionRows);
539
870
  }
871
+ if (needsVerification.length > 0) {
872
+ sections.push("", "## Needs verification", "", ...needsVerification);
873
+ }
874
+ if (hardeningChecked.length > 0) {
875
+ sections.push("", "## Hardening & checked notes", "", ...hardeningChecked);
876
+ }
540
877
  sections.push("", "## Execution order & status", "", "| Plan | Title | Priority | Effort | Depends on | Status |", "|------|-------|----------|--------|------------|--------|", executionRows);
541
878
  if (rejectedRows !== "") {
542
879
  sections.push("", "## Findings considered and rejected", "", rejectedRows);
@@ -550,11 +887,14 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
550
887
  const date = options.date ?? new Date().toISOString().slice(0, 10);
551
888
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
552
889
  mkdirSync3(outDir, { recursive: true });
890
+ const existingReadme = join4(outDir, "README.md");
891
+ const carried = existsSync3(existingReadme) ? extractSecurityDispositionSections(readFileSync3(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
553
892
  const existing = readdirSync2(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
554
893
  let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
894
+ const redactedFindings = findings.map(redactFinding);
555
895
  const written = [];
556
896
  const usedSlugs = new Set;
557
- for (const finding of findings) {
897
+ for (const finding of redactedFindings) {
558
898
  const num = String(next).padStart(3, "0");
559
899
  let slug = slugify(finding.title);
560
900
  if (usedSlugs.has(slug)) {
@@ -588,7 +928,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
588
928
  });
589
929
  const byNum = new Map(rows.map((r) => [r.num, r]));
590
930
  written.forEach((file, i) => {
591
- const finding = findings[i];
931
+ const finding = redactedFindings[i];
592
932
  if (finding === undefined)
593
933
  return;
594
934
  const row = byNum.get(file.slice(0, 3));
@@ -603,12 +943,16 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
603
943
  row.dependsOn = finding.dependsOn ?? "none";
604
944
  }
605
945
  });
946
+ const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
947
+ const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(redactText(hc.text))}`) : carried.hardeningChecked;
606
948
  writeFileSync3(join4(outDir, "README.md"), renderIndex({
607
949
  date,
608
950
  repoName: options.repoName ?? "repo",
609
951
  repoShortSha: options.repoShortSha ?? "unknown",
610
952
  rows,
611
- rejected: options.rejected ?? []
953
+ rejected: (options.rejected ?? []).map((r) => ({ title: redactText(r.title), reason: redactText(r.reason) })),
954
+ needsVerification: needsVerificationLines,
955
+ hardeningChecked: hardeningCheckedLines
612
956
  }));
613
957
  return { outDir: resolve4(outDir), date, files: written, nextNumber: next };
614
958
  }
@@ -745,11 +1089,18 @@ function planFileRel(outDir, planFile) {
745
1089
  }
746
1090
  export {
747
1091
  validateAuditStatusBlocks,
1092
+ supplyChainChecks,
1093
+ scanSecrets,
748
1094
  scaffoldAuditPlan,
749
1095
  redactSecrets,
750
1096
  promoteAuditPlans,
1097
+ WHOLE_MATCH_PATTERNS,
1098
+ VALUE_PATTERNS,
1099
+ NEVER_COMMIT_FILENAMES,
1100
+ CI_IAC_LEAK_SHAPES,
751
1101
  AUDIT_RISKS,
752
1102
  AUDIT_PRIORITIES,
753
1103
  AUDIT_EFFORTS,
1104
+ AUDIT_CONFIDENCES,
754
1105
  AUDIT_CATEGORIES
755
1106
  };