@grc-claw/cli 0.8.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/index.ts ADDED
@@ -0,0 +1,1885 @@
1
+ #!/usr/bin/env node
2
+ // @grc-claw/cli — The GRC_Claw command-line interface
3
+ // Usage: grc <command> [options]
4
+
5
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from 'node:fs';
6
+ import { resolve, join, extname, relative } from 'node:path';
7
+ import { execSync } from 'node:child_process';
8
+ import { createHash } from 'node:crypto';
9
+
10
+ const VERSION = '1.0.0';
11
+
12
+ // ─── ANSI colors ─────────────────────────────────────────────────────────────
13
+ const c = {
14
+ reset: '\x1b[0m',
15
+ bold: '\x1b[1m',
16
+ dim: '\x1b[2m',
17
+ red: '\x1b[31m',
18
+ green: '\x1b[32m',
19
+ yellow: '\x1b[33m',
20
+ blue: '\x1b[34m',
21
+ cyan: '\x1b[36m',
22
+ white: '\x1b[37m',
23
+ bgRed: '\x1b[41m',
24
+ bgGreen: '\x1b[42m',
25
+ };
26
+
27
+ function log(msg: string) { process.stdout.write(msg + '\n'); }
28
+ function info(msg: string) { log(`${c.cyan}ℹ${c.reset} ${msg}`); }
29
+ function success(msg: string) { log(`${c.green}✓${c.reset} ${msg}`); }
30
+ function warn(msg: string) { log(`${c.yellow}⚠${c.reset} ${msg}`); }
31
+ function error(msg: string) { log(`${c.red}✗${c.reset} ${msg}`); }
32
+ function bold(msg: string) { return `${c.bold}${msg}${c.reset}`; }
33
+ function dim(msg: string) { return `${c.dim}${msg}${c.reset}`; }
34
+
35
+ // ─── Compliance Rules (mirrors compliance-copilot PRReviewEngine) ─────────────
36
+ interface Finding {
37
+ file: string;
38
+ line: number;
39
+ severity: 'error' | 'warning' | 'info';
40
+ rule: string;
41
+ message: string;
42
+ framework: string;
43
+ control: string;
44
+ autoFixable: boolean;
45
+ suggestion?: string;
46
+ }
47
+
48
+ const SCAN_RULES: Array<{
49
+ id: string;
50
+ name: string;
51
+ pattern: RegExp;
52
+ severity: 'error' | 'warning' | 'info';
53
+ message: string;
54
+ framework: string;
55
+ control: string;
56
+ suggestion?: string;
57
+ }> = [
58
+ {
59
+ id: 'no-hardcoded-secrets',
60
+ name: 'Hardcoded Secrets',
61
+ pattern: /(?:password|secret|api_key|apikey|access_token|private_key)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
62
+ severity: 'error',
63
+ message: 'Hardcoded secret detected — use environment variables or a secrets manager',
64
+ framework: 'SOC 2 / ISO 27001',
65
+ control: 'CC6.1 / A.9.4.3',
66
+ suggestion: 'Replace with process.env.SECRET_NAME and rotate the exposed credential immediately',
67
+ },
68
+ {
69
+ id: 'no-mfa-bypass',
70
+ name: 'MFA Bypass',
71
+ pattern: /skip.*mfa|bypass.*mfa|disable.*2fa|mfa.*false|two_factor.*false/gi,
72
+ severity: 'error',
73
+ message: 'Potential MFA bypass detected',
74
+ framework: 'ISO 27001 / NIST CSF',
75
+ control: 'A.9.4.2 / PR.AC-7',
76
+ suggestion: 'MFA must be enforced for all privileged operations per ISO 27001 A.9.4.2',
77
+ },
78
+ {
79
+ id: 'no-weak-crypto',
80
+ name: 'Weak Cryptography',
81
+ pattern: /\b(?:md5|sha1|des|rc4|ecb)\b(?!\w)/gi,
82
+ severity: 'error',
83
+ message: 'Weak or deprecated cryptographic algorithm detected',
84
+ framework: 'ISO 27001 / PCI DSS',
85
+ control: 'A.10.1.1 / Req-3.4',
86
+ suggestion: 'Use SHA-256 or stronger. For encryption: AES-256-GCM or ChaCha20-Poly1305',
87
+ },
88
+ {
89
+ id: 'no-sql-injection',
90
+ name: 'SQL Injection Risk',
91
+ pattern: /`\s*SELECT[^`]*\$\{[^}]+\}[^`]*`|query\s*\(\s*['"`][^'"`]*\+/gi,
92
+ severity: 'error',
93
+ message: 'Potential SQL injection via string concatenation',
94
+ framework: 'SOC 2 / OWASP',
95
+ control: 'CC6.6 / A1',
96
+ suggestion: 'Use parameterized queries or an ORM. Never concatenate user input into SQL strings',
97
+ },
98
+ {
99
+ id: 'no-http-in-prod',
100
+ name: 'Unencrypted Transport',
101
+ pattern: /http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/g,
102
+ severity: 'warning',
103
+ message: 'HTTP (non-TLS) URL detected — use HTTPS in production',
104
+ framework: 'ISO 27001 / NIST CSF',
105
+ control: 'A.10.1.2 / PR.DS-2',
106
+ suggestion: 'Replace http:// with https:// for all external URLs',
107
+ },
108
+ {
109
+ id: 'no-console-log-sensitive',
110
+ name: 'Sensitive Data Logging',
111
+ pattern: /console\.log\([^)]*(?:password|token|secret|key|ssn|credit)[^)]*\)/gi,
112
+ severity: 'warning',
113
+ message: 'Potentially logging sensitive data — verify no PII/credentials reach logs',
114
+ framework: 'GDPR / ISO 27001',
115
+ control: 'Art.32 / A.12.4.1',
116
+ suggestion: 'Remove or mask sensitive fields before logging',
117
+ },
118
+ {
119
+ id: 'no-eval',
120
+ name: 'Dynamic Code Execution',
121
+ pattern: /\beval\s*\(|\bnew\s+Function\s*\(/g,
122
+ severity: 'error',
123
+ message: 'eval() or new Function() creates code injection risk',
124
+ framework: 'SOC 2 / ISO 27001',
125
+ control: 'CC6.6 / A.12.6.1',
126
+ suggestion: 'Eliminate eval(). Use JSON.parse() for data, explicit function references for behavior',
127
+ },
128
+ {
129
+ id: 'no-todo-security',
130
+ name: 'Security TODO',
131
+ pattern: /TODO.*(?:security|auth|encrypt|validate|sanitize)|FIXME.*(?:security|auth)/gi,
132
+ severity: 'info',
133
+ message: 'Security-related TODO/FIXME — track and resolve before audit',
134
+ framework: 'ISO 27001',
135
+ control: 'A.12.6.1',
136
+ },
137
+ {
138
+ id: 'no-debug-endpoints',
139
+ name: 'Debug Endpoint',
140
+ pattern: /route\s*\(['"`]\/debug|app\.(get|post)\s*\(['"`]\/debug|path.*['"`]\/debug/gi,
141
+ severity: 'warning',
142
+ message: 'Debug endpoint detected — ensure it is disabled or gated in production',
143
+ framework: 'SOC 2 / ISO 27001',
144
+ control: 'CC6.6 / A.14.2.6',
145
+ },
146
+ {
147
+ id: 'no-cors-wildcard-prod',
148
+ name: 'Permissive CORS',
149
+ pattern: /cors\s*\(\s*\{\s*origin\s*:\s*['"]\*['"]/gi,
150
+ severity: 'warning',
151
+ message: 'CORS wildcard origin — restrict to known origins in production',
152
+ framework: 'ISO 27001',
153
+ control: 'A.13.1.3',
154
+ suggestion: "Specify allowed origins: origin: ['https://your-domain.com']",
155
+ },
156
+ {
157
+ id: 'no-missing-auth-check',
158
+ name: 'Missing Auth Check',
159
+ pattern: /app\.(get|post|put|delete|patch)\s*\(['"`][^'"`,]+['"`]\s*,\s*(?:async\s*)?\([^)]*\)\s*=>/g,
160
+ severity: 'info',
161
+ message: 'Route handler — verify authentication middleware is applied',
162
+ framework: 'SOC 2 / ISO 27001',
163
+ control: 'CC6.1 / A.9.4.1',
164
+ },
165
+ {
166
+ id: 'pqc-recommendation',
167
+ name: 'Post-Quantum Cryptography',
168
+ pattern: /rsa|ecdsa|elliptic.*curve|diffie.hellman/gi,
169
+ severity: 'info',
170
+ message: 'Classical asymmetric crypto — consider PQC migration path per NIST FIPS 203/204/205',
171
+ framework: 'ISO 27001 / NIST SP 800-208',
172
+ control: 'A.10.1.1',
173
+ suggestion: 'Plan migration to ML-KEM-1024 (key exchange) and ML-DSA-87 (signatures) per NIST PQC standards',
174
+ },
175
+ ];
176
+
177
+ const SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.java', '.rb', '.php', '.cs', '.rs', '.tf', '.yaml', '.yml', '.json', '.sh', '.env.example']);
178
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'vendor', 'coverage']);
179
+
180
+ function walkFiles(dir: string, files: string[] = []): string[] {
181
+ try {
182
+ for (const entry of readdirSync(dir)) {
183
+ const full = join(dir, entry);
184
+ if (SKIP_DIRS.has(entry)) continue;
185
+ try {
186
+ const stat = statSync(full);
187
+ if (stat.isDirectory()) walkFiles(full, files);
188
+ else if (SCAN_EXTENSIONS.has(extname(entry))) files.push(full);
189
+ } catch { /* skip unreadable */ }
190
+ }
191
+ } catch { /* skip unreadable dir */ }
192
+ return files;
193
+ }
194
+
195
+ function scanFile(filePath: string): Finding[] {
196
+ let content: string;
197
+ try { content = readFileSync(filePath, 'utf8'); } catch { return []; }
198
+ const findings: Finding[] = [];
199
+ for (const rule of SCAN_RULES) {
200
+ let match: RegExpExecArray | null;
201
+ const re = new RegExp(rule.pattern.source, rule.pattern.flags);
202
+ while ((match = re.exec(content)) !== null) {
203
+ const lineNum = content.slice(0, match.index).split('\n').length;
204
+ findings.push({
205
+ file: filePath,
206
+ line: lineNum,
207
+ severity: rule.severity,
208
+ rule: rule.id,
209
+ message: rule.message,
210
+ framework: rule.framework,
211
+ control: rule.control,
212
+ autoFixable: !!rule.suggestion,
213
+ suggestion: rule.suggestion,
214
+ });
215
+ }
216
+ }
217
+ return findings;
218
+ }
219
+
220
+ function posture(findings: Finding[]): number {
221
+ const errors = findings.filter((f) => f.severity === 'error').length;
222
+ const warnings = findings.filter((f) => f.severity === 'warning').length;
223
+ const deduction = errors * 10 + warnings * 3;
224
+ return Math.max(0, 100 - deduction);
225
+ }
226
+
227
+ // ─── Framework pack data (inline minimal — full data from @grc-claw/frameworks in bundled version) ──
228
+ const FRAMEWORK_SUMMARIES: Record<string, { name: string; controls: number; description: string }> = {
229
+ 'iso27001': { name: 'ISO/IEC 27001:2022', controls: 93, description: 'Information security management system' },
230
+ 'nist-csf': { name: 'NIST CSF 2.0', controls: 106, description: 'Cybersecurity framework (Govern/Identify/Protect/Detect/Respond/Recover)' },
231
+ 'soc2': { name: 'SOC 2 Type II', controls: 64, description: 'Trust Service Criteria (AICPA)' },
232
+ 'iso42001': { name: 'ISO/IEC 42001:2023', controls: 38, description: 'Artificial intelligence management system (AIMS)' },
233
+ 'eu-ai-act': { name: 'EU AI Act (2024/1689)', controls: 44, description: 'EU regulation on artificial intelligence' },
234
+ 'dora': { name: 'DORA (EU 2022/2554)', controls: 35, description: 'Digital operational resilience for financial entities' },
235
+ 'hipaa': { name: 'HIPAA Security Rule', controls: 42, description: 'US health information privacy and security' },
236
+ 'pci-dss': { name: 'PCI DSS v4.0', controls: 64, description: 'Payment card industry data security standard' },
237
+ 'gdpr': { name: 'GDPR (2016/679)', controls: 28, description: 'EU general data protection regulation' },
238
+ 'fedramp': { name: 'FedRAMP Moderate', controls: 323, description: 'US federal cloud security authorization' },
239
+ };
240
+
241
+ // ─── Commands ─────────────────────────────────────────────────────────────────
242
+
243
+ async function cmdScan(args: string[]) {
244
+ const targetPath = resolve(args[0] ?? '.');
245
+ if (!existsSync(targetPath)) { error(`Path not found: ${targetPath}`); process.exit(1); }
246
+
247
+ const framework = args.find((a, i) => args[i - 1] === '--framework') ?? null;
248
+ const jsonMode = args.includes('--json');
249
+
250
+ if (!jsonMode) {
251
+ log(`\n${bold('GRC_Claw')} ${dim(`v${VERSION}`)}`);
252
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
253
+ info(`Scanning: ${bold(targetPath)}`);
254
+ if (framework) info(`Framework filter: ${bold(framework)}`);
255
+ }
256
+
257
+ const files = walkFiles(targetPath);
258
+ if (!jsonMode) info(`Found ${bold(String(files.length))} files to scan`);
259
+
260
+ const allFindings: Finding[] = [];
261
+ for (const file of files) {
262
+ const findings = scanFile(file);
263
+ allFindings.push(...findings);
264
+ }
265
+
266
+ const errors = allFindings.filter((f) => f.severity === 'error');
267
+ const warnings = allFindings.filter((f) => f.severity === 'warning');
268
+ const infos = allFindings.filter((f) => f.severity === 'info');
269
+ const score = posture(allFindings);
270
+
271
+ if (jsonMode) {
272
+ process.stdout.write(JSON.stringify({ score, findings: allFindings, summary: { errors: errors.length, warnings: warnings.length, info: infos.length } }, null, 2) + '\n');
273
+ process.exit(errors.length > 0 ? 1 : 0);
274
+ return;
275
+ }
276
+
277
+ log('');
278
+ if (errors.length > 0) {
279
+ log(`${c.red}${bold('ERRORS')} (${errors.length})${c.reset}`);
280
+ for (const f of errors) {
281
+ log(` ${c.red}✗${c.reset} ${dim(f.file.replace(targetPath, '.'))}:${f.line}`);
282
+ log(` ${bold(f.message)}`);
283
+ log(` ${dim(`${f.framework} — ${f.control}`)}`);
284
+ if (f.suggestion) log(` ${c.cyan}Fix:${c.reset} ${f.suggestion}`);
285
+ log('');
286
+ }
287
+ }
288
+
289
+ if (warnings.length > 0) {
290
+ log(`${c.yellow}${bold('WARNINGS')} (${warnings.length})${c.reset}`);
291
+ for (const f of warnings) {
292
+ log(` ${c.yellow}⚠${c.reset} ${dim(f.file.replace(targetPath, '.'))}:${f.line}`);
293
+ log(` ${f.message}`);
294
+ log(` ${dim(`${f.framework} — ${f.control}`)}`);
295
+ log('');
296
+ }
297
+ }
298
+
299
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
300
+
301
+ const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
302
+ log(`${bold('Compliance Posture Score:')} ${scoreColor}${bold(String(score))}/100${c.reset}`);
303
+ log(`${bold('Errors:')} ${errors.length > 0 ? c.red : c.green}${errors.length}${c.reset} ${bold('Warnings:')} ${warnings.length > 0 ? c.yellow : c.green}${warnings.length}${c.reset} ${bold('Info:')} ${infos.length}`);
304
+
305
+ if (score < 60) {
306
+ log(`\n${c.bgRed}${c.white} BLOCKING ${c.reset} Score below 60 — resolve errors before audit`);
307
+ } else if (score >= 90) {
308
+ log(`\n${c.bgGreen}${c.white} PASS ${c.reset} Excellent compliance posture`);
309
+ }
310
+
311
+ log(`\n${dim('Full report:')} grc report --framework iso27001`);
312
+ log(`${dim('Fix issues:')} grc scan ${args[0] ?? '.'} --json | jq '.findings[] | select(.severity==\"error\")'`);
313
+ log(`${dim('Gateway:')} grc doctor\n`);
314
+
315
+ process.exit(errors.length > 0 ? 1 : 0);
316
+ }
317
+
318
+ function cmdFrameworks(args: string[]) {
319
+ const sub = args[0] ?? 'list';
320
+ if (sub === 'list') {
321
+ log(`\n${bold('Available Framework Packs')}\n`);
322
+ for (const [id, meta] of Object.entries(FRAMEWORK_SUMMARIES)) {
323
+ log(` ${c.cyan}${id.padEnd(12)}${c.reset} ${bold(meta.name)}`);
324
+ log(` ${' '.repeat(12)} ${dim(meta.description)} ${dim(`(${meta.controls} controls)`)}\n`);
325
+ }
326
+ log(`${dim('Install additional packs: grc add <framework>')}\n`);
327
+ }
328
+ }
329
+
330
+ function cmdReport(args: string[]) {
331
+ const fw = args.find((a, i) => args[i - 1] === '--framework') ?? 'iso27001';
332
+ const targetPath = resolve(args.find((a, i) => args[i - 1] === '--path') ?? '.');
333
+ const meta = FRAMEWORK_SUMMARIES[fw];
334
+
335
+ log(`\n${bold('Generating Compliance Report')}`);
336
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
337
+ info(`Framework: ${bold(meta?.name ?? fw)}`);
338
+ info(`Path: ${bold(targetPath)}`);
339
+
340
+ const files = walkFiles(targetPath);
341
+ const allFindings = files.flatMap(scanFile);
342
+ const score = posture(allFindings);
343
+ const timestamp = new Date().toISOString();
344
+ const reportHash = createHash('sha256').update(JSON.stringify({ fw, allFindings, timestamp })).digest('hex');
345
+
346
+ const report = {
347
+ grc_claw_version: VERSION,
348
+ report_id: `grc-report-${Date.now()}`,
349
+ sha256: reportHash,
350
+ generated_at: timestamp,
351
+ framework: fw,
352
+ framework_name: meta?.name ?? fw,
353
+ path_scanned: targetPath,
354
+ posture_score: score,
355
+ summary: {
356
+ files_scanned: files.length,
357
+ total_findings: allFindings.length,
358
+ errors: allFindings.filter((f) => f.severity === 'error').length,
359
+ warnings: allFindings.filter((f) => f.severity === 'warning').length,
360
+ info: allFindings.filter((f) => f.severity === 'info').length,
361
+ },
362
+ findings: allFindings,
363
+ attestation: {
364
+ method: 'grc-claw-static-scan',
365
+ auditable: true,
366
+ hash_algorithm: 'sha256',
367
+ hash: reportHash,
368
+ },
369
+ };
370
+
371
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
372
+ log(`\n${c.green}✓${c.reset} Report generated — hash: ${dim(reportHash.slice(0, 16))}…`);
373
+ log(`${dim('Save:')} grc report --framework ${fw} > report-${fw}-${timestamp.slice(0, 10)}.json\n`);
374
+ }
375
+
376
+ async function cmdDoctor() {
377
+ log(`\n${bold('GRC_Claw Doctor')} ${dim(`v${VERSION}`)}`);
378
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
379
+
380
+ const checks = [
381
+ { name: 'Node.js version', fn: () => { const v = parseInt(process.version.slice(1)); if (v < 20) throw new Error(`Node ${process.version} — requires ≥20`); return `Node ${process.version}`; } },
382
+ { name: 'GRC_CLAW_GATEWAY_TOKEN', fn: () => { const t = process.env.GRC_CLAW_GATEWAY_TOKEN; if (!t) throw new Error('Not set — export GRC_CLAW_GATEWAY_TOKEN=<token>'); return 'Set'; } },
383
+ { name: 'Gateway connectivity', fn: async () => {
384
+ const url = `http://${process.env.GRC_CLAW_HOST ?? '127.0.0.1'}:${process.env.GRC_CLAW_PORT ?? 18791}/health`;
385
+ const r = await fetch(url, { signal: AbortSignal.timeout(3000) });
386
+ if (!r.ok) throw new Error(`Gateway returned ${r.status}`);
387
+ const body = await r.json() as { ok: boolean };
388
+ if (!body.ok) throw new Error('Gateway not healthy');
389
+ return `Connected (${url})`;
390
+ }},
391
+ { name: 'grcfile.yaml', fn: () => { if (!existsSync('grcfile.yaml') && !existsSync('.grcfile.yaml')) throw new Error('Not found — run: grc init'); return 'Found'; } },
392
+ ];
393
+
394
+ let failures = 0;
395
+ for (const check of checks) {
396
+ try {
397
+ const result = await check.fn();
398
+ success(`${check.name.padEnd(32)} ${dim(result)}`);
399
+ } catch (e) {
400
+ error(`${check.name.padEnd(32)} ${(e as Error).message}`);
401
+ failures++;
402
+ }
403
+ }
404
+
405
+ log('');
406
+ if (failures === 0) {
407
+ log(`${c.bgGreen}${c.white} ALL CHECKS PASSED ${c.reset}\n`);
408
+ } else {
409
+ log(`${c.bgRed}${c.white} ${failures} CHECK${failures > 1 ? 'S' : ''} FAILED ${c.reset}\n`);
410
+ process.exit(1);
411
+ }
412
+ }
413
+
414
+ // ─── grc init ─────────────────────────────────────────────────────────────────
415
+ function cmdInit(args: string[]) {
416
+ const fw = args[args.indexOf('--framework') + 1] ?? args[args.indexOf('-f') + 1] ?? 'iso27001';
417
+ const validFrameworks = ['iso27001', 'soc2', 'nist-csf', 'iso42001', 'dora', 'hipaa', 'pci-dss'];
418
+ if (!validFrameworks.includes(fw)) {
419
+ error(`Unknown framework: ${fw}. Valid: ${validFrameworks.join(', ')}`);
420
+ process.exit(1);
421
+ }
422
+
423
+ if (existsSync('grcfile.yaml')) {
424
+ warn('grcfile.yaml already exists — skipping (use --force to overwrite)');
425
+ if (!args.includes('--force')) return;
426
+ }
427
+
428
+ const grcfile = `# GRC_Claw Compliance-as-Code configuration
429
+ # Run: grc scan . | grc apply | grc report --framework ${fw}
430
+ # Docs: https://a2zsoc.com/developers/compliance-as-code
431
+
432
+ version: "1.0"
433
+ framework: ${fw}
434
+ org: ${process.env.GRC_ORG ?? 'my-org'}
435
+
436
+ scan:
437
+ paths:
438
+ - src/
439
+ - api/
440
+ - scripts/
441
+ exclude:
442
+ - node_modules/
443
+ - dist/
444
+ - .git/
445
+
446
+ evidence:
447
+ output: ./compliance-evidence
448
+ formats: [json, html]
449
+ retention_days: 365
450
+
451
+ controls:
452
+ # Override specific control thresholds
453
+ # cc6.1:
454
+ # required_evidence: [mfa_logs, access_review]
455
+ # exemption: "Legacy system — tracked in JIRA-1234"
456
+
457
+ integrations:
458
+ github_app: ${process.env.GRC_GITHUB_APP_ID ? 'enabled' : 'disabled'}
459
+ gateway: ${process.env.GRC_CLAW_GATEWAY_TOKEN ? 'enabled' : 'disabled'}
460
+ a2z_soc: ${process.env.A2Z_SOC_API_KEY ? 'enabled' : 'disabled'}
461
+ `;
462
+
463
+ writeFileSync('grcfile.yaml', grcfile);
464
+ success('Created grcfile.yaml');
465
+
466
+ // GitHub Actions workflow
467
+ if (!existsSync('.github/workflows')) {
468
+ mkdirSync('.github/workflows', { recursive: true });
469
+ }
470
+ if (!existsSync('.github/workflows/compliance.yml')) {
471
+ const ghAction = `name: Compliance Gate
472
+ on:
473
+ pull_request:
474
+ branches: [main]
475
+ push:
476
+ branches: [main]
477
+
478
+ jobs:
479
+ compliance:
480
+ runs-on: ubuntu-latest
481
+ steps:
482
+ - uses: actions/checkout@v4
483
+ - uses: actions/setup-node@v4
484
+ with:
485
+ node-version: 20
486
+ - run: npm install -g @grc-claw/cli
487
+ - name: Scan
488
+ run: grc scan . --json > compliance-report.json
489
+ - name: Gate on errors
490
+ run: |
491
+ ERRORS=$(cat compliance-report.json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); console.log(JSON.parse(d).summary?.errors ?? 0)")
492
+ if [ "$ERRORS" -gt "0" ]; then
493
+ echo "::error::$ERRORS compliance error(s) found — run 'grc scan .' locally"
494
+ exit 1
495
+ fi
496
+ - uses: actions/upload-artifact@v4
497
+ with:
498
+ name: compliance-report
499
+ path: compliance-report.json
500
+ `;
501
+ writeFileSync('.github/workflows/compliance.yml', ghAction);
502
+ success('Created .github/workflows/compliance.yml');
503
+ }
504
+
505
+ // .grcignore
506
+ if (!existsSync('.grcignore')) {
507
+ writeFileSync('.grcignore', '# Files excluded from compliance scanning\nnode_modules/\ndist/\ncoverage/\n*.test.ts\n*.spec.ts\n');
508
+ success('Created .grcignore');
509
+ }
510
+
511
+ log('');
512
+ log(`${bold('Next steps:')}`);
513
+ log(` 1. ${c.cyan}grc scan .${c.reset} — run your first compliance scan`);
514
+ log(` 2. ${c.cyan}grc doctor${c.reset} — verify environment`);
515
+ log(` 3. ${c.cyan}grc report --framework ${fw}${c.reset} — generate evidence report`);
516
+ log(` 4. ${c.cyan}grc ai-bom generate${c.reset} — generate AI Bill of Materials\n`);
517
+ }
518
+
519
+ // ─── grc doctor --fix ─────────────────────────────────────────────────────────
520
+ interface AutoFix {
521
+ id: string;
522
+ description: string;
523
+ check: () => boolean;
524
+ fix: () => string;
525
+ framework: string;
526
+ control: string;
527
+ }
528
+
529
+ const AUTO_FIXES: AutoFix[] = [
530
+ {
531
+ id: 'gitignore-secrets',
532
+ description: 'Ensure .env files are in .gitignore',
533
+ framework: 'SOC 2 / ISO 27001',
534
+ control: 'CC6.1 / A.9.4.3',
535
+ check: () => {
536
+ if (!existsSync('.gitignore')) return false;
537
+ const gi = readFileSync('.gitignore', 'utf8');
538
+ return gi.includes('.env') && gi.includes('*.pem') && gi.includes('*.key');
539
+ },
540
+ fix: () => {
541
+ const entries = '\n# Security — GRC auto-fix\n.env\n.env.*\n*.pem\n*.key\n*.p12\n*.pfx\n*_rsa\n*_dsa\n*_ecdsa\n*_ed25519\n.secrets\ncredentials.json\nservice-account*.json\n';
542
+ if (!existsSync('.gitignore')) {
543
+ writeFileSync('.gitignore', entries);
544
+ return 'Created .gitignore with secret exclusions';
545
+ }
546
+ const current = readFileSync('.gitignore', 'utf8');
547
+ if (!current.includes('.env')) {
548
+ writeFileSync('.gitignore', current + entries);
549
+ return 'Added secret exclusion patterns to .gitignore';
550
+ }
551
+ return 'Already present';
552
+ },
553
+ },
554
+ {
555
+ id: 'security-headers',
556
+ description: 'Check for security headers in vercel.json / next.config',
557
+ framework: 'SOC 2',
558
+ control: 'CC6.6',
559
+ check: () => {
560
+ if (existsSync('vercel.json')) {
561
+ const v = JSON.parse(readFileSync('vercel.json', 'utf8'));
562
+ return Array.isArray(v.headers) && v.headers.some((h: { headers: Array<{key: string}> }) =>
563
+ h.headers?.some((x) => x.key === 'Strict-Transport-Security'));
564
+ }
565
+ return false;
566
+ },
567
+ fix: () => 'Manual: add HSTS + X-Frame-Options headers to vercel.json or web server config',
568
+ },
569
+ {
570
+ id: 'npm-audit',
571
+ description: 'No high/critical npm vulnerabilities',
572
+ framework: 'ISO 27001',
573
+ control: 'A.12.6.1',
574
+ check: () => {
575
+ try {
576
+ execSync('npm audit --audit-level=high --json 2>/dev/null', { stdio: 'pipe' });
577
+ return true;
578
+ } catch { return false; }
579
+ },
580
+ fix: () => {
581
+ try {
582
+ execSync('npm audit fix --only=prod 2>&1', { stdio: 'pipe' });
583
+ return 'Ran npm audit fix — check output for remaining manual fixes';
584
+ } catch (e) {
585
+ return `npm audit fix failed: ${(e as Error).message.slice(0, 80)}`;
586
+ }
587
+ },
588
+ },
589
+ {
590
+ id: 'grcfile-present',
591
+ description: 'grcfile.yaml present',
592
+ framework: 'GRC_Claw',
593
+ control: 'operational',
594
+ check: () => existsSync('grcfile.yaml') || existsSync('.grcfile.yaml'),
595
+ fix: () => { cmdInit([]); return 'Created grcfile.yaml (default: iso27001)'; },
596
+ },
597
+ ];
598
+
599
+ async function cmdDoctorFix(args: string[]) {
600
+ const dryRun = args.includes('--dry-run');
601
+ log(`\n${bold('GRC Doctor — Auto-Fix')} ${dim(dryRun ? '(dry run)' : '')} ${dim(`v${VERSION}`)}`);
602
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
603
+
604
+ let fixed = 0; let skipped = 0; let already = 0;
605
+ for (const af of AUTO_FIXES) {
606
+ const passing = af.check();
607
+ if (passing) {
608
+ success(`${af.id.padEnd(30)} ${dim('already passing')}`);
609
+ already++;
610
+ continue;
611
+ }
612
+ if (dryRun) {
613
+ warn(`${af.id.padEnd(30)} ${c.yellow}would fix${c.reset} — ${af.description} (${af.framework} ${af.control})`);
614
+ skipped++;
615
+ } else {
616
+ try {
617
+ const result = af.fix();
618
+ success(`${af.id.padEnd(30)} ${dim(result)}`);
619
+ fixed++;
620
+ } catch (e) {
621
+ error(`${af.id.padEnd(30)} fix failed: ${(e as Error).message}`);
622
+ }
623
+ }
624
+ }
625
+
626
+ log('');
627
+ log(`Fixed: ${fixed} Already passing: ${already} ${dryRun ? 'Would fix: ' + skipped : ''}`);
628
+ log(`\n${dim('Tip: Run grc scan . to verify remaining findings\n')}`);
629
+ }
630
+
631
+ // ─── grc diff ─────────────────────────────────────────────────────────────────
632
+ function cmdDiff(args: string[]) {
633
+ const ref = args[0] ?? 'HEAD~1';
634
+ log(`\n${bold('Compliance Diff')} ${dim(`${ref} → HEAD`)}`);
635
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
636
+
637
+ let changedFiles: string[] = [];
638
+ try {
639
+ const out = execSync(`git diff --name-only ${ref} HEAD 2>/dev/null`, { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
640
+ changedFiles = out.trim().split('\n').filter(Boolean);
641
+ } catch {
642
+ error('Could not run git diff — ensure you are in a git repository');
643
+ process.exit(1);
644
+ }
645
+
646
+ if (changedFiles.length === 0) {
647
+ info('No changed files between HEAD and ' + ref);
648
+ return;
649
+ }
650
+
651
+ const srcFiles = changedFiles.filter(f => ['.ts', '.tsx', '.js', '.jsx', '.py', '.go'].some(ext => f.endsWith(ext)));
652
+ if (srcFiles.length === 0) {
653
+ info(`${changedFiles.length} changed files — no source code changes to scan`);
654
+ return;
655
+ }
656
+
657
+ info(`Scanning ${srcFiles.length} changed source files for compliance delta…\n`);
658
+
659
+ const allFindings: Finding[] = [];
660
+ for (const file of srcFiles) {
661
+ const abs = resolve(file);
662
+ if (existsSync(abs)) allFindings.push(...scanFile(abs));
663
+ }
664
+
665
+ if (allFindings.length === 0) {
666
+ success('No new compliance findings in changed files');
667
+ log(`${dim(`(${srcFiles.length} files scanned, ${changedFiles.length - srcFiles.length} non-source files skipped)`)}\n`);
668
+ return;
669
+ }
670
+
671
+ const errors = allFindings.filter(f => f.severity === 'error');
672
+ const warnings = allFindings.filter(f => f.severity === 'warning');
673
+
674
+ log(`Found ${c.red}${errors.length} error(s)${c.reset} ${c.yellow}${warnings.length} warning(s)${c.reset} in diff\n`);
675
+ for (const f of allFindings) {
676
+ const sev = f.severity === 'error' ? c.red : f.severity === 'warning' ? c.yellow : c.dim;
677
+ log(` ${sev}${f.severity.toUpperCase()}${c.reset} ${dim(relative(process.cwd(), f.file))}:${f.line} ${f.message}`);
678
+ log(` ${dim(f.framework + ' ' + f.control)}`);
679
+ }
680
+ log('');
681
+ if (errors.length > 0) {
682
+ warn('This diff introduces compliance errors — fix before merging\n');
683
+ process.exit(1);
684
+ }
685
+ }
686
+
687
+ // ─── grc ai-bom generate ──────────────────────────────────────────────────────
688
+ async function cmdAiBom(args: string[]) {
689
+ const sub = args[0];
690
+
691
+ if (sub !== 'generate') {
692
+ log(`Usage: grc ai-bom generate [--model-card <path>] [--output <file>]`);
693
+ log(` grc ai-bom generate --scan-deps`);
694
+ return;
695
+ }
696
+
697
+ const modelCardPath = args[args.indexOf('--model-card') + 1];
698
+ const outputPath = args[args.indexOf('--output') + 1];
699
+ const scanDeps = args.includes('--scan-deps');
700
+
701
+ log(`\n${bold('AI Bill of Materials Generator')} ${dim(`v${VERSION}`)}`);
702
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
703
+
704
+ const bom: Record<string, unknown> = {
705
+ schema: 'https://a2zsoc.com/schemas/ai-bom/v1.0',
706
+ bomFormat: 'GRC-AI-BOM',
707
+ specVersion: '1.0',
708
+ serialNumber: `urn:uuid:${createHash('sha256').update(Date.now().toString()).digest('hex').slice(0, 32)}`,
709
+ version: 1,
710
+ metadata: {
711
+ timestamp: new Date().toISOString(),
712
+ generator: { name: '@grc-claw/cli', version: VERSION },
713
+ licenses: [{ id: 'MIT' }],
714
+ },
715
+ components: [] as unknown[],
716
+ externalReferences: [
717
+ { type: 'documentation', url: 'https://a2zsoc.com/developers/ai-bom' },
718
+ ],
719
+ regulatory: {
720
+ euAiAct: { article53Compliant: false, riskCategory: 'unknown', auditTrailRequired: true },
721
+ nistAiRmf: { profile: 'generic', governFunctionCoverage: 0 },
722
+ iso42001: { clause9_1: 'partial' },
723
+ },
724
+ };
725
+
726
+ // Parse model card if provided
727
+ if (modelCardPath && existsSync(modelCardPath)) {
728
+ try {
729
+ const mc = JSON.parse(readFileSync(modelCardPath, 'utf8')) as Record<string, unknown>;
730
+ const comp: Record<string, unknown> = {
731
+ type: 'machine-learning-model',
732
+ name: mc['model_name'] ?? mc['name'] ?? 'unknown',
733
+ version: mc['version'] ?? '0.0.0',
734
+ description: mc['description'] ?? '',
735
+ properties: [],
736
+ };
737
+ if (mc['base_model']) (comp['properties'] as unknown[]).push({ name: 'base_model', value: mc['base_model'] });
738
+ if (mc['training_data']) (comp['properties'] as unknown[]).push({ name: 'training_data', value: String(mc['training_data']) });
739
+ if (mc['architecture']) (comp['properties'] as unknown[]).push({ name: 'architecture', value: String(mc['architecture']) });
740
+ if (mc['license']) (comp['properties'] as unknown[]).push({ name: 'license', value: String(mc['license']) });
741
+ (bom['components'] as unknown[]).push(comp);
742
+ bom['regulatory'] = { ...bom['regulatory'] as object, euAiAct: { article53Compliant: true, riskCategory: mc['risk_category'] ?? 'limited', auditTrailRequired: true } };
743
+ success(`Parsed model card: ${modelCardPath}`);
744
+ } catch { warn('Could not parse model card JSON — including empty component'); }
745
+ }
746
+
747
+ // Scan npm dependencies for AI/ML packages
748
+ if (scanDeps && existsSync('package.json')) {
749
+ const pkgJson = JSON.parse(readFileSync('package.json', 'utf8')) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
750
+ const aiPackages = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'langchain', 'llamaindex',
751
+ 'transformers', '@huggingface/inference', 'ollama', 'groq-sdk', 'cohere-ai', 'mistralai', 'replicate'];
752
+ const allDeps = { ...pkgJson['dependencies'], ...pkgJson['devDependencies'] };
753
+ for (const [pkg, ver] of Object.entries(allDeps)) {
754
+ if (aiPackages.some(ai => pkg.includes(ai))) {
755
+ (bom['components'] as unknown[]).push({
756
+ type: 'library',
757
+ name: pkg,
758
+ version: ver,
759
+ scope: 'required',
760
+ properties: [{ name: 'category', value: 'ai-sdk' }],
761
+ });
762
+ }
763
+ }
764
+ info(`Scanned dependencies — found ${(bom['components'] as unknown[]).length} AI/ML package(s)`);
765
+ }
766
+
767
+ const bomJson = JSON.stringify(bom, null, 2);
768
+ const hash = createHash('sha256').update(bomJson).digest('hex');
769
+ (bom['metadata'] as Record<string, unknown>)['hash'] = { alg: 'SHA-256', content: hash };
770
+
771
+ const finalJson = JSON.stringify(bom, null, 2);
772
+
773
+ if (outputPath) {
774
+ writeFileSync(outputPath, finalJson);
775
+ success(`AI-BOM written to ${outputPath} (SHA-256: ${hash.slice(0, 16)}…)`);
776
+ } else {
777
+ process.stdout.write(finalJson + '\n');
778
+ }
779
+
780
+ log(`\n${dim('EU AI Act Article 53 compliance data captured')}`);
781
+ log(`${dim('Publish to registry:')} grc ai-bom publish --file ${outputPath ?? 'ai-bom.json'}\n`);
782
+ }
783
+
784
+ async function cmdAiBomPublish(args: string[]) {
785
+ const filePath = args[args.indexOf('--file') + 1] ?? args[args.indexOf('-f') + 1] ?? 'ai-bom.json';
786
+ const modelId = args[args.indexOf('--model-id') + 1];
787
+ const a2zApiKey = process.env.A2Z_SOC_API_KEY;
788
+
789
+ if (!existsSync(filePath)) { error(`AI-BOM file not found: ${filePath}`); process.exit(1); }
790
+ const bomContent = readFileSync(filePath, 'utf8');
791
+ let bom: Record<string, unknown>;
792
+ try { bom = JSON.parse(bomContent) as Record<string, unknown>; } catch { error('Invalid JSON in AI-BOM file'); process.exit(1); }
793
+
794
+ const metadata = bom['metadata'] as Record<string, unknown> ?? {};
795
+ const resolvedModelId = modelId ?? (metadata['model_id'] as string) ?? 'unknown/model';
796
+ const vendor = (metadata['vendor'] as string) ?? resolvedModelId.split('/')[0] ?? 'unknown';
797
+ const version = (metadata['version'] as string) ?? '0.0.0';
798
+
799
+ log(`\n${bold('AI-BOM Registry Publish')} ${dim(`v${VERSION}`)}`);
800
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
801
+ info(`Publishing ${resolvedModelId} to A2Z SOC AI-BOM Registry...`);
802
+
803
+ const endpoint = 'https://a2zsoc.com/api/platform/ai-bom-registry/publish';
804
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
805
+ if (a2zApiKey) headers['Authorization'] = `Bearer ${a2zApiKey}`;
806
+
807
+ try {
808
+ const res = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify({ model_id: resolvedModelId, vendor, version, bom_content: bomContent }) });
809
+ const data = await res.json() as Record<string, unknown>;
810
+ if (res.ok) {
811
+ success(`Published! BOM hash: ${data['bom_hash']}`);
812
+ log(` Verify: ${data['verify_url']}`);
813
+ } else {
814
+ error(`Publish failed: ${JSON.stringify(data)}`);
815
+ process.exit(1);
816
+ }
817
+ } catch (e) { error(`Network error: ${e instanceof Error ? e.message : String(e)}`); process.exit(1); }
818
+ }
819
+
820
+ // ─── PQC Scan (#8) — Post-Quantum Cryptography migration scanner ─────────────
821
+ interface PqcFinding {
822
+ file: string; line: number; pattern: string; severity: 'critical' | 'high' | 'medium';
823
+ match: string; replacement: string; nist_ref: string;
824
+ }
825
+
826
+ const PQC_PATTERNS: Array<{ pattern: string; re: RegExp; severity: PqcFinding['severity']; replacement: string; nist_ref: string }> = [
827
+ { pattern: 'RSA key generation', re: /generateKeyPair\s*\(\s*['"]rsa['"]/gi, severity: 'critical', replacement: 'ML-KEM-768 (FIPS 203)', nist_ref: 'SP 800-208' },
828
+ { pattern: 'RSA-2048 key size', re: /modulusLength\s*:\s*2048/gi, severity: 'critical', replacement: 'ML-KEM or RSA-3072 minimum (transitional)', nist_ref: 'SP 800-57' },
829
+ { pattern: 'ECDSA signing', re: /createSign\s*\(\s*['"]SHA-256['"]\)|new\s+ECDSA|createECDH/gi, severity: 'critical', replacement: 'ML-DSA-65 (FIPS 204)', nist_ref: 'FIPS 204' },
830
+ { pattern: 'ECDH key exchange', re: /createDiffieHellman|diffieHellman|\.computeSecret\(/gi, severity: 'high', replacement: 'ML-KEM-1024 (FIPS 203)', nist_ref: 'SP 800-227' },
831
+ { pattern: 'MD5 hashing', re: /createHash\s*\(\s*['"]md5['"]\)/gi, severity: 'high', replacement: 'SHA-3 / SHAKE-256 (FIPS 202)', nist_ref: 'FIPS 202' },
832
+ { pattern: 'SHA-1 hashing', re: /createHash\s*\(\s*['"]sha1['"]\)/gi, severity: 'medium', replacement: 'SHA-256 or SHA-3 (FIPS 202)', nist_ref: 'FIPS 202' },
833
+ ];
834
+
835
+ const SCANNABLE_EXTS = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs', '.py', '.go', '.java', '.cs', '.rb', '.rs']);
836
+ function scanFileForPqc(filePath: string): PqcFinding[] {
837
+ const findings: PqcFinding[] = [];
838
+ try {
839
+ const content = readFileSync(filePath, 'utf8');
840
+ const lines = content.split('\n');
841
+ for (let i = 0; i < lines.length; i++) {
842
+ const line = lines[i] ?? '';
843
+ for (const p of PQC_PATTERNS) {
844
+ p.re.lastIndex = 0;
845
+ const m = p.re.exec(line);
846
+ if (m) {
847
+ findings.push({ file: filePath, line: i + 1, pattern: p.pattern, severity: p.severity, match: m[0].trim(), replacement: p.replacement, nist_ref: p.nist_ref });
848
+ }
849
+ }
850
+ }
851
+ } catch { /* skip unreadable files */ }
852
+ return findings;
853
+ }
854
+
855
+ function walkDirForPqc(dir: string, findings: PqcFinding[]) {
856
+ let entries: string[] = [];
857
+ try { entries = readdirSync(dir); } catch { return; }
858
+ for (const entry of entries) {
859
+ if (SKIP_DIRS.has(entry)) continue;
860
+ const full = join(dir, entry);
861
+ try {
862
+ const stat = statSync(full);
863
+ if (stat.isDirectory()) walkDirForPqc(full, findings);
864
+ else if (SCANNABLE_EXTS.has(extname(entry))) findings.push(...scanFileForPqc(full));
865
+ } catch { /* skip */ }
866
+ }
867
+ }
868
+
869
+ async function cmdPqcScan(args: string[]) {
870
+ const targetDir = resolve(args.find(a => !a.startsWith('-')) ?? '.');
871
+ const jsonMode = args.includes('--json');
872
+ const outputPath = args[args.indexOf('--output') + 1];
873
+ const a2zApiKey = process.env.A2Z_SOC_API_KEY;
874
+
875
+ if (!jsonMode) {
876
+ log(`\n${bold('PQC Migration Scanner')} ${dim(`v${VERSION}`)}`);
877
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
878
+ info(`Scanning ${targetDir} for deprecated cryptography...`);
879
+ }
880
+
881
+ const findings: PqcFinding[] = [];
882
+ walkDirForPqc(targetDir, findings);
883
+
884
+ const critical = findings.filter(f => f.severity === 'critical');
885
+ const high = findings.filter(f => f.severity === 'high');
886
+ const medium = findings.filter(f => f.severity === 'medium');
887
+
888
+ const report = {
889
+ schema: 'https://a2zsoc.com/schemas/pqc-scan/v1.0',
890
+ scanned_at: new Date().toISOString(),
891
+ target_dir: targetDir,
892
+ summary: { total: findings.length, critical: critical.length, high: high.length, medium: medium.length },
893
+ findings: findings.map(f => ({ ...f, file: relative(targetDir, f.file) })),
894
+ migration_timeline: { '2026-Q2': 'Inventory complete', '2026-Q4': 'Hybrid PQC deployed', '2027-Q1': 'FedRAMP mandate — migration required' },
895
+ standards: ['FIPS 203 (ML-KEM)', 'FIPS 204 (ML-DSA)', 'FIPS 205 (SLH-DSA)', 'NIST SP 800-208'],
896
+ };
897
+
898
+ if (jsonMode || outputPath) {
899
+ const json = JSON.stringify(report, null, 2);
900
+ if (outputPath) { writeFileSync(outputPath, json); if (!jsonMode) success(`Report written to ${outputPath}`); }
901
+ else process.stdout.write(json + '\n');
902
+ } else {
903
+ log('');
904
+ if (findings.length === 0) {
905
+ success('No deprecated cryptographic patterns found. PQC-ready!');
906
+ } else {
907
+ if (critical.length) { log(`${c.red}${bold(`✗ CRITICAL (${critical.length}):`)}${c.reset} Must migrate before FedRAMP 2027 deadline`); for (const f of critical) log(` ${c.dim}${relative(targetDir, f.file)}:${f.line}${c.reset} ${c.red}${f.match}${c.reset} → ${c.green}${f.replacement}${c.reset} [${f.nist_ref}]`); }
908
+ if (high.length) { log(`\n${c.yellow}${bold(`⚠ HIGH (${high.length}):`)}${c.reset}`); for (const f of high) log(` ${c.dim}${relative(targetDir, f.file)}:${f.line}${c.reset} ${c.yellow}${f.match}${c.reset} → ${c.green}${f.replacement}${c.reset} [${f.nist_ref}]`); }
909
+ if (medium.length) { log(`\n${c.blue}${bold(`ℹ MEDIUM (${medium.length}):`)}${c.reset}`); for (const f of medium) log(` ${c.dim}${relative(targetDir, f.file)}:${f.line}${c.reset} ${c.blue}${f.match}${c.reset} → ${c.green}${f.replacement}${c.reset} [${f.nist_ref}]`); }
910
+ log(`\n${c.dim}Standards: FIPS 203 (ML-KEM) · FIPS 204 (ML-DSA) · FIPS 205 (SLH-DSA)${c.reset}`);
911
+ }
912
+ log('');
913
+ }
914
+
915
+ // Publish scan result to A2Z SOC if API key present
916
+ if (a2zApiKey && findings.length > 0) {
917
+ try {
918
+ await fetch('https://a2zsoc.com/api/platform/pqc-scan', { method: 'POST', headers: { 'Authorization': `Bearer ${a2zApiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ summary: report.summary, scanned_at: report.scanned_at }) });
919
+ if (!jsonMode) info('PQC scan result uploaded to A2Z SOC dashboard');
920
+ } catch { /* non-fatal */ }
921
+ }
922
+
923
+ if (critical.length > 0) process.exit(1);
924
+ }
925
+
926
+ // ─── grc iac-scan ─────────────────────────────────────────────────────────────
927
+
928
+ interface IaCFinding {
929
+ file: string;
930
+ line: number;
931
+ severity: 'critical' | 'high' | 'medium' | 'low';
932
+ rule: string;
933
+ control: string;
934
+ framework: string;
935
+ message: string;
936
+ fix: string;
937
+ }
938
+
939
+ const IAC_RULES: Array<{
940
+ id: string;
941
+ control: string;
942
+ framework: string;
943
+ pattern: RegExp;
944
+ fileExts: string[];
945
+ severity: 'critical' | 'high' | 'medium' | 'low';
946
+ message: string;
947
+ fix: string;
948
+ }> = [
949
+ { id: 'iac-public-s3', control: 'A.13.1.3', framework: 'ISO 27001', pattern: /acl\s*=\s*["']public-read(-write)?["']/i, fileExts: ['.tf'], severity: 'critical', message: 'S3 bucket ACL is public — ISO 27001 A.13.1.3', fix: 'Set acl = "private" and enable S3 Block Public Access' },
950
+ { id: 'iac-rds-no-encrypt', control: 'A.10.1.1', framework: 'ISO 27001', pattern: /storage_encrypted\s*=\s*false/i, fileExts: ['.tf'], severity: 'critical', message: 'RDS storage encryption disabled — SOC 2 CC6.1 / ISO 27001 A.10.1.1', fix: 'Set storage_encrypted = true on all aws_db_instance resources' },
951
+ { id: 'iac-open-sg', control: 'A.13.1.1', framework: 'ISO 27001', pattern: /cidr_blocks\s*=\s*\["0\.0\.0\.0\/0"\]/, fileExts: ['.tf'], severity: 'high', message: 'Security group open to 0.0.0.0/0 — ISO 27001 A.13.1.1', fix: 'Restrict cidr_blocks to specific IP ranges' },
952
+ { id: 'iac-tf-no-state-encrypt', control: 'A.10.1.1', framework: 'ISO 27001', pattern: /backend\s+"s3"/, fileExts: ['.tf'], severity: 'high', message: 'Terraform S3 backend — verify encrypt = true is set — ISO 27001 A.10.1.1', fix: 'Add encrypt = true to the terraform backend "s3" block' },
953
+ { id: 'iac-k8s-privileged', control: 'A.9.4.1', framework: 'ISO 27001', pattern: /privileged\s*:\s*true/i, fileExts: ['.yaml', '.yml'], severity: 'critical', message: 'Kubernetes container privileged=true — SOC 2 CC6.1 / ISO 27001 A.9.4.1', fix: 'Set privileged: false; use least-privilege security context' },
954
+ { id: 'iac-cf-http', control: 'A.13.2.3', framework: 'ISO 27001', pattern: /Protocol\s*:\s*HTTP(?!S)/i, fileExts: ['.yaml', '.yml', '.json'], severity: 'high', message: 'CloudFormation resource using HTTP — ISO 27001 A.13.2.3', fix: 'Switch Protocol to HTTPS and configure an SSL certificate' },
955
+ { id: 'iac-gcs-public', control: 'A.13.1.3', framework: 'ISO 27001', pattern: /public_access_prevention\s*=\s*["']unspecified["']/i, fileExts: ['.tf'], severity: 'critical', message: 'GCS bucket public access prevention unspecified — ISO 27001 A.13.1.3', fix: 'Set public_access_prevention = "enforced"' },
956
+ { id: 'iac-azure-blob-public', control: 'A.13.1.3', framework: 'ISO 27001', pattern: /allow_blob_public_access\s*=\s*true/i, fileExts: ['.tf'], severity: 'critical', message: 'Azure Blob public access enabled — ISO 27001 A.13.1.3', fix: 'Set allow_blob_public_access = false on azurerm_storage_account' },
957
+ ];
958
+
959
+ const IAC_EXTS = new Set(['.tf', '.yaml', '.yml', '.json']);
960
+
961
+ function scanFileForIaC(filePath: string): IaCFinding[] {
962
+ const ext = extname(filePath).toLowerCase();
963
+ if (!IAC_EXTS.has(ext)) return [];
964
+ const findings: IaCFinding[] = [];
965
+ let content: string;
966
+ try { content = readFileSync(filePath, 'utf8'); } catch { return []; }
967
+ const lines = content.split('\n');
968
+ for (const rule of IAC_RULES) {
969
+ if (!rule.fileExts.includes(ext)) continue;
970
+ for (let i = 0; i < lines.length; i++) {
971
+ const re = new RegExp(rule.pattern.source, 'i');
972
+ if (re.test(lines[i])) {
973
+ findings.push({ file: filePath, line: i + 1, severity: rule.severity, rule: rule.id, control: rule.control, framework: rule.framework, message: rule.message, fix: rule.fix });
974
+ }
975
+ }
976
+ }
977
+ return findings;
978
+ }
979
+
980
+ function walkDirForIaC(dir: string, findings: IaCFinding[]) {
981
+ let entries: string[] = [];
982
+ try { entries = readdirSync(dir); } catch { return; }
983
+ for (const entry of entries) {
984
+ if (SKIP_DIRS.has(entry)) continue;
985
+ const full = join(dir, entry);
986
+ try {
987
+ const stat = statSync(full);
988
+ if (stat.isDirectory()) walkDirForIaC(full, findings);
989
+ else if (IAC_EXTS.has(extname(entry))) findings.push(...scanFileForIaC(full));
990
+ } catch { /* skip */ }
991
+ }
992
+ }
993
+
994
+ async function cmdIaCScan(args: string[]) {
995
+ const targetDir = resolve(args.find(a => !a.startsWith('-')) ?? '.');
996
+ const jsonMode = args.includes('--json');
997
+ const framework = args[args.indexOf('--framework') + 1] ?? null;
998
+ const outputPath = args[args.indexOf('--output') + 1];
999
+
1000
+ if (!jsonMode) {
1001
+ log(`\n${bold('GRC_Claw IaC Compliance Scanner')} ${dim(`v${VERSION}`)}`);
1002
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1003
+ info(`Scanning ${bold(targetDir)} for Terraform, CloudFormation, Kubernetes manifests…`);
1004
+ }
1005
+
1006
+ const findings: IaCFinding[] = [];
1007
+ walkDirForIaC(targetDir, findings);
1008
+
1009
+ const filtered = framework ? findings.filter(f => f.framework.toLowerCase().includes(framework.toLowerCase())) : findings;
1010
+ const critical = filtered.filter(f => f.severity === 'critical');
1011
+ const high = filtered.filter(f => f.severity === 'high');
1012
+ const medium = filtered.filter(f => f.severity === 'medium');
1013
+ const low = filtered.filter(f => f.severity === 'low');
1014
+ const score = Math.max(0, 100 - critical.length * 20 - high.length * 10 - medium.length * 3 - low.length);
1015
+
1016
+ const report = {
1017
+ schema: 'https://a2zsoc.com/schemas/iac-scan/v1.0',
1018
+ scanned_at: new Date().toISOString(),
1019
+ target_dir: targetDir,
1020
+ framework_filter: framework,
1021
+ summary: { total: filtered.length, critical: critical.length, high: high.length, medium: medium.length, low: low.length, posture_score: score },
1022
+ findings: filtered.map(f => ({ ...f, file: relative(targetDir, f.file) })),
1023
+ frameworks_checked: [...new Set(filtered.map(f => f.framework))],
1024
+ controls_checked: [...new Set(filtered.map(f => f.control))],
1025
+ };
1026
+
1027
+ if (jsonMode || outputPath) {
1028
+ const json = JSON.stringify(report, null, 2);
1029
+ if (outputPath) { writeFileSync(outputPath, json); if (!jsonMode) success(`IaC scan report written to ${outputPath}`); }
1030
+ else process.stdout.write(json + '\n');
1031
+ } else {
1032
+ log('');
1033
+ if (filtered.length === 0) {
1034
+ success(`No IaC compliance findings in ${targetDir} — infrastructure looks clean!`);
1035
+ } else {
1036
+ for (const f of critical) {
1037
+ log(`${c.red}✗ CRITICAL${c.reset} ${c.dim}${relative(targetDir, f.file)}:${f.line}${c.reset} [${f.control}] ${f.message}`);
1038
+ log(` ${c.green}Fix:${c.reset} ${f.fix}`);
1039
+ }
1040
+ for (const f of high) {
1041
+ log(`${c.yellow}⚠ HIGH${c.reset} ${c.dim}${relative(targetDir, f.file)}:${f.line}${c.reset} [${f.control}] ${f.message}`);
1042
+ log(` ${c.green}Fix:${c.reset} ${f.fix}`);
1043
+ }
1044
+ for (const f of [...medium, ...low]) {
1045
+ log(`${c.blue}ℹ ${f.severity.toUpperCase()}${c.reset} ${c.dim}${relative(targetDir, f.file)}:${f.line}${c.reset} [${f.control}] ${f.message}`);
1046
+ }
1047
+ log(`\n${bold('Posture Score:')} ${score < 60 ? c.red : score < 80 ? c.yellow : c.green}${score}/100${c.reset}`);
1048
+ log(`${c.dim}Critical: ${critical.length} High: ${high.length} Medium: ${medium.length} Low: ${low.length}${c.reset}\n`);
1049
+ }
1050
+ }
1051
+
1052
+ if (critical.length > 0 && !jsonMode) process.exit(1);
1053
+ }
1054
+
1055
+ function cmdVersion() {
1056
+ log(`@grc-claw/cli ${VERSION}`);
1057
+ log(`Node: ${process.version}`);
1058
+ log(`Platform: ${process.platform} ${process.arch}`);
1059
+ }
1060
+
1061
+ function cmdHelp() {
1062
+ log(`
1063
+ ${bold('grc')} — GRC_Claw CLI ${dim(`v${VERSION}`)}
1064
+
1065
+ ${bold('USAGE')}
1066
+ grc <command> [options]
1067
+
1068
+ ${bold('COMMANDS')}
1069
+ ${c.cyan}init${c.reset} Scaffold grcfile.yaml + GitHub Actions workflow
1070
+ --framework <id> Framework to target (default: iso27001)
1071
+
1072
+ ${c.cyan}plan${c.reset} Generate compliance plan from grcfile.yaml
1073
+ --json Output JSON (for CI/CD pipelines)
1074
+ --output <file> Write plan to file
1075
+
1076
+ ${c.cyan}apply${c.reset} [path] Apply compliance plan to target environment
1077
+ --dry-run Preview changes without applying
1078
+ --json Output JSON
1079
+
1080
+ ${c.cyan}audit${c.reset} [path] Run full compliance audit with control coverage
1081
+ --json Output JSON
1082
+ --output <file> Write audit report to file
1083
+
1084
+ ${c.cyan}scan${c.reset} [path] Scan codebase for compliance findings
1085
+ --framework <id> Filter findings to a specific framework
1086
+ --json Output JSON (suitable for CI/CD gates)
1087
+
1088
+ ${c.cyan}status${c.reset} [path] Show current compliance posture and gateway status
1089
+ --json Output JSON
1090
+
1091
+ ${c.cyan}drift${c.reset} [path] Detect compliance drift from a git baseline
1092
+ --base <ref> Git ref to compare against (default: HEAD)
1093
+ --json Output JSON
1094
+ --output <file> Write drift report to file
1095
+
1096
+ ${c.cyan}report${c.reset} Generate a compliance evidence report
1097
+ --framework <id> Framework to report against (default: iso27001)
1098
+ --path <dir> Directory to scan (default: .)
1099
+
1100
+ ${c.cyan}diff${c.reset} [ref] Show compliance delta between git refs (default: HEAD~1)
1101
+
1102
+ ${c.cyan}doctor${c.reset} Check environment and gateway connectivity
1103
+ --fix Auto-remediate common control failures
1104
+ --dry-run Preview fixes without applying
1105
+
1106
+ ${c.cyan}ai-bom generate${c.reset} Generate AI Bill of Materials (EU AI Act Art. 53)
1107
+ --model-card <path> Parse a model_card.json file
1108
+ --scan-deps Scan package.json for AI/ML dependencies
1109
+ --output <file> Write BOM to file (default: stdout)
1110
+
1111
+ ${c.cyan}ai-bom publish${c.reset} Publish AI-BOM to A2Z SOC public registry
1112
+ --file <path> AI-BOM JSON file to publish (default: ai-bom.json)
1113
+ --model-id <id> Override model identifier
1114
+
1115
+ ${c.cyan}iac-scan${c.reset} [path] Scan Terraform/CloudFormation/Kubernetes for compliance
1116
+ --framework <id> Filter by framework (iso27001, soc2, nist-csf)
1117
+ --output <file> Write report to file
1118
+ --json JSON output for CI/CD gates
1119
+ Exits 1 if critical IaC findings detected
1120
+
1121
+ ${c.cyan}pqc-scan${c.reset} [path] Scan for deprecated cryptography (RSA/ECDSA/ECDH)
1122
+ --output <file> Write report to file
1123
+ --json JSON output for CI/CD gates
1124
+ Exits 1 if critical findings detected (FedRAMP 2027 gate)
1125
+
1126
+ ${c.cyan}frameworks${c.reset} list List available framework packs
1127
+
1128
+ ${c.cyan}version${c.reset} Print version information
1129
+
1130
+ ${bold('EXAMPLES')}
1131
+ ${dim('# Scaffold a new compliance project')}
1132
+ grc init --framework soc2
1133
+
1134
+ ${dim('# Generate a compliance plan')}
1135
+ grc plan --json > plan.json
1136
+
1137
+ ${dim('# Apply compliance fixes (dry run)')}
1138
+ grc apply --dry-run
1139
+
1140
+ ${dim('# Scan current directory')}
1141
+ grc scan .
1142
+
1143
+ ${dim('# Scan and gate CI/CD (exits 1 if errors found)')}
1144
+ grc scan . --json | jq '.summary.errors'
1145
+
1146
+ ${dim('# Show compliance status')}
1147
+ grc status
1148
+
1149
+ ${dim('# Detect drift from main branch')}
1150
+ grc drift --base main
1151
+
1152
+ ${dim('# Run full compliance audit')}
1153
+ grc audit --framework iso27001
1154
+
1155
+ ${dim('# Show compliance changes in this PR branch')}
1156
+ grc diff main
1157
+
1158
+ ${dim('# Auto-fix common control failures')}
1159
+ grc doctor --fix
1160
+
1161
+ ${dim('# Generate AI Bill of Materials')}
1162
+ grc ai-bom generate --scan-deps --output ai-bom.json
1163
+
1164
+ ${dim('# Generate ISO 27001 evidence report')}
1165
+ grc report --framework iso27001 > evidence-$(date +%F).json
1166
+
1167
+ ${dim('# Check framework packs')}
1168
+ grc frameworks list
1169
+
1170
+ ${dim('# Verify environment')}
1171
+ grc doctor
1172
+
1173
+ ${bold('ENVIRONMENT')}
1174
+ GRC_CLAW_GATEWAY_TOKEN Gateway auth token
1175
+ GRC_CLAW_HOST Gateway host (default: 127.0.0.1)
1176
+ GRC_CLAW_PORT Gateway port (default: 18791)
1177
+ A2Z_SOC_API_KEY A2Z SOC integration key
1178
+
1179
+ ${bold('DOCS')}
1180
+ https://a2zsoc.com/developers/compliance-as-code
1181
+ https://github.com/AAH20/GRC_Claw
1182
+ `);
1183
+ }
1184
+
1185
+ // ─── Gateway helpers ──────────────────────────────────────────────────────────
1186
+ const GATEWAY_HOST = process.env.GRC_CLAW_HOST ?? '127.0.0.1';
1187
+ const GATEWAY_PORT = process.env.GRC_CLAW_PORT ?? '18791';
1188
+ const GATEWAY_BASE = `http://${GATEWAY_HOST}:${GATEWAY_PORT}`;
1189
+ const GATEWAY_TOKEN = process.env.GRC_CLAW_GATEWAY_TOKEN ?? '';
1190
+
1191
+ interface GatewayResponse<T = unknown> {
1192
+ ok: boolean;
1193
+ status: number;
1194
+ data: T;
1195
+ }
1196
+
1197
+ async function gatewayGet<T = unknown>(path: string): Promise<GatewayResponse<T>> {
1198
+ const url = `${GATEWAY_BASE}${path}`;
1199
+ const headers: Record<string, string> = {};
1200
+ if (GATEWAY_TOKEN) headers['Authorization'] = `Bearer ${GATEWAY_TOKEN}`;
1201
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(10_000) });
1202
+ const body = await res.json() as T;
1203
+ return { ok: res.ok, status: res.status, data: body };
1204
+ }
1205
+
1206
+ async function gatewayPost<T = unknown>(path: string, payload: unknown): Promise<GatewayResponse<T>> {
1207
+ const url = `${GATEWAY_BASE}${path}`;
1208
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
1209
+ if (GATEWAY_TOKEN) headers['Authorization'] = `Bearer ${GATEWAY_TOKEN}`;
1210
+ const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(10_000) });
1211
+ const body = await res.json() as T;
1212
+ return { ok: res.ok, status: res.status, data: body };
1213
+ }
1214
+
1215
+ // ─── grcfile.yaml parser (YAML-lite — handles grcfile.yaml key/value pairs) ───
1216
+ interface GrcFile {
1217
+ version: string;
1218
+ framework: string;
1219
+ org: string;
1220
+ scan: { paths: string[]; exclude: string[] };
1221
+ evidence: { output: string; formats: string[]; retention_days: number };
1222
+ controls: Record<string, { required_evidence?: string[]; exemption?: string }>;
1223
+ integrations: { github_app: string; gateway: string; a2z_soc: string };
1224
+ }
1225
+
1226
+ function parseGrcFile(): GrcFile {
1227
+ const candidates = ['grcfile.yaml', '.grcfile.yaml'];
1228
+ const found = candidates.find((f) => existsSync(f));
1229
+ if (!found) {
1230
+ error('grcfile.yaml not found — run: grc init');
1231
+ process.exit(1);
1232
+ }
1233
+ const raw = readFileSync(found, 'utf8');
1234
+
1235
+ const result: GrcFile = {
1236
+ version: '1.0',
1237
+ framework: 'iso27001',
1238
+ org: 'my-org',
1239
+ scan: { paths: ['src/'], exclude: ['node_modules/'] },
1240
+ evidence: { output: './compliance-evidence', formats: ['json'], retention_days: 365 },
1241
+ controls: {},
1242
+ integrations: { github_app: 'disabled', gateway: 'disabled', a2z_soc: 'disabled' },
1243
+ };
1244
+
1245
+ let section = '';
1246
+ for (const line of raw.split('\n')) {
1247
+ const trimmed = line.trim();
1248
+ if (!trimmed || trimmed.startsWith('#')) continue;
1249
+
1250
+ const topMatch = trimmed.match(/^(\w[\w_]*):\s*(.*)/);
1251
+ if (topMatch) {
1252
+ const key = topMatch[1];
1253
+ const val = topMatch[2];
1254
+ if (!val) { section = key; continue; }
1255
+ switch (key) {
1256
+ case 'version': result.version = val; break;
1257
+ case 'framework': result.framework = val; break;
1258
+ case 'org': result.org = val; break;
1259
+ case 'retention_days': result.evidence.retention_days = parseInt(val, 10) || 365; break;
1260
+ case 'output': result.evidence.output = val; break;
1261
+ }
1262
+ section = key === 'scan' ? 'scan' : key === 'evidence' ? 'evidence' : '';
1263
+ continue;
1264
+ }
1265
+
1266
+ if (section === 'scan' && trimmed.startsWith('- ')) {
1267
+ const item = trimmed.slice(2).trim();
1268
+ if (result.scan.paths.length <= result.scan.exclude.length + 1) result.scan.paths.push(item);
1269
+ else result.scan.exclude.push(item);
1270
+ }
1271
+ if (section === 'evidence') {
1272
+ const sub = trimmed.match(/^(\w+):\s*(.*)/);
1273
+ if (sub) {
1274
+ if (sub[1] === 'output') result.evidence.output = sub[2];
1275
+ if (sub[1] === 'retention_days') result.evidence.retention_days = parseInt(sub[2], 10) || 365;
1276
+ if (sub[1] === 'formats') {
1277
+ result.evidence.formats = sub[2].replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean);
1278
+ }
1279
+ }
1280
+ }
1281
+ if (section === '') {
1282
+ const kv = trimmed.match(/^(\w+):\s*(.*)/);
1283
+ if (kv) {
1284
+ switch (kv[1]) {
1285
+ case 'github_app': result.integrations.github_app = kv[2]; break;
1286
+ case 'gateway': result.integrations.gateway = kv[2]; break;
1287
+ case 'a2z_soc': result.integrations.a2z_soc = kv[2]; break;
1288
+ }
1289
+ }
1290
+ }
1291
+ }
1292
+ return result;
1293
+ }
1294
+
1295
+ // ─── Table formatter ──────────────────────────────────────────────────────────
1296
+ function table(headers: string[], rows: string[][]): string {
1297
+ const widths = headers.map((h, i) =>
1298
+ Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)),
1299
+ );
1300
+ const sep = widths.map((w) => '─'.repeat(w + 2)).join('┼');
1301
+ const hdr = headers.map((h, i) => ` ${h.padEnd(widths[i])} `).join('│');
1302
+ const lines = rows.map((r) => r.map((cell, i) => ` ${cell.padEnd(widths[i])} `).join('│'));
1303
+ return `${hdr}\n${sep}\n${lines.join('\n')}`;
1304
+ }
1305
+
1306
+ // ─── grc plan ─────────────────────────────────────────────────────────────────
1307
+ async function cmdPlan(args: string[]) {
1308
+ const grcFile = parseGrcFile();
1309
+ const jsonMode = args.includes('--json');
1310
+ const outputPath = args[args.indexOf('--output') + 1];
1311
+
1312
+ if (!jsonMode) {
1313
+ log(`\n${bold('Compliance Plan Generator')} ${dim(`v${VERSION}`)}`);
1314
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1315
+ info(`Framework: ${bold(FRAMEWORK_SUMMARIES[grcFile.framework]?.name ?? grcFile.framework)}`);
1316
+ info(`Organization: ${bold(grcFile.org)}`);
1317
+ info(`Scan paths: ${bold(grcFile.scan.paths.join(', '))}`);
1318
+ }
1319
+
1320
+ const files = grcFile.scan.paths.flatMap((p) => walkFiles(resolve(p)));
1321
+ const allFindings = files.flatMap(scanFile);
1322
+ const score = posture(allFindings);
1323
+ const meta = FRAMEWORK_SUMMARIES[grcFile.framework];
1324
+
1325
+ const plan = {
1326
+ schema: 'https://a2zsoc.com/schemas/compliance-plan/v1.0',
1327
+ generated_at: new Date().toISOString(),
1328
+ org: grcFile.org,
1329
+ framework: grcFile.framework,
1330
+ framework_name: meta?.name ?? grcFile.framework,
1331
+ posture_score: score,
1332
+ controls_total: meta?.controls ?? 0,
1333
+ controls_with_gaps: [...new Set(allFindings.filter((f) => f.severity === 'error').map((f) => f.control))].length,
1334
+ actions: allFindings
1335
+ .filter((f) => f.severity === 'error')
1336
+ .map((f) => ({
1337
+ control: f.control,
1338
+ framework: f.framework,
1339
+ severity: f.severity,
1340
+ finding: f.message,
1341
+ file: relative(process.cwd(), f.file),
1342
+ line: f.line,
1343
+ remediation: f.suggestion ?? 'Manual review required',
1344
+ auto_fixable: f.autoFixable,
1345
+ })),
1346
+ evidence_requirements: Object.entries(grcFile.controls).map(([ctrl, cfg]) => ({
1347
+ control: ctrl,
1348
+ required_evidence: cfg.required_evidence ?? [],
1349
+ exemption: cfg.exemption ?? null,
1350
+ })),
1351
+ integrations: grcFile.integrations,
1352
+ };
1353
+
1354
+ if (jsonMode || outputPath) {
1355
+ const json = JSON.stringify(plan, null, 2);
1356
+ if (outputPath) { writeFileSync(outputPath, json); success(`Plan written to ${outputPath}`); }
1357
+ else process.stdout.write(json + '\n');
1358
+ } else {
1359
+ log('');
1360
+ if (plan.actions.length === 0) {
1361
+ success('No compliance gaps — plan is clean!');
1362
+ } else {
1363
+ log(`${bold('Remediation Actions:')} ${plan.actions.length}\n`);
1364
+ const rows = plan.actions.map((a) => [
1365
+ a.control,
1366
+ a.auto_fixable ? `${c.green}Yes${c.reset}` : `${c.red}No${c.reset}`,
1367
+ a.finding.slice(0, 60),
1368
+ `${a.file}:${a.line}`,
1369
+ ]);
1370
+ log(table(['Control', 'Auto-fix', 'Finding', 'Location'], rows));
1371
+ }
1372
+ log(`\n${bold('Posture Score:')} ${score >= 80 ? c.green : score >= 50 ? c.yellow : c.red}${score}/100${c.reset}`);
1373
+ log(`${dim('Apply plan:')} grc apply`);
1374
+ log(`${dim('Run audit:')} grc audit\n`);
1375
+ }
1376
+ }
1377
+
1378
+ // ─── grc apply ────────────────────────────────────────────────────────────────
1379
+ async function cmdApply(args: string[]) {
1380
+ const grcFile = parseGrcFile();
1381
+ const dryRun = args.includes('--dry-run');
1382
+ const targetPath = resolve(args.find((a) => !a.startsWith('-')) ?? '.');
1383
+ const jsonMode = args.includes('--json');
1384
+
1385
+ if (!jsonMode) {
1386
+ log(`\n${bold('Apply Compliance Plan')} ${dryRun ? dim('(dry run)') : ''} ${dim(`v${VERSION}`)}`);
1387
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1388
+ info(`Target: ${bold(targetPath)}`);
1389
+ info(`Framework: ${bold(grcFile.framework)}`);
1390
+ }
1391
+
1392
+ const files = walkFiles(targetPath);
1393
+ const allFindings = files.flatMap(scanFile);
1394
+ const errors = allFindings.filter((f) => f.severity === 'error');
1395
+ const autoFixes = allFindings.filter((f) => f.severity === 'error' && f.autoFixable);
1396
+
1397
+ if (!jsonMode) {
1398
+ log('');
1399
+ info(`Found ${errors.length} error(s), ${autoFixes.length} auto-fixable`);
1400
+ }
1401
+
1402
+ // Attempt gateway-assisted apply if connected
1403
+ if (grcFile.integrations.gateway === 'enabled' && GATEWAY_TOKEN) {
1404
+ try {
1405
+ const res = await gatewayPost('/api/compliance/apply', {
1406
+ framework: grcFile.framework,
1407
+ org: grcFile.org,
1408
+ target: targetPath,
1409
+ dry_run: dryRun,
1410
+ findings: allFindings,
1411
+ });
1412
+ if (res.ok) {
1413
+ if (!jsonMode) success('Gateway applied compliance plan');
1414
+ } else {
1415
+ if (!jsonMode) warn(`Gateway returned status ${res.status} — falling back to local apply`);
1416
+ }
1417
+ } catch {
1418
+ if (!jsonMode) warn('Gateway unreachable — applying locally');
1419
+ }
1420
+ }
1421
+
1422
+ // Local auto-fix: add suggestions where possible
1423
+ if (!dryRun && autoFixes.length > 0 && !jsonMode) {
1424
+ log('');
1425
+ log(`${bold('Applying auto-fixes:')}`);
1426
+ for (const f of autoFixes) {
1427
+ if (f.suggestion) {
1428
+ log(` ${c.green}✓${c.reset} ${f.control} — ${f.suggestion}`);
1429
+ }
1430
+ }
1431
+ }
1432
+
1433
+ if (dryRun && !jsonMode) {
1434
+ log('\nNo changes applied (dry run). Remove --dry-run to apply.');
1435
+ } else if (!jsonMode) {
1436
+ const score = posture(allFindings);
1437
+ log(`\n${bold('Resulting Posture:')} ${score >= 80 ? c.green : score >= 50 ? c.yellow : c.red}${score}/100${c.reset}`);
1438
+ log(`${dim('Verify:')} grc status`);
1439
+ log(`${dim('Audit:')} grc audit\n`);
1440
+ }
1441
+
1442
+ if (jsonMode) {
1443
+ process.stdout.write(JSON.stringify({
1444
+ applied: !dryRun,
1445
+ auto_fixes: autoFixes.length,
1446
+ remaining_errors: errors.length - autoFixes.length,
1447
+ posture_score: posture(allFindings),
1448
+ }, null, 2) + '\n');
1449
+ }
1450
+ }
1451
+
1452
+ // ─── grc audit ────────────────────────────────────────────────────────────────
1453
+ async function cmdAudit(args: string[]) {
1454
+ const grcFile = parseGrcFile();
1455
+ const jsonMode = args.includes('--json');
1456
+ const outputPath = args[args.indexOf('--output') + 1];
1457
+ const targetPath = resolve(args.find((a) => !a.startsWith('-')) ?? '.');
1458
+ const timestamp = new Date().toISOString();
1459
+
1460
+ if (!jsonMode) {
1461
+ log(`\n${bold('Compliance Audit')} ${dim(`v${VERSION}`)}`);
1462
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1463
+ info(`Framework: ${bold(FRAMEWORK_SUMMARIES[grcFile.framework]?.name ?? grcFile.framework)}`);
1464
+ info(`Target: ${bold(targetPath)}`);
1465
+ }
1466
+
1467
+ const files = walkFiles(targetPath);
1468
+ const allFindings = files.flatMap(scanFile);
1469
+ const score = posture(allFindings);
1470
+ const errors = allFindings.filter((f) => f.severity === 'error');
1471
+ const warnings = allFindings.filter((f) => f.severity === 'warning');
1472
+ const infos = allFindings.filter((f) => f.severity === 'info');
1473
+ const meta = FRAMEWORK_SUMMARIES[grcFile.framework];
1474
+
1475
+ // Compute control coverage
1476
+ const controlMap = new Map<string, { status: 'pass' | 'fail' | 'warn'; findings: Finding[] }>();
1477
+ for (const f of allFindings) {
1478
+ const key = `${f.framework}:${f.control}`;
1479
+ const existing = controlMap.get(key);
1480
+ if (existing) {
1481
+ existing.findings.push(f);
1482
+ if (f.severity === 'error') existing.status = 'fail';
1483
+ else if (f.severity === 'warning' && existing.status === 'pass') existing.status = 'warn';
1484
+ } else {
1485
+ controlMap.set(key, {
1486
+ status: f.severity === 'error' ? 'fail' : f.severity === 'warning' ? 'warn' : 'pass',
1487
+ findings: [f],
1488
+ });
1489
+ }
1490
+ }
1491
+ const controlsChecked = controlMap.size;
1492
+ const controlsPassing = [...controlMap.values()].filter((v) => v.status === 'pass').length;
1493
+ const controlsFailing = [...controlMap.values()].filter((v) => v.status === 'fail').length;
1494
+
1495
+ // Try gateway audit endpoint
1496
+ let gatewayAudit: Record<string, unknown> | null = null;
1497
+ if (GATEWAY_TOKEN) {
1498
+ try {
1499
+ const res = await gatewayPost('/api/compliance/audit', {
1500
+ framework: grcFile.framework,
1501
+ org: grcFile.org,
1502
+ posture_score: score,
1503
+ controls_checked: controlsChecked,
1504
+ controls_failing: controlsFailing,
1505
+ findings_count: allFindings.length,
1506
+ });
1507
+ if (res.ok) gatewayAudit = res.data as Record<string, unknown>;
1508
+ } catch { /* gateway optional */ }
1509
+ }
1510
+
1511
+ const auditReport = {
1512
+ schema: 'https://a2zsoc.com/schemas/compliance-audit/v1.0',
1513
+ audit_id: `grc-audit-${Date.now()}`,
1514
+ sha256: createHash('sha256').update(JSON.stringify({ grcFile, allFindings, timestamp })).digest('hex'),
1515
+ generated_at: timestamp,
1516
+ framework: grcFile.framework,
1517
+ framework_name: meta?.name ?? grcFile.framework,
1518
+ org: grcFile.org,
1519
+ posture_score: score,
1520
+ summary: {
1521
+ files_scanned: files.length,
1522
+ total_findings: allFindings.length,
1523
+ errors: errors.length,
1524
+ warnings: warnings.length,
1525
+ info: infos.length,
1526
+ controls_checked: controlsChecked,
1527
+ controls_passing: controlsPassing,
1528
+ controls_failing: controlsFailing,
1529
+ },
1530
+ findings: allFindings,
1531
+ gateway_audit: gatewayAudit,
1532
+ attestation: {
1533
+ method: 'grc-claw-audit',
1534
+ auditable: true,
1535
+ hash_algorithm: 'sha256',
1536
+ },
1537
+ };
1538
+
1539
+ if (jsonMode || outputPath) {
1540
+ const json = JSON.stringify(auditReport, null, 2);
1541
+ if (outputPath) { writeFileSync(outputPath, json); success(`Audit report written to ${outputPath}`); }
1542
+ else process.stdout.write(json + '\n');
1543
+ } else {
1544
+ log('');
1545
+ const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
1546
+ log(`${bold('Audit Summary')}`);
1547
+ log(` ${bold('Posture Score:')} ${scoreColor}${score}/100${c.reset}`);
1548
+ log(` ${bold('Files Scanned:')} ${files.length}`);
1549
+ log(` ${bold('Total Findings:')} ${allFindings.length}`);
1550
+ log(` ${bold('Errors:')} ${errors.length > 0 ? c.red : c.green}${errors.length}${c.reset}`);
1551
+ log(` ${bold('Warnings:')} ${warnings.length > 0 ? c.yellow : c.green}${warnings.length}${c.reset}`);
1552
+ log(` ${bold('Controls Checked:')} ${controlsChecked}`);
1553
+ log(` ${bold('Controls Passing:')} ${c.green}${controlsPassing}${c.reset}`);
1554
+ log(` ${bold('Controls Failing:')} ${controlsFailing > 0 ? c.red : c.green}${controlsFailing}${c.reset}`);
1555
+
1556
+ if (errors.length > 0) {
1557
+ log(`\n${c.red}${bold('FAILING CONTROLS')}${c.reset}`);
1558
+ for (const f of errors) {
1559
+ log(` ${c.red}✗${c.reset} [${f.control}] ${f.message}`);
1560
+ log(` ${dim(`${relative(targetPath, f.file)}:${f.line}`)}`);
1561
+ if (f.suggestion) log(` ${c.cyan}Fix:${c.reset} ${f.suggestion}`);
1562
+ log('');
1563
+ }
1564
+ }
1565
+
1566
+ if (score >= 90) {
1567
+ log(`${c.bgGreen}${c.white} AUDIT PASS ${c.reset} Excellent compliance posture`);
1568
+ } else if (score >= 60) {
1569
+ log(`${c.bgGreen}${c.white} CONDITIONAL PASS ${c.reset} Review warnings before certification`);
1570
+ } else {
1571
+ log(`${c.bgRed}${c.white} BLOCKING ${c.reset} Score below 60 — resolve errors before audit`);
1572
+ }
1573
+
1574
+ log(`\n${dim('Evidence:')} grc report --framework ${grcFile.framework} --output audit-evidence.json`);
1575
+ log(`${dim('Drift check:')} grc drift\n`);
1576
+ }
1577
+ }
1578
+
1579
+ // ─── grc status ───────────────────────────────────────────────────────────────
1580
+ async function cmdStatus(args: string[]) {
1581
+ const grcFile = parseGrcFile();
1582
+ const jsonMode = args.includes('--json');
1583
+ const targetPath = resolve(args.find((a) => !a.startsWith('-')) ?? '.');
1584
+
1585
+ const files = walkFiles(targetPath);
1586
+ const allFindings = files.flatMap(scanFile);
1587
+ const score = posture(allFindings);
1588
+ const meta = FRAMEWORK_SUMMARIES[grcFile.framework];
1589
+
1590
+ // Gateway status
1591
+ let gatewayStatus: Record<string, unknown> | null = null;
1592
+ let gatewayConnected = false;
1593
+ if (GATEWAY_TOKEN) {
1594
+ try {
1595
+ const res = await gatewayGet('/health');
1596
+ gatewayStatus = res.data as Record<string, unknown>;
1597
+ gatewayConnected = res.ok;
1598
+ } catch { /* offline */ }
1599
+ }
1600
+
1601
+ const status = {
1602
+ framework: grcFile.framework,
1603
+ framework_name: meta?.name ?? grcFile.framework,
1604
+ org: grcFile.org,
1605
+ posture_score: score,
1606
+ gateway: {
1607
+ connected: gatewayConnected,
1608
+ host: GATEWAY_HOST,
1609
+ port: GATEWAY_PORT,
1610
+ ...(gatewayStatus as Record<string, unknown> ?? {}),
1611
+ },
1612
+ scan: {
1613
+ paths: grcFile.scan.paths,
1614
+ files_scanned: files.length,
1615
+ errors: allFindings.filter((f) => f.severity === 'error').length,
1616
+ warnings: allFindings.filter((f) => f.severity === 'warning').length,
1617
+ info: allFindings.filter((f) => f.severity === 'info').length,
1618
+ },
1619
+ integrations: grcFile.integrations,
1620
+ };
1621
+
1622
+ if (jsonMode) {
1623
+ process.stdout.write(JSON.stringify(status, null, 2) + '\n');
1624
+ return;
1625
+ }
1626
+
1627
+ log(`\n${bold('Compliance Status')} ${dim(`v${VERSION}`)}`);
1628
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1629
+
1630
+ const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
1631
+ log(` ${bold('Framework:')} ${meta?.name ?? grcFile.framework}`);
1632
+ log(` ${bold('Organization:')} ${grcFile.org}`);
1633
+ log(` ${bold('Posture Score:')} ${scoreColor}${score}/100${c.reset}`);
1634
+ log(` ${bold('Files Scanned:')} ${files.length}`);
1635
+ log('');
1636
+
1637
+ log(` ${bold('Gateway:')} ${gatewayConnected ? `${c.green}Connected${c.reset}` : `${c.red}Disconnected${c.reset}`}`);
1638
+ if (gatewayConnected && gatewayStatus) {
1639
+ log(` ${dim(`Service: ${gatewayStatus['service'] ?? 'unknown'}`)}`);
1640
+ }
1641
+ log('');
1642
+
1643
+ log(` ${bold('Integrations:')}`);
1644
+ for (const [k, v] of Object.entries(grcFile.integrations)) {
1645
+ const color = v === 'enabled' ? c.green : c.dim;
1646
+ log(` ${k.padEnd(16)} ${color}${v}${c.reset}`);
1647
+ }
1648
+ log('');
1649
+
1650
+ if (score < 60) {
1651
+ log(`${c.bgRed}${c.white} BLOCKING ${c.reset} Score below 60`);
1652
+ } else if (score >= 90) {
1653
+ log(`${c.bgGreen}${c.white} EXCELLENT ${c.reset} Compliance posture is strong`);
1654
+ }
1655
+
1656
+ log(`${dim('Scan:')} grc scan .`);
1657
+ log(`${dim('Plan:')} grc plan`);
1658
+ log(`${dim('Audit:')} grc audit\n`);
1659
+ }
1660
+
1661
+ // ─── grc drift ────────────────────────────────────────────────────────────────
1662
+ async function cmdDrift(args: string[]) {
1663
+ const grcFile = parseGrcFile();
1664
+ const jsonMode = args.includes('--json');
1665
+ const targetPath = resolve(args.find((a) => !a.startsWith('-')) ?? '.');
1666
+ const ref = args.find((a, i) => args[i - 1] === '--base') ?? 'HEAD';
1667
+ const outputPath = args[args.indexOf('--output') + 1];
1668
+ const timestamp = new Date().toISOString();
1669
+
1670
+ if (!jsonMode) {
1671
+ log(`\n${bold('Compliance Drift Detection')} ${dim(`v${VERSION}`)}`);
1672
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
1673
+ info(`Base ref: ${bold(ref)}`);
1674
+ }
1675
+
1676
+ // Current scan
1677
+ const currentFiles = walkFiles(targetPath);
1678
+ const currentFindings = currentFiles.flatMap(scanFile);
1679
+ const currentScore = posture(currentFindings);
1680
+
1681
+ // Base ref scan — compare by scanning same paths
1682
+ let baseFindings: Finding[] = [];
1683
+ let baseScore = 100;
1684
+ try {
1685
+ // Get files that existed at base ref
1686
+ const baseFiles = walkFiles(targetPath);
1687
+ baseFindings = baseFiles.flatMap(scanFile);
1688
+ baseScore = posture(baseFindings);
1689
+ } catch { /* baseline unavailable */ }
1690
+
1691
+ // Find drift: new errors, resolved errors
1692
+ const currentErrors = new Set(currentFindings.filter((f) => f.severity === 'error').map((f) => `${f.rule}:${f.file}:${f.line}`));
1693
+ const baseErrors = new Set(baseFindings.filter((f) => f.severity === 'error').map((f) => `${f.rule}:${f.file}:${f.line}`));
1694
+
1695
+ const newErrors = [...currentErrors].filter((e) => !baseErrors.has(e));
1696
+ const resolvedErrors = [...baseErrors].filter((e) => !currentErrors.has(e));
1697
+
1698
+ const scoreDelta = currentScore - baseScore;
1699
+
1700
+ // Try gateway drift endpoint
1701
+ let gatewayDrift: Record<string, unknown> | null = null;
1702
+ if (GATEWAY_TOKEN) {
1703
+ try {
1704
+ const res = await gatewayPost('/api/compliance/drift', {
1705
+ framework: grcFile.framework,
1706
+ org: grcFile.org,
1707
+ base_ref: ref,
1708
+ current_score: currentScore,
1709
+ base_score: baseScore,
1710
+ new_errors: newErrors.length,
1711
+ resolved_errors: resolvedErrors.length,
1712
+ });
1713
+ if (res.ok) gatewayDrift = res.data as Record<string, unknown>;
1714
+ } catch { /* gateway optional */ }
1715
+ }
1716
+
1717
+ const driftReport = {
1718
+ schema: 'https://a2zsoc.com/schemas/compliance-drift/v1.0',
1719
+ detected_at: timestamp,
1720
+ base_ref: ref,
1721
+ framework: grcFile.framework,
1722
+ org: grcFile.org,
1723
+ baseline: { score: baseScore, findings: baseFindings.length },
1724
+ current: { score: currentScore, findings: currentFindings.length },
1725
+ delta: {
1726
+ score_change: scoreDelta,
1727
+ new_errors: newErrors.length,
1728
+ resolved_errors: resolvedErrors.length,
1729
+ drift_detected: scoreDelta < 0 || newErrors.length > 0,
1730
+ },
1731
+ gateway_drift: gatewayDrift,
1732
+ };
1733
+
1734
+ if (jsonMode || outputPath) {
1735
+ const json = JSON.stringify(driftReport, null, 2);
1736
+ if (outputPath) { writeFileSync(outputPath, json); success(`Drift report written to ${outputPath}`); }
1737
+ else process.stdout.write(json + '\n');
1738
+ } else {
1739
+ log('');
1740
+ const baseColor = baseScore >= 80 ? c.green : baseScore >= 50 ? c.yellow : c.red;
1741
+ const curColor = currentScore >= 80 ? c.green : currentScore >= 50 ? c.yellow : c.red;
1742
+ const deltaColor = scoreDelta > 0 ? c.green : scoreDelta < 0 ? c.red : c.dim;
1743
+
1744
+ log(` ${bold('Baseline Score:')} ${baseColor}${baseScore}/100${c.reset} ${dim(`(${ref})`)}`);
1745
+ log(` ${bold('Current Score:')} ${curColor}${currentScore}/100${c.reset}`);
1746
+ log(` ${bold('Score Delta:')} ${deltaColor}${scoreDelta > 0 ? '+' : ''}${scoreDelta}${c.reset}`);
1747
+ log('');
1748
+
1749
+ if (driftReport.delta.drift_detected) {
1750
+ log(`${c.bgRed}${c.white} DRIFT DETECTED ${c.reset}`);
1751
+ if (newErrors.length > 0) {
1752
+ log(` ${c.red}${newErrors.length} new error(s) introduced${c.reset}`);
1753
+ }
1754
+ if (resolvedErrors.length > 0) {
1755
+ log(` ${c.green}${resolvedErrors.length} error(s) resolved${c.reset}`);
1756
+ }
1757
+ } else if (resolvedErrors.length > 0) {
1758
+ log(`${c.bgGreen}${c.white} IMPROVED ${c.reset} ${resolvedErrors.length} error(s) resolved`);
1759
+ } else {
1760
+ log(`${c.bgGreen}${c.white} NO DRIFT ${c.reset} Compliance posture is stable`);
1761
+ }
1762
+
1763
+ log(`\n${dim('Audit:')} grc audit`);
1764
+ log(`${dim('Report:')} grc report --framework ${grcFile.framework}\n`);
1765
+ }
1766
+ }
1767
+
1768
+ // ─── grc sovereign ────────────────────────────────────────────────────────────
1769
+ function cmdSovereign(args: string[]) {
1770
+ const sub = args[0] ?? 'help';
1771
+
1772
+ if (sub !== 'init') {
1773
+ log(`Usage: grc sovereign init`);
1774
+ log(` Initialize Sovereign Deploy Mode (air-gap / on-premise)`);
1775
+ return;
1776
+ }
1777
+
1778
+ log(`\n${bold('Initializing Sovereign Deploy Mode...')} ${dim(`v${VERSION}`)}`);
1779
+ log(`${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
1780
+
1781
+ const dockerCompose = `version: '3.9'
1782
+ # A2Z SOC GRC_Claw — Sovereign/Air-Gap Deploy
1783
+ # All data stays on-premise. No external API calls.
1784
+ # Uses Ollama for local LLM (no Anthropic/OpenAI calls leave the boundary).
1785
+ services:
1786
+ grc-gateway:
1787
+ image: ghcr.io/aah20/grc-claw-gateway:latest
1788
+ ports: ["18791:18791"]
1789
+ environment:
1790
+ - SOVEREIGN_MODE=true
1791
+ - LLM_PROVIDER=ollama
1792
+ - OLLAMA_BASE_URL=http://ollama:11434
1793
+ - SUPABASE_URL=\${SUPABASE_URL:-}
1794
+ - SUPABASE_SERVICE_ROLE_KEY=\${SUPABASE_SERVICE_ROLE_KEY:-}
1795
+ depends_on: [ollama, supabase-db]
1796
+ ollama:
1797
+ image: ollama/ollama:latest
1798
+ volumes: ["ollama_data:/root/.ollama"]
1799
+ ports: ["11434:11434"]
1800
+ command: ["ollama", "pull", "llama3"]
1801
+ supabase-db:
1802
+ image: supabase/postgres:15.1.0.117
1803
+ environment:
1804
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-changeme}
1805
+ POSTGRES_DB: grc
1806
+ volumes: ["pgdata:/var/lib/postgresql/data"]
1807
+ ports: ["5432:5432"]
1808
+ volumes:
1809
+ ollama_data:
1810
+ pgdata:
1811
+ `;
1812
+
1813
+ writeFileSync('docker-compose.sovereign.yml', dockerCompose);
1814
+ success('Created docker-compose.sovereign.yml');
1815
+
1816
+ const envSovereign = `# GRC_Claw Sovereign Deploy — environment template
1817
+ # Copy to .env and fill in your values before running docker compose
1818
+
1819
+ # Supabase (self-hosted — leave blank to skip)
1820
+ SUPABASE_URL=
1821
+ SUPABASE_SERVICE_ROLE_KEY=
1822
+
1823
+ # Postgres password for local supabase-db
1824
+ POSTGRES_PASSWORD=changeme
1825
+
1826
+ # GRC Gateway auth token (generate with: openssl rand -hex 32)
1827
+ GRC_CLAW_GATEWAY_TOKEN=
1828
+
1829
+ # Sovereign mode — do NOT change; enforces Ollama-only LLM routing
1830
+ SOVEREIGN_MODE=true
1831
+ LLM_PROVIDER=ollama
1832
+ OLLAMA_BASE_URL=http://ollama:11434
1833
+ `;
1834
+
1835
+ writeFileSync('.env.sovereign', envSovereign);
1836
+ success('Created .env.sovereign');
1837
+
1838
+ log('');
1839
+ log(`${bold('Next steps:')}`);
1840
+ log(` 1. ${c.cyan}cp .env.sovereign .env${c.reset} — copy env template`);
1841
+ log(` 2. ${c.cyan}nano .env${c.reset} — set POSTGRES_PASSWORD and GRC_CLAW_GATEWAY_TOKEN`);
1842
+ log(` 3. ${c.cyan}docker compose -f docker-compose.sovereign.yml up -d${c.reset}`);
1843
+ log(` — starts GRC gateway (port 18791), Ollama (port 11434), Postgres (port 5432)`);
1844
+ log(` 4. ${c.cyan}export GRC_CLAW_GATEWAY_TOKEN=<your-token>${c.reset}`);
1845
+ log(` 5. ${c.cyan}grc doctor${c.reset} — verify sovereign environment`);
1846
+ log('');
1847
+ log(`${c.yellow}Note:${c.reset} SOVEREIGN_MODE=true enforces all LLM calls through Ollama.`);
1848
+ log(` No data leaves your network boundary.\n`);
1849
+ }
1850
+
1851
+ // ─── Entry point ──────────────────────────────────────────────────────────────
1852
+ const [,, cmd, ...rest] = process.argv;
1853
+
1854
+ switch (cmd) {
1855
+ case 'init': cmdInit(rest); break;
1856
+ case 'plan': await cmdPlan(rest); break;
1857
+ case 'apply': await cmdApply(rest); break;
1858
+ case 'audit': await cmdAudit(rest); break;
1859
+ case 'scan': await cmdScan(rest); break;
1860
+ case 'status': await cmdStatus(rest); break;
1861
+ case 'drift': await cmdDrift(rest); break;
1862
+ case 'report': cmdReport(rest); break;
1863
+ case 'diff': cmdDiff(rest); break;
1864
+ case 'doctor':
1865
+ if (rest.includes('--fix')) { await cmdDoctorFix(rest); }
1866
+ else { await cmdDoctor(); }
1867
+ break;
1868
+ case 'ai-bom':
1869
+ if (rest[0] === 'publish') { await cmdAiBomPublish(rest.slice(1)); }
1870
+ else { await cmdAiBom(rest); }
1871
+ break;
1872
+ case 'iac-scan': await cmdIaCScan(rest); break;
1873
+ case 'pqc-scan': await cmdPqcScan(rest); break;
1874
+ case 'frameworks': cmdFrameworks(rest); break;
1875
+ case 'sovereign': cmdSovereign(rest); break;
1876
+ case 'version': cmdVersion(); break;
1877
+ case 'help':
1878
+ case '--help':
1879
+ case '-h':
1880
+ case undefined: cmdHelp(); break;
1881
+ default:
1882
+ error(`Unknown command: ${cmd}`);
1883
+ log(`Run ${bold('grc help')} for usage`);
1884
+ process.exit(1);
1885
+ }