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