codictator 0.1.2 → 0.2.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/builtin-packs/core/fixtures/conventional-commit/passing/basic/case.json +1 -1
- package/dist/builtin-packs/core/fixtures/conventional-commit/violating/basic/case.json +1 -1
- package/dist/builtin-packs/typescript/fixtures/lint/passing/exported/case.json +8 -0
- package/dist/builtin-packs/typescript/pack.json +1 -1
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +3 -1
- package/dist/cli/gate-scope.d.ts +1 -0
- package/dist/cli/gate-scope.js +11 -0
- package/dist/cli/git-hook.js +13 -8
- package/dist/cli/guardrail.d.ts +2 -0
- package/dist/cli/guardrail.js +11 -13
- package/dist/cli/init-plan.d.ts +7 -0
- package/dist/cli/init-plan.js +21 -2
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +8 -17
- package/dist/cli/plan-launcher.d.ts +1 -0
- package/dist/cli/plan-launcher.js +53 -3
- package/dist/cli/serve-run.js +1 -0
- package/dist/cli/setup-gitignore.d.ts +7 -0
- package/dist/cli/setup-gitignore.js +41 -0
- package/dist/cli/setup-handoff.d.ts +7 -0
- package/dist/cli/setup-handoff.js +44 -0
- package/dist/cli/setup-install.d.ts +11 -0
- package/dist/cli/setup-install.js +84 -0
- package/dist/cli/setup.d.ts +9 -0
- package/dist/cli/setup.js +143 -0
- package/dist/cli/task-advance.js +16 -16
- package/dist/features/adapters/claude-shaped.d.ts +1 -0
- package/dist/features/adapters/claude-shaped.js +29 -31
- package/dist/features/adapters/copilot.js +22 -29
- package/dist/features/audit/append.js +9 -4
- package/dist/features/audit/surfaces.js +15 -8
- package/dist/features/ci/protection.js +37 -13
- package/dist/features/deps/npm.js +39 -24
- package/dist/features/deps/registry.js +45 -34
- package/dist/features/deps/stable.js +52 -27
- package/dist/features/deps/upgrade.d.ts +1 -1
- package/dist/features/deps/upgrade.js +29 -19
- package/dist/features/engine/evaluate.js +6 -1
- package/dist/features/packs/enforcers/ci-guards.js +4 -1
- package/dist/features/packs/enforcers/code.js +2 -1
- package/dist/features/packs/enforcers/engine-version-guards.d.ts +6 -0
- package/dist/features/packs/enforcers/engine-version-guards.js +42 -0
- package/dist/features/packs/enforcers/external-bind.js +62 -28
- package/dist/features/packs/enforcers/external-sarif.js +30 -24
- package/dist/features/packs/enforcers/gate-guards.d.ts +0 -1
- package/dist/features/packs/enforcers/gate-guards.js +83 -136
- package/dist/features/packs/enforcers/health-measures.d.ts +2 -0
- package/dist/features/packs/enforcers/health-measures.js +10 -1
- package/dist/features/packs/enforcers/health-refs.d.ts +1 -2
- package/dist/features/packs/enforcers/health-refs.js +71 -56
- package/dist/features/packs/enforcers/secret-scan.d.ts +4 -20
- package/dist/features/packs/enforcers/secret-scan.js +61 -67
- package/dist/features/packs/enforcers/ts-checks.js +104 -58
- package/dist/features/packs/enforcers/ts-security-patterns.js +12 -8
- package/dist/features/packs/manifest-decl.d.ts +13 -0
- package/dist/features/packs/manifest-decl.js +121 -0
- package/dist/features/packs/manifest.d.ts +2 -12
- package/dist/features/packs/manifest.js +5 -90
- package/dist/features/packs/selftest.js +22 -5
- package/dist/features/remediation/adoption.js +40 -29
- package/dist/features/testing/scenarios.js +49 -27
- package/dist/features/testing/tap-parser.js +38 -26
- package/dist/features/workflow/phases.js +45 -42
- package/dist/features/workflow/review.js +17 -10
- package/dist/kernel/git.js +4 -1
- package/dist/kernel/pattern.js +120 -86
- package/dist/kernel/secret-paths.js +34 -20
- package/dist/kernel/shell-cmd.js +23 -6
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -42,6 +42,54 @@ function reachableModules(test, importMap) {
|
|
|
42
42
|
}
|
|
43
43
|
return reached;
|
|
44
44
|
}
|
|
45
|
+
function coversChangedModule(input, globs, test, changed) {
|
|
46
|
+
if (!globs.some((glob) => globMatch(glob, test.path))) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const reached = reachableModules(test, input.importMap);
|
|
50
|
+
return [...changed].some((module) => reached.has(module));
|
|
51
|
+
}
|
|
52
|
+
function coveringTests(input, kind, changed) {
|
|
53
|
+
const globs = input.kindGlobs[kind] ?? [];
|
|
54
|
+
return input.changedTests.filter((test) => coversChangedModule(input, globs, test, changed));
|
|
55
|
+
}
|
|
56
|
+
function architectureFinding(input, changed) {
|
|
57
|
+
const unmapped = [...changed].filter((module) => !input.mappedModules.includes(module));
|
|
58
|
+
if (unmapped.length === 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return kindFinding(`architecture test missing: ${unmapped.join(", ")} map to no architecture module`, `Map ${unmapped.join(", ")} in config.architecture so the arch guard covers them.`);
|
|
62
|
+
}
|
|
63
|
+
function missingKindFinding(input, kind, changed) {
|
|
64
|
+
const globs = input.kindGlobs[kind] ?? [];
|
|
65
|
+
return kindFinding(`${kind} test missing for changed ${[...changed].join(", ") || "files"}`, `Add a ${kind} test under ${globs.join(", ") || "its kind globs"} importing a changed module.`);
|
|
66
|
+
}
|
|
67
|
+
function budgetFindings(kind, covering, budgets) {
|
|
68
|
+
const findings = [];
|
|
69
|
+
for (const test of covering) {
|
|
70
|
+
if (budgets[test.path] !== true) {
|
|
71
|
+
findings.push(kindFinding(`${kind} test without a budget line: ${test.path}`, `Declare a codictator:budget line (p95, p99, rps or maxMs) in ${test.path}.`));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return findings;
|
|
75
|
+
}
|
|
76
|
+
function collectKindFindings(input, kind, changed, findings) {
|
|
77
|
+
if (kind === "architecture") {
|
|
78
|
+
const missing = architectureFinding(input, changed);
|
|
79
|
+
if (missing !== undefined) {
|
|
80
|
+
findings.push(missing);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const covering = coveringTests(input, kind, changed);
|
|
85
|
+
if (covering.length === 0) {
|
|
86
|
+
findings.push(missingKindFinding(input, kind, changed));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (kind === "stress" || kind === "load") {
|
|
90
|
+
findings.push(...budgetFindings(kind, covering, input.budgets));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
45
93
|
// A kind is satisfied by a changed test file under its kind globs that
|
|
46
94
|
// imports (transitively, depth 2) a changed module. Architecture is
|
|
47
95
|
// satisfied when config.architecture maps every changed module, because
|
|
@@ -51,33 +99,7 @@ export function checkTestKinds(input) {
|
|
|
51
99
|
const findings = [];
|
|
52
100
|
const changed = new Set(input.changedModules);
|
|
53
101
|
for (const kind of input.required) {
|
|
54
|
-
|
|
55
|
-
const unmapped = [...changed].filter((module) => !input.mappedModules.includes(module));
|
|
56
|
-
if (unmapped.length === 0) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
findings.push(kindFinding(`architecture test missing: ${unmapped.join(", ")} map to no architecture module`, `Map ${unmapped.join(", ")} in config.architecture so the arch guard covers them.`));
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const globs = input.kindGlobs[kind] ?? [];
|
|
63
|
-
const covering = input.changedTests.filter((test) => {
|
|
64
|
-
if (!globs.some((glob) => globMatch(glob, test.path))) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
const reached = reachableModules(test, input.importMap);
|
|
68
|
-
return [...changed].some((module) => reached.has(module));
|
|
69
|
-
});
|
|
70
|
-
if (covering.length === 0) {
|
|
71
|
-
findings.push(kindFinding(`${kind} test missing for changed ${[...changed].join(", ") || "files"}`, `Add a ${kind} test under ${globs.join(", ") || "its kind globs"} importing a changed module.`));
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (kind === "stress" || kind === "load") {
|
|
75
|
-
for (const test of covering) {
|
|
76
|
-
if (input.budgets[test.path] !== true) {
|
|
77
|
-
findings.push(kindFinding(`${kind} test without a budget line: ${test.path}`, `Declare a codictator:budget line (p95, p99, rps or maxMs) in ${test.path}.`));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
102
|
+
collectKindFindings(input, kind, changed, findings);
|
|
81
103
|
}
|
|
82
104
|
return findings;
|
|
83
105
|
}
|
|
@@ -48,33 +48,36 @@ function recordResult(stack, indent, name, passed, outcomes) {
|
|
|
48
48
|
});
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
51
|
+
// The result pattern lives at module scope so its quantifiers never count
|
|
52
|
+
// as branches of the function that applies it.
|
|
53
|
+
const TAP_RESULT_PATTERN = /^( *)(not )?ok [0-9]+ - (.*)$/;
|
|
54
|
+
function recordTapResult(stack, outcomes, line) {
|
|
55
|
+
const match = TAP_RESULT_PATTERN.exec(line);
|
|
56
|
+
if (match === null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const indent = (match[1] ?? "").length;
|
|
60
|
+
const passed = match[2] === undefined;
|
|
61
|
+
const name = unescapeTapName((match[3] ?? "").trimEnd());
|
|
62
|
+
recordResult(stack, indent, name, passed, outcomes);
|
|
63
|
+
}
|
|
64
|
+
function recordTapLine(stack, outcomes, line) {
|
|
65
|
+
const trimmed = line.trimStart();
|
|
66
|
+
if (trimmed.startsWith("# Subtest: ")) {
|
|
67
|
+
openChild(stack);
|
|
68
|
+
stack.push({
|
|
69
|
+
name: unescapeTapName(trimmed.slice("# Subtest: ".length).trimEnd()),
|
|
70
|
+
indent: countIndent(line),
|
|
71
|
+
hasChildren: false,
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (trimmed.startsWith("#")) {
|
|
76
|
+
return;
|
|
77
77
|
}
|
|
78
|
+
recordTapResult(stack, outcomes, line);
|
|
79
|
+
}
|
|
80
|
+
function finishTapFile(outcomes, filePath) {
|
|
78
81
|
const leaves = outcomes.filter((outcome) => outcome.title !== filePath);
|
|
79
82
|
const fileLevelFailed = outcomes.some((outcome) => outcome.title === filePath && !outcome.passed);
|
|
80
83
|
return {
|
|
@@ -82,3 +85,12 @@ export function parseTap(tap, filePath) {
|
|
|
82
85
|
loadFailed: fileLevelFailed && leaves.length === 0,
|
|
83
86
|
};
|
|
84
87
|
}
|
|
88
|
+
export function parseTap(tap, filePath) {
|
|
89
|
+
const stack = [];
|
|
90
|
+
const outcomes = [];
|
|
91
|
+
for (const rawLine of tap.split("\n")) {
|
|
92
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
93
|
+
recordTapLine(stack, outcomes, line);
|
|
94
|
+
}
|
|
95
|
+
return finishTapFile(outcomes, filePath);
|
|
96
|
+
}
|
|
@@ -58,53 +58,56 @@ function e606TwoTasks(open, task) {
|
|
|
58
58
|
}
|
|
59
59
|
// SPEC §4.12.2 exit gates: the evidence each phase owes before the next
|
|
60
60
|
// phase opens. Implementation and integrate owe no exit evidence.
|
|
61
|
+
function missingDiscoveryEvidence(task) {
|
|
62
|
+
const missing = [];
|
|
63
|
+
if (task.spec.length === 0) {
|
|
64
|
+
missing.push("spec refs");
|
|
65
|
+
}
|
|
66
|
+
if (task.type === "fix" && (task.diagnosis ?? "").length === 0) {
|
|
67
|
+
missing.push("diagnosis");
|
|
68
|
+
}
|
|
69
|
+
return missing;
|
|
70
|
+
}
|
|
71
|
+
function missingPlanningEvidence(task) {
|
|
72
|
+
const missing = [];
|
|
73
|
+
if (task.files.allow.length === 0) {
|
|
74
|
+
missing.push("files.allow globs");
|
|
75
|
+
}
|
|
76
|
+
if (task.acceptanceCriteria.length === 0) {
|
|
77
|
+
missing.push("acceptance criteria");
|
|
78
|
+
}
|
|
79
|
+
if ((task.tests ?? []).length === 0) {
|
|
80
|
+
missing.push("declared tests");
|
|
81
|
+
}
|
|
82
|
+
if (task.redProof === undefined) {
|
|
83
|
+
missing.push("red proof");
|
|
84
|
+
}
|
|
85
|
+
return missing;
|
|
86
|
+
}
|
|
87
|
+
function missingRecordEvidence(task) {
|
|
88
|
+
const missing = [];
|
|
89
|
+
if (task.historyStaged !== true) {
|
|
90
|
+
missing.push("staged history entry");
|
|
91
|
+
}
|
|
92
|
+
if (task.type === "fix" && task.bugRowUpdated !== true) {
|
|
93
|
+
missing.push("bug row update");
|
|
94
|
+
}
|
|
95
|
+
return missing;
|
|
96
|
+
}
|
|
61
97
|
function missingExitEvidence(task, phase) {
|
|
62
98
|
switch (phase) {
|
|
63
|
-
case "discovery":
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (task.type === "fix" && (task.diagnosis ?? "").length === 0) {
|
|
69
|
-
missing.push("diagnosis");
|
|
70
|
-
}
|
|
71
|
-
return missing;
|
|
72
|
-
}
|
|
73
|
-
case "planning": {
|
|
74
|
-
const missing = [];
|
|
75
|
-
if (task.files.allow.length === 0) {
|
|
76
|
-
missing.push("files.allow globs");
|
|
77
|
-
}
|
|
78
|
-
if (task.acceptanceCriteria.length === 0) {
|
|
79
|
-
missing.push("acceptance criteria");
|
|
80
|
-
}
|
|
81
|
-
if ((task.tests ?? []).length === 0) {
|
|
82
|
-
missing.push("declared tests");
|
|
83
|
-
}
|
|
84
|
-
if (task.redProof === undefined) {
|
|
85
|
-
missing.push("red proof");
|
|
86
|
-
}
|
|
87
|
-
return missing;
|
|
88
|
-
}
|
|
89
|
-
case "validation": {
|
|
99
|
+
case "discovery":
|
|
100
|
+
return missingDiscoveryEvidence(task);
|
|
101
|
+
case "planning":
|
|
102
|
+
return missingPlanningEvidence(task);
|
|
103
|
+
case "validation":
|
|
90
104
|
return task.greenProof === undefined ? ["green proof"] : [];
|
|
91
|
-
|
|
92
|
-
case "review": {
|
|
105
|
+
case "review":
|
|
93
106
|
return missingReviewEvidence(task);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (task.historyStaged !== true) {
|
|
98
|
-
missing.push("staged history entry");
|
|
99
|
-
}
|
|
100
|
-
if (task.type === "fix" && task.bugRowUpdated !== true) {
|
|
101
|
-
missing.push("bug row update");
|
|
102
|
-
}
|
|
103
|
-
return missing;
|
|
104
|
-
}
|
|
105
|
-
default: {
|
|
107
|
+
case "record":
|
|
108
|
+
return missingRecordEvidence(task);
|
|
109
|
+
default:
|
|
106
110
|
return [];
|
|
107
|
-
}
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
function missingReviewEvidence(task) {
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
const SEVERITIES = ["blocker", "major", "minor", "nit"];
|
|
2
|
+
function invalidFindingReason(finding) {
|
|
3
|
+
if (typeof finding.id !== "string" ||
|
|
4
|
+
typeof finding.severity !== "string" ||
|
|
5
|
+
typeof finding.path !== "string" ||
|
|
6
|
+
typeof finding.message !== "string" ||
|
|
7
|
+
finding.id.length === 0 ||
|
|
8
|
+
finding.message.length === 0) {
|
|
9
|
+
return "a finding misses id, severity, path or message";
|
|
10
|
+
}
|
|
11
|
+
if (!SEVERITIES.includes(finding.severity)) {
|
|
12
|
+
return `severity "${finding.severity}" is not blocker, major, minor or nit`;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
2
16
|
function malformedReason(file) {
|
|
3
17
|
if (!Array.isArray(file.findings)) {
|
|
4
18
|
return "findings is not a list";
|
|
5
19
|
}
|
|
6
20
|
const seen = new Set();
|
|
7
21
|
for (const finding of file.findings) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
typeof finding.message !== "string" ||
|
|
12
|
-
finding.id.length === 0 ||
|
|
13
|
-
finding.message.length === 0) {
|
|
14
|
-
return "a finding misses id, severity, path or message";
|
|
15
|
-
}
|
|
16
|
-
if (!SEVERITIES.includes(finding.severity)) {
|
|
17
|
-
return `severity "${finding.severity}" is not blocker, major, minor or nit`;
|
|
22
|
+
const reason = invalidFindingReason(finding);
|
|
23
|
+
if (reason !== undefined) {
|
|
24
|
+
return reason;
|
|
18
25
|
}
|
|
19
26
|
if (seen.has(finding.id)) {
|
|
20
27
|
return `duplicate finding id "${finding.id}"`;
|
package/dist/kernel/git.js
CHANGED
|
@@ -84,7 +84,10 @@ export class NodeGit {
|
|
|
84
84
|
return ok(result.stdout);
|
|
85
85
|
}
|
|
86
86
|
async show(rev, path) {
|
|
87
|
-
|
|
87
|
+
// Assembled off the call: an inline template inside exec( trips the
|
|
88
|
+
// exec-template pattern (SPEC §6.1), while argv entries never run a shell.
|
|
89
|
+
const revPath = `${rev}:${path}`;
|
|
90
|
+
const result = await this.exec(["git", "show", revPath]);
|
|
88
91
|
const failure = procFailure("show", result);
|
|
89
92
|
if (failure !== undefined) {
|
|
90
93
|
return err(failure);
|
package/dist/kernel/pattern.js
CHANGED
|
@@ -18,99 +18,133 @@ function backreferenceError() {
|
|
|
18
18
|
function invalidError() {
|
|
19
19
|
return err(makeError("CDX-E902", "pattern has unbalanced parentheses", "Balance every opening parenthesis before using the pattern."));
|
|
20
20
|
}
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
// The brace-quantifier pattern lives at module scope so its quantifiers
|
|
22
|
+
// never count as branches of the function that applies it.
|
|
23
|
+
const BRACE_QUANTIFIER_PATTERN = /^\{\d+(,\d*)?\}/;
|
|
24
|
+
function scanPatternEscape(pattern, state) {
|
|
25
|
+
const next = pattern.charAt(state.index + 1);
|
|
26
|
+
if (next !== "" && next >= "1" && next <= "9") {
|
|
27
|
+
return backreferenceError();
|
|
26
28
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (current === "[") {
|
|
44
|
-
index += 1;
|
|
45
|
-
if (pattern.charAt(index) === "^") {
|
|
46
|
-
index += 1;
|
|
47
|
-
}
|
|
48
|
-
if (pattern.charAt(index) === "]") {
|
|
49
|
-
index += 1;
|
|
50
|
-
}
|
|
51
|
-
while (index < pattern.length && pattern.charAt(index) !== "]") {
|
|
52
|
-
if (pattern.charAt(index) === "\\") {
|
|
53
|
-
index += 1;
|
|
54
|
-
}
|
|
55
|
-
index += 1;
|
|
56
|
-
}
|
|
57
|
-
index += 1;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (current === "(") {
|
|
61
|
-
groups.push({ quantifiedInside });
|
|
62
|
-
quantifiedInside = false;
|
|
63
|
-
index += 1;
|
|
64
|
-
// SPEC §9.2: a ? directly after ( opens a non-capturing or
|
|
65
|
-
// lookaround group; it quantifies nothing.
|
|
66
|
-
if (pattern.charAt(index) === "?") {
|
|
67
|
-
index += 1;
|
|
68
|
-
const marker = pattern.charAt(index);
|
|
69
|
-
if (marker === ":" || marker === "=" || marker === "!") {
|
|
70
|
-
index += 1;
|
|
71
|
-
}
|
|
72
|
-
else if (marker === "<") {
|
|
73
|
-
const close = pattern.indexOf(">", index);
|
|
74
|
-
if (close >= 0) {
|
|
75
|
-
index = close + 1;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (current === ")") {
|
|
82
|
-
const frame = groups.pop();
|
|
83
|
-
if (frame === undefined) {
|
|
84
|
-
return invalidError();
|
|
85
|
-
}
|
|
86
|
-
const quantifier = pattern.charAt(index + 1);
|
|
87
|
-
const groupQuantified = quantifier === "+" || quantifier === "*" || quantifier === "?";
|
|
88
|
-
const braceQuantified = quantifier === "{";
|
|
89
|
-
if ((groupQuantified || braceQuantified) && quantifiedInside) {
|
|
90
|
-
return nestedError();
|
|
91
|
-
}
|
|
92
|
-
quantifiedInside =
|
|
93
|
-
frame.quantifiedInside || groupQuantified || braceQuantified;
|
|
29
|
+
if (next === "k") {
|
|
30
|
+
return backreferenceError();
|
|
31
|
+
}
|
|
32
|
+
state.index += 2;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function scanPatternClass(pattern, state) {
|
|
36
|
+
let index = state.index + 1;
|
|
37
|
+
if (pattern.charAt(index) === "^") {
|
|
38
|
+
index += 1;
|
|
39
|
+
}
|
|
40
|
+
if (pattern.charAt(index) === "]") {
|
|
41
|
+
index += 1;
|
|
42
|
+
}
|
|
43
|
+
while (index < pattern.length && pattern.charAt(index) !== "]") {
|
|
44
|
+
if (pattern.charAt(index) === "\\") {
|
|
94
45
|
index += 1;
|
|
95
|
-
continue;
|
|
96
46
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
47
|
+
index += 1;
|
|
48
|
+
}
|
|
49
|
+
state.index = index + 1;
|
|
50
|
+
}
|
|
51
|
+
function openPatternGroup(pattern, state) {
|
|
52
|
+
state.groups.push({ quantifiedInside: state.quantifiedInside });
|
|
53
|
+
state.quantifiedInside = false;
|
|
54
|
+
const afterParen = state.index + 1;
|
|
55
|
+
// SPEC §9.2: a ? directly after ( opens a non-capturing or
|
|
56
|
+
// lookaround group; it quantifies nothing.
|
|
57
|
+
if (pattern.charAt(afterParen) !== "?") {
|
|
58
|
+
state.index = afterParen;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const marker = pattern.charAt(afterParen + 1);
|
|
62
|
+
if (marker === ":" || marker === "=" || marker === "!") {
|
|
63
|
+
state.index = afterParen + 2;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (marker === "<") {
|
|
67
|
+
const close = pattern.indexOf(">", afterParen + 1);
|
|
68
|
+
state.index = close >= 0 ? close + 1 : afterParen + 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
state.index = afterParen + 1;
|
|
72
|
+
}
|
|
73
|
+
function isGroupQuantifier(quantifier) {
|
|
74
|
+
return (quantifier === "+" ||
|
|
75
|
+
quantifier === "*" ||
|
|
76
|
+
quantifier === "?" ||
|
|
77
|
+
quantifier === "{");
|
|
78
|
+
}
|
|
79
|
+
function closePatternGroup(pattern, state) {
|
|
80
|
+
const frame = state.groups.pop();
|
|
81
|
+
if (frame === undefined) {
|
|
82
|
+
return invalidError();
|
|
83
|
+
}
|
|
84
|
+
const quantified = isGroupQuantifier(pattern.charAt(state.index + 1));
|
|
85
|
+
if (quantified && state.quantifiedInside) {
|
|
86
|
+
return nestedError();
|
|
87
|
+
}
|
|
88
|
+
state.quantifiedInside = frame.quantifiedInside || quantified;
|
|
89
|
+
state.index += 1;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
function markPatternQuantifier(pattern, state) {
|
|
93
|
+
const current = pattern.charAt(state.index);
|
|
94
|
+
if (current === "{") {
|
|
95
|
+
if (BRACE_QUANTIFIER_PATTERN.test(pattern.slice(state.index))) {
|
|
96
|
+
state.quantifiedInside = true;
|
|
101
97
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
state.quantifiedInside = true;
|
|
101
|
+
}
|
|
102
|
+
state.index += 1;
|
|
103
|
+
}
|
|
104
|
+
function isQuantifierStart(current) {
|
|
105
|
+
return (current === "+" || current === "*" || current === "?" || current === "{");
|
|
106
|
+
}
|
|
107
|
+
function scanPatternStep(pattern, state) {
|
|
108
|
+
const current = pattern.charAt(state.index);
|
|
109
|
+
if (current === "\\") {
|
|
110
|
+
return scanPatternEscape(pattern, state);
|
|
111
|
+
}
|
|
112
|
+
if (current === "[") {
|
|
113
|
+
scanPatternClass(pattern, state);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (current === "(") {
|
|
117
|
+
openPatternGroup(pattern, state);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
if (current === ")") {
|
|
121
|
+
return closePatternGroup(pattern, state);
|
|
122
|
+
}
|
|
123
|
+
if (isQuantifierStart(current)) {
|
|
124
|
+
markPatternQuantifier(pattern, state);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
state.index += 1;
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
function scanPatternBody(pattern) {
|
|
131
|
+
const state = { groups: [], quantifiedInside: false, index: 0 };
|
|
132
|
+
while (state.index < pattern.length) {
|
|
133
|
+
const failed = scanPatternStep(pattern, state);
|
|
134
|
+
if (failed !== undefined) {
|
|
135
|
+
return failed;
|
|
109
136
|
}
|
|
110
|
-
index += 1;
|
|
111
137
|
}
|
|
112
|
-
if (groups.length > 0) {
|
|
138
|
+
if (state.groups.length > 0) {
|
|
113
139
|
return invalidError();
|
|
114
140
|
}
|
|
115
141
|
return ok(undefined);
|
|
116
142
|
}
|
|
143
|
+
// SPEC §9.2: scan with escape and character-class awareness so an escaped
|
|
144
|
+
// parenthesis or a bracketed quantifier cannot hide a nested quantifier.
|
|
145
|
+
export function checkPattern(pattern) {
|
|
146
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
147
|
+
return tooLongError();
|
|
148
|
+
}
|
|
149
|
+
return scanPatternBody(pattern);
|
|
150
|
+
}
|
|
@@ -15,37 +15,51 @@ export const DEFAULT_FIXTURE_GLOBS = [
|
|
|
15
15
|
"**/test-data/**",
|
|
16
16
|
"**/*.fixture.*",
|
|
17
17
|
];
|
|
18
|
+
// The home-prefix patterns live at module scope so their quantifiers
|
|
19
|
+
// never count as branches of the function that applies them.
|
|
20
|
+
const HOME_PREFIX_PATTERNS = [
|
|
21
|
+
/^~[/\\]?/,
|
|
22
|
+
/^\$home[/\\]?/,
|
|
23
|
+
/^\$\{home\}[/\\]?/,
|
|
24
|
+
/^%userprofile%[/\\]?/,
|
|
25
|
+
];
|
|
26
|
+
function isEnvSecretFile(base) {
|
|
27
|
+
return (base === ".env" || (base.startsWith(".env.") && base !== ".env.example"));
|
|
28
|
+
}
|
|
29
|
+
function isKeyMaterialFile(base) {
|
|
30
|
+
return (base.endsWith(".pem") ||
|
|
31
|
+
base.endsWith(".key") ||
|
|
32
|
+
base.endsWith(".p12") ||
|
|
33
|
+
base.endsWith(".pfx") ||
|
|
34
|
+
base.startsWith("id_rsa") ||
|
|
35
|
+
base.startsWith("id_ed25519"));
|
|
36
|
+
}
|
|
37
|
+
function homeRelativePath(slashed) {
|
|
38
|
+
let relative = slashed.toLowerCase();
|
|
39
|
+
for (const pattern of HOME_PREFIX_PATTERNS) {
|
|
40
|
+
relative = relative.replace(pattern, "");
|
|
41
|
+
}
|
|
42
|
+
return relative;
|
|
43
|
+
}
|
|
44
|
+
function isSecretDirPath(slashed) {
|
|
45
|
+
const segments = homeRelativePath(slashed).split("/");
|
|
46
|
+
return (segments.some((segment) => SECRET_DIR_SEGMENTS.has(segment)) ||
|
|
47
|
+
(segments[0] === ".config" && segments[1] === "gh"));
|
|
48
|
+
}
|
|
18
49
|
// SPEC §4.7.1: the secret-read guard paths.
|
|
19
50
|
export function isSecretReadPath(path) {
|
|
20
51
|
const slashed = path.replace(/\\/g, "/");
|
|
21
52
|
const base = slashed.split("/").pop() ?? "";
|
|
22
|
-
if (base
|
|
23
|
-
(base.startsWith(".env.") && base !== ".env.example")) {
|
|
53
|
+
if (isEnvSecretFile(base)) {
|
|
24
54
|
return true;
|
|
25
55
|
}
|
|
26
|
-
if (base
|
|
27
|
-
base.endsWith(".key") ||
|
|
28
|
-
base.endsWith(".p12") ||
|
|
29
|
-
base.endsWith(".pfx") ||
|
|
30
|
-
base.startsWith("id_rsa") ||
|
|
31
|
-
base.startsWith("id_ed25519")) {
|
|
56
|
+
if (isKeyMaterialFile(base)) {
|
|
32
57
|
return true;
|
|
33
58
|
}
|
|
34
59
|
if (SECRET_BASENAMES.has(base)) {
|
|
35
60
|
return true;
|
|
36
61
|
}
|
|
37
|
-
|
|
38
|
-
const homeRelative = lowered
|
|
39
|
-
.replace(/^~[/\\]?/, "")
|
|
40
|
-
.replace(/^\$home[/\\]?/, "")
|
|
41
|
-
.replace(/^\$\{home\}[/\\]?/, "")
|
|
42
|
-
.replace(/^%userprofile%[/\\]?/, "");
|
|
43
|
-
const segments = homeRelative.split("/");
|
|
44
|
-
if (segments.some((segment) => SECRET_DIR_SEGMENTS.has(segment)) ||
|
|
45
|
-
(segments[0] === ".config" && segments[1] === "gh")) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
62
|
+
return isSecretDirPath(slashed);
|
|
49
63
|
}
|
|
50
64
|
// SPEC §4.7.1: placeholder credentials and test vectors under a declared
|
|
51
65
|
// fixture glob are allowlisted. The kernel default covers the common
|
package/dist/kernel/shell-cmd.js
CHANGED
|
@@ -85,6 +85,25 @@ function skipFlagValues(words, valueFlags) {
|
|
|
85
85
|
}
|
|
86
86
|
return kept;
|
|
87
87
|
}
|
|
88
|
+
const SUDO_VALUE_FLAGS = new Set([
|
|
89
|
+
"-u",
|
|
90
|
+
"--user",
|
|
91
|
+
"-g",
|
|
92
|
+
"--group",
|
|
93
|
+
]);
|
|
94
|
+
function stripSudoPrefix(rest) {
|
|
95
|
+
const skipped = skipFlagValues(rest.slice(1), SUDO_VALUE_FLAGS);
|
|
96
|
+
if (skipped.length > 0 &&
|
|
97
|
+
(skipped[0] === "-c" || skipped[0] === "--command")) {
|
|
98
|
+
const script = skipped[1];
|
|
99
|
+
return {
|
|
100
|
+
rest: [],
|
|
101
|
+
scripts: script === undefined ? [] : [script],
|
|
102
|
+
bare: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { rest: skipped, scripts: [], bare: false };
|
|
106
|
+
}
|
|
88
107
|
function stripPrefixes(words) {
|
|
89
108
|
let rest = [...words];
|
|
90
109
|
let raised = false;
|
|
@@ -100,13 +119,11 @@ function stripPrefixes(words) {
|
|
|
100
119
|
}
|
|
101
120
|
const lowered = baseName(head).toLowerCase();
|
|
102
121
|
if (lowered === "sudo" || lowered === "su") {
|
|
122
|
+
const sudo = stripSudoPrefix(rest);
|
|
103
123
|
raised = true;
|
|
104
|
-
rest =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (script !== undefined) {
|
|
108
|
-
scripts.push(script);
|
|
109
|
-
}
|
|
124
|
+
rest = [...sudo.rest];
|
|
125
|
+
scripts.push(...sudo.scripts);
|
|
126
|
+
if (sudo.bare) {
|
|
110
127
|
return { rest: [], raised, scripts, bare: true };
|
|
111
128
|
}
|
|
112
129
|
continue;
|