@hatem427/code-guard-ci 3.3.0 โ 3.4.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/config/angular.config.ts +29 -708
- package/config/guidelines.config.ts +5 -130
- package/config/nextjs.config.ts +27 -511
- package/config/react.config.ts +19 -614
- package/dist/config/angular.config.d.ts +5 -8
- package/dist/config/angular.config.d.ts.map +1 -1
- package/dist/config/angular.config.js +28 -666
- package/dist/config/angular.config.js.map +1 -1
- package/dist/config/guidelines.config.d.ts.map +1 -1
- package/dist/config/guidelines.config.js +5 -127
- package/dist/config/guidelines.config.js.map +1 -1
- package/dist/config/nextjs.config.d.ts +7 -9
- package/dist/config/nextjs.config.d.ts.map +1 -1
- package/dist/config/nextjs.config.js +26 -472
- package/dist/config/nextjs.config.js.map +1 -1
- package/dist/config/react.config.d.ts +4 -5
- package/dist/config/react.config.d.ts.map +1 -1
- package/dist/config/react.config.js +19 -586
- package/dist/config/react.config.js.map +1 -1
- package/dist/scripts/auto-fix.d.ts +0 -5
- package/dist/scripts/auto-fix.d.ts.map +1 -1
- package/dist/scripts/auto-fix.js +0 -5
- package/dist/scripts/auto-fix.js.map +1 -1
- package/dist/scripts/cli.js +211 -415
- package/dist/scripts/cli.js.map +1 -1
- package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/ai-config-generator.js +71 -15
- package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
- package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/eslint-generator.js +13 -625
- package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
- package/dist/scripts/config-generators/index.d.ts +0 -1
- package/dist/scripts/config-generators/index.d.ts.map +1 -1
- package/dist/scripts/config-generators/index.js +1 -5
- package/dist/scripts/config-generators/index.js.map +1 -1
- package/dist/scripts/config-generators/typescript-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/typescript-generator.js +0 -33
- package/dist/scripts/config-generators/typescript-generator.js.map +1 -1
- package/dist/scripts/config-generators/vscode-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/vscode-generator.js +28 -171
- package/dist/scripts/config-generators/vscode-generator.js.map +1 -1
- package/dist/scripts/generate-pr-checklist.d.ts +0 -5
- package/dist/scripts/generate-pr-checklist.d.ts.map +1 -1
- package/dist/scripts/generate-pr-checklist.js +1 -6
- package/dist/scripts/generate-pr-checklist.js.map +1 -1
- package/dist/scripts/postinstall.js +0 -38
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/precommit-check.d.ts +0 -5
- package/dist/scripts/precommit-check.d.ts.map +1 -1
- package/dist/scripts/precommit-check.js +92 -149
- package/dist/scripts/precommit-check.js.map +1 -1
- package/dist/scripts/utils/naming-validator.d.ts.map +1 -1
- package/dist/scripts/utils/naming-validator.js +2 -96
- package/dist/scripts/utils/naming-validator.js.map +1 -1
- package/dist/scripts/utils/project-detector.d.ts +9 -12
- package/dist/scripts/utils/project-detector.d.ts.map +1 -1
- package/dist/scripts/utils/project-detector.js +11 -63
- package/dist/scripts/utils/project-detector.js.map +1 -1
- package/dist/scripts/utils/report-generator.js +5 -17
- package/dist/scripts/utils/report-generator.js.map +1 -1
- package/dist/scripts/utils/structure-validator.d.ts.map +1 -1
- package/dist/scripts/utils/structure-validator.js +0 -50
- package/dist/scripts/utils/structure-validator.js.map +1 -1
- package/package.json +1 -12
- package/scripts/auto-fix.ts +0 -5
- package/scripts/cli.ts +226 -451
- package/scripts/config-generators/ai-config-generator.ts +78 -28
- package/scripts/config-generators/eslint-generator.ts +7 -621
- package/scripts/config-generators/index.ts +0 -1
- package/scripts/config-generators/typescript-generator.ts +0 -36
- package/scripts/config-generators/vscode-generator.ts +40 -178
- package/scripts/generate-pr-checklist.ts +1 -6
- package/scripts/postinstall.ts +0 -38
- package/scripts/precommit-check.ts +113 -278
- package/scripts/utils/naming-validator.ts +2 -104
- package/scripts/utils/project-detector.ts +11 -78
- package/scripts/utils/report-generator.ts +5 -19
- package/scripts/utils/structure-validator.ts +0 -54
- package/config/fastify.config.ts +0 -326
- package/config/hono.config.ts +0 -331
- package/config/nestjs.config.ts +0 -500
- package/config/python.config.ts +0 -512
- package/templates/feature-doc-api.md +0 -101
- package/templates/feature-doc-backend.md +0 -114
- package/templates/feature-doc-service.md +0 -113
- package/templates/feature-doc-ui.md +0 -91
|
@@ -36,11 +36,6 @@ import { verifyBypassPassword, recordBypass } from './utils/bypass-manager';
|
|
|
36
36
|
import '../config/angular.config';
|
|
37
37
|
import '../config/react.config';
|
|
38
38
|
import '../config/nextjs.config';
|
|
39
|
-
import '../config/nestjs.config';
|
|
40
|
-
import '../config/node.config';
|
|
41
|
-
import '../config/fastify.config';
|
|
42
|
-
import '../config/hono.config';
|
|
43
|
-
import '../config/python.config';
|
|
44
39
|
|
|
45
40
|
// โโ Constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
46
41
|
|
|
@@ -50,6 +45,28 @@ const LINTABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'css', 'scss'];
|
|
|
50
45
|
/** File extensions for code guardian rule checks */
|
|
51
46
|
const CHECKABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'css', 'scss', 'sass', 'less'];
|
|
52
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Returns true when lint-staged is configured in the project.
|
|
50
|
+
* When lint-staged is active it ALREADY ran ESLint --fix and Prettier --write
|
|
51
|
+
* and re-staged the modified files before this script is invoked.
|
|
52
|
+
* Running them again would produce a duplicate git-add and confuse developers.
|
|
53
|
+
*/
|
|
54
|
+
function lintStagedIsActive(): boolean {
|
|
55
|
+
const fs = require('fs');
|
|
56
|
+
const lintStagedFiles = [
|
|
57
|
+
'.lintstagedrc', '.lintstagedrc.json', '.lintstagedrc.js',
|
|
58
|
+
'.lintstagedrc.cjs', '.lintstagedrc.yaml', '.lintstagedrc.yml',
|
|
59
|
+
'lint-staged.config.js', 'lint-staged.config.cjs',
|
|
60
|
+
];
|
|
61
|
+
if (lintStagedFiles.some((f: string) => fs.existsSync(f))) return true;
|
|
62
|
+
try {
|
|
63
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
64
|
+
return !!pkg['lint-staged'];
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
/** Flag in commit message to request bypass */
|
|
54
71
|
const BYPASS_COMMIT_FLAG = '#bypass-rules';
|
|
55
72
|
|
|
@@ -134,7 +151,12 @@ function runEslint(stagedFiles: string[]): boolean {
|
|
|
134
151
|
return true;
|
|
135
152
|
}
|
|
136
153
|
|
|
137
|
-
|
|
154
|
+
const lintStagedActive = lintStagedIsActive();
|
|
155
|
+
if (lintStagedActive) {
|
|
156
|
+
logger.info(`ESLint check on ${lintableFiles.length} file(s) (lint-staged already auto-fixed)...`);
|
|
157
|
+
} else {
|
|
158
|
+
logger.info(`Running ESLint on ${lintableFiles.length} file(s)...`);
|
|
159
|
+
}
|
|
138
160
|
|
|
139
161
|
try {
|
|
140
162
|
// Security: Use array args to prevent command injection
|
|
@@ -193,6 +215,14 @@ function runEslint(stagedFiles: string[]): boolean {
|
|
|
193
215
|
* If formatting issues are found, runs Prettier --write and re-stages the files.
|
|
194
216
|
*/
|
|
195
217
|
function runPrettier(stagedFiles: string[]): boolean {
|
|
218
|
+
// When lint-staged is active it already ran `prettier --write` and
|
|
219
|
+
// re-staged the formatted files. Running Prettier again would produce a
|
|
220
|
+
// redundant `git add` and confuse developers.
|
|
221
|
+
if (lintStagedIsActive()) {
|
|
222
|
+
logger.success('Prettier: handled by lint-staged (skipping duplicate run).');
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
196
226
|
const formattableFiles = stagedFiles.filter((f) => {
|
|
197
227
|
const ext = path.extname(f).replace(/^\./, '');
|
|
198
228
|
return LINTABLE_EXTENSIONS.includes(ext);
|
|
@@ -206,48 +236,42 @@ function runPrettier(stagedFiles: string[]): boolean {
|
|
|
206
236
|
logger.info(`Running Prettier on ${formattableFiles.length} file(s)...`);
|
|
207
237
|
|
|
208
238
|
try {
|
|
209
|
-
// Security: Use array args to prevent command injection
|
|
210
239
|
const { spawnSync } = require('child_process');
|
|
211
240
|
const checkResult = spawnSync('npx', ['prettier', '--check', ...formattableFiles], {
|
|
212
241
|
stdio: 'pipe',
|
|
213
242
|
encoding: 'utf-8',
|
|
214
243
|
});
|
|
215
|
-
|
|
244
|
+
|
|
216
245
|
if (checkResult.status === 0) {
|
|
217
246
|
logger.success('Prettier check passed.');
|
|
218
247
|
return true;
|
|
219
248
|
}
|
|
220
|
-
|
|
221
|
-
logger.warn('Prettier found formatting issues โ auto-fixing...');
|
|
222
249
|
|
|
223
|
-
|
|
224
|
-
const writeResult = spawnSync('npx', ['prettier', '--write', ...formattableFiles], {
|
|
225
|
-
stdio: 'inherit',
|
|
226
|
-
encoding: 'utf-8',
|
|
227
|
-
});
|
|
250
|
+
logger.warn('Prettier found formatting issues โ auto-fixing...');
|
|
228
251
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
const writeResult = spawnSync('npx', ['prettier', '--write', ...formattableFiles], {
|
|
253
|
+
stdio: 'inherit',
|
|
254
|
+
encoding: 'utf-8',
|
|
255
|
+
});
|
|
233
256
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
});
|
|
257
|
+
if (writeResult.status !== 0) {
|
|
258
|
+
logger.error('Prettier auto-fix failed.');
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
239
261
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
262
|
+
// Re-stage only in standalone mode (lint-staged is NOT active)
|
|
263
|
+
const gitResult = spawnSync('git', ['add', ...formattableFiles], {
|
|
264
|
+
stdio: 'inherit',
|
|
265
|
+
encoding: 'utf-8',
|
|
266
|
+
});
|
|
244
267
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
} catch (error) {
|
|
248
|
-
logger.error('Prettier auto-fix failed.');
|
|
268
|
+
if (gitResult.status !== 0) {
|
|
269
|
+
logger.error('Failed to re-stage auto-formatted files.');
|
|
249
270
|
return false;
|
|
250
271
|
}
|
|
272
|
+
|
|
273
|
+
logger.success('Prettier auto-fixed and re-staged files.');
|
|
274
|
+
return true;
|
|
251
275
|
} catch (error) {
|
|
252
276
|
logger.error('Prettier check failed.');
|
|
253
277
|
return false;
|
|
@@ -267,56 +291,28 @@ export interface SecurityScanResult {
|
|
|
267
291
|
|
|
268
292
|
/**
|
|
269
293
|
* Layer 1 โ npm audit
|
|
270
|
-
* Checks all installed npm dependencies against the GitHub Advisory Database.
|
|
271
|
-
* Blocks on critical/high severity by default.
|
|
272
294
|
*/
|
|
273
295
|
function runNpmAudit(): SecurityScanResult {
|
|
274
|
-
const result: SecurityScanResult = {
|
|
275
|
-
tool: 'npm audit',
|
|
276
|
-
passed: true,
|
|
277
|
-
skipped: false,
|
|
278
|
-
summary: '',
|
|
279
|
-
criticalCount: 0,
|
|
280
|
-
highCount: 0,
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
// Skip if not an npm project
|
|
296
|
+
const result: SecurityScanResult = { tool: 'npm audit', passed: true, skipped: false, summary: '', criticalCount: 0, highCount: 0 };
|
|
284
297
|
const fs = require('fs');
|
|
285
298
|
if (!fs.existsSync('package.json')) {
|
|
286
|
-
result.skipped = true;
|
|
287
|
-
|
|
288
|
-
logger.dim(' npm audit: skipped (no package.json)');
|
|
289
|
-
return result;
|
|
299
|
+
result.skipped = true; result.summary = 'No package.json found โ skipping.';
|
|
300
|
+
logger.dim(' npm audit: skipped (no package.json)'); return result;
|
|
290
301
|
}
|
|
291
|
-
|
|
292
302
|
logger.info('Layer 1/3 โ Running npm audit...');
|
|
293
|
-
|
|
294
303
|
try {
|
|
295
304
|
const { spawnSync } = require('child_process');
|
|
296
|
-
const auditResult = spawnSync(
|
|
297
|
-
'npm',
|
|
298
|
-
['audit', '--json', '--audit-level=high'],
|
|
299
|
-
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
300
|
-
);
|
|
301
|
-
|
|
305
|
+
const auditResult = spawnSync('npm', ['audit', '--json', '--audit-level=high'], { encoding: 'utf-8', cwd: process.cwd() });
|
|
302
306
|
let auditData: any = {};
|
|
303
|
-
try {
|
|
304
|
-
auditData = JSON.parse(auditResult.stdout || '{}');
|
|
305
|
-
} catch {
|
|
306
|
-
// npm audit output is not valid JSON (e.g. no lock file)
|
|
307
|
+
try { auditData = JSON.parse(auditResult.stdout || '{}'); } catch {
|
|
307
308
|
if (auditResult.stderr?.includes('ENOLOCK') || auditResult.stdout?.includes('ENOLOCK')) {
|
|
308
|
-
result.skipped = true;
|
|
309
|
-
|
|
310
|
-
logger.warn(' npm audit: skipped (no lock file โ run npm install first)');
|
|
311
|
-
return result;
|
|
309
|
+
result.skipped = true; result.summary = 'No lock file found โ run `npm install` first.';
|
|
310
|
+
logger.warn(' npm audit: skipped (no lock file)'); return result;
|
|
312
311
|
}
|
|
313
312
|
}
|
|
314
|
-
|
|
315
313
|
const vulns = auditData?.metadata?.vulnerabilities ?? {};
|
|
316
|
-
result.criticalCount = vulns.critical ?? 0;
|
|
317
|
-
result.highCount = vulns.high ?? 0;
|
|
314
|
+
result.criticalCount = vulns.critical ?? 0; result.highCount = vulns.high ?? 0;
|
|
318
315
|
const total = Object.values(vulns).reduce((acc: number, v) => acc + (v as number), 0);
|
|
319
|
-
|
|
320
316
|
if (result.criticalCount > 0 || result.highCount > 0) {
|
|
321
317
|
result.passed = false;
|
|
322
318
|
result.summary = `Found ${result.criticalCount} critical, ${result.highCount} high severity vulnerabilities (${total} total).`;
|
|
@@ -329,116 +325,58 @@ function runNpmAudit(): SecurityScanResult {
|
|
|
329
325
|
result.summary = 'No known vulnerabilities found.';
|
|
330
326
|
logger.success(' npm audit: โ
No vulnerabilities found.');
|
|
331
327
|
}
|
|
332
|
-
} catch {
|
|
333
|
-
result.skipped = true;
|
|
334
|
-
result.summary = 'npm not available โ skipping.';
|
|
335
|
-
logger.dim(' npm audit: skipped (npm not available)');
|
|
336
|
-
}
|
|
337
|
-
|
|
328
|
+
} catch { result.skipped = true; result.summary = 'npm not available.'; logger.dim(' npm audit: skipped'); }
|
|
338
329
|
return result;
|
|
339
330
|
}
|
|
340
331
|
|
|
341
332
|
/**
|
|
342
|
-
* Layer 2 โ RetireJS
|
|
343
|
-
* Scans JavaScript files and node_modules for libraries with known CVEs.
|
|
344
|
-
* Complements npm audit by catching bundled/vendored libraries that npm
|
|
345
|
-
* audit cannot see.
|
|
333
|
+
* Layer 2 โ RetireJS
|
|
346
334
|
*/
|
|
347
335
|
function runRetireJs(): SecurityScanResult {
|
|
348
|
-
const result: SecurityScanResult = {
|
|
349
|
-
tool: 'retire.js',
|
|
350
|
-
passed: true,
|
|
351
|
-
skipped: false,
|
|
352
|
-
summary: '',
|
|
353
|
-
criticalCount: 0,
|
|
354
|
-
highCount: 0,
|
|
355
|
-
};
|
|
356
|
-
|
|
336
|
+
const result: SecurityScanResult = { tool: 'retire.js', passed: true, skipped: false, summary: '', criticalCount: 0, highCount: 0 };
|
|
357
337
|
logger.info('Layer 2/3 โ Running RetireJS...');
|
|
358
|
-
|
|
359
338
|
try {
|
|
360
339
|
const { spawnSync } = require('child_process');
|
|
361
|
-
const retireResult = spawnSync(
|
|
362
|
-
'npx',
|
|
363
|
-
['--yes', 'retire', '--outputformat', 'json', '--exitwith', '0'],
|
|
364
|
-
{ encoding: 'utf-8', cwd: process.cwd(), timeout: 60_000 }
|
|
365
|
-
);
|
|
366
|
-
|
|
340
|
+
const retireResult = spawnSync('npx', ['--yes', 'retire', '--outputformat', 'json', '--exitwith', '0'], { encoding: 'utf-8', cwd: process.cwd(), timeout: 60_000 });
|
|
367
341
|
let findings: any[] = [];
|
|
368
342
|
try {
|
|
369
343
|
const parsed = JSON.parse(retireResult.stdout || '[]');
|
|
370
344
|
findings = Array.isArray(parsed) ? parsed : (parsed.data ?? []);
|
|
371
345
|
} catch {
|
|
372
|
-
|
|
373
|
-
if (retireResult.status === 0) {
|
|
374
|
-
result.summary = 'No vulnerable libraries detected.';
|
|
375
|
-
logger.success(' retire.js: โ
No vulnerable libraries found.');
|
|
376
|
-
return result;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (findings.length === 0) {
|
|
381
|
-
result.summary = 'No vulnerable libraries detected.';
|
|
382
|
-
logger.success(' retire.js: โ
No vulnerable libraries found.');
|
|
383
|
-
return result;
|
|
346
|
+
if (retireResult.status === 0) { result.summary = 'No vulnerable libraries detected.'; logger.success(' retire.js: โ
No vulnerable libraries found.'); return result; }
|
|
384
347
|
}
|
|
385
|
-
|
|
386
|
-
// Count by severity
|
|
348
|
+
if (findings.length === 0) { result.summary = 'No vulnerable libraries detected.'; logger.success(' retire.js: โ
No vulnerable libraries found.'); return result; }
|
|
387
349
|
for (const file of findings) {
|
|
388
|
-
for (const
|
|
389
|
-
for (const vuln of (
|
|
350
|
+
for (const r of (file.results ?? [])) {
|
|
351
|
+
for (const vuln of (r.vulnerabilities ?? [])) {
|
|
390
352
|
const sev = (vuln.severity ?? '').toLowerCase();
|
|
391
|
-
if (sev === 'critical') result.criticalCount++;
|
|
392
|
-
else if (sev === 'high') result.highCount++;
|
|
353
|
+
if (sev === 'critical') result.criticalCount++; else if (sev === 'high') result.highCount++;
|
|
393
354
|
}
|
|
394
355
|
}
|
|
395
356
|
}
|
|
396
|
-
|
|
397
357
|
if (result.criticalCount > 0 || result.highCount > 0) {
|
|
398
358
|
result.passed = false;
|
|
399
359
|
result.summary = `Found ${result.criticalCount} critical, ${result.highCount} high severity vulnerable libraries (${findings.length} affected files).`;
|
|
400
360
|
logger.error(` retire.js: โ ${result.summary}`);
|
|
401
|
-
logger.dim(' Fix: Update or replace the flagged libraries.');
|
|
402
361
|
} else {
|
|
403
362
|
result.summary = `${findings.length} affected file(s) with low/moderate severity findings. Review when possible.`;
|
|
404
363
|
logger.warn(` retire.js: โ ๏ธ ${result.summary}`);
|
|
405
364
|
}
|
|
406
|
-
} catch {
|
|
407
|
-
result.skipped = true;
|
|
408
|
-
result.summary = 'RetireJS could not be executed โ skipping.';
|
|
409
|
-
logger.dim(' retire.js: skipped (could not execute npx retire)');
|
|
410
|
-
}
|
|
411
|
-
|
|
365
|
+
} catch { result.skipped = true; result.summary = 'RetireJS could not be executed.'; logger.dim(' retire.js: skipped'); }
|
|
412
366
|
return result;
|
|
413
367
|
}
|
|
414
368
|
|
|
415
369
|
/**
|
|
416
370
|
* Layer 3 โ Syft + Grype
|
|
417
|
-
* Syft generates a Software Bill of Materials (SBOM) for the project.
|
|
418
|
-
* Grype scans the SBOM for CVEs across ALL package ecosystems (npm, pip,
|
|
419
|
-
* Go, Maven, etc.), including OS-level packages if running inside a container.
|
|
420
|
-
*
|
|
421
|
-
* Falls back to running `grype dir:.` directly if Syft is not installed.
|
|
422
371
|
*/
|
|
423
372
|
function runGrype(): SecurityScanResult {
|
|
424
|
-
const result: SecurityScanResult = {
|
|
425
|
-
tool: 'Syft + Grype',
|
|
426
|
-
passed: true,
|
|
427
|
-
skipped: false,
|
|
428
|
-
summary: '',
|
|
429
|
-
criticalCount: 0,
|
|
430
|
-
highCount: 0,
|
|
431
|
-
};
|
|
432
|
-
|
|
373
|
+
const result: SecurityScanResult = { tool: 'Syft + Grype', passed: true, skipped: false, summary: '', criticalCount: 0, highCount: 0 };
|
|
433
374
|
logger.info('Layer 3/3 โ Running Syft + Grype...');
|
|
434
|
-
|
|
435
375
|
const { spawnSync } = require('child_process');
|
|
436
|
-
|
|
437
|
-
// Check if grype is available
|
|
438
376
|
const grypeCheck = spawnSync('which', ['grype'], { encoding: 'utf-8' });
|
|
439
377
|
if (grypeCheck.status !== 0) {
|
|
440
378
|
result.skipped = true;
|
|
441
|
-
result.summary = 'grype not installed
|
|
379
|
+
result.summary = 'grype not installed';
|
|
442
380
|
logger.warn(' Syft+Grype: โ ๏ธ grype not installed โ Layer 3 skipped.');
|
|
443
381
|
logger.dim('');
|
|
444
382
|
logger.dim(' To enable full SBOM vulnerability scanning, install Grype once:');
|
|
@@ -450,100 +388,42 @@ function runGrype(): SecurityScanResult {
|
|
|
450
388
|
logger.dim(' macOS (Homebrew):');
|
|
451
389
|
logger.dim(' brew install anchore/grype/grype');
|
|
452
390
|
logger.dim('');
|
|
453
|
-
logger.dim(' Optionally install Syft for full SBOM generation:');
|
|
454
|
-
logger.dim(' brew install anchore/syft/syft');
|
|
455
|
-
logger.dim(' (or see https://github.com/anchore/syft)');
|
|
456
|
-
logger.dim('');
|
|
457
391
|
return result;
|
|
458
392
|
}
|
|
459
|
-
|
|
460
393
|
try {
|
|
461
394
|
const fs = require('fs');
|
|
462
395
|
const syftCheck = spawnSync('which', ['syft'], { encoding: 'utf-8' });
|
|
463
|
-
|
|
464
396
|
if (syftCheck.status === 0) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// 3. dir:. โ generic fallback (may miss transitive deps)
|
|
469
|
-
let syftSource = 'dir:.';
|
|
470
|
-
let syftMode = 'generic dir';
|
|
471
|
-
|
|
472
|
-
if (fs.existsSync('node_modules')) {
|
|
473
|
-
syftSource = 'dir:node_modules';
|
|
474
|
-
syftMode = 'node_modules (full transitive tree)';
|
|
475
|
-
} else if (fs.existsSync('package-lock.json')) {
|
|
476
|
-
syftSource = 'file:package-lock.json';
|
|
477
|
-
syftMode = 'package-lock.json';
|
|
478
|
-
}
|
|
479
|
-
|
|
397
|
+
let syftSource = 'dir:.'; let syftMode = 'generic dir';
|
|
398
|
+
if (fs.existsSync('node_modules')) { syftSource = 'dir:node_modules'; syftMode = 'node_modules (full transitive tree)'; }
|
|
399
|
+
else if (fs.existsSync('package-lock.json')) { syftSource = 'file:package-lock.json'; syftMode = 'package-lock.json'; }
|
|
480
400
|
logger.dim(` Generating SBOM with Syft (${syftMode})...`);
|
|
481
|
-
const syftResult = spawnSync(
|
|
482
|
-
'syft',
|
|
483
|
-
[syftSource, '-o', 'cyclonedx-json', '--quiet'],
|
|
484
|
-
{ encoding: 'utf-8', cwd: process.cwd(), timeout: 180_000 }
|
|
485
|
-
);
|
|
486
|
-
|
|
401
|
+
const syftResult = spawnSync('syft', [syftSource, '-o', 'cyclonedx-json', '--quiet'], { encoding: 'utf-8', cwd: process.cwd(), timeout: 180_000 });
|
|
487
402
|
if (syftResult.status === 0 && (syftResult.stdout as string)?.trim()) {
|
|
488
|
-
const
|
|
489
|
-
const grypeResult = spawnSync('grype', grypeArgs, {
|
|
490
|
-
input: syftResult.stdout as string,
|
|
491
|
-
encoding: 'utf-8',
|
|
492
|
-
cwd: process.cwd(),
|
|
493
|
-
timeout: 120_000,
|
|
494
|
-
});
|
|
403
|
+
const grypeResult = spawnSync('grype', ['sbom:-', '--output', 'json', '--fail-on', 'high'], { input: syftResult.stdout as string, encoding: 'utf-8', cwd: process.cwd(), timeout: 120_000 });
|
|
495
404
|
return _parseGrypeOutput(grypeResult, result, `Syft SBOM (${syftMode})`);
|
|
496
405
|
}
|
|
497
406
|
}
|
|
498
|
-
|
|
499
|
-
// Fallback: run grype directly โ point at node_modules if it exists so
|
|
500
|
-
// Grype resolves the full dependency tree rather than just package.json
|
|
501
|
-
const fs2 = require('fs');
|
|
502
|
-
const grypeTarget = fs2.existsSync('node_modules') ? 'dir:node_modules' : 'dir:.';
|
|
407
|
+
const grypeTarget = fs.existsSync('node_modules') ? 'dir:node_modules' : 'dir:.';
|
|
503
408
|
logger.dim(` Running Grype directly on ${grypeTarget}...`);
|
|
504
|
-
const
|
|
505
|
-
const grypeResult = spawnSync('grype', grypeArgs, {
|
|
506
|
-
encoding: 'utf-8',
|
|
507
|
-
cwd: process.cwd(),
|
|
508
|
-
timeout: 120_000,
|
|
509
|
-
});
|
|
409
|
+
const grypeResult = spawnSync('grype', [grypeTarget, '--output', 'json', '--fail-on', 'high'], { encoding: 'utf-8', cwd: process.cwd(), timeout: 120_000 });
|
|
510
410
|
return _parseGrypeOutput(grypeResult, result, `direct scan (${grypeTarget})`);
|
|
511
|
-
} catch {
|
|
512
|
-
result.skipped = true;
|
|
513
|
-
result.summary = 'Grype scan failed โ check your installation.';
|
|
514
|
-
logger.warn(' Syft+Grype: โ ๏ธ Scan failed โ check grype installation.');
|
|
515
|
-
return result;
|
|
516
|
-
}
|
|
411
|
+
} catch { result.skipped = true; result.summary = 'Grype scan failed.'; logger.warn(' Syft+Grype: โ ๏ธ Scan failed.'); return result; }
|
|
517
412
|
}
|
|
518
413
|
|
|
519
|
-
|
|
520
|
-
function _parseGrypeOutput(
|
|
521
|
-
spawnResult: ReturnType<typeof import('child_process').spawnSync>,
|
|
522
|
-
result: SecurityScanResult,
|
|
523
|
-
mode: string
|
|
524
|
-
): SecurityScanResult {
|
|
414
|
+
function _parseGrypeOutput(spawnResult: any, result: SecurityScanResult, mode: string): SecurityScanResult {
|
|
525
415
|
let matches: any[] = [];
|
|
526
416
|
try {
|
|
527
417
|
const parsed = JSON.parse((spawnResult.stdout as string) || '{}');
|
|
528
418
|
matches = parsed.matches ?? [];
|
|
529
419
|
} catch {
|
|
530
|
-
if (spawnResult.status === 0) {
|
|
531
|
-
|
|
532
|
-
logger.success(` Syft+Grype: โ
No vulnerabilities found (${mode}).`);
|
|
533
|
-
return result;
|
|
534
|
-
}
|
|
535
|
-
result.skipped = true;
|
|
536
|
-
result.summary = 'Could not parse Grype output.';
|
|
537
|
-
logger.warn(' Syft+Grype: โ ๏ธ Could not parse output.');
|
|
538
|
-
return result;
|
|
420
|
+
if (spawnResult.status === 0) { result.summary = 'No vulnerabilities found.'; logger.success(` Syft+Grype: โ
No vulnerabilities found (${mode}).`); return result; }
|
|
421
|
+
result.skipped = true; result.summary = 'Could not parse Grype output.'; logger.warn(' Syft+Grype: โ ๏ธ Could not parse output.'); return result;
|
|
539
422
|
}
|
|
540
|
-
|
|
541
423
|
for (const match of matches) {
|
|
542
424
|
const sev = (match.vulnerability?.severity ?? '').toLowerCase();
|
|
543
|
-
if (sev === 'critical') result.criticalCount++;
|
|
544
|
-
else if (sev === 'high') result.highCount++;
|
|
425
|
+
if (sev === 'critical') result.criticalCount++; else if (sev === 'high') result.highCount++;
|
|
545
426
|
}
|
|
546
|
-
|
|
547
427
|
if (result.criticalCount > 0 || result.highCount > 0) {
|
|
548
428
|
result.passed = false;
|
|
549
429
|
result.summary = `Found ${result.criticalCount} critical, ${result.highCount} high severity CVEs (${matches.length} total matches).`;
|
|
@@ -556,104 +436,55 @@ function _parseGrypeOutput(
|
|
|
556
436
|
result.summary = 'No vulnerabilities found.';
|
|
557
437
|
logger.success(` Syft+Grype: โ
No vulnerabilities found (${mode}).`);
|
|
558
438
|
}
|
|
559
|
-
|
|
560
439
|
return result;
|
|
561
440
|
}
|
|
562
441
|
|
|
563
|
-
/**
|
|
564
|
-
* Run all three security scan layers and return the combined results.
|
|
565
|
-
* When vulnerabilities are found the caller is responsible for prompting
|
|
566
|
-
* the developer โ this function only scans and reports.
|
|
567
|
-
*/
|
|
568
442
|
function runSecurityScans(): { passed: boolean; results: SecurityScanResult[] } {
|
|
569
443
|
logger.header('๐ Security Scanning (3 layers)');
|
|
570
|
-
|
|
571
444
|
const npmResult = runNpmAudit();
|
|
572
445
|
const retireResult = runRetireJs();
|
|
573
446
|
const grypeResult = runGrype();
|
|
574
|
-
|
|
575
447
|
const results = [npmResult, retireResult, grypeResult];
|
|
576
448
|
const passed = results.every((r) => r.skipped || r.passed);
|
|
577
|
-
|
|
578
449
|
console.log('');
|
|
579
450
|
if (!passed) {
|
|
580
451
|
const blocking = results.filter((r) => !r.skipped && !r.passed);
|
|
581
452
|
logger.error(`Security scan found vulnerabilities โ ${blocking.length} tool(s) reported critical/high issues:`);
|
|
582
|
-
for (const r of blocking) {
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
} else {
|
|
586
|
-
logger.success('Security scans passed (all layers).');
|
|
587
|
-
}
|
|
588
|
-
|
|
453
|
+
for (const r of blocking) { logger.error(` โข ${r.tool}: ${r.summary}`); }
|
|
454
|
+
} else { logger.success('Security scans passed (all layers).'); }
|
|
589
455
|
return { passed, results };
|
|
590
456
|
}
|
|
591
457
|
|
|
592
|
-
/**
|
|
593
|
-
* When security scans find vulnerabilities, prompt the developer to decide
|
|
594
|
-
* whether to proceed or abort the commit.
|
|
595
|
-
* Returns true โ developer chose to continue (risks acknowledged).
|
|
596
|
-
* Returns false โ developer chose to abort.
|
|
597
|
-
*/
|
|
598
458
|
async function promptSecurityDecision(results: SecurityScanResult[]): Promise<boolean> {
|
|
599
|
-
const pad = (line: string, width = 63) => {
|
|
600
|
-
// strip ANSI codes for length calculation
|
|
601
|
-
const plain = line.replace(/\x1b\[[0-9;]*m/g, '');
|
|
602
|
-
return line + ' '.repeat(Math.max(0, width - plain.length)) + 'โ';
|
|
603
|
-
};
|
|
604
|
-
|
|
605
459
|
console.log('');
|
|
606
460
|
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
607
461
|
console.log('โ โ ๏ธ VULNERABILITY REPORT โ all 3 layers โ');
|
|
608
|
-
console.log('
|
|
462
|
+
console.log('โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค');
|
|
609
463
|
console.log('โ Tool โ Result โ');
|
|
610
|
-
console.log('
|
|
611
|
-
|
|
464
|
+
console.log('โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค');
|
|
612
465
|
for (const r of results) {
|
|
613
466
|
const toolCol = `โ ${r.tool}`.padEnd(18) + 'โ';
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
status = ' โฌ skipped';
|
|
617
|
-
} else if (!r.passed) {
|
|
618
|
-
status = ` โ ${r.criticalCount} critical, ${r.highCount} high`;
|
|
619
|
-
} else {
|
|
620
|
-
status = ' โ
clean';
|
|
621
|
-
}
|
|
622
|
-
const statusCol = status.padEnd(42) + 'โ';
|
|
623
|
-
console.log(toolCol + statusCol);
|
|
467
|
+
const status = r.skipped ? ' โฌ skipped' : !r.passed ? ` โ ${r.criticalCount} critical, ${r.highCount} high` : ' โ
clean';
|
|
468
|
+
console.log(toolCol + status.padEnd(42) + 'โ');
|
|
624
469
|
}
|
|
625
|
-
|
|
626
|
-
console.log('โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค');
|
|
627
|
-
|
|
628
|
-
// Detail lines for failing tools
|
|
470
|
+
console.log('โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค');
|
|
629
471
|
const blocking = results.filter((r) => !r.skipped && !r.passed);
|
|
630
472
|
for (const r of blocking) {
|
|
631
473
|
const summary = r.summary.length > 57 ? r.summary.slice(0, 54) + '...' : r.summary;
|
|
632
|
-
|
|
474
|
+
const line = `โ โคต ${r.tool}: ${summary}`;
|
|
475
|
+
console.log(line.padEnd(63) + 'โ');
|
|
633
476
|
}
|
|
634
|
-
|
|
635
477
|
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค');
|
|
636
478
|
console.log('โ Fix: npm audit fix | npm audit (for full details) โ');
|
|
637
479
|
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
638
480
|
console.log('');
|
|
639
|
-
|
|
640
|
-
const answer = await promptUser(
|
|
641
|
-
' Proceed with commit despite vulnerabilities? [y/N]: '
|
|
642
|
-
);
|
|
643
|
-
|
|
481
|
+
const answer = await promptUser(' Proceed with commit despite vulnerabilities? [y/N]: ');
|
|
644
482
|
const confirmed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
logger.warn('โ ๏ธ Proceeding with known vulnerabilities โ please fix them as soon as possible.');
|
|
648
|
-
} else {
|
|
649
|
-
logger.error('โ Commit aborted. Fix the vulnerabilities and try again.');
|
|
650
|
-
}
|
|
651
|
-
|
|
483
|
+
if (confirmed) { logger.warn('โ ๏ธ Proceeding with known vulnerabilities โ please fix them soon.'); }
|
|
484
|
+
else { logger.error('โ Commit aborted. Fix the vulnerabilities and try again.'); }
|
|
652
485
|
return confirmed;
|
|
653
486
|
}
|
|
654
487
|
|
|
655
|
-
// โโ Feature doc detection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
656
|
-
|
|
657
488
|
/**
|
|
658
489
|
* If the commit looks like a new feature (branch name contains "feat/" or
|
|
659
490
|
* "feature/"), remind the user to generate docs.
|
|
@@ -738,16 +569,22 @@ async function main(): Promise<void> {
|
|
|
738
569
|
// Step 5: Run ESLint
|
|
739
570
|
const eslintPassed = runEslint(stagedPaths);
|
|
740
571
|
|
|
741
|
-
// Step 6: Run Prettier (
|
|
572
|
+
// Step 6: Run Prettier (skipped when lint-staged is active)
|
|
742
573
|
const prettierPassed = runPrettier(stagedPaths);
|
|
743
574
|
|
|
744
|
-
// Step 7:
|
|
745
|
-
|
|
575
|
+
// Step 7: Security scans โ ask developer first
|
|
576
|
+
let securityConfirmed = true;
|
|
577
|
+
const runScans = await promptUser(' ๐ Run security scans (npm audit + RetireJS + Grype)? [Y/n]: ');
|
|
578
|
+
const wantsScans = runScans === '' || runScans.toLowerCase() === 'y' || runScans.toLowerCase() === 'yes';
|
|
746
579
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
580
|
+
if (wantsScans) {
|
|
581
|
+
const { passed: securityPassed, results: securityResults } = runSecurityScans();
|
|
582
|
+
securityConfirmed = securityPassed;
|
|
583
|
+
if (!securityPassed) {
|
|
584
|
+
securityConfirmed = await promptSecurityDecision(securityResults);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
logger.dim(' Security scans skipped.');
|
|
751
588
|
}
|
|
752
589
|
|
|
753
590
|
// Step 8: Check for feature docs
|
|
@@ -773,8 +610,6 @@ async function main(): Promise<void> {
|
|
|
773
610
|
if (report.errorCount > 0 || !eslintPassed || !securityConfirmed) {
|
|
774
611
|
logger.error('โ Commit BLOCKED โ fix the errors above and try again.');
|
|
775
612
|
logger.dim(' ๐ก To bypass: BYPASS_RULES=true git commit ... or add #bypass-rules to commit message');
|
|
776
|
-
|
|
777
|
-
// Exit immediately with error code
|
|
778
613
|
process.exitCode = 1;
|
|
779
614
|
process.exit(1);
|
|
780
615
|
}
|