@codapult/guard 0.9.0 → 0.11.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/README.md +154 -10
- package/dist/adapters/agents/agent-integration.js +26 -3
- package/dist/cli/commands/guard.d.ts +2 -0
- package/dist/cli/commands/guard.js +77 -11
- package/dist/cli/index.js +2 -0
- package/dist/core/guard.d.ts +28 -1
- package/dist/core/guard.js +37 -0
- package/dist/core/history/runs.d.ts +107 -2
- package/dist/core/history/runs.js +244 -18
- package/dist/core/output/attestation.d.ts +17 -0
- package/dist/core/output/attestation.js +22 -0
- package/dist/core/policy/schemas.d.ts +47 -0
- package/dist/core/policy/schemas.js +67 -0
- package/dist/core/verification/github-actions.d.ts +25 -0
- package/dist/core/verification/github-actions.js +397 -0
- package/dist/core/verification/github-context.d.ts +8 -0
- package/dist/core/verification/github-context.js +41 -0
- package/dist/core/verification/verify.d.ts +23 -4
- package/dist/core/verification/verify.js +701 -40
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/prompts.js +7 -4
- package/dist/mcp/tools/guard.js +9 -3
- package/docs/extensions.md +79 -0
- package/docs/guard-agent-integration.md +51 -0
- package/docs/guard-ci.yml +202 -0
- package/docs/guard-fixtures.md +127 -0
- package/docs/guard-trusted-pr-attestation.yml +213 -0
- package/docs/guard.md +1125 -0
- package/docs/integrations/README.md +38 -0
- package/docs/integrations/claude-code.md +92 -0
- package/docs/integrations/codex.md +58 -0
- package/docs/integrations/cursor.md +116 -0
- package/docs/integrations/gemini-cli.md +47 -0
- package/docs/integrations/generic.md +41 -0
- package/docs/integrations/github-copilot.md +72 -0
- package/docs/policy-mutations.md +75 -0
- package/docs/releasing.md +58 -0
- package/docs/verify-guard-attestation.mjs +166 -0
- package/package.json +11 -9
|
@@ -1,9 +1,275 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { extname, relative, resolve, sep } from 'node:path';
|
|
1
6
|
import {} from '../../adapters/command.js';
|
|
2
7
|
import { runProjectAdapters, runProjectChecks, runWorkspaceProjectChecks, inspectProjectRuntime, } from '../../adapters/project-checks.js';
|
|
3
|
-
import { loadGuardPolicyAtRevision, loadGuardWorkingTreePolicy, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
|
|
8
|
+
import { GUARD_DIR, GUARD_HISTORY_DIR, loadGuardPolicyAtRevision, loadGuardWorkingTreePolicy, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
|
|
4
9
|
import { analyzeProjectImpact } from '../analysis/impact.js';
|
|
5
10
|
import { discoverProject } from '../discovery/discovery.js';
|
|
6
11
|
import { finishGuardRun, recordGuardRunStage, startGuardRun, } from '../history/runs.js';
|
|
12
|
+
import { auditGitHubActions } from './github-actions.js';
|
|
13
|
+
import { readGuardGitHubContext } from './github-context.js';
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const packageJson = require('../../../package.json');
|
|
16
|
+
const DEFAULT_PROTECTED_PATHS = [
|
|
17
|
+
'.codapult/guard/**',
|
|
18
|
+
'.github/workflows/**',
|
|
19
|
+
'CODEOWNERS',
|
|
20
|
+
'.github/CODEOWNERS',
|
|
21
|
+
'package.json',
|
|
22
|
+
'**/package.json',
|
|
23
|
+
'pnpm-lock.yaml',
|
|
24
|
+
'**/pnpm-lock.yaml',
|
|
25
|
+
'yarn.lock',
|
|
26
|
+
'**/yarn.lock',
|
|
27
|
+
'package-lock.json',
|
|
28
|
+
'**/package-lock.json',
|
|
29
|
+
'npm-shrinkwrap.json',
|
|
30
|
+
'**/npm-shrinkwrap.json',
|
|
31
|
+
'bun.lock',
|
|
32
|
+
'**/bun.lock',
|
|
33
|
+
'bun.lockb',
|
|
34
|
+
'**/bun.lockb',
|
|
35
|
+
'pnpm-workspace.yaml',
|
|
36
|
+
'**/pnpm-workspace.yaml',
|
|
37
|
+
'.npmrc',
|
|
38
|
+
'**/.npmrc',
|
|
39
|
+
'.pnpmfile.cjs',
|
|
40
|
+
'**/.pnpmfile.cjs',
|
|
41
|
+
'pnpmfile.cjs',
|
|
42
|
+
'**/pnpmfile.cjs',
|
|
43
|
+
'.yarnrc',
|
|
44
|
+
'**/.yarnrc',
|
|
45
|
+
'.yarnrc.yml',
|
|
46
|
+
'**/.yarnrc.yml',
|
|
47
|
+
'.pnp.cjs',
|
|
48
|
+
'**/.pnp.cjs',
|
|
49
|
+
'.pnp.loader.mjs',
|
|
50
|
+
'**/.pnp.loader.mjs',
|
|
51
|
+
'.gitignore',
|
|
52
|
+
'.ignore',
|
|
53
|
+
'.dockerignore',
|
|
54
|
+
'.npmignore',
|
|
55
|
+
'.prettierignore',
|
|
56
|
+
'.eslintignore',
|
|
57
|
+
'eslint.config.*',
|
|
58
|
+
'.eslintrc*',
|
|
59
|
+
'biome.json',
|
|
60
|
+
'biome.jsonc',
|
|
61
|
+
'.biomeignore',
|
|
62
|
+
'prettier.config.*',
|
|
63
|
+
'.prettierrc*',
|
|
64
|
+
'tsconfig.json',
|
|
65
|
+
'vitest.config.*',
|
|
66
|
+
'jest.config.*',
|
|
67
|
+
'playwright.config.*',
|
|
68
|
+
'next.config.*',
|
|
69
|
+
'babel.config.*',
|
|
70
|
+
'.babelrc*',
|
|
71
|
+
'webpack.config.*',
|
|
72
|
+
'stylelint.config.*',
|
|
73
|
+
'.stylelintrc*',
|
|
74
|
+
'nyc.config.*',
|
|
75
|
+
'tsconfig*.json',
|
|
76
|
+
];
|
|
77
|
+
function matchesPathPattern(path, pattern) {
|
|
78
|
+
let expression = '^';
|
|
79
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
80
|
+
const character = pattern[index];
|
|
81
|
+
if (character === '*' && pattern[index + 1] === '*') {
|
|
82
|
+
if (pattern[index + 2] === '/') {
|
|
83
|
+
expression += '(?:.*/)?';
|
|
84
|
+
index += 2;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
expression += '.*';
|
|
88
|
+
index += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (character === '*')
|
|
92
|
+
expression += '[^/]*';
|
|
93
|
+
else if (character === '?')
|
|
94
|
+
expression += '[^/]';
|
|
95
|
+
else
|
|
96
|
+
expression += '.+^${}()|[]\\'.includes(character) ? `\\${character}` : character;
|
|
97
|
+
}
|
|
98
|
+
return new RegExp(`${expression}$`).test(path);
|
|
99
|
+
}
|
|
100
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
101
|
+
'.js',
|
|
102
|
+
'.jsx',
|
|
103
|
+
'.ts',
|
|
104
|
+
'.tsx',
|
|
105
|
+
'.mts',
|
|
106
|
+
'.cts',
|
|
107
|
+
'.mjs',
|
|
108
|
+
'.cjs',
|
|
109
|
+
'.html',
|
|
110
|
+
'.md',
|
|
111
|
+
'.mdx',
|
|
112
|
+
'.py',
|
|
113
|
+
'.pyi',
|
|
114
|
+
'.sh',
|
|
115
|
+
'.vue',
|
|
116
|
+
'.svelte',
|
|
117
|
+
]);
|
|
118
|
+
const SUPPRESSION_COMMENT = /(?:\/\/|\/\*|\*|<!--|#)\s*(eslint-disable|@ts-ignore|@ts-nocheck|@ts-expect-error|biome-ignore|prettier-ignore|tslint:disable|stylelint-disable|standard:disable|svelte-ignore|noqa|nosec|type:\s*ignore|pragma:\s*no\s+cover|istanbul\s+ignore|c8\s+ignore)\b/i;
|
|
119
|
+
function addedSuppressionMarkers(root, baseRef) {
|
|
120
|
+
if (!baseRef)
|
|
121
|
+
return undefined;
|
|
122
|
+
try {
|
|
123
|
+
const diffs = [
|
|
124
|
+
execFileSync('git', ['diff', '--no-ext-diff', '--unified=0', `${baseRef}...HEAD`], {
|
|
125
|
+
cwd: root,
|
|
126
|
+
stdio: 'pipe',
|
|
127
|
+
timeout: 5_000,
|
|
128
|
+
maxBuffer: 8_000_000,
|
|
129
|
+
}).toString(),
|
|
130
|
+
execFileSync('git', ['diff', '--no-ext-diff', '--unified=0', 'HEAD'], {
|
|
131
|
+
cwd: root,
|
|
132
|
+
stdio: 'pipe',
|
|
133
|
+
timeout: 5_000,
|
|
134
|
+
maxBuffer: 8_000_000,
|
|
135
|
+
}).toString(),
|
|
136
|
+
];
|
|
137
|
+
const found = new Set();
|
|
138
|
+
for (const diff of diffs) {
|
|
139
|
+
let file = '';
|
|
140
|
+
for (const line of diff.split('\n')) {
|
|
141
|
+
if (line.startsWith('+++ "'))
|
|
142
|
+
return undefined;
|
|
143
|
+
if (line.startsWith('+++ b/'))
|
|
144
|
+
file = line.slice(6);
|
|
145
|
+
else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
146
|
+
const extension = extname(file).toLowerCase();
|
|
147
|
+
const match = SUPPRESSION_COMMENT.exec(line);
|
|
148
|
+
if (file && SOURCE_EXTENSIONS.has(extension) && match?.[1]) {
|
|
149
|
+
found.add(`${file}:${match[1].toLowerCase()}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
|
|
155
|
+
cwd: root,
|
|
156
|
+
stdio: 'pipe',
|
|
157
|
+
timeout: 5_000,
|
|
158
|
+
maxBuffer: 8_000_000,
|
|
159
|
+
})
|
|
160
|
+
.toString()
|
|
161
|
+
.split('\0')
|
|
162
|
+
.filter(Boolean);
|
|
163
|
+
const realRoot = realpathSync(root);
|
|
164
|
+
for (const file of untracked) {
|
|
165
|
+
if (!SOURCE_EXTENSIONS.has(extname(file).toLowerCase()))
|
|
166
|
+
continue;
|
|
167
|
+
const absolutePath = resolve(root, file);
|
|
168
|
+
const realFilePath = realpathSync(absolutePath);
|
|
169
|
+
const fileRelative = relative(realRoot, realFilePath);
|
|
170
|
+
if (fileRelative === '..' || fileRelative.startsWith(`..${sep}`))
|
|
171
|
+
return undefined;
|
|
172
|
+
const stats = lstatSync(absolutePath);
|
|
173
|
+
if (stats.isSymbolicLink() || !stats.isFile() || stats.size > 2_000_000)
|
|
174
|
+
return undefined;
|
|
175
|
+
const source = readFileSync(absolutePath, 'utf8');
|
|
176
|
+
if (source.includes('\0'))
|
|
177
|
+
continue;
|
|
178
|
+
for (const line of source.split('\n')) {
|
|
179
|
+
const match = SUPPRESSION_COMMENT.exec(line);
|
|
180
|
+
if (match?.[1])
|
|
181
|
+
found.add(`${file}:${match[1].toLowerCase()}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...found].sort();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function isGeneratedGuardArtifact(path) {
|
|
191
|
+
const normalized = path.replaceAll('\\', '/');
|
|
192
|
+
return (normalized === `${GUARD_DIR}/cache.json` ||
|
|
193
|
+
normalized === GUARD_HISTORY_DIR ||
|
|
194
|
+
normalized.startsWith(`${GUARD_HISTORY_DIR}/`));
|
|
195
|
+
}
|
|
196
|
+
function gitPaths(root, commands) {
|
|
197
|
+
try {
|
|
198
|
+
const pathLists = commands.map((args) => execFileSync('git', args, {
|
|
199
|
+
cwd: root,
|
|
200
|
+
stdio: 'pipe',
|
|
201
|
+
timeout: 5_000,
|
|
202
|
+
}).toString());
|
|
203
|
+
return [...new Set(pathLists.flatMap((paths) => paths.split('\0')))]
|
|
204
|
+
.map((file) => file.replace(/\\/g, '/'))
|
|
205
|
+
.filter((file) => Boolean(file) && !isGeneratedGuardArtifact(file))
|
|
206
|
+
.sort();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function committedFilesSince(root, baseRef) {
|
|
213
|
+
if (!baseRef)
|
|
214
|
+
return undefined;
|
|
215
|
+
return gitPaths(root, [['diff', '--name-only', '-z', `${baseRef}...HEAD`]]);
|
|
216
|
+
}
|
|
217
|
+
function workingTreeChangedFiles(root) {
|
|
218
|
+
return gitPaths(root, [
|
|
219
|
+
['diff', '--name-only', '-z', 'HEAD'],
|
|
220
|
+
['ls-files', '--others', '--exclude-standard', '-z'],
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
function changedFilesSince(root, baseRef) {
|
|
224
|
+
if (!baseRef)
|
|
225
|
+
return undefined;
|
|
226
|
+
const committed = committedFilesSince(root, baseRef);
|
|
227
|
+
const workingTree = workingTreeChangedFiles(root);
|
|
228
|
+
if (!committed || !workingTree)
|
|
229
|
+
return undefined;
|
|
230
|
+
return [...new Set([...committed, ...workingTree])].sort();
|
|
231
|
+
}
|
|
232
|
+
function getPolicySurface(root, baseRef, configPaths = [], policySurfaceConfig) {
|
|
233
|
+
const baseChangedPaths = changedFilesSince(root, baseRef);
|
|
234
|
+
const changedPaths = baseChangedPaths ?? [];
|
|
235
|
+
const suppressionMarkers = addedSuppressionMarkers(root, baseRef);
|
|
236
|
+
const defaultEntries = DEFAULT_PROTECTED_PATHS.map((pattern) => ({
|
|
237
|
+
pattern,
|
|
238
|
+
kind: pattern.startsWith('.github/workflows/')
|
|
239
|
+
? 'ci-workflow'
|
|
240
|
+
: pattern.startsWith('.codapult/guard/')
|
|
241
|
+
? 'guard-policy'
|
|
242
|
+
: pattern.toLowerCase().includes('codeowners')
|
|
243
|
+
? 'ownership'
|
|
244
|
+
: ['package.json', 'lock.yaml', 'lock.json', '.npmrc'].some((part) => pattern.includes(part))
|
|
245
|
+
? 'dependency-input'
|
|
246
|
+
: pattern.includes('ignore')
|
|
247
|
+
? 'ignore-config'
|
|
248
|
+
: 'tool-config',
|
|
249
|
+
severity: 'error',
|
|
250
|
+
}));
|
|
251
|
+
const entries = [
|
|
252
|
+
...defaultEntries,
|
|
253
|
+
...configPaths.map((pattern) => ({ pattern, kind: 'custom', severity: 'error' })),
|
|
254
|
+
...(policySurfaceConfig?.paths ?? []).map((entry) => ({
|
|
255
|
+
...entry,
|
|
256
|
+
severity: entry.severity ?? 'error',
|
|
257
|
+
})),
|
|
258
|
+
];
|
|
259
|
+
const protectedPaths = [...new Set(entries.map((entry) => entry.pattern))].sort();
|
|
260
|
+
const changes = changedPaths.flatMap((path) => entries
|
|
261
|
+
.filter((entry) => matchesPathPattern(path, entry.pattern))
|
|
262
|
+
.map((entry) => ({ path, ...entry })));
|
|
263
|
+
const matched = [...new Set(changes.map((change) => change.path))].sort();
|
|
264
|
+
return {
|
|
265
|
+
protectedPaths,
|
|
266
|
+
changedPaths: matched,
|
|
267
|
+
changes,
|
|
268
|
+
suppressionMarkers: suppressionMarkers ?? [],
|
|
269
|
+
workflowChanged: matched.some((path) => path.startsWith('.github/workflows/')),
|
|
270
|
+
complete: baseChangedPaths !== undefined && suppressionMarkers !== undefined,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
7
273
|
function buildRunPolicy(snapshot, base, changedFromBase, decisionIds = []) {
|
|
8
274
|
return {
|
|
9
275
|
...(snapshot.revision !== undefined ? { revision: snapshot.revision } : {}),
|
|
@@ -14,6 +280,254 @@ function buildRunPolicy(snapshot, base, changedFromBase, decisionIds = []) {
|
|
|
14
280
|
...(decisionIds.length > 0 ? { decisionIds } : {}),
|
|
15
281
|
};
|
|
16
282
|
}
|
|
283
|
+
function stripControlCharacters(value) {
|
|
284
|
+
let output = '';
|
|
285
|
+
for (const character of value) {
|
|
286
|
+
const code = character.charCodeAt(0);
|
|
287
|
+
output += code <= 0x1f || code === 0x7f ? ' ' : character;
|
|
288
|
+
}
|
|
289
|
+
return output;
|
|
290
|
+
}
|
|
291
|
+
function sanitizeCommand(command) {
|
|
292
|
+
return stripControlCharacters(command.slice(0, 2_000))
|
|
293
|
+
.replace(/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, '[REDACTED PRIVATE KEY]')
|
|
294
|
+
.replace(/\b(?:sk_(?:live|test)_|pk_(?:live|test)_|AKIA|gh[pousr]_|github_pat_)[A-Za-z0-9_-]+/g, '[REDACTED TOKEN]')
|
|
295
|
+
.replace(new RegExp('((?:api[_-]?key|secret|token|password|authorization|database[_-]?url)\\s*[:=]\\s*)[^\\s,}]+', 'gi'), '$1[REDACTED]')
|
|
296
|
+
.replace(/(--(?:token|secret|password|api-key|authorization)(?:=|\s+))[^\s]+/gi, '$1[REDACTED]');
|
|
297
|
+
}
|
|
298
|
+
function sanitizeProjectChecks(results) {
|
|
299
|
+
return Object.fromEntries(Object.entries(results).map(([name, result]) => [
|
|
300
|
+
name,
|
|
301
|
+
{ ...result, command: sanitizeCommand(result.command) },
|
|
302
|
+
]));
|
|
303
|
+
}
|
|
304
|
+
function receiptCheck(result) {
|
|
305
|
+
const command = sanitizeCommand(result.command);
|
|
306
|
+
const args = command.trim().split(/\s+/).filter(Boolean).slice(1);
|
|
307
|
+
return {
|
|
308
|
+
status: result.status,
|
|
309
|
+
exitCode: result.exitCode,
|
|
310
|
+
durationMs: result.durationMs,
|
|
311
|
+
command,
|
|
312
|
+
args,
|
|
313
|
+
execution: result.status === 'not-configured'
|
|
314
|
+
? 'not-configured'
|
|
315
|
+
: result.stderr.includes('Unsafe or empty project command rejected.')
|
|
316
|
+
? 'rejected'
|
|
317
|
+
: 'attempted',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function effectiveVerifierInvocation(options) {
|
|
321
|
+
const args = ['--tools=' + options.tools];
|
|
322
|
+
if (!options.projectChecks)
|
|
323
|
+
args.push('--no-project-checks');
|
|
324
|
+
if (options.checks.length > 0)
|
|
325
|
+
args.push(`--checks=${options.checks.join(',')}`);
|
|
326
|
+
if (options.strict)
|
|
327
|
+
args.push('--strict');
|
|
328
|
+
if (options.changedOnly)
|
|
329
|
+
args.push('--changed');
|
|
330
|
+
if (options.policyBase)
|
|
331
|
+
args.push(`--policy-base=${options.policyBase}`);
|
|
332
|
+
if (options.failOnPolicyChange)
|
|
333
|
+
args.push('--fail-on-policy-change');
|
|
334
|
+
if (options.failOnProtectedChanges)
|
|
335
|
+
args.push('--fail-on-protected-changes');
|
|
336
|
+
return { command: 'codapult-guard verify', args };
|
|
337
|
+
}
|
|
338
|
+
function resolveGitCommit(root, ref) {
|
|
339
|
+
if (!ref)
|
|
340
|
+
return undefined;
|
|
341
|
+
try {
|
|
342
|
+
const commit = execFileSync('git', ['rev-parse', `${ref}^{commit}`], {
|
|
343
|
+
cwd: root,
|
|
344
|
+
stdio: 'pipe',
|
|
345
|
+
timeout: 5_000,
|
|
346
|
+
})
|
|
347
|
+
.toString()
|
|
348
|
+
.trim();
|
|
349
|
+
return /^[0-9a-f]{40}$/i.test(commit) ? commit : undefined;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function hasWorkingTreeChanges(root) {
|
|
356
|
+
try {
|
|
357
|
+
return (execFileSync('git', ['status', '--porcelain', '-z'], {
|
|
358
|
+
cwd: root,
|
|
359
|
+
stdio: 'pipe',
|
|
360
|
+
timeout: 5_000,
|
|
361
|
+
}).length > 0);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function sha256(bytes) {
|
|
368
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
369
|
+
}
|
|
370
|
+
function baseFileDigest(root, revisions, path) {
|
|
371
|
+
if (!path || path.startsWith('/') || path.split('/').includes('..'))
|
|
372
|
+
return undefined;
|
|
373
|
+
for (const revision of revisions) {
|
|
374
|
+
try {
|
|
375
|
+
const content = execFileSync('git', ['show', `${revision}:${path}`], {
|
|
376
|
+
cwd: root,
|
|
377
|
+
stdio: 'pipe',
|
|
378
|
+
timeout: 5_000,
|
|
379
|
+
maxBuffer: 32_000_000,
|
|
380
|
+
});
|
|
381
|
+
return sha256(content);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// A deleted working-tree file may exist at HEAD even when absent from the base ref.
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
function verificationScope(root, inventory, changedPaths, policy) {
|
|
390
|
+
const revisions = [resolveGitCommit(root, 'HEAD'), resolveGitCommit(root, policy.ref)].filter((revision) => revision !== undefined);
|
|
391
|
+
const paths = [
|
|
392
|
+
...new Set([...inventory.map((file) => file.path), ...changedPaths]
|
|
393
|
+
.map((path) => path.replaceAll('\\', '/'))
|
|
394
|
+
.filter((path) => !isGeneratedGuardArtifact(path))),
|
|
395
|
+
].sort();
|
|
396
|
+
let complete = true;
|
|
397
|
+
const files = paths.map((path) => {
|
|
398
|
+
try {
|
|
399
|
+
if (!path ||
|
|
400
|
+
path.startsWith('/') ||
|
|
401
|
+
/^[a-z]:\//i.test(path) ||
|
|
402
|
+
path.split('/').includes('..')) {
|
|
403
|
+
complete = false;
|
|
404
|
+
return { path, status: 'unavailable' };
|
|
405
|
+
}
|
|
406
|
+
const fullPath = resolve(root, path);
|
|
407
|
+
const stats = lstatSync(fullPath);
|
|
408
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
409
|
+
complete = false;
|
|
410
|
+
return { path, status: 'unavailable' };
|
|
411
|
+
}
|
|
412
|
+
return { path, status: 'present', sha256: sha256(readFileSync(fullPath)) };
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
const digest = baseFileDigest(root, revisions, path);
|
|
416
|
+
if (digest)
|
|
417
|
+
return { path, status: 'deleted', sha256: digest };
|
|
418
|
+
complete = false;
|
|
419
|
+
return { path, status: 'unavailable' };
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
const fingerprint = createHash('sha256')
|
|
423
|
+
.update(JSON.stringify({
|
|
424
|
+
algorithm: 'sha256',
|
|
425
|
+
policyFingerprint: policy.fingerprint,
|
|
426
|
+
tool: { name: '@codapult/guard', version: packageJson.version },
|
|
427
|
+
files,
|
|
428
|
+
}))
|
|
429
|
+
.digest('hex');
|
|
430
|
+
return { algorithm: 'sha256', fingerprint, complete, stable: true, files };
|
|
431
|
+
}
|
|
432
|
+
function buildVerificationReceipt(root, policy, impact, checks, workspaceChecks, adapters, architecture, contractIssues, stages, policySurface, ciIntegrity, scope, requiredStages, incompleteStages, effectiveInvocation) {
|
|
433
|
+
const githubContext = readGuardGitHubContext(process.env.GITHUB_EVENT_NAME, process.env.GITHUB_EVENT_PATH, resolveGitCommit(root, 'HEAD'), process.env.GITHUB_SHA);
|
|
434
|
+
const baseCommit = resolveGitCommit(root, policy.ref);
|
|
435
|
+
const baseChangedFiles = committedFilesSince(root, policy.ref);
|
|
436
|
+
const workingTreeFiles = workingTreeChangedFiles(root) ?? impact.requestedFiles;
|
|
437
|
+
const receiptChecks = {};
|
|
438
|
+
for (const [name, result] of Object.entries(checks)) {
|
|
439
|
+
receiptChecks[name] = receiptCheck(result);
|
|
440
|
+
}
|
|
441
|
+
for (const [workspacePackage, packageChecks] of Object.entries(workspaceChecks)) {
|
|
442
|
+
for (const [name, result] of Object.entries(packageChecks)) {
|
|
443
|
+
receiptChecks[`${workspacePackage}:${name}`] = receiptCheck(result);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const [name, result] of Object.entries(adapters)) {
|
|
447
|
+
receiptChecks[`adapter:${name}`] = receiptCheck(result);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
version: 1,
|
|
451
|
+
kind: 'verification-receipt',
|
|
452
|
+
subject: {
|
|
453
|
+
...(githubContext?.event ? { githubEvent: githubContext.event } : {}),
|
|
454
|
+
...(githubContext?.headSha ? { headSha: githubContext.headSha } : {}),
|
|
455
|
+
...(githubContext?.baseSha ? { baseSha: githubContext.baseSha } : {}),
|
|
456
|
+
...(githubContext?.mergeSha ? { mergeSha: githubContext.mergeSha } : {}),
|
|
457
|
+
...(policy.ref ? { baseRef: policy.ref } : {}),
|
|
458
|
+
...(baseCommit ? { baseCommit } : {}),
|
|
459
|
+
...(baseChangedFiles ? { baseChangedFiles } : {}),
|
|
460
|
+
...(workingTreeFiles.length > 0 ? { workingTreeFiles } : {}),
|
|
461
|
+
changedFiles: [...new Set([...(baseChangedFiles ?? []), ...workingTreeFiles])].sort(),
|
|
462
|
+
workingTreeDirty: workingTreeFiles.length > 0,
|
|
463
|
+
},
|
|
464
|
+
policy,
|
|
465
|
+
verificationScope: scope,
|
|
466
|
+
policySurface,
|
|
467
|
+
...(ciIntegrity ? { ciIntegrity } : {}),
|
|
468
|
+
requiredStages,
|
|
469
|
+
incompleteStages,
|
|
470
|
+
effectiveInvocation,
|
|
471
|
+
stages,
|
|
472
|
+
checks: receiptChecks,
|
|
473
|
+
findings: {
|
|
474
|
+
total: architecture.findings.length,
|
|
475
|
+
errors: architecture.findings.filter((finding) => finding.severity === 'error').length,
|
|
476
|
+
warnings: architecture.findings.filter((finding) => finding.severity === 'warning').length +
|
|
477
|
+
(policySurface.changes ?? []).filter((change) => change.severity === 'warning').length,
|
|
478
|
+
waived: architecture.waived,
|
|
479
|
+
waiverWarnings: architecture.waiverWarnings.length,
|
|
480
|
+
expiredWaivers: architecture.expiredWaivers.length,
|
|
481
|
+
fingerprints: architecture.findings.map((finding) => finding.fingerprint).sort(),
|
|
482
|
+
ruleIds: [...new Set(architecture.findings.map((finding) => finding.ruleId))].sort(),
|
|
483
|
+
},
|
|
484
|
+
contractIssues: contractIssues.map((issue) => issue.contractId),
|
|
485
|
+
decisionIds: policy.decisionIds ?? [],
|
|
486
|
+
tool: { name: '@codapult/guard', version: packageJson.version },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function buildPolicyGateReceipt(root, policy, policySurface, ciIntegrity, stages) {
|
|
490
|
+
const githubContext = readGuardGitHubContext(process.env.GITHUB_EVENT_NAME, process.env.GITHUB_EVENT_PATH, resolveGitCommit(root, 'HEAD'), process.env.GITHUB_SHA);
|
|
491
|
+
const baseChangedFiles = committedFilesSince(root, policy.ref);
|
|
492
|
+
const workingTreeFiles = workingTreeChangedFiles(root) ?? [];
|
|
493
|
+
const baseCommit = resolveGitCommit(root, policy.ref);
|
|
494
|
+
return {
|
|
495
|
+
version: 1,
|
|
496
|
+
kind: 'verification-receipt',
|
|
497
|
+
subject: {
|
|
498
|
+
...(githubContext?.event ? { githubEvent: githubContext.event } : {}),
|
|
499
|
+
...(githubContext?.headSha ? { headSha: githubContext.headSha } : {}),
|
|
500
|
+
...(githubContext?.baseSha ? { baseSha: githubContext.baseSha } : {}),
|
|
501
|
+
...(githubContext?.mergeSha ? { mergeSha: githubContext.mergeSha } : {}),
|
|
502
|
+
...(policy.ref ? { baseRef: policy.ref } : {}),
|
|
503
|
+
...(baseCommit ? { baseCommit } : {}),
|
|
504
|
+
...(baseChangedFiles ? { baseChangedFiles } : {}),
|
|
505
|
+
...(workingTreeFiles.length > 0 ? { workingTreeFiles } : {}),
|
|
506
|
+
changedFiles: [...new Set([...(baseChangedFiles ?? []), ...workingTreeFiles])].sort(),
|
|
507
|
+
workingTreeDirty: hasWorkingTreeChanges(root),
|
|
508
|
+
},
|
|
509
|
+
policy,
|
|
510
|
+
policySurface,
|
|
511
|
+
...(ciIntegrity ? { ciIntegrity } : {}),
|
|
512
|
+
requiredStages: [],
|
|
513
|
+
incompleteStages: [],
|
|
514
|
+
stages,
|
|
515
|
+
checks: {},
|
|
516
|
+
findings: {
|
|
517
|
+
total: 0,
|
|
518
|
+
errors: 0,
|
|
519
|
+
warnings: 0,
|
|
520
|
+
waived: 0,
|
|
521
|
+
waiverWarnings: 0,
|
|
522
|
+
expiredWaivers: 0,
|
|
523
|
+
fingerprints: [],
|
|
524
|
+
ruleIds: [],
|
|
525
|
+
},
|
|
526
|
+
contractIssues: [],
|
|
527
|
+
decisionIds: policy.decisionIds ?? [],
|
|
528
|
+
tool: { name: '@codapult/guard', version: packageJson.version },
|
|
529
|
+
};
|
|
530
|
+
}
|
|
17
531
|
export function runGuardVerification(root, options = {}) {
|
|
18
532
|
const runContext = startGuardRun();
|
|
19
533
|
const requirement = {
|
|
@@ -26,7 +540,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
26
540
|
const runtimeStartedAt = Date.now();
|
|
27
541
|
const runtime = inspectProjectRuntime(root);
|
|
28
542
|
recordGuardRunStage(runContext, 'runtime', 'ok', runtimeStartedAt);
|
|
29
|
-
if (options.failOnPolicyChange && !options.policyBase) {
|
|
543
|
+
if ((options.failOnPolicyChange || options.failOnProtectedChanges) && !options.policyBase) {
|
|
30
544
|
const run = finishGuardRun(root, runContext, 'fail', 'policy-base');
|
|
31
545
|
return {
|
|
32
546
|
status: 'fail',
|
|
@@ -37,7 +551,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
37
551
|
workspaceChecks: {},
|
|
38
552
|
adapters: {},
|
|
39
553
|
runtime,
|
|
40
|
-
configError: '
|
|
554
|
+
configError: 'Strict policy checks require `policyBase`.',
|
|
41
555
|
contractIssues: [],
|
|
42
556
|
requirement,
|
|
43
557
|
run,
|
|
@@ -65,11 +579,12 @@ export function runGuardVerification(root, options = {}) {
|
|
|
65
579
|
};
|
|
66
580
|
}
|
|
67
581
|
if (!workingTreeState) {
|
|
68
|
-
const
|
|
582
|
+
const policyEnforcementFailed = options.failOnPolicyChange === true;
|
|
583
|
+
const run = finishGuardRun(root, runContext, policyEnforcementFailed ? 'fail' : 'not-configured', policyEnforcementFailed ? 'policy-change' : 'configuration');
|
|
69
584
|
return {
|
|
70
|
-
status: 'not-configured',
|
|
71
|
-
outcome: 'not-configured',
|
|
72
|
-
errorCode: 'GUARD_NOT_CONFIGURED',
|
|
585
|
+
status: policyEnforcementFailed ? 'fail' : 'not-configured',
|
|
586
|
+
outcome: policyEnforcementFailed ? 'fail' : 'not-configured',
|
|
587
|
+
errorCode: policyEnforcementFailed ? 'GUARD_POLICY_CHANGED' : 'GUARD_NOT_CONFIGURED',
|
|
73
588
|
recoverable: true,
|
|
74
589
|
checks: {},
|
|
75
590
|
workspaceChecks: {},
|
|
@@ -83,6 +598,9 @@ export function runGuardVerification(root, options = {}) {
|
|
|
83
598
|
let policySnapshot;
|
|
84
599
|
let workingTreeSnapshot;
|
|
85
600
|
let policyChanged;
|
|
601
|
+
let policySurface;
|
|
602
|
+
let ciIntegrity;
|
|
603
|
+
let uncheckedProtectedPaths;
|
|
86
604
|
let policyDecisionIds = [];
|
|
87
605
|
try {
|
|
88
606
|
workingTreeSnapshot = workingTreeState.snapshot;
|
|
@@ -109,6 +627,25 @@ export function runGuardVerification(root, options = {}) {
|
|
|
109
627
|
policySnapshot = baseSnapshot ?? workingTreeSnapshot;
|
|
110
628
|
policyChanged =
|
|
111
629
|
baseSnapshot !== undefined && baseSnapshot.fingerprint !== workingTreeSnapshot.fingerprint;
|
|
630
|
+
const policySurfaceStartedAt = Date.now();
|
|
631
|
+
policySurface = getPolicySurface(root, options.policyBase, baseSnapshot?.config.protectedPaths ?? [], baseSnapshot?.config.policySurface);
|
|
632
|
+
recordGuardRunStage(runContext, 'policy-surface', policySurface.complete ? 'ok' : 'skipped', policySurfaceStartedAt, policySurface.complete ? undefined : 'A policy base was not available for comparison.');
|
|
633
|
+
const ciAuditStartedAt = Date.now();
|
|
634
|
+
ciIntegrity = auditGitHubActions(root, policySnapshot.config.githubActions, options.policyBase);
|
|
635
|
+
const contractedWorkflowPaths = new Set(policySnapshot.config.githubActions?.workflows.map((workflow) => workflow.path) ?? []);
|
|
636
|
+
uncheckedProtectedPaths = [
|
|
637
|
+
...new Set(policySurface.changes
|
|
638
|
+
.filter((change) => change.severity === 'error')
|
|
639
|
+
.map((change) => change.path)
|
|
640
|
+
.filter((path) => !(path.startsWith('.github/workflows/') && contractedWorkflowPaths.has(path)))),
|
|
641
|
+
];
|
|
642
|
+
recordGuardRunStage(runContext, 'ci-integrity', ciIntegrity === undefined
|
|
643
|
+
? 'skipped'
|
|
644
|
+
: ciIntegrity.status === 'ok'
|
|
645
|
+
? 'ok'
|
|
646
|
+
: ciIntegrity.status === 'policy-weakened'
|
|
647
|
+
? 'fail'
|
|
648
|
+
: 'skipped', ciAuditStartedAt, ciIntegrity === undefined ? 'No GitHub Actions integrity contract is configured.' : undefined);
|
|
112
649
|
if (policySnapshot.source === 'working-tree') {
|
|
113
650
|
policyDecisionIds = (workingTreeState.proposals?.decisions ?? [])
|
|
114
651
|
.filter((decision) => decision.policyFingerprint === policySnapshot.fingerprint)
|
|
@@ -118,7 +655,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
118
655
|
}
|
|
119
656
|
if (options.failOnPolicyChange && policyChanged) {
|
|
120
657
|
const policy = buildRunPolicy(policySnapshot, options.policyBase, policyChanged, policyDecisionIds);
|
|
121
|
-
const run = finishGuardRun(root, runContext, 'fail', 'policy-change', 'verify', policy);
|
|
658
|
+
const run = finishGuardRun(root, runContext, 'fail', 'policy-change', 'verify', policy, buildPolicyGateReceipt(root, policy, policySurface, ciIntegrity, Object.fromEntries(Object.entries(runContext.stages).map(([name, stage]) => [name, stage.status]))));
|
|
122
659
|
return {
|
|
123
660
|
status: 'fail',
|
|
124
661
|
outcome: 'fail',
|
|
@@ -134,6 +671,40 @@ export function runGuardVerification(root, options = {}) {
|
|
|
134
671
|
run,
|
|
135
672
|
policy,
|
|
136
673
|
policyChanged,
|
|
674
|
+
policySurface,
|
|
675
|
+
...(ciIntegrity ? { ciIntegrity } : {}),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (options.failOnProtectedChanges &&
|
|
679
|
+
(!policySurface.complete ||
|
|
680
|
+
uncheckedProtectedPaths.length > 0 ||
|
|
681
|
+
policySurface.suppressionMarkers.length > 0)) {
|
|
682
|
+
const policy = buildRunPolicy(policySnapshot, options.policyBase, policyChanged, policyDecisionIds);
|
|
683
|
+
const run = finishGuardRun(root, runContext, 'fail', 'protected-path-change', 'verify', policy, buildPolicyGateReceipt(root, policy, policySurface, ciIntegrity, Object.fromEntries(Object.entries(runContext.stages).map(([name, stage]) => [name, stage.status]))));
|
|
684
|
+
return {
|
|
685
|
+
status: 'fail',
|
|
686
|
+
outcome: 'fail',
|
|
687
|
+
errorCode: policySurface.complete
|
|
688
|
+
? 'GUARD_PROTECTED_PATH_CHANGED'
|
|
689
|
+
: 'GUARD_POLICY_SURFACE_INCOMPLETE',
|
|
690
|
+
recoverable: true,
|
|
691
|
+
checks: {},
|
|
692
|
+
workspaceChecks: {},
|
|
693
|
+
adapters: {},
|
|
694
|
+
runtime,
|
|
695
|
+
configError: policySurface.complete
|
|
696
|
+
? `Verification-sensitive paths or inline suppression markers changed: ${[
|
|
697
|
+
...uncheckedProtectedPaths,
|
|
698
|
+
...policySurface.suppressionMarkers,
|
|
699
|
+
].join(', ')}`
|
|
700
|
+
: 'Could not inspect the complete policy surface against the base ref; strict verification failed closed.',
|
|
701
|
+
contractIssues: [],
|
|
702
|
+
requirement,
|
|
703
|
+
run,
|
|
704
|
+
policy,
|
|
705
|
+
policyChanged,
|
|
706
|
+
policySurface,
|
|
707
|
+
...(ciIntegrity ? { ciIntegrity } : {}),
|
|
137
708
|
};
|
|
138
709
|
}
|
|
139
710
|
}
|
|
@@ -158,11 +729,13 @@ export function runGuardVerification(root, options = {}) {
|
|
|
158
729
|
const agentConfig = policySnapshot.agentConfig;
|
|
159
730
|
const projectChecks = options.projectChecks ?? agentConfig.completionGate.projectChecks;
|
|
160
731
|
const checks = projectChecks ? (options.checks ?? agentConfig.completionGate.checks) : [];
|
|
732
|
+
const discoveryStartedAt = Date.now();
|
|
733
|
+
const workspaceModel = discoverProject(root);
|
|
734
|
+
const scopeChangedPaths = changedFilesSince(root, policy.ref) ?? workspaceModel.git.changedFiles;
|
|
735
|
+
const initialScope = verificationScope(root, workspaceModel.files, scopeChangedPaths, policy);
|
|
161
736
|
const checksStartedAt = Date.now();
|
|
162
737
|
const results = runProjectChecks(root, checks, { timeout: options.timeout });
|
|
163
738
|
recordGuardRunStage(runContext, 'project-checks', projectChecks ? 'ok' : 'skipped', checksStartedAt);
|
|
164
|
-
const discoveryStartedAt = Date.now();
|
|
165
|
-
const workspaceModel = discoverProject(root);
|
|
166
739
|
const impact = analyzeProjectImpact(workspaceModel, options.changedOnly === false
|
|
167
740
|
? workspaceModel.modules.map((module) => module.path)
|
|
168
741
|
: workspaceModel.git.changedFiles, policySnapshot.config.contracts ?? []);
|
|
@@ -190,60 +763,144 @@ export function runGuardVerification(root, options = {}) {
|
|
|
190
763
|
});
|
|
191
764
|
const contractIssues = validateGuardPolicy(root, policySnapshot.config);
|
|
192
765
|
recordGuardRunStage(runContext, 'architecture-policy', 'ok', architectureStartedAt);
|
|
766
|
+
const finalWorkspaceModel = discoverProject(root);
|
|
767
|
+
const finalScope = verificationScope(root, finalWorkspaceModel.files, changedFilesSince(root, policy.ref) ?? finalWorkspaceModel.git.changedFiles, policy);
|
|
768
|
+
const scopeStable = initialScope.complete &&
|
|
769
|
+
finalScope.complete &&
|
|
770
|
+
initialScope.fingerprint === finalScope.fingerprint;
|
|
771
|
+
const scope = {
|
|
772
|
+
...initialScope,
|
|
773
|
+
complete: initialScope.complete && finalScope.complete,
|
|
774
|
+
stable: scopeStable,
|
|
775
|
+
};
|
|
776
|
+
recordGuardRunStage(runContext, 'verification-scope', scope.complete && scope.stable ? 'ok' : 'skipped', discoveryStartedAt, scope.complete && scope.stable
|
|
777
|
+
? undefined
|
|
778
|
+
: 'The complete verification input scope could not be hashed consistently for the entire run.');
|
|
193
779
|
const commandFailed = Object.values(results).some((result) => result.status === 'failed');
|
|
194
780
|
const workspaceCommandFailed = Object.values(workspaceChecks).some((packageResults) => Object.values(packageResults).some((result) => result.status === 'failed'));
|
|
195
781
|
const adapterFailed = Object.values(adapters).some((result) => result.status === 'failed');
|
|
196
|
-
const
|
|
197
|
-
|
|
782
|
+
const missingProjectChecks = projectChecks &&
|
|
783
|
+
(checks.length === 0 ||
|
|
784
|
+
Object.values(results).some((result) => result.status === 'not-configured') ||
|
|
785
|
+
Object.values(workspaceChecks).some((packageResults) => Object.values(packageResults).some((result) => result.status === 'not-configured')));
|
|
786
|
+
const missingAdapterTools = options.strict && Object.values(adapters).some((result) => result.status === 'not-configured');
|
|
787
|
+
const runtimeFailed = projectChecks && !runtime.compatible;
|
|
198
788
|
const architectureFailed = architecture.findings.some((finding) => finding.severity === 'error');
|
|
789
|
+
const expiredWaivers = architecture.expiredWaivers.length > 0;
|
|
199
790
|
const architectureWarnings = architecture.findings.some((finding) => finding.severity === 'warning') ||
|
|
200
791
|
(architecture.waiverWarnings ?? []).length > 0;
|
|
201
|
-
const
|
|
792
|
+
const policySurfaceWarnings = policySurface.changes.some((change) => change.severity === 'warning');
|
|
793
|
+
const otherFailures = runtimeFailed ||
|
|
202
794
|
commandFailed ||
|
|
203
795
|
workspaceCommandFailed ||
|
|
204
796
|
adapterFailed ||
|
|
205
797
|
architectureFailed ||
|
|
798
|
+
expiredWaivers ||
|
|
206
799
|
contractIssues.length > 0 ||
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
800
|
+
missingProjectChecks ||
|
|
801
|
+
missingAdapterTools;
|
|
802
|
+
const ciWeakened = ciIntegrity?.status === 'policy-weakened';
|
|
803
|
+
const ciIncomplete = ciIntegrity?.status === 'incomplete';
|
|
804
|
+
const failed = otherFailures || ciWeakened;
|
|
805
|
+
const projectChecksFailed = commandFailed || workspaceCommandFailed || missingProjectChecks || runtimeFailed;
|
|
806
|
+
const adaptersFailed = adapterFailed || missingAdapterTools;
|
|
210
807
|
runContext.stages['project-checks'] = {
|
|
211
808
|
...runContext.stages['project-checks'],
|
|
212
809
|
status: projectChecks ? (projectChecksFailed ? 'fail' : 'ok') : 'skipped',
|
|
213
810
|
};
|
|
214
811
|
runContext.stages.adapters = {
|
|
215
812
|
...runContext.stages.adapters,
|
|
216
|
-
status: toolMode === 'off'
|
|
813
|
+
status: toolMode === 'off' ||
|
|
814
|
+
!Object.values(adapters).some((result) => result.status !== 'not-configured')
|
|
815
|
+
? 'skipped'
|
|
816
|
+
: adaptersFailed
|
|
817
|
+
? 'fail'
|
|
818
|
+
: 'ok',
|
|
819
|
+
};
|
|
820
|
+
runContext.stages.runtime = {
|
|
821
|
+
...runContext.stages.runtime,
|
|
822
|
+
status: projectChecks ? (runtimeFailed ? 'fail' : 'ok') : 'skipped',
|
|
217
823
|
};
|
|
218
824
|
runContext.stages['architecture-policy'] = {
|
|
219
825
|
...runContext.stages['architecture-policy'],
|
|
220
|
-
status: architectureFailed || contractIssues.length > 0 ? 'fail' : 'ok',
|
|
826
|
+
status: architectureFailed || expiredWaivers || contractIssues.length > 0 ? 'fail' : 'ok',
|
|
221
827
|
};
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
828
|
+
const requiredStages = agentConfig.completionGate.requiredStages ?? ['architecture-policy'];
|
|
829
|
+
const effectiveRequiredStages = [
|
|
830
|
+
...new Set([
|
|
831
|
+
...requiredStages,
|
|
832
|
+
...(policySnapshot.source === 'git-ref' ? ['policy-surface'] : []),
|
|
833
|
+
...(ciIntegrity !== undefined ? ['ci-integrity'] : []),
|
|
834
|
+
'verification-scope',
|
|
835
|
+
]),
|
|
836
|
+
];
|
|
837
|
+
const effectiveIncompleteStages = effectiveRequiredStages.filter((stage) => runContext.stages[stage].status !== 'ok');
|
|
838
|
+
const incomplete = ciIncomplete || effectiveIncompleteStages.length > 0;
|
|
839
|
+
const outcome = ciWeakened && !otherFailures
|
|
840
|
+
? 'policy-weakened'
|
|
841
|
+
: classifyGuardOutcome({
|
|
842
|
+
errors: failed ? 1 : 0,
|
|
843
|
+
warnings: architectureWarnings || policySurfaceWarnings ? 1 : 0,
|
|
844
|
+
incomplete: !failed && incomplete,
|
|
845
|
+
});
|
|
846
|
+
const gate = ciWeakened
|
|
847
|
+
? 'ci-integrity'
|
|
848
|
+
: architectureFailed || expiredWaivers
|
|
849
|
+
? 'architecture'
|
|
850
|
+
: contractIssues.length > 0
|
|
851
|
+
? 'contracts'
|
|
852
|
+
: commandFailed || workspaceCommandFailed
|
|
853
|
+
? 'project-checks'
|
|
854
|
+
: adapterFailed
|
|
855
|
+
? 'adapters'
|
|
856
|
+
: missingProjectChecks
|
|
857
|
+
? 'project-checks'
|
|
858
|
+
: missingAdapterTools
|
|
859
|
+
? 'required-tools'
|
|
860
|
+
: runtimeFailed
|
|
861
|
+
? 'runtime'
|
|
862
|
+
: incomplete
|
|
863
|
+
? 'required-stages'
|
|
864
|
+
: 'none';
|
|
865
|
+
const receipt = buildVerificationReceipt(root, policy, impact, results, workspaceChecks, adapters, architecture, contractIssues, Object.fromEntries(Object.entries(runContext.stages).map(([name, stage]) => [name, stage.status])), policySurface, ciIntegrity, scope, effectiveRequiredStages, effectiveIncompleteStages, effectiveVerifierInvocation({
|
|
866
|
+
checks,
|
|
867
|
+
tools: toolMode,
|
|
868
|
+
projectChecks,
|
|
869
|
+
strict: options.strict,
|
|
870
|
+
changedOnly: options.changedOnly,
|
|
871
|
+
policyBase: options.policyBase,
|
|
872
|
+
failOnPolicyChange: options.failOnPolicyChange,
|
|
873
|
+
failOnProtectedChanges: options.failOnProtectedChanges,
|
|
874
|
+
}));
|
|
875
|
+
const run = finishGuardRun(root, runContext, outcome, gate, 'verify', policy, receipt);
|
|
240
876
|
return {
|
|
241
|
-
status:
|
|
877
|
+
status: outcome === 'policy-weakened'
|
|
878
|
+
? 'policy-weakened'
|
|
879
|
+
: failed
|
|
880
|
+
? 'fail'
|
|
881
|
+
: incomplete
|
|
882
|
+
? 'incomplete'
|
|
883
|
+
: 'ok',
|
|
884
|
+
...(ciWeakened || ciIncomplete || incomplete
|
|
885
|
+
? {
|
|
886
|
+
errorCode: ciWeakened
|
|
887
|
+
? 'GUARD_CI_POLICY_WEAKENED'
|
|
888
|
+
: ciIncomplete
|
|
889
|
+
? 'GUARD_CI_POLICY_INCOMPLETE'
|
|
890
|
+
: 'GUARD_REQUIRED_STAGE_INCOMPLETE',
|
|
891
|
+
}
|
|
892
|
+
: {}),
|
|
242
893
|
outcome,
|
|
243
894
|
tools: toolMode,
|
|
244
|
-
checks: results,
|
|
245
|
-
workspaceChecks,
|
|
246
|
-
|
|
895
|
+
checks: sanitizeProjectChecks(results),
|
|
896
|
+
workspaceChecks: Object.fromEntries(Object.entries(workspaceChecks).map(([name, packageChecks]) => [
|
|
897
|
+
name,
|
|
898
|
+
sanitizeProjectChecks(packageChecks),
|
|
899
|
+
])),
|
|
900
|
+
adapters: Object.fromEntries(Object.entries(adapters).map(([name, result]) => [
|
|
901
|
+
name,
|
|
902
|
+
{ ...result, command: sanitizeCommand(result.command) },
|
|
903
|
+
])),
|
|
247
904
|
runtime,
|
|
248
905
|
architecture,
|
|
249
906
|
impact,
|
|
@@ -252,5 +909,9 @@ export function runGuardVerification(root, options = {}) {
|
|
|
252
909
|
run,
|
|
253
910
|
policy,
|
|
254
911
|
...(policy.changedFromBase !== undefined ? { policyChanged: policy.changedFromBase } : {}),
|
|
912
|
+
policySurface,
|
|
913
|
+
...(ciIntegrity ? { ciIntegrity } : {}),
|
|
914
|
+
requiredStages: effectiveRequiredStages,
|
|
915
|
+
incompleteStages: effectiveIncompleteStages,
|
|
255
916
|
};
|
|
256
917
|
}
|