@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.d.ts +125 -2
- package/dist/audit.js +397 -46
- package/dist/engine.js +2524 -1397
- package/dist/index.d.ts +12 -7
- package/dist/path.d.ts +31 -5
- package/dist/project.d.ts +57 -0
- package/dist/prreview.d.ts +335 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
472
|
-
|
|
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
|
|
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 =
|
|
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
|
};
|