@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
|
@@ -13,6 +13,108 @@ export interface GuardRunPolicy {
|
|
|
13
13
|
changedFromBase?: boolean | undefined;
|
|
14
14
|
decisionIds?: string[] | undefined;
|
|
15
15
|
}
|
|
16
|
+
export interface GuardVerificationReceiptCheck {
|
|
17
|
+
status: 'passed' | 'failed' | 'not-configured';
|
|
18
|
+
exitCode: number;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
command?: string | undefined;
|
|
21
|
+
args?: string[] | undefined;
|
|
22
|
+
execution?: 'attempted' | 'rejected' | 'not-configured' | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface GuardVerificationReceiptPolicySurface {
|
|
25
|
+
protectedPaths: string[];
|
|
26
|
+
changedPaths: string[];
|
|
27
|
+
changes?: {
|
|
28
|
+
path: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
severity: 'warning' | 'error';
|
|
32
|
+
}[];
|
|
33
|
+
suppressionMarkers: string[];
|
|
34
|
+
workflowChanged: boolean;
|
|
35
|
+
complete: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface GuardVerificationScope {
|
|
38
|
+
algorithm: 'sha256';
|
|
39
|
+
fingerprint: string;
|
|
40
|
+
complete: boolean;
|
|
41
|
+
stable: boolean;
|
|
42
|
+
files: {
|
|
43
|
+
path: string;
|
|
44
|
+
status: 'present' | 'deleted' | 'unavailable';
|
|
45
|
+
sha256?: string | undefined;
|
|
46
|
+
}[];
|
|
47
|
+
}
|
|
48
|
+
export interface GuardVerificationCiIntegrity {
|
|
49
|
+
status: 'ok' | 'policy-weakened' | 'incomplete';
|
|
50
|
+
workflows: {
|
|
51
|
+
path: string;
|
|
52
|
+
job: string;
|
|
53
|
+
digest?: string | undefined;
|
|
54
|
+
baseDigest?: string | undefined;
|
|
55
|
+
actions: string[];
|
|
56
|
+
baseActions?: string[] | undefined;
|
|
57
|
+
actionChanges?: {
|
|
58
|
+
name: string;
|
|
59
|
+
base: string[];
|
|
60
|
+
current: string[];
|
|
61
|
+
}[] | undefined;
|
|
62
|
+
status: 'ok' | 'policy-weakened' | 'incomplete';
|
|
63
|
+
issues: {
|
|
64
|
+
code: string;
|
|
65
|
+
message: string;
|
|
66
|
+
}[];
|
|
67
|
+
}[];
|
|
68
|
+
}
|
|
69
|
+
export interface GuardVerificationReceipt {
|
|
70
|
+
version: 1;
|
|
71
|
+
kind: 'verification-receipt';
|
|
72
|
+
runId: string;
|
|
73
|
+
generatedAt: string;
|
|
74
|
+
outcome: GuardRunManifest['outcome'];
|
|
75
|
+
gate: string;
|
|
76
|
+
subject: {
|
|
77
|
+
commit?: string | undefined;
|
|
78
|
+
baseRef?: string | undefined;
|
|
79
|
+
baseCommit?: string | undefined;
|
|
80
|
+
headSha?: string | undefined;
|
|
81
|
+
baseSha?: string | undefined;
|
|
82
|
+
mergeSha?: string | undefined;
|
|
83
|
+
githubEvent?: string | undefined;
|
|
84
|
+
changedFiles: string[];
|
|
85
|
+
baseChangedFiles?: string[] | undefined;
|
|
86
|
+
workingTreeFiles?: string[] | undefined;
|
|
87
|
+
workingTreeDirty: boolean;
|
|
88
|
+
};
|
|
89
|
+
policy?: GuardRunPolicy | undefined;
|
|
90
|
+
verificationScope?: GuardVerificationScope | undefined;
|
|
91
|
+
policySurface?: GuardVerificationReceiptPolicySurface | undefined;
|
|
92
|
+
ciIntegrity?: GuardVerificationCiIntegrity | undefined;
|
|
93
|
+
requiredStages?: string[] | undefined;
|
|
94
|
+
incompleteStages?: string[] | undefined;
|
|
95
|
+
effectiveInvocation?: {
|
|
96
|
+
command: string;
|
|
97
|
+
args: string[];
|
|
98
|
+
} | undefined;
|
|
99
|
+
stages: Record<string, 'ok' | 'fail' | 'skipped'>;
|
|
100
|
+
checks: Record<string, GuardVerificationReceiptCheck>;
|
|
101
|
+
findings: {
|
|
102
|
+
total: number;
|
|
103
|
+
errors: number;
|
|
104
|
+
warnings: number;
|
|
105
|
+
waived: number;
|
|
106
|
+
waiverWarnings: number;
|
|
107
|
+
expiredWaivers: number;
|
|
108
|
+
fingerprints: string[];
|
|
109
|
+
ruleIds: string[];
|
|
110
|
+
};
|
|
111
|
+
contractIssues: string[];
|
|
112
|
+
decisionIds: string[];
|
|
113
|
+
tool: {
|
|
114
|
+
name: string;
|
|
115
|
+
version: string;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
16
118
|
export interface GuardRunManifest {
|
|
17
119
|
version: 1;
|
|
18
120
|
runId: string;
|
|
@@ -20,11 +122,12 @@ export interface GuardRunManifest {
|
|
|
20
122
|
startedAt: string;
|
|
21
123
|
completedAt: string;
|
|
22
124
|
durationMs: number;
|
|
23
|
-
outcome: 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
|
|
125
|
+
outcome: 'pass' | 'fail' | 'incomplete' | 'policy-weakened' | 'warning' | 'needs-review' | 'not-configured';
|
|
24
126
|
gate: string;
|
|
25
127
|
stages: Record<string, GuardRunStage>;
|
|
26
128
|
commit?: string | undefined;
|
|
27
129
|
policy?: GuardRunPolicy | undefined;
|
|
130
|
+
receipt?: GuardVerificationReceipt | undefined;
|
|
28
131
|
}
|
|
29
132
|
export interface GuardRunContext {
|
|
30
133
|
runId: string;
|
|
@@ -36,6 +139,8 @@ export interface GuardRunSummary {
|
|
|
36
139
|
total: number;
|
|
37
140
|
passed: number;
|
|
38
141
|
failed: number;
|
|
142
|
+
incomplete: number;
|
|
143
|
+
policyWeakened: number;
|
|
39
144
|
warnings: number;
|
|
40
145
|
averageDurationMs: number;
|
|
41
146
|
last?: GuardRunManifest | undefined;
|
|
@@ -49,4 +154,4 @@ export declare function writeGuardRun(root: string, manifest: GuardRunManifest):
|
|
|
49
154
|
/** Reads locally persisted run manifests without reaching a remote service. */
|
|
50
155
|
export declare function listGuardRuns(root: string, limit?: number): GuardRunManifest[];
|
|
51
156
|
export declare function summarizeGuardRuns(root: string, limit?: number): GuardRunSummary;
|
|
52
|
-
export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string, policy?: GuardRunPolicy): GuardRunManifest;
|
|
157
|
+
export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string, policy?: GuardRunPolicy, receipt?: Omit<GuardVerificationReceipt, 'runId' | 'generatedAt' | 'outcome' | 'gate' | 'subject'> & Pick<GuardVerificationReceipt, 'subject'>): GuardRunManifest;
|
|
@@ -12,11 +12,208 @@ export class GuardRunManifestError extends Error {
|
|
|
12
12
|
this.name = 'GuardRunManifestError';
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
function isGuardRunPolicy(value) {
|
|
16
|
+
if (value === null || typeof value !== 'object')
|
|
17
|
+
return false;
|
|
18
|
+
const policy = value;
|
|
19
|
+
return (typeof policy.fingerprint === 'string' &&
|
|
20
|
+
policy.fingerprint.length > 0 &&
|
|
21
|
+
(policy.source === 'working-tree' || policy.source === 'git-ref') &&
|
|
22
|
+
(policy.revision === undefined ||
|
|
23
|
+
(Number.isInteger(policy.revision) && policy.revision >= 0)) &&
|
|
24
|
+
(policy.ref === undefined || typeof policy.ref === 'string') &&
|
|
25
|
+
(policy.changedFromBase === undefined || typeof policy.changedFromBase === 'boolean') &&
|
|
26
|
+
(policy.decisionIds === undefined ||
|
|
27
|
+
(Array.isArray(policy.decisionIds) &&
|
|
28
|
+
new Set(policy.decisionIds).size === policy.decisionIds.length &&
|
|
29
|
+
policy.decisionIds.every((decisionId) => typeof decisionId === 'string' && decisionId.trim().length > 0) &&
|
|
30
|
+
(policy.source === 'working-tree' || policy.decisionIds.length === 0))));
|
|
31
|
+
}
|
|
15
32
|
function isGuardRunManifest(value) {
|
|
16
33
|
if (value === null || typeof value !== 'object')
|
|
17
34
|
return false;
|
|
18
35
|
const manifest = value;
|
|
19
36
|
const stages = manifest.stages;
|
|
37
|
+
const receipt = manifest.receipt;
|
|
38
|
+
const validReceipt = receipt === undefined ||
|
|
39
|
+
(receipt !== null &&
|
|
40
|
+
typeof receipt === 'object' &&
|
|
41
|
+
receipt.version === 1 &&
|
|
42
|
+
receipt.kind === 'verification-receipt' &&
|
|
43
|
+
typeof receipt.runId === 'string' &&
|
|
44
|
+
receipt.runId === manifest.runId &&
|
|
45
|
+
typeof receipt.generatedAt === 'string' &&
|
|
46
|
+
Number.isFinite(Date.parse(receipt.generatedAt)) &&
|
|
47
|
+
(receipt.outcome === 'pass' ||
|
|
48
|
+
receipt.outcome === 'fail' ||
|
|
49
|
+
receipt.outcome === 'incomplete' ||
|
|
50
|
+
receipt.outcome === 'policy-weakened' ||
|
|
51
|
+
receipt.outcome === 'warning' ||
|
|
52
|
+
receipt.outcome === 'needs-review' ||
|
|
53
|
+
receipt.outcome === 'not-configured') &&
|
|
54
|
+
typeof receipt.gate === 'string' &&
|
|
55
|
+
receipt.subject !== null &&
|
|
56
|
+
typeof receipt.subject === 'object' &&
|
|
57
|
+
Array.isArray(receipt.subject.changedFiles) &&
|
|
58
|
+
receipt.subject.changedFiles.every((file) => typeof file === 'string') &&
|
|
59
|
+
(receipt.subject.baseChangedFiles === undefined ||
|
|
60
|
+
(Array.isArray(receipt.subject.baseChangedFiles) &&
|
|
61
|
+
receipt.subject.baseChangedFiles.every((file) => typeof file === 'string'))) &&
|
|
62
|
+
(receipt.subject.workingTreeFiles === undefined ||
|
|
63
|
+
(Array.isArray(receipt.subject.workingTreeFiles) &&
|
|
64
|
+
receipt.subject.workingTreeFiles.every((file) => typeof file === 'string'))) &&
|
|
65
|
+
typeof receipt.subject.workingTreeDirty === 'boolean' &&
|
|
66
|
+
(receipt.subject.commit === undefined || typeof receipt.subject.commit === 'string') &&
|
|
67
|
+
(receipt.subject.baseRef === undefined || typeof receipt.subject.baseRef === 'string') &&
|
|
68
|
+
(receipt.subject.baseCommit === undefined ||
|
|
69
|
+
typeof receipt.subject.baseCommit === 'string') &&
|
|
70
|
+
(receipt.subject.headSha === undefined || /^[a-f0-9]{40}$/i.test(receipt.subject.headSha)) &&
|
|
71
|
+
(receipt.subject.baseSha === undefined || /^[a-f0-9]{40}$/i.test(receipt.subject.baseSha)) &&
|
|
72
|
+
(receipt.subject.mergeSha === undefined ||
|
|
73
|
+
/^[a-f0-9]{40}$/i.test(receipt.subject.mergeSha)) &&
|
|
74
|
+
(receipt.subject.githubEvent === undefined ||
|
|
75
|
+
typeof receipt.subject.githubEvent === 'string') &&
|
|
76
|
+
(receipt.policy === undefined || isGuardRunPolicy(receipt.policy)) &&
|
|
77
|
+
(receipt.verificationScope === undefined ||
|
|
78
|
+
(receipt.verificationScope !== null &&
|
|
79
|
+
typeof receipt.verificationScope === 'object' &&
|
|
80
|
+
receipt.verificationScope.algorithm === 'sha256' &&
|
|
81
|
+
/^[a-f0-9]{64}$/.test(receipt.verificationScope.fingerprint) &&
|
|
82
|
+
typeof receipt.verificationScope.complete === 'boolean' &&
|
|
83
|
+
typeof receipt.verificationScope.stable === 'boolean' &&
|
|
84
|
+
Array.isArray(receipt.verificationScope.files) &&
|
|
85
|
+
receipt.verificationScope.files.every((file) => file !== null &&
|
|
86
|
+
typeof file === 'object' &&
|
|
87
|
+
typeof file.path === 'string' &&
|
|
88
|
+
(file.status === 'present' ||
|
|
89
|
+
file.status === 'deleted' ||
|
|
90
|
+
file.status === 'unavailable') &&
|
|
91
|
+
(file.status === 'unavailable'
|
|
92
|
+
? file.sha256 === undefined
|
|
93
|
+
: typeof file.sha256 === 'string' && /^[a-f0-9]{64}$/.test(file.sha256))))) &&
|
|
94
|
+
(receipt.policySurface === undefined ||
|
|
95
|
+
(receipt.policySurface !== null &&
|
|
96
|
+
typeof receipt.policySurface === 'object' &&
|
|
97
|
+
Array.isArray(receipt.policySurface.protectedPaths) &&
|
|
98
|
+
receipt.policySurface.protectedPaths.every((path) => typeof path === 'string') &&
|
|
99
|
+
Array.isArray(receipt.policySurface.changedPaths) &&
|
|
100
|
+
receipt.policySurface.changedPaths.every((path) => typeof path === 'string') &&
|
|
101
|
+
(receipt.policySurface.changes === undefined ||
|
|
102
|
+
(Array.isArray(receipt.policySurface.changes) &&
|
|
103
|
+
receipt.policySurface.changes.every((change) => change !== null &&
|
|
104
|
+
typeof change === 'object' &&
|
|
105
|
+
typeof change.path === 'string' &&
|
|
106
|
+
typeof change.pattern === 'string' &&
|
|
107
|
+
typeof change.kind === 'string' &&
|
|
108
|
+
(change.severity === 'warning' || change.severity === 'error')))) &&
|
|
109
|
+
Array.isArray(receipt.policySurface.suppressionMarkers) &&
|
|
110
|
+
receipt.policySurface.suppressionMarkers.every((marker) => typeof marker === 'string') &&
|
|
111
|
+
typeof receipt.policySurface.workflowChanged === 'boolean' &&
|
|
112
|
+
typeof receipt.policySurface.complete === 'boolean')) &&
|
|
113
|
+
(receipt.ciIntegrity === undefined ||
|
|
114
|
+
(receipt.ciIntegrity !== null &&
|
|
115
|
+
typeof receipt.ciIntegrity === 'object' &&
|
|
116
|
+
(receipt.ciIntegrity.status === 'ok' ||
|
|
117
|
+
receipt.ciIntegrity.status === 'policy-weakened' ||
|
|
118
|
+
receipt.ciIntegrity.status === 'incomplete') &&
|
|
119
|
+
Array.isArray(receipt.ciIntegrity.workflows) &&
|
|
120
|
+
receipt.ciIntegrity.workflows.every((workflow) => workflow !== null &&
|
|
121
|
+
typeof workflow === 'object' &&
|
|
122
|
+
typeof workflow.path === 'string' &&
|
|
123
|
+
typeof workflow.job === 'string' &&
|
|
124
|
+
(workflow.digest === undefined || typeof workflow.digest === 'string') &&
|
|
125
|
+
(workflow.baseDigest === undefined || typeof workflow.baseDigest === 'string') &&
|
|
126
|
+
Array.isArray(workflow.actions) &&
|
|
127
|
+
workflow.actions.every((action) => typeof action === 'string') &&
|
|
128
|
+
(workflow.baseActions === undefined ||
|
|
129
|
+
(Array.isArray(workflow.baseActions) &&
|
|
130
|
+
workflow.baseActions.every((action) => typeof action === 'string'))) &&
|
|
131
|
+
(workflow.actionChanges === undefined ||
|
|
132
|
+
(Array.isArray(workflow.actionChanges) &&
|
|
133
|
+
workflow.actionChanges.every((change) => change !== null &&
|
|
134
|
+
typeof change === 'object' &&
|
|
135
|
+
typeof change.name === 'string' &&
|
|
136
|
+
Array.isArray(change.base) &&
|
|
137
|
+
change.base.every((action) => typeof action === 'string') &&
|
|
138
|
+
Array.isArray(change.current) &&
|
|
139
|
+
change.current.every((action) => typeof action === 'string')))) &&
|
|
140
|
+
(workflow.status === 'ok' ||
|
|
141
|
+
workflow.status === 'policy-weakened' ||
|
|
142
|
+
workflow.status === 'incomplete') &&
|
|
143
|
+
Array.isArray(workflow.issues) &&
|
|
144
|
+
workflow.issues.every((issue) => issue !== null &&
|
|
145
|
+
typeof issue === 'object' &&
|
|
146
|
+
typeof issue.code === 'string' &&
|
|
147
|
+
typeof issue.message === 'string')))) &&
|
|
148
|
+
(receipt.requiredStages === undefined ||
|
|
149
|
+
(Array.isArray(receipt.requiredStages) &&
|
|
150
|
+
receipt.requiredStages.every((stage) => typeof stage === 'string'))) &&
|
|
151
|
+
(receipt.incompleteStages === undefined ||
|
|
152
|
+
(Array.isArray(receipt.incompleteStages) &&
|
|
153
|
+
receipt.incompleteStages.every((stage) => typeof stage === 'string'))) &&
|
|
154
|
+
(receipt.effectiveInvocation === undefined ||
|
|
155
|
+
(receipt.effectiveInvocation !== null &&
|
|
156
|
+
typeof receipt.effectiveInvocation === 'object' &&
|
|
157
|
+
typeof receipt.effectiveInvocation.command === 'string' &&
|
|
158
|
+
Array.isArray(receipt.effectiveInvocation.args) &&
|
|
159
|
+
receipt.effectiveInvocation.args.every((argument) => typeof argument === 'string'))) &&
|
|
160
|
+
receipt.stages !== null &&
|
|
161
|
+
typeof receipt.stages === 'object' &&
|
|
162
|
+
!Array.isArray(receipt.stages) &&
|
|
163
|
+
Object.values(receipt.stages).every((status) => status === 'ok' || status === 'fail' || status === 'skipped') &&
|
|
164
|
+
receipt.checks !== null &&
|
|
165
|
+
typeof receipt.checks === 'object' &&
|
|
166
|
+
!Array.isArray(receipt.checks) &&
|
|
167
|
+
Object.values(receipt.checks).every((check) => {
|
|
168
|
+
if (check === null || typeof check !== 'object')
|
|
169
|
+
return false;
|
|
170
|
+
const candidate = check;
|
|
171
|
+
return ((candidate.status === 'passed' ||
|
|
172
|
+
candidate.status === 'failed' ||
|
|
173
|
+
candidate.status === 'not-configured') &&
|
|
174
|
+
(candidate.command === undefined || typeof candidate.command === 'string') &&
|
|
175
|
+
(candidate.args === undefined ||
|
|
176
|
+
(Array.isArray(candidate.args) &&
|
|
177
|
+
candidate.args.every((argument) => typeof argument === 'string'))) &&
|
|
178
|
+
(candidate.execution === undefined ||
|
|
179
|
+
candidate.execution === 'attempted' ||
|
|
180
|
+
candidate.execution === 'rejected' ||
|
|
181
|
+
candidate.execution === 'not-configured') &&
|
|
182
|
+
Number.isInteger(candidate.exitCode) &&
|
|
183
|
+
typeof candidate.durationMs === 'number' &&
|
|
184
|
+
Number.isFinite(candidate.durationMs) &&
|
|
185
|
+
candidate.durationMs >= 0);
|
|
186
|
+
}) &&
|
|
187
|
+
receipt.findings !== null &&
|
|
188
|
+
typeof receipt.findings === 'object' &&
|
|
189
|
+
['total', 'errors', 'warnings', 'waived', 'waiverWarnings', 'expiredWaivers'].every((key) => {
|
|
190
|
+
const countValue = receipt.findings[key];
|
|
191
|
+
return typeof countValue === 'number' && Number.isInteger(countValue) && countValue >= 0;
|
|
192
|
+
}) &&
|
|
193
|
+
Array.isArray(receipt.findings.fingerprints) &&
|
|
194
|
+
receipt.findings.fingerprints.every((fingerprint) => typeof fingerprint === 'string') &&
|
|
195
|
+
Array.isArray(receipt.findings.ruleIds) &&
|
|
196
|
+
receipt.findings.ruleIds.every((ruleId) => typeof ruleId === 'string') &&
|
|
197
|
+
Array.isArray(receipt.contractIssues) &&
|
|
198
|
+
receipt.contractIssues.every((issue) => typeof issue === 'string') &&
|
|
199
|
+
Array.isArray(receipt.decisionIds) &&
|
|
200
|
+
new Set(receipt.decisionIds).size === receipt.decisionIds.length &&
|
|
201
|
+
receipt.decisionIds.every((decisionId) => typeof decisionId === 'string') &&
|
|
202
|
+
receipt.tool !== null &&
|
|
203
|
+
typeof receipt.tool === 'object' &&
|
|
204
|
+
typeof receipt.tool.name === 'string' &&
|
|
205
|
+
typeof receipt.tool.version === 'string');
|
|
206
|
+
const receiptMatchesManifest = receipt === undefined ||
|
|
207
|
+
(receipt.outcome === manifest.outcome &&
|
|
208
|
+
receipt.gate === manifest.gate &&
|
|
209
|
+
(manifest.commit === undefined
|
|
210
|
+
? receipt.subject.commit === undefined
|
|
211
|
+
: receipt.subject.commit === manifest.commit) &&
|
|
212
|
+
(manifest.policy === undefined
|
|
213
|
+
? receipt.policy === undefined
|
|
214
|
+
: receipt.policy?.fingerprint === manifest.policy.fingerprint &&
|
|
215
|
+
receipt.policy.source === manifest.policy.source &&
|
|
216
|
+
receipt.policy.revision === manifest.policy.revision));
|
|
20
217
|
const validStages = stages !== null &&
|
|
21
218
|
typeof stages === 'object' &&
|
|
22
219
|
!Array.isArray(stages) &&
|
|
@@ -46,27 +243,16 @@ function isGuardRunManifest(value) {
|
|
|
46
243
|
manifest.durationMs >= 0 &&
|
|
47
244
|
(manifest.outcome === 'pass' ||
|
|
48
245
|
manifest.outcome === 'fail' ||
|
|
246
|
+
manifest.outcome === 'incomplete' ||
|
|
247
|
+
manifest.outcome === 'policy-weakened' ||
|
|
49
248
|
manifest.outcome === 'warning' ||
|
|
50
249
|
manifest.outcome === 'needs-review' ||
|
|
51
250
|
manifest.outcome === 'not-configured') &&
|
|
52
251
|
typeof manifest.gate === 'string' &&
|
|
53
252
|
validStages &&
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
typeof manifest.policy.fingerprint === 'string' &&
|
|
58
|
-
manifest.policy.fingerprint.length > 0 &&
|
|
59
|
-
(manifest.policy.source === 'working-tree' || manifest.policy.source === 'git-ref') &&
|
|
60
|
-
(manifest.policy.revision === undefined ||
|
|
61
|
-
(Number.isInteger(manifest.policy.revision) && manifest.policy.revision >= 0)) &&
|
|
62
|
-
(manifest.policy.ref === undefined || typeof manifest.policy.ref === 'string') &&
|
|
63
|
-
(manifest.policy.changedFromBase === undefined ||
|
|
64
|
-
typeof manifest.policy.changedFromBase === 'boolean') &&
|
|
65
|
-
(manifest.policy.decisionIds === undefined ||
|
|
66
|
-
(manifest.policy.source === 'working-tree' &&
|
|
67
|
-
Array.isArray(manifest.policy.decisionIds) &&
|
|
68
|
-
new Set(manifest.policy.decisionIds).size === manifest.policy.decisionIds.length &&
|
|
69
|
-
manifest.policy.decisionIds.every((decisionId) => typeof decisionId === 'string' && decisionId.trim().length > 0))))));
|
|
253
|
+
validReceipt &&
|
|
254
|
+
receiptMatchesManifest &&
|
|
255
|
+
(manifest.policy === undefined || isGuardRunPolicy(manifest.policy)));
|
|
70
256
|
}
|
|
71
257
|
export function startGuardRun() {
|
|
72
258
|
return {
|
|
@@ -172,12 +358,14 @@ export function summarizeGuardRuns(root, limit = 100) {
|
|
|
172
358
|
total,
|
|
173
359
|
passed: runs.filter((run) => run.outcome === 'pass').length,
|
|
174
360
|
failed: runs.filter((run) => run.outcome === 'fail').length,
|
|
361
|
+
incomplete: runs.filter((run) => run.outcome === 'incomplete').length,
|
|
362
|
+
policyWeakened: runs.filter((run) => run.outcome === 'policy-weakened').length,
|
|
175
363
|
warnings: runs.filter((run) => run.outcome === 'warning').length,
|
|
176
364
|
averageDurationMs: total === 0 ? 0 : Math.round(runs.reduce((sum, run) => sum + run.durationMs, 0) / total),
|
|
177
365
|
...(total > 0 ? { last: runs[0] } : {}),
|
|
178
366
|
};
|
|
179
367
|
}
|
|
180
|
-
export function finishGuardRun(root, context, outcome, gate, command = 'verify', policy) {
|
|
368
|
+
export function finishGuardRun(root, context, outcome, gate, command = 'verify', policy, receipt) {
|
|
181
369
|
let commit;
|
|
182
370
|
try {
|
|
183
371
|
commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, stdio: 'pipe' })
|
|
@@ -187,18 +375,56 @@ export function finishGuardRun(root, context, outcome, gate, command = 'verify',
|
|
|
187
375
|
catch {
|
|
188
376
|
commit = undefined;
|
|
189
377
|
}
|
|
378
|
+
const completedAt = new Date().toISOString();
|
|
379
|
+
const completedReceipt = {
|
|
380
|
+
version: 1,
|
|
381
|
+
kind: 'verification-receipt',
|
|
382
|
+
runId: context.runId,
|
|
383
|
+
generatedAt: completedAt,
|
|
384
|
+
outcome,
|
|
385
|
+
gate,
|
|
386
|
+
subject: {
|
|
387
|
+
...receipt?.subject,
|
|
388
|
+
...(commit ? { commit } : {}),
|
|
389
|
+
changedFiles: receipt?.subject.changedFiles ?? [],
|
|
390
|
+
workingTreeDirty: receipt?.subject.workingTreeDirty ?? false,
|
|
391
|
+
},
|
|
392
|
+
...(policy ? { policy } : {}),
|
|
393
|
+
...(receipt?.verificationScope ? { verificationScope: receipt.verificationScope } : {}),
|
|
394
|
+
...(receipt?.policySurface ? { policySurface: receipt.policySurface } : {}),
|
|
395
|
+
...(receipt?.ciIntegrity ? { ciIntegrity: receipt.ciIntegrity } : {}),
|
|
396
|
+
requiredStages: receipt?.requiredStages ?? [],
|
|
397
|
+
incompleteStages: receipt?.incompleteStages ?? [],
|
|
398
|
+
...(receipt?.effectiveInvocation ? { effectiveInvocation: receipt.effectiveInvocation } : {}),
|
|
399
|
+
stages: receipt?.stages ?? {},
|
|
400
|
+
checks: receipt?.checks ?? {},
|
|
401
|
+
findings: receipt?.findings ?? {
|
|
402
|
+
total: 0,
|
|
403
|
+
errors: 0,
|
|
404
|
+
warnings: 0,
|
|
405
|
+
waived: 0,
|
|
406
|
+
waiverWarnings: 0,
|
|
407
|
+
expiredWaivers: 0,
|
|
408
|
+
fingerprints: [],
|
|
409
|
+
ruleIds: [],
|
|
410
|
+
},
|
|
411
|
+
contractIssues: receipt?.contractIssues ?? [],
|
|
412
|
+
decisionIds: receipt?.decisionIds ?? policy?.decisionIds ?? [],
|
|
413
|
+
tool: receipt?.tool ?? { name: '@codapult/guard', version: 'unknown' },
|
|
414
|
+
};
|
|
190
415
|
const manifest = {
|
|
191
416
|
version: 1,
|
|
192
417
|
runId: context.runId,
|
|
193
418
|
command,
|
|
194
419
|
startedAt: context.startedAt,
|
|
195
|
-
completedAt
|
|
420
|
+
completedAt,
|
|
196
421
|
durationMs: Math.max(0, Date.now() - context.startedAtMs),
|
|
197
422
|
outcome,
|
|
198
423
|
gate,
|
|
199
424
|
stages: context.stages,
|
|
200
425
|
...(commit ? { commit } : {}),
|
|
201
426
|
...(policy ? { policy } : {}),
|
|
427
|
+
receipt: completedReceipt,
|
|
202
428
|
};
|
|
203
429
|
try {
|
|
204
430
|
writeGuardRun(root, manifest);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { GuardVerificationReceipt } from '../history/runs.js';
|
|
2
|
+
export declare const GUARD_VERIFICATION_PREDICATE_TYPE = "https://codapult.dev/attestations/guard-verification/v1";
|
|
3
|
+
export interface GuardVerificationInTotoStatement {
|
|
4
|
+
_type: 'https://in-toto.io/Statement/v1';
|
|
5
|
+
subject: {
|
|
6
|
+
name: 'codapult-guard:verification-context';
|
|
7
|
+
digest: {
|
|
8
|
+
sha256: string;
|
|
9
|
+
};
|
|
10
|
+
}[];
|
|
11
|
+
predicateType: typeof GUARD_VERIFICATION_PREDICATE_TYPE;
|
|
12
|
+
predicate: {
|
|
13
|
+
receipt: GuardVerificationReceipt;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Bind a successful Guard receipt to the exact policy and file scope it verified. */
|
|
17
|
+
export declare function guardReceiptToInToto(receipt: GuardVerificationReceipt): GuardVerificationInTotoStatement;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const GUARD_VERIFICATION_PREDICATE_TYPE = 'https://codapult.dev/attestations/guard-verification/v1';
|
|
2
|
+
/** Bind a successful Guard receipt to the exact policy and file scope it verified. */
|
|
3
|
+
export function guardReceiptToInToto(receipt) {
|
|
4
|
+
const scope = receipt.verificationScope;
|
|
5
|
+
if (receipt.outcome !== 'pass' ||
|
|
6
|
+
!scope?.complete ||
|
|
7
|
+
!scope.stable ||
|
|
8
|
+
!/^[a-f0-9]{64}$/.test(scope.fingerprint)) {
|
|
9
|
+
throw new Error('Only a passing receipt with a complete, stable verification scope can be attested.');
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
_type: 'https://in-toto.io/Statement/v1',
|
|
13
|
+
subject: [
|
|
14
|
+
{
|
|
15
|
+
name: 'codapult-guard:verification-context',
|
|
16
|
+
digest: { sha256: scope.fingerprint },
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
predicateType: GUARD_VERIFICATION_PREDICATE_TYPE,
|
|
20
|
+
predicate: { receipt },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -226,6 +226,44 @@ export declare const guardConfigSchema: z.ZodObject<{
|
|
|
226
226
|
warningDays: z.ZodNumber;
|
|
227
227
|
maxDays: z.ZodOptional<z.ZodNumber>;
|
|
228
228
|
}, z.core.$strip>>;
|
|
229
|
+
protectedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
230
|
+
policySurface: z.ZodOptional<z.ZodObject<{
|
|
231
|
+
paths: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
232
|
+
pattern: z.ZodString;
|
|
233
|
+
kind: z.ZodEnum<{
|
|
234
|
+
"ci-workflow": "ci-workflow";
|
|
235
|
+
custom: "custom";
|
|
236
|
+
"dependency-input": "dependency-input";
|
|
237
|
+
"guard-policy": "guard-policy";
|
|
238
|
+
"ignore-config": "ignore-config";
|
|
239
|
+
ownership: "ownership";
|
|
240
|
+
"suppression-source": "suppression-source";
|
|
241
|
+
"tool-config": "tool-config";
|
|
242
|
+
}>;
|
|
243
|
+
severity: z.ZodDefault<z.ZodEnum<{
|
|
244
|
+
error: "error";
|
|
245
|
+
warning: "warning";
|
|
246
|
+
}>>;
|
|
247
|
+
}, z.core.$strip>>>;
|
|
248
|
+
}, z.core.$strip>>;
|
|
249
|
+
githubActions: z.ZodOptional<z.ZodObject<{
|
|
250
|
+
workflows: z.ZodArray<z.ZodObject<{
|
|
251
|
+
path: z.ZodString;
|
|
252
|
+
job: z.ZodString;
|
|
253
|
+
requiredRunIncludes: z.ZodArray<z.ZodString>;
|
|
254
|
+
requiredNeeds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
255
|
+
requirePullRequest: z.ZodDefault<z.ZodBoolean>;
|
|
256
|
+
requireUnfilteredPullRequest: z.ZodDefault<z.ZodBoolean>;
|
|
257
|
+
requireUnconditionalJob: z.ZodDefault<z.ZodBoolean>;
|
|
258
|
+
requireFullShaActions: z.ZodDefault<z.ZodBoolean>;
|
|
259
|
+
disallowContinueOnError: z.ZodDefault<z.ZodBoolean>;
|
|
260
|
+
maxPermissions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
261
|
+
none: "none";
|
|
262
|
+
read: "read";
|
|
263
|
+
write: "write";
|
|
264
|
+
}>>>;
|
|
265
|
+
}, z.core.$strip>>;
|
|
266
|
+
}, z.core.$strip>>;
|
|
229
267
|
}, z.core.$strip>;
|
|
230
268
|
export declare const guardContractsFileSchema: z.ZodObject<{
|
|
231
269
|
version: z.ZodLiteral<1>;
|
|
@@ -408,5 +446,14 @@ export declare const guardAgentConfigSchema: z.ZodObject<{
|
|
|
408
446
|
test: "test";
|
|
409
447
|
typecheck: "typecheck";
|
|
410
448
|
}>>;
|
|
449
|
+
requiredStages: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
450
|
+
adapters: "adapters";
|
|
451
|
+
"architecture-policy": "architecture-policy";
|
|
452
|
+
"ci-integrity": "ci-integrity";
|
|
453
|
+
"policy-surface": "policy-surface";
|
|
454
|
+
"project-checks": "project-checks";
|
|
455
|
+
runtime: "runtime";
|
|
456
|
+
"verification-scope": "verification-scope";
|
|
457
|
+
}>>>;
|
|
411
458
|
}, z.core.$strip>;
|
|
412
459
|
}, z.core.$strip>;
|
|
@@ -89,6 +89,61 @@ export const guardConfigSchema = z.object({
|
|
|
89
89
|
path: ['maxDays'],
|
|
90
90
|
})
|
|
91
91
|
.optional(),
|
|
92
|
+
protectedPaths: z
|
|
93
|
+
.array(z
|
|
94
|
+
.string()
|
|
95
|
+
.trim()
|
|
96
|
+
.min(1)
|
|
97
|
+
.refine((path) => !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..'), 'protectedPaths entries must be repository-relative paths without parent traversal'))
|
|
98
|
+
.optional(),
|
|
99
|
+
policySurface: z
|
|
100
|
+
.object({
|
|
101
|
+
paths: z
|
|
102
|
+
.array(z.object({
|
|
103
|
+
pattern: z
|
|
104
|
+
.string()
|
|
105
|
+
.trim()
|
|
106
|
+
.min(1)
|
|
107
|
+
.refine((path) => !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..'), 'policySurface paths must be repository-relative without parent traversal'),
|
|
108
|
+
kind: z.enum([
|
|
109
|
+
'ci-workflow',
|
|
110
|
+
'guard-policy',
|
|
111
|
+
'ownership',
|
|
112
|
+
'dependency-input',
|
|
113
|
+
'tool-config',
|
|
114
|
+
'ignore-config',
|
|
115
|
+
'suppression-source',
|
|
116
|
+
'custom',
|
|
117
|
+
]),
|
|
118
|
+
severity: z.enum(['warning', 'error']).default('error'),
|
|
119
|
+
}))
|
|
120
|
+
.default([]),
|
|
121
|
+
})
|
|
122
|
+
.optional(),
|
|
123
|
+
githubActions: z
|
|
124
|
+
.object({
|
|
125
|
+
workflows: z
|
|
126
|
+
.array(z.object({
|
|
127
|
+
path: z
|
|
128
|
+
.string()
|
|
129
|
+
.trim()
|
|
130
|
+
.min(1)
|
|
131
|
+
.refine((path) => !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..'), 'GitHub Actions workflow paths must be repository-relative without parent traversal'),
|
|
132
|
+
job: z.string().trim().min(1),
|
|
133
|
+
requiredRunIncludes: z.array(z.string().trim().min(1)).min(1),
|
|
134
|
+
requiredNeeds: z.array(z.string().trim().min(1)).optional(),
|
|
135
|
+
requirePullRequest: z.boolean().default(true),
|
|
136
|
+
requireUnfilteredPullRequest: z.boolean().default(true),
|
|
137
|
+
requireUnconditionalJob: z.boolean().default(true),
|
|
138
|
+
requireFullShaActions: z.boolean().default(true),
|
|
139
|
+
disallowContinueOnError: z.boolean().default(true),
|
|
140
|
+
maxPermissions: z
|
|
141
|
+
.record(z.string().trim().min(1), z.enum(['none', 'read', 'write']))
|
|
142
|
+
.optional(),
|
|
143
|
+
}))
|
|
144
|
+
.min(1),
|
|
145
|
+
})
|
|
146
|
+
.optional(),
|
|
92
147
|
});
|
|
93
148
|
export const guardContractsFileSchema = z.object({
|
|
94
149
|
version: z.literal(1),
|
|
@@ -167,5 +222,17 @@ export const guardAgentConfigSchema = z.object({
|
|
|
167
222
|
maxIterations: z.number().int().min(1).max(10),
|
|
168
223
|
projectChecks: z.boolean(),
|
|
169
224
|
checks: z.array(z.enum(['lint', 'typecheck', 'test', 'build'])),
|
|
225
|
+
requiredStages: z
|
|
226
|
+
.array(z.enum([
|
|
227
|
+
'runtime',
|
|
228
|
+
'project-checks',
|
|
229
|
+
'adapters',
|
|
230
|
+
'architecture-policy',
|
|
231
|
+
'policy-surface',
|
|
232
|
+
'ci-integrity',
|
|
233
|
+
'verification-scope',
|
|
234
|
+
]))
|
|
235
|
+
.min(1)
|
|
236
|
+
.default(['architecture-policy']),
|
|
170
237
|
}),
|
|
171
238
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GuardGitHubActionsPolicy } from '../guard.js';
|
|
2
|
+
export interface GuardCiIntegrityIssue {
|
|
3
|
+
code: 'workflow-unreadable' | 'workflow-invalid' | 'pull-request-trigger-missing' | 'pull-request-filtered' | 'pull-request-target-present' | 'job-missing' | 'job-conditional' | 'command-missing' | 'step-conditional' | 'continue-on-error' | 'dependency-missing' | 'dependency-conditional' | 'dependency-continue-on-error' | 'action-not-sha-pinned' | 'action-reference-changed' | 'base-workflow-unavailable' | 'permission-expansion';
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GuardCiIntegrityWorkflowResult {
|
|
7
|
+
path: string;
|
|
8
|
+
job: string;
|
|
9
|
+
digest?: string | undefined;
|
|
10
|
+
baseDigest?: string | undefined;
|
|
11
|
+
actions: string[];
|
|
12
|
+
baseActions?: string[] | undefined;
|
|
13
|
+
actionChanges?: {
|
|
14
|
+
name: string;
|
|
15
|
+
base: string[];
|
|
16
|
+
current: string[];
|
|
17
|
+
}[] | undefined;
|
|
18
|
+
status: 'ok' | 'policy-weakened' | 'incomplete';
|
|
19
|
+
issues: GuardCiIntegrityIssue[];
|
|
20
|
+
}
|
|
21
|
+
export interface GuardCiIntegrityResult {
|
|
22
|
+
status: 'ok' | 'policy-weakened' | 'incomplete';
|
|
23
|
+
workflows: GuardCiIntegrityWorkflowResult[];
|
|
24
|
+
}
|
|
25
|
+
export declare function auditGitHubActions(root: string, policy: GuardGitHubActionsPolicy | undefined, baseRef?: string): GuardCiIntegrityResult | undefined;
|