@grc-claw/cli 2.0.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,887 @@
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('Submit to your compliance platform:')} grc report --attach-bom ${outputPath ?? 'ai-bom.json'}\n`);
782
+ }
783
+
784
+ function cmdVersion() {
785
+ log(`@grc-claw/cli ${VERSION}`);
786
+ log(`Node: ${process.version}`);
787
+ log(`Platform: ${process.platform} ${process.arch}`);
788
+ }
789
+
790
+ function cmdHelp() {
791
+ log(`
792
+ ${bold('grc')} — GRC_Claw CLI ${dim(`v${VERSION}`)}
793
+
794
+ ${bold('USAGE')}
795
+ grc <command> [options]
796
+
797
+ ${bold('COMMANDS')}
798
+ ${c.cyan}init${c.reset} Scaffold grcfile.yaml + GitHub Actions workflow
799
+ --framework <id> Framework to target (default: iso27001)
800
+
801
+ ${c.cyan}scan${c.reset} [path] Scan codebase for compliance findings
802
+ --framework <id> Filter findings to a specific framework
803
+ --json Output JSON (suitable for CI/CD gates)
804
+
805
+ ${c.cyan}report${c.reset} Generate a compliance evidence report
806
+ --framework <id> Framework to report against (default: iso27001)
807
+ --path <dir> Directory to scan (default: .)
808
+
809
+ ${c.cyan}diff${c.reset} [ref] Show compliance delta between git refs (default: HEAD~1)
810
+
811
+ ${c.cyan}doctor${c.reset} Check environment and gateway connectivity
812
+ --fix Auto-remediate common control failures
813
+ --dry-run Preview fixes without applying
814
+
815
+ ${c.cyan}ai-bom generate${c.reset} Generate AI Bill of Materials (EU AI Act Art. 53)
816
+ --model-card <path> Parse a model_card.json file
817
+ --scan-deps Scan package.json for AI/ML dependencies
818
+ --output <file> Write BOM to file (default: stdout)
819
+
820
+ ${c.cyan}frameworks${c.reset} list List available framework packs
821
+
822
+ ${c.cyan}version${c.reset} Print version information
823
+
824
+ ${bold('EXAMPLES')}
825
+ ${dim('# Scaffold a new compliance project')}
826
+ grc init --framework soc2
827
+
828
+ ${dim('# Scan current directory')}
829
+ grc scan .
830
+
831
+ ${dim('# Scan and gate CI/CD (exits 1 if errors found)')}
832
+ grc scan . --json | jq '.summary.errors'
833
+
834
+ ${dim('# Show compliance changes in this PR branch')}
835
+ grc diff main
836
+
837
+ ${dim('# Auto-fix common control failures')}
838
+ grc doctor --fix
839
+
840
+ ${dim('# Generate AI Bill of Materials')}
841
+ grc ai-bom generate --scan-deps --output ai-bom.json
842
+
843
+ ${dim('# Generate ISO 27001 evidence report')}
844
+ grc report --framework iso27001 > evidence-$(date +%F).json
845
+
846
+ ${dim('# Check framework packs')}
847
+ grc frameworks list
848
+
849
+ ${dim('# Verify environment')}
850
+ grc doctor
851
+
852
+ ${bold('ENVIRONMENT')}
853
+ GRC_CLAW_GATEWAY_TOKEN Gateway auth token
854
+ GRC_CLAW_HOST Gateway host (default: 127.0.0.1)
855
+ GRC_CLAW_PORT Gateway port (default: 18791)
856
+ A2Z_SOC_API_KEY A2Z SOC integration key
857
+
858
+ ${bold('DOCS')}
859
+ https://a2zsoc.com/developers/compliance-as-code
860
+ https://github.com/AAH20/GRC_Claw
861
+ `);
862
+ }
863
+
864
+ // ─── Entry point ──────────────────────────────────────────────────────────────
865
+ const [,, cmd, ...rest] = process.argv;
866
+
867
+ switch (cmd) {
868
+ case 'init': cmdInit(rest); break;
869
+ case 'scan': await cmdScan(rest); break;
870
+ case 'report': cmdReport(rest); break;
871
+ case 'diff': cmdDiff(rest); break;
872
+ case 'doctor':
873
+ if (rest.includes('--fix')) { await cmdDoctorFix(rest); }
874
+ else { await cmdDoctor(); }
875
+ break;
876
+ case 'ai-bom': await cmdAiBom(rest); break;
877
+ case 'frameworks': cmdFrameworks(rest); break;
878
+ case 'version': cmdVersion(); break;
879
+ case 'help':
880
+ case '--help':
881
+ case '-h':
882
+ case undefined: cmdHelp(); break;
883
+ default:
884
+ error(`Unknown command: ${cmd}`);
885
+ log(`Run ${bold('grc help')} for usage`);
886
+ process.exit(1);
887
+ }