@baselineos/cli 0.1.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/src/verify.ts ADDED
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Baseline Verify Command
3
+ *
4
+ * Verifies governance policy compliance and evidence bundle integrity.
5
+ * Used for "Baseline-governed" certification checks.
6
+ *
7
+ * Usage:
8
+ * baseline verify --policy <path> --evidence <path>
9
+ * baseline verify --evidence <path>
10
+ * baseline verify --audit-trail <path>
11
+ */
12
+
13
+ import { readFile } from 'node:fs/promises';
14
+ import { existsSync } from 'node:fs';
15
+ import { createHash } from 'node:crypto';
16
+
17
+ export interface VerifyOptions {
18
+ policyPath?: string;
19
+ evidencePath?: string;
20
+ auditTrailPath?: string;
21
+ }
22
+
23
+ export interface VerifyCheck {
24
+ name: string;
25
+ status: 'pass' | 'fail' | 'warn' | 'skip';
26
+ message: string;
27
+ }
28
+
29
+ export interface VerifyResult {
30
+ success: boolean;
31
+ passed: number;
32
+ failed: number;
33
+ warnings: number;
34
+ skipped: number;
35
+ checks: VerifyCheck[];
36
+ hash?: string;
37
+ verifiedAt: string;
38
+ }
39
+
40
+ export async function runVerify(options: VerifyOptions): Promise<VerifyResult> {
41
+ const checks: VerifyCheck[] = [];
42
+
43
+ // Verify evidence bundle
44
+ if (options.evidencePath) {
45
+ const evidenceChecks = await verifyEvidenceBundle(options.evidencePath);
46
+ checks.push(...evidenceChecks);
47
+ }
48
+
49
+ // Verify policy
50
+ if (options.policyPath) {
51
+ const policyChecks = await verifyPolicy(options.policyPath);
52
+ checks.push(...policyChecks);
53
+ }
54
+
55
+ // Verify audit trail
56
+ if (options.auditTrailPath) {
57
+ const auditChecks = await verifyAuditTrail(options.auditTrailPath);
58
+ checks.push(...auditChecks);
59
+ }
60
+
61
+ if (checks.length === 0) {
62
+ checks.push({
63
+ name: 'input',
64
+ status: 'fail',
65
+ message: 'No files provided. Use --policy, --evidence, or --audit-trail.',
66
+ });
67
+ }
68
+
69
+ const passed = checks.filter(c => c.status === 'pass').length;
70
+ const failed = checks.filter(c => c.status === 'fail').length;
71
+ const warnings = checks.filter(c => c.status === 'warn').length;
72
+ const skipped = checks.filter(c => c.status === 'skip').length;
73
+
74
+ // Generate verification hash
75
+ const hashInput = checks.map(c => `${c.name}:${c.status}:${c.message}`).join('|');
76
+ const hash = createHash('sha256').update(hashInput).digest('hex').slice(0, 16);
77
+
78
+ return {
79
+ success: failed === 0,
80
+ passed,
81
+ failed,
82
+ warnings,
83
+ skipped,
84
+ checks,
85
+ hash,
86
+ verifiedAt: new Date().toISOString(),
87
+ };
88
+ }
89
+
90
+ async function verifyEvidenceBundle(filePath: string): Promise<VerifyCheck[]> {
91
+ const checks: VerifyCheck[] = [];
92
+
93
+ if (!existsSync(filePath)) {
94
+ checks.push({ name: 'evidence.exists', status: 'fail', message: `Evidence file not found: ${filePath}` });
95
+ return checks;
96
+ }
97
+ checks.push({ name: 'evidence.exists', status: 'pass', message: 'Evidence file found' });
98
+
99
+ try {
100
+ const raw = await readFile(filePath, 'utf-8');
101
+ const bundle = JSON.parse(raw);
102
+
103
+ // Check bundle structure
104
+ if (bundle.policy) {
105
+ checks.push({ name: 'evidence.policy', status: 'pass', message: `Policy: ${bundle.policy.name ?? bundle.policy.id}` });
106
+ } else {
107
+ checks.push({ name: 'evidence.policy', status: 'warn', message: 'Evidence bundle has no policy reference' });
108
+ }
109
+
110
+ if (Array.isArray(bundle.auditTrail)) {
111
+ checks.push({
112
+ name: 'evidence.auditTrail',
113
+ status: bundle.auditTrail.length > 0 ? 'pass' : 'warn',
114
+ message: `Audit trail: ${bundle.auditTrail.length} events`,
115
+ });
116
+ } else {
117
+ checks.push({ name: 'evidence.auditTrail', status: 'fail', message: 'Evidence bundle missing audit trail' });
118
+ }
119
+
120
+ if (bundle.latestEvaluation) {
121
+ const eval_ = bundle.latestEvaluation;
122
+ checks.push({
123
+ name: 'evidence.evaluation',
124
+ status: eval_.passed ? 'pass' : 'fail',
125
+ message: `Evaluation: ${eval_.passed ? 'passed' : 'failed'} (score: ${eval_.score ?? 'N/A'})`,
126
+ });
127
+ } else {
128
+ checks.push({ name: 'evidence.evaluation', status: 'skip', message: 'No evaluation in evidence bundle' });
129
+ }
130
+
131
+ if (Array.isArray(bundle.approvals)) {
132
+ const approved = bundle.approvals.filter((a: { status: string }) => a.status === 'approved');
133
+ checks.push({
134
+ name: 'evidence.approvals',
135
+ status: 'pass',
136
+ message: `Approvals: ${approved.length}/${bundle.approvals.length} approved`,
137
+ });
138
+ }
139
+
140
+ // Integrity hash
141
+ const contentHash = createHash('sha256').update(raw).digest('hex');
142
+ checks.push({ name: 'evidence.integrity', status: 'pass', message: `SHA-256: ${contentHash.slice(0, 16)}...` });
143
+
144
+ } catch (error) {
145
+ checks.push({
146
+ name: 'evidence.parse',
147
+ status: 'fail',
148
+ message: `Failed to parse evidence: ${error instanceof Error ? error.message : String(error)}`,
149
+ });
150
+ }
151
+
152
+ return checks;
153
+ }
154
+
155
+ async function verifyPolicy(filePath: string): Promise<VerifyCheck[]> {
156
+ const checks: VerifyCheck[] = [];
157
+
158
+ if (!existsSync(filePath)) {
159
+ checks.push({ name: 'policy.exists', status: 'fail', message: `Policy file not found: ${filePath}` });
160
+ return checks;
161
+ }
162
+ checks.push({ name: 'policy.exists', status: 'pass', message: 'Policy file found' });
163
+
164
+ try {
165
+ const raw = await readFile(filePath, 'utf-8');
166
+ const policy = JSON.parse(raw);
167
+
168
+ if (policy.id && policy.name) {
169
+ checks.push({ name: 'policy.identity', status: 'pass', message: `Policy: ${policy.name} (${policy.id})` });
170
+ } else {
171
+ checks.push({ name: 'policy.identity', status: 'fail', message: 'Policy missing id or name' });
172
+ }
173
+
174
+ if (policy.status === 'approved' || policy.status === 'enforced') {
175
+ checks.push({ name: 'policy.status', status: 'pass', message: `Status: ${policy.status}` });
176
+ } else {
177
+ checks.push({ name: 'policy.status', status: 'warn', message: `Policy status is ${policy.status ?? 'unknown'}, not approved/enforced` });
178
+ }
179
+
180
+ if (policy.version) {
181
+ checks.push({ name: 'policy.version', status: 'pass', message: `Version: ${policy.version}` });
182
+ } else {
183
+ checks.push({ name: 'policy.version', status: 'warn', message: 'Policy has no version' });
184
+ }
185
+
186
+ if (Array.isArray(policy.rules) && policy.rules.length > 0) {
187
+ checks.push({ name: 'policy.rules', status: 'pass', message: `${policy.rules.length} rules defined` });
188
+ } else {
189
+ checks.push({ name: 'policy.rules', status: 'warn', message: 'Policy has no rules defined' });
190
+ }
191
+
192
+ } catch (error) {
193
+ checks.push({
194
+ name: 'policy.parse',
195
+ status: 'fail',
196
+ message: `Failed to parse policy: ${error instanceof Error ? error.message : String(error)}`,
197
+ });
198
+ }
199
+
200
+ return checks;
201
+ }
202
+
203
+ async function verifyAuditTrail(filePath: string): Promise<VerifyCheck[]> {
204
+ const checks: VerifyCheck[] = [];
205
+
206
+ if (!existsSync(filePath)) {
207
+ checks.push({ name: 'audit.exists', status: 'fail', message: `Audit trail not found: ${filePath}` });
208
+ return checks;
209
+ }
210
+ checks.push({ name: 'audit.exists', status: 'pass', message: 'Audit trail found' });
211
+
212
+ try {
213
+ const raw = await readFile(filePath, 'utf-8');
214
+ const events = JSON.parse(raw);
215
+
216
+ if (!Array.isArray(events)) {
217
+ checks.push({ name: 'audit.format', status: 'fail', message: 'Audit trail is not an array' });
218
+ return checks;
219
+ }
220
+
221
+ checks.push({ name: 'audit.count', status: 'pass', message: `${events.length} audit events` });
222
+
223
+ // Check chronological ordering
224
+ let ordered = true;
225
+ for (let i = 1; i < events.length; i++) {
226
+ if (events[i].timestamp < events[i - 1].timestamp) {
227
+ ordered = false;
228
+ break;
229
+ }
230
+ }
231
+ checks.push({
232
+ name: 'audit.order',
233
+ status: ordered ? 'pass' : 'warn',
234
+ message: ordered ? 'Events are chronologically ordered' : 'Events are NOT chronologically ordered',
235
+ });
236
+
237
+ // Check event types
238
+ const types = new Set(events.map((e: { type: string }) => e.type));
239
+ checks.push({ name: 'audit.types', status: 'pass', message: `Event types: ${Array.from(types).join(', ')}` });
240
+
241
+ // Integrity
242
+ const contentHash = createHash('sha256').update(raw).digest('hex');
243
+ checks.push({ name: 'audit.integrity', status: 'pass', message: `SHA-256: ${contentHash.slice(0, 16)}...` });
244
+
245
+ } catch (error) {
246
+ checks.push({
247
+ name: 'audit.parse',
248
+ status: 'fail',
249
+ message: `Failed to parse audit trail: ${error instanceof Error ? error.message : String(error)}`,
250
+ });
251
+ }
252
+
253
+ return checks;
254
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }