@codapult/guard 0.10.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 +144 -4
- 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 +64 -1
- package/dist/core/history/runs.js +109 -0
- 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 +601 -47
- 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 +4 -2
|
@@ -1,13 +1,275 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
4
|
import { createRequire } from 'node:module';
|
|
5
|
+
import { extname, relative, resolve, sep } from 'node:path';
|
|
3
6
|
import {} from '../../adapters/command.js';
|
|
4
7
|
import { runProjectAdapters, runProjectChecks, runWorkspaceProjectChecks, inspectProjectRuntime, } from '../../adapters/project-checks.js';
|
|
5
|
-
import { loadGuardPolicyAtRevision, loadGuardWorkingTreePolicy, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
|
|
8
|
+
import { GUARD_DIR, GUARD_HISTORY_DIR, loadGuardPolicyAtRevision, loadGuardWorkingTreePolicy, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
|
|
6
9
|
import { analyzeProjectImpact } from '../analysis/impact.js';
|
|
7
10
|
import { discoverProject } from '../discovery/discovery.js';
|
|
8
11
|
import { finishGuardRun, recordGuardRunStage, startGuardRun, } from '../history/runs.js';
|
|
12
|
+
import { auditGitHubActions } from './github-actions.js';
|
|
13
|
+
import { readGuardGitHubContext } from './github-context.js';
|
|
9
14
|
const require = createRequire(import.meta.url);
|
|
10
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
|
+
}
|
|
11
273
|
function buildRunPolicy(snapshot, base, changedFromBase, decisionIds = []) {
|
|
12
274
|
return {
|
|
13
275
|
...(snapshot.revision !== undefined ? { revision: snapshot.revision } : {}),
|
|
@@ -18,13 +280,61 @@ function buildRunPolicy(snapshot, base, changedFromBase, decisionIds = []) {
|
|
|
18
280
|
...(decisionIds.length > 0 ? { decisionIds } : {}),
|
|
19
281
|
};
|
|
20
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
|
+
}
|
|
21
304
|
function receiptCheck(result) {
|
|
305
|
+
const command = sanitizeCommand(result.command);
|
|
306
|
+
const args = command.trim().split(/\s+/).filter(Boolean).slice(1);
|
|
22
307
|
return {
|
|
23
308
|
status: result.status,
|
|
24
309
|
exitCode: result.exitCode,
|
|
25
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',
|
|
26
318
|
};
|
|
27
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
|
+
}
|
|
28
338
|
function resolveGitCommit(root, ref) {
|
|
29
339
|
if (!ref)
|
|
30
340
|
return undefined;
|
|
@@ -42,29 +352,88 @@ function resolveGitCommit(root, ref) {
|
|
|
42
352
|
return undefined;
|
|
43
353
|
}
|
|
44
354
|
}
|
|
45
|
-
function
|
|
46
|
-
if (!baseRef)
|
|
47
|
-
return undefined;
|
|
355
|
+
function hasWorkingTreeChanges(root) {
|
|
48
356
|
try {
|
|
49
|
-
return execFileSync('git', ['
|
|
357
|
+
return (execFileSync('git', ['status', '--porcelain', '-z'], {
|
|
50
358
|
cwd: root,
|
|
51
359
|
stdio: 'pipe',
|
|
52
360
|
timeout: 5_000,
|
|
53
|
-
})
|
|
54
|
-
.toString()
|
|
55
|
-
.split('\n')
|
|
56
|
-
.map((file) => file.trim().replace(/\\/g, '/'))
|
|
57
|
-
.filter(Boolean)
|
|
58
|
-
.sort();
|
|
361
|
+
}).length > 0);
|
|
59
362
|
}
|
|
60
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('..'))
|
|
61
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
|
+
}
|
|
62
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 };
|
|
63
431
|
}
|
|
64
|
-
function buildVerificationReceipt(root, policy, impact, checks, workspaceChecks, adapters, architecture, contractIssues, stages) {
|
|
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);
|
|
65
434
|
const baseCommit = resolveGitCommit(root, policy.ref);
|
|
66
|
-
const baseChangedFiles =
|
|
67
|
-
const workingTreeFiles = impact.requestedFiles;
|
|
435
|
+
const baseChangedFiles = committedFilesSince(root, policy.ref);
|
|
436
|
+
const workingTreeFiles = workingTreeChangedFiles(root) ?? impact.requestedFiles;
|
|
68
437
|
const receiptChecks = {};
|
|
69
438
|
for (const [name, result] of Object.entries(checks)) {
|
|
70
439
|
receiptChecks[name] = receiptCheck(result);
|
|
@@ -81,6 +450,10 @@ function buildVerificationReceipt(root, policy, impact, checks, workspaceChecks,
|
|
|
81
450
|
version: 1,
|
|
82
451
|
kind: 'verification-receipt',
|
|
83
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 } : {}),
|
|
84
457
|
...(policy.ref ? { baseRef: policy.ref } : {}),
|
|
85
458
|
...(baseCommit ? { baseCommit } : {}),
|
|
86
459
|
...(baseChangedFiles ? { baseChangedFiles } : {}),
|
|
@@ -89,12 +462,19 @@ function buildVerificationReceipt(root, policy, impact, checks, workspaceChecks,
|
|
|
89
462
|
workingTreeDirty: workingTreeFiles.length > 0,
|
|
90
463
|
},
|
|
91
464
|
policy,
|
|
465
|
+
verificationScope: scope,
|
|
466
|
+
policySurface,
|
|
467
|
+
...(ciIntegrity ? { ciIntegrity } : {}),
|
|
468
|
+
requiredStages,
|
|
469
|
+
incompleteStages,
|
|
470
|
+
effectiveInvocation,
|
|
92
471
|
stages,
|
|
93
472
|
checks: receiptChecks,
|
|
94
473
|
findings: {
|
|
95
474
|
total: architecture.findings.length,
|
|
96
475
|
errors: architecture.findings.filter((finding) => finding.severity === 'error').length,
|
|
97
|
-
warnings: architecture.findings.filter((finding) => finding.severity === 'warning').length
|
|
476
|
+
warnings: architecture.findings.filter((finding) => finding.severity === 'warning').length +
|
|
477
|
+
(policySurface.changes ?? []).filter((change) => change.severity === 'warning').length,
|
|
98
478
|
waived: architecture.waived,
|
|
99
479
|
waiverWarnings: architecture.waiverWarnings.length,
|
|
100
480
|
expiredWaivers: architecture.expiredWaivers.length,
|
|
@@ -106,6 +486,48 @@ function buildVerificationReceipt(root, policy, impact, checks, workspaceChecks,
|
|
|
106
486
|
tool: { name: '@codapult/guard', version: packageJson.version },
|
|
107
487
|
};
|
|
108
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
|
+
}
|
|
109
531
|
export function runGuardVerification(root, options = {}) {
|
|
110
532
|
const runContext = startGuardRun();
|
|
111
533
|
const requirement = {
|
|
@@ -118,7 +540,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
118
540
|
const runtimeStartedAt = Date.now();
|
|
119
541
|
const runtime = inspectProjectRuntime(root);
|
|
120
542
|
recordGuardRunStage(runContext, 'runtime', 'ok', runtimeStartedAt);
|
|
121
|
-
if (options.failOnPolicyChange && !options.policyBase) {
|
|
543
|
+
if ((options.failOnPolicyChange || options.failOnProtectedChanges) && !options.policyBase) {
|
|
122
544
|
const run = finishGuardRun(root, runContext, 'fail', 'policy-base');
|
|
123
545
|
return {
|
|
124
546
|
status: 'fail',
|
|
@@ -129,7 +551,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
129
551
|
workspaceChecks: {},
|
|
130
552
|
adapters: {},
|
|
131
553
|
runtime,
|
|
132
|
-
configError: '
|
|
554
|
+
configError: 'Strict policy checks require `policyBase`.',
|
|
133
555
|
contractIssues: [],
|
|
134
556
|
requirement,
|
|
135
557
|
run,
|
|
@@ -176,6 +598,9 @@ export function runGuardVerification(root, options = {}) {
|
|
|
176
598
|
let policySnapshot;
|
|
177
599
|
let workingTreeSnapshot;
|
|
178
600
|
let policyChanged;
|
|
601
|
+
let policySurface;
|
|
602
|
+
let ciIntegrity;
|
|
603
|
+
let uncheckedProtectedPaths;
|
|
179
604
|
let policyDecisionIds = [];
|
|
180
605
|
try {
|
|
181
606
|
workingTreeSnapshot = workingTreeState.snapshot;
|
|
@@ -202,6 +627,25 @@ export function runGuardVerification(root, options = {}) {
|
|
|
202
627
|
policySnapshot = baseSnapshot ?? workingTreeSnapshot;
|
|
203
628
|
policyChanged =
|
|
204
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);
|
|
205
649
|
if (policySnapshot.source === 'working-tree') {
|
|
206
650
|
policyDecisionIds = (workingTreeState.proposals?.decisions ?? [])
|
|
207
651
|
.filter((decision) => decision.policyFingerprint === policySnapshot.fingerprint)
|
|
@@ -211,7 +655,7 @@ export function runGuardVerification(root, options = {}) {
|
|
|
211
655
|
}
|
|
212
656
|
if (options.failOnPolicyChange && policyChanged) {
|
|
213
657
|
const policy = buildRunPolicy(policySnapshot, options.policyBase, policyChanged, policyDecisionIds);
|
|
214
|
-
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]))));
|
|
215
659
|
return {
|
|
216
660
|
status: 'fail',
|
|
217
661
|
outcome: 'fail',
|
|
@@ -227,6 +671,40 @@ export function runGuardVerification(root, options = {}) {
|
|
|
227
671
|
run,
|
|
228
672
|
policy,
|
|
229
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 } : {}),
|
|
230
708
|
};
|
|
231
709
|
}
|
|
232
710
|
}
|
|
@@ -251,11 +729,13 @@ export function runGuardVerification(root, options = {}) {
|
|
|
251
729
|
const agentConfig = policySnapshot.agentConfig;
|
|
252
730
|
const projectChecks = options.projectChecks ?? agentConfig.completionGate.projectChecks;
|
|
253
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);
|
|
254
736
|
const checksStartedAt = Date.now();
|
|
255
737
|
const results = runProjectChecks(root, checks, { timeout: options.timeout });
|
|
256
738
|
recordGuardRunStage(runContext, 'project-checks', projectChecks ? 'ok' : 'skipped', checksStartedAt);
|
|
257
|
-
const discoveryStartedAt = Date.now();
|
|
258
|
-
const workspaceModel = discoverProject(root);
|
|
259
739
|
const impact = analyzeProjectImpact(workspaceModel, options.changedOnly === false
|
|
260
740
|
? workspaceModel.modules.map((module) => module.path)
|
|
261
741
|
: workspaceModel.git.changedFiles, policySnapshot.config.contracts ?? []);
|
|
@@ -283,6 +763,19 @@ export function runGuardVerification(root, options = {}) {
|
|
|
283
763
|
});
|
|
284
764
|
const contractIssues = validateGuardPolicy(root, policySnapshot.config);
|
|
285
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.');
|
|
286
779
|
const commandFailed = Object.values(results).some((result) => result.status === 'failed');
|
|
287
780
|
const workspaceCommandFailed = Object.values(workspaceChecks).some((packageResults) => Object.values(packageResults).some((result) => result.status === 'failed'));
|
|
288
781
|
const adapterFailed = Object.values(adapters).some((result) => result.status === 'failed');
|
|
@@ -296,7 +789,8 @@ export function runGuardVerification(root, options = {}) {
|
|
|
296
789
|
const expiredWaivers = architecture.expiredWaivers.length > 0;
|
|
297
790
|
const architectureWarnings = architecture.findings.some((finding) => finding.severity === 'warning') ||
|
|
298
791
|
(architecture.waiverWarnings ?? []).length > 0;
|
|
299
|
-
const
|
|
792
|
+
const policySurfaceWarnings = policySurface.changes.some((change) => change.severity === 'warning');
|
|
793
|
+
const otherFailures = runtimeFailed ||
|
|
300
794
|
commandFailed ||
|
|
301
795
|
workspaceCommandFailed ||
|
|
302
796
|
adapterFailed ||
|
|
@@ -305,6 +799,9 @@ export function runGuardVerification(root, options = {}) {
|
|
|
305
799
|
contractIssues.length > 0 ||
|
|
306
800
|
missingProjectChecks ||
|
|
307
801
|
missingAdapterTools;
|
|
802
|
+
const ciWeakened = ciIntegrity?.status === 'policy-weakened';
|
|
803
|
+
const ciIncomplete = ciIntegrity?.status === 'incomplete';
|
|
804
|
+
const failed = otherFailures || ciWeakened;
|
|
308
805
|
const projectChecksFailed = commandFailed || workspaceCommandFailed || missingProjectChecks || runtimeFailed;
|
|
309
806
|
const adaptersFailed = adapterFailed || missingAdapterTools;
|
|
310
807
|
runContext.stages['project-checks'] = {
|
|
@@ -313,44 +810,97 @@ export function runGuardVerification(root, options = {}) {
|
|
|
313
810
|
};
|
|
314
811
|
runContext.stages.adapters = {
|
|
315
812
|
...runContext.stages.adapters,
|
|
316
|
-
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',
|
|
317
819
|
};
|
|
318
820
|
runContext.stages.runtime = {
|
|
319
821
|
...runContext.stages.runtime,
|
|
320
|
-
status: runtimeFailed ? 'fail' : 'ok',
|
|
822
|
+
status: projectChecks ? (runtimeFailed ? 'fail' : 'ok') : 'skipped',
|
|
321
823
|
};
|
|
322
824
|
runContext.stages['architecture-policy'] = {
|
|
323
825
|
...runContext.stages['architecture-policy'],
|
|
324
826
|
status: architectureFailed || expiredWaivers || contractIssues.length > 0 ? 'fail' : 'ok',
|
|
325
827
|
};
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
}));
|
|
346
875
|
const run = finishGuardRun(root, runContext, outcome, gate, 'verify', policy, receipt);
|
|
347
876
|
return {
|
|
348
|
-
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
|
+
: {}),
|
|
349
893
|
outcome,
|
|
350
894
|
tools: toolMode,
|
|
351
|
-
checks: results,
|
|
352
|
-
workspaceChecks,
|
|
353
|
-
|
|
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
|
+
])),
|
|
354
904
|
runtime,
|
|
355
905
|
architecture,
|
|
356
906
|
impact,
|
|
@@ -359,5 +909,9 @@ export function runGuardVerification(root, options = {}) {
|
|
|
359
909
|
run,
|
|
360
910
|
policy,
|
|
361
911
|
...(policy.changedFromBase !== undefined ? { policyChanged: policy.changedFromBase } : {}),
|
|
912
|
+
policySurface,
|
|
913
|
+
...(ciIntegrity ? { ciIntegrity } : {}),
|
|
914
|
+
requiredStages: effectiveRequiredStages,
|
|
915
|
+
incompleteStages: effectiveIncompleteStages,
|
|
362
916
|
};
|
|
363
917
|
}
|