@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/CHANGELOG.md +19 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +849 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
- package/src/index.ts +887 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
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('Submit to your compliance platform:')} grc report --attach-bom ${outputPath ?? 'ai-bom.json'}\n`);
|
|
727
|
+
}
|
|
728
|
+
function cmdVersion() {
|
|
729
|
+
log(`@grc-claw/cli ${VERSION}`);
|
|
730
|
+
log(`Node: ${process.version}`);
|
|
731
|
+
log(`Platform: ${process.platform} ${process.arch}`);
|
|
732
|
+
}
|
|
733
|
+
function cmdHelp() {
|
|
734
|
+
log(`
|
|
735
|
+
${bold('grc')} — GRC_Claw CLI ${dim(`v${VERSION}`)}
|
|
736
|
+
|
|
737
|
+
${bold('USAGE')}
|
|
738
|
+
grc <command> [options]
|
|
739
|
+
|
|
740
|
+
${bold('COMMANDS')}
|
|
741
|
+
${c.cyan}init${c.reset} Scaffold grcfile.yaml + GitHub Actions workflow
|
|
742
|
+
--framework <id> Framework to target (default: iso27001)
|
|
743
|
+
|
|
744
|
+
${c.cyan}scan${c.reset} [path] Scan codebase for compliance findings
|
|
745
|
+
--framework <id> Filter findings to a specific framework
|
|
746
|
+
--json Output JSON (suitable for CI/CD gates)
|
|
747
|
+
|
|
748
|
+
${c.cyan}report${c.reset} Generate a compliance evidence report
|
|
749
|
+
--framework <id> Framework to report against (default: iso27001)
|
|
750
|
+
--path <dir> Directory to scan (default: .)
|
|
751
|
+
|
|
752
|
+
${c.cyan}diff${c.reset} [ref] Show compliance delta between git refs (default: HEAD~1)
|
|
753
|
+
|
|
754
|
+
${c.cyan}doctor${c.reset} Check environment and gateway connectivity
|
|
755
|
+
--fix Auto-remediate common control failures
|
|
756
|
+
--dry-run Preview fixes without applying
|
|
757
|
+
|
|
758
|
+
${c.cyan}ai-bom generate${c.reset} Generate AI Bill of Materials (EU AI Act Art. 53)
|
|
759
|
+
--model-card <path> Parse a model_card.json file
|
|
760
|
+
--scan-deps Scan package.json for AI/ML dependencies
|
|
761
|
+
--output <file> Write BOM to file (default: stdout)
|
|
762
|
+
|
|
763
|
+
${c.cyan}frameworks${c.reset} list List available framework packs
|
|
764
|
+
|
|
765
|
+
${c.cyan}version${c.reset} Print version information
|
|
766
|
+
|
|
767
|
+
${bold('EXAMPLES')}
|
|
768
|
+
${dim('# Scaffold a new compliance project')}
|
|
769
|
+
grc init --framework soc2
|
|
770
|
+
|
|
771
|
+
${dim('# Scan current directory')}
|
|
772
|
+
grc scan .
|
|
773
|
+
|
|
774
|
+
${dim('# Scan and gate CI/CD (exits 1 if errors found)')}
|
|
775
|
+
grc scan . --json | jq '.summary.errors'
|
|
776
|
+
|
|
777
|
+
${dim('# Show compliance changes in this PR branch')}
|
|
778
|
+
grc diff main
|
|
779
|
+
|
|
780
|
+
${dim('# Auto-fix common control failures')}
|
|
781
|
+
grc doctor --fix
|
|
782
|
+
|
|
783
|
+
${dim('# Generate AI Bill of Materials')}
|
|
784
|
+
grc ai-bom generate --scan-deps --output ai-bom.json
|
|
785
|
+
|
|
786
|
+
${dim('# Generate ISO 27001 evidence report')}
|
|
787
|
+
grc report --framework iso27001 > evidence-$(date +%F).json
|
|
788
|
+
|
|
789
|
+
${dim('# Check framework packs')}
|
|
790
|
+
grc frameworks list
|
|
791
|
+
|
|
792
|
+
${dim('# Verify environment')}
|
|
793
|
+
grc doctor
|
|
794
|
+
|
|
795
|
+
${bold('ENVIRONMENT')}
|
|
796
|
+
GRC_CLAW_GATEWAY_TOKEN Gateway auth token
|
|
797
|
+
GRC_CLAW_HOST Gateway host (default: 127.0.0.1)
|
|
798
|
+
GRC_CLAW_PORT Gateway port (default: 18791)
|
|
799
|
+
A2Z_SOC_API_KEY A2Z SOC integration key
|
|
800
|
+
|
|
801
|
+
${bold('DOCS')}
|
|
802
|
+
https://a2zsoc.com/developers/compliance-as-code
|
|
803
|
+
https://github.com/AAH20/GRC_Claw
|
|
804
|
+
`);
|
|
805
|
+
}
|
|
806
|
+
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
807
|
+
const [, , cmd, ...rest] = process.argv;
|
|
808
|
+
switch (cmd) {
|
|
809
|
+
case 'init':
|
|
810
|
+
cmdInit(rest);
|
|
811
|
+
break;
|
|
812
|
+
case 'scan':
|
|
813
|
+
await cmdScan(rest);
|
|
814
|
+
break;
|
|
815
|
+
case 'report':
|
|
816
|
+
cmdReport(rest);
|
|
817
|
+
break;
|
|
818
|
+
case 'diff':
|
|
819
|
+
cmdDiff(rest);
|
|
820
|
+
break;
|
|
821
|
+
case 'doctor':
|
|
822
|
+
if (rest.includes('--fix')) {
|
|
823
|
+
await cmdDoctorFix(rest);
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
await cmdDoctor();
|
|
827
|
+
}
|
|
828
|
+
break;
|
|
829
|
+
case 'ai-bom':
|
|
830
|
+
await cmdAiBom(rest);
|
|
831
|
+
break;
|
|
832
|
+
case 'frameworks':
|
|
833
|
+
cmdFrameworks(rest);
|
|
834
|
+
break;
|
|
835
|
+
case 'version':
|
|
836
|
+
cmdVersion();
|
|
837
|
+
break;
|
|
838
|
+
case 'help':
|
|
839
|
+
case '--help':
|
|
840
|
+
case '-h':
|
|
841
|
+
case undefined:
|
|
842
|
+
cmdHelp();
|
|
843
|
+
break;
|
|
844
|
+
default:
|
|
845
|
+
error(`Unknown command: ${cmd}`);
|
|
846
|
+
log(`Run ${bold('grc help')} for usage`);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
//# sourceMappingURL=index.js.map
|