codeslick-cli 1.5.2 → 1.5.4
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/bin/codeslick.cjs +4 -185
- package/build.mjs +70 -0
- package/dist/codeslick-bundle.cjs +52342 -0
- package/package.json +10 -11
- package/src/cli-entry.ts +178 -0
- package/src/commands/scan.ts +58 -2
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeslick-cli",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "CodeSlick CLI tool for pre-commit security scanning
|
|
3
|
+
"version": "1.5.4",
|
|
4
|
+
"description": "CodeSlick CLI tool for pre-commit security scanning — 308 checks across JS, TS, Python, Java, Go",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"codeslick": "./bin/codeslick.cjs",
|
|
8
8
|
"cs": "./bin/codeslick.cjs"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "
|
|
11
|
+
"build": "node build.mjs",
|
|
12
|
+
"build:typecheck": "tsc --noEmit",
|
|
12
13
|
"dev": "tsc --watch",
|
|
13
14
|
"test": "vitest",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
@@ -20,27 +21,24 @@
|
|
|
20
21
|
"pre-commit",
|
|
21
22
|
"git-hook",
|
|
22
23
|
"vulnerability-scanner",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"sast",
|
|
25
|
+
"owasp",
|
|
26
|
+
"devsecops"
|
|
26
27
|
],
|
|
27
28
|
"author": "CodeSlick <support@codeslick.dev>",
|
|
28
29
|
"license": "MIT",
|
|
29
30
|
"repository": {
|
|
30
31
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/VitorLourenco/
|
|
32
|
-
"directory": "packages/cli"
|
|
32
|
+
"url": "https://github.com/VitorLourenco/codeslick-cli.git"
|
|
33
33
|
},
|
|
34
34
|
"bugs": {
|
|
35
|
-
"url": "https://github.com/VitorLourenco/
|
|
35
|
+
"url": "https://github.com/VitorLourenco/codeslick-cli/issues"
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://codeslick.dev",
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18.0.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"acorn": "^8.16.0",
|
|
43
|
-
"acorn-walk": "^8.3.5",
|
|
44
42
|
"chalk": "^4.1.2",
|
|
45
43
|
"cli-table3": "^0.6.3",
|
|
46
44
|
"glob": "^13.0.1",
|
|
@@ -51,6 +49,7 @@
|
|
|
51
49
|
"devDependencies": {
|
|
52
50
|
"@types/node": "^20.10.0",
|
|
53
51
|
"@types/yargs": "^17.0.32",
|
|
52
|
+
"esbuild": "^0.24.2",
|
|
54
53
|
"vitest": "^1.0.4"
|
|
55
54
|
}
|
|
56
55
|
}
|
package/src/cli-entry.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CodeSlick CLI Entry Point
|
|
4
|
+
*
|
|
5
|
+
* This file is the esbuild bundle entry. It replaces bin/codeslick.cjs as the
|
|
6
|
+
* main logic container. The bin script is now a thin shim that calls this bundle.
|
|
7
|
+
*
|
|
8
|
+
* Bundled output: dist/codeslick-bundle.cjs
|
|
9
|
+
* All internal deps (@codeslick/*, acorn, analyzers) are bundled here.
|
|
10
|
+
* Runtime deps (typescript, glob, chalk, ora, yargs) stay external.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import yargs from 'yargs';
|
|
14
|
+
import { hideBin } from 'yargs/helpers';
|
|
15
|
+
import { scanCommand } from './commands/scan';
|
|
16
|
+
import { initCommand } from './commands/init';
|
|
17
|
+
import { configCommand } from './commands/config';
|
|
18
|
+
import { loginCommand, logoutCommand, whoamiCommand } from './commands/auth';
|
|
19
|
+
import { startBackgroundUpdateCheck } from './utils/version-check';
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
22
|
+
const { version } = require('../package.json') as { version: string };
|
|
23
|
+
|
|
24
|
+
// Start version check in background (non-blocking)
|
|
25
|
+
void startBackgroundUpdateCheck();
|
|
26
|
+
|
|
27
|
+
// Detect if running as 'cs' or 'codeslick'
|
|
28
|
+
const scriptName = process.argv[1]?.includes('/cs') ? 'cs' : 'codeslick';
|
|
29
|
+
|
|
30
|
+
// Main CLI application
|
|
31
|
+
yargs(hideBin(process.argv))
|
|
32
|
+
.scriptName(scriptName)
|
|
33
|
+
.usage('$0 <command> [options]')
|
|
34
|
+
.command(
|
|
35
|
+
'init',
|
|
36
|
+
'Initialize CodeSlick in your repository',
|
|
37
|
+
(yargs) => {
|
|
38
|
+
return yargs
|
|
39
|
+
.option('force', {
|
|
40
|
+
alias: 'f',
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
description: 'Force re-initialization (overwrite existing config)',
|
|
43
|
+
default: false,
|
|
44
|
+
})
|
|
45
|
+
.option('severity', {
|
|
46
|
+
alias: 's',
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Default severity threshold (critical|high|medium|low)',
|
|
49
|
+
default: 'critical',
|
|
50
|
+
choices: ['critical', 'high', 'medium', 'low'],
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
initCommand
|
|
54
|
+
)
|
|
55
|
+
.command(
|
|
56
|
+
'scan [files..]',
|
|
57
|
+
'Scan files for security vulnerabilities',
|
|
58
|
+
(yargs) => {
|
|
59
|
+
return yargs
|
|
60
|
+
.positional('files', {
|
|
61
|
+
type: 'string',
|
|
62
|
+
array: true,
|
|
63
|
+
description: 'Files or patterns to scan (default: staged files)',
|
|
64
|
+
})
|
|
65
|
+
.option('staged', {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
description: 'Scan only staged files (git) - this is the default',
|
|
68
|
+
default: true,
|
|
69
|
+
})
|
|
70
|
+
.option('all', {
|
|
71
|
+
alias: 'a',
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
description: 'Scan all files in repository (overrides --staged)',
|
|
74
|
+
default: false,
|
|
75
|
+
})
|
|
76
|
+
.option('quick', {
|
|
77
|
+
alias: 'q',
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
description: 'Quick scan - skip deep TypeScript type checking for speed',
|
|
80
|
+
default: false,
|
|
81
|
+
})
|
|
82
|
+
.option('verbose', {
|
|
83
|
+
alias: 'v',
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
description: 'Show detailed results for all files (default: top 10 only)',
|
|
86
|
+
default: false,
|
|
87
|
+
})
|
|
88
|
+
.option('severity', {
|
|
89
|
+
alias: 's',
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: 'Severity threshold (critical|high|medium|low)',
|
|
92
|
+
choices: ['critical', 'high', 'medium', 'low'],
|
|
93
|
+
})
|
|
94
|
+
.option('fix', {
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
description: 'Auto-apply fixes (where possible)',
|
|
97
|
+
default: false,
|
|
98
|
+
})
|
|
99
|
+
.option('json', {
|
|
100
|
+
type: 'boolean',
|
|
101
|
+
description: 'Output results as JSON',
|
|
102
|
+
default: false,
|
|
103
|
+
})
|
|
104
|
+
.option('verify', {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
description: 'Run tests after security scan (combined pass/fail)',
|
|
107
|
+
default: false,
|
|
108
|
+
})
|
|
109
|
+
.option('test-command', {
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: 'Custom test command (e.g., "npm test", "pytest")',
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
scanCommand
|
|
115
|
+
)
|
|
116
|
+
.command(
|
|
117
|
+
'config <action> [key] [value]',
|
|
118
|
+
'Manage CodeSlick configuration',
|
|
119
|
+
(yargs) => {
|
|
120
|
+
return yargs
|
|
121
|
+
.positional('action', {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Action to perform (get|set|list)',
|
|
124
|
+
choices: ['get', 'set', 'list'],
|
|
125
|
+
})
|
|
126
|
+
.positional('key', {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Configuration key',
|
|
129
|
+
})
|
|
130
|
+
.positional('value', {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Configuration value',
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
configCommand
|
|
136
|
+
)
|
|
137
|
+
.command(
|
|
138
|
+
'auth <action>',
|
|
139
|
+
'Manage CLI authentication',
|
|
140
|
+
(yargs) => {
|
|
141
|
+
return yargs
|
|
142
|
+
.positional('action', {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Action to perform (login|logout|whoami)',
|
|
145
|
+
choices: ['login', 'logout', 'whoami'],
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
async (argv) => {
|
|
149
|
+
switch (argv.action) {
|
|
150
|
+
case 'login':
|
|
151
|
+
await loginCommand();
|
|
152
|
+
break;
|
|
153
|
+
case 'logout':
|
|
154
|
+
await logoutCommand();
|
|
155
|
+
break;
|
|
156
|
+
case 'whoami':
|
|
157
|
+
await whoamiCommand();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
.example('$0 init', 'Initialize CodeSlick in your repository')
|
|
163
|
+
.example('$0 scan', 'Scan all staged files')
|
|
164
|
+
.example('$0 scan src/**/*.js', 'Scan specific files')
|
|
165
|
+
.example('$0 scan --staged --severity high', 'Scan staged files, block on HIGH+')
|
|
166
|
+
.example('$0 scan --verify', 'Scan files AND run tests (combined pass/fail)')
|
|
167
|
+
.example('$0 config set severity critical', 'Set severity threshold')
|
|
168
|
+
.example('$0 config list', 'List all configuration')
|
|
169
|
+
.example('$0 auth login', 'Authenticate CLI via browser')
|
|
170
|
+
.example('$0 auth whoami', 'Show current user and quota')
|
|
171
|
+
.demandCommand(1, 'You must provide a command')
|
|
172
|
+
.help()
|
|
173
|
+
.alias('help', 'h')
|
|
174
|
+
.version(version)
|
|
175
|
+
.alias('version', 'v')
|
|
176
|
+
.epilog('For more information, visit https://codeslick.dev/docs/cli')
|
|
177
|
+
.strict()
|
|
178
|
+
.parse();
|
package/src/commands/scan.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { exec } from 'child_process';
|
|
20
20
|
import { promisify } from 'util';
|
|
21
21
|
import { resolve } from 'path';
|
|
22
|
+
import * as fs from 'fs';
|
|
22
23
|
import { glob } from 'glob';
|
|
23
24
|
import ora from 'ora';
|
|
24
25
|
import chalk from 'chalk';
|
|
@@ -45,6 +46,12 @@ import {
|
|
|
45
46
|
printThresholdResult,
|
|
46
47
|
} from '../utils/threshold-handler';
|
|
47
48
|
import { DEFAULT_THRESHOLD_CONFIG, type ThresholdConfig } from '../../../../src/lib/security/threshold-evaluator';
|
|
49
|
+
import {
|
|
50
|
+
findConfigFile,
|
|
51
|
+
evaluatePolicy,
|
|
52
|
+
exceedsThreshold as exceedsPolicyThreshold,
|
|
53
|
+
type PolicyViolation,
|
|
54
|
+
} from '../../../../src/lib/policy/policy-engine';
|
|
48
55
|
|
|
49
56
|
const execAsync = promisify(exec);
|
|
50
57
|
|
|
@@ -235,6 +242,27 @@ export async function scanCommand(args: ScanArgs): Promise<void> {
|
|
|
235
242
|
spinner.succeed(`Analyzed ${results.length} files`);
|
|
236
243
|
}
|
|
237
244
|
|
|
245
|
+
// SR2-3: Policy Engine evaluation (.codeslick.yml)
|
|
246
|
+
const policyConfigPath = findConfigFile(process.cwd());
|
|
247
|
+
const allPolicyViolations: PolicyViolation[] = [];
|
|
248
|
+
let policyThresholds = null;
|
|
249
|
+
let policyUsingDefaults = true;
|
|
250
|
+
|
|
251
|
+
if (policyConfigPath) {
|
|
252
|
+
for (const fileResult of results) {
|
|
253
|
+
try {
|
|
254
|
+
const code = fs.readFileSync(fileResult.filePath, 'utf8');
|
|
255
|
+
const policyResult = evaluatePolicy(code, fileResult.filePath, policyConfigPath);
|
|
256
|
+
allPolicyViolations.push(...policyResult.violations);
|
|
257
|
+
// All files share one config — last assignment is identical to any other
|
|
258
|
+
policyThresholds = policyResult.thresholds;
|
|
259
|
+
policyUsingDefaults = policyResult.usingDefaults;
|
|
260
|
+
} catch {
|
|
261
|
+
// File unreadable — skip policy evaluation for this file
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
238
266
|
const duration = Date.now() - startTime;
|
|
239
267
|
|
|
240
268
|
// Track unsupported files (files that were in the glob but not scanned)
|
|
@@ -282,6 +310,24 @@ export async function scanCommand(args: ScanArgs): Promise<void> {
|
|
|
282
310
|
}
|
|
283
311
|
}
|
|
284
312
|
|
|
313
|
+
// SR2-3: Print policy violations block
|
|
314
|
+
if (!args.json && allPolicyViolations.length > 0) {
|
|
315
|
+
console.log('');
|
|
316
|
+
console.log(chalk.yellow.bold(' Policy Violations (.codeslick.yml)'));
|
|
317
|
+
console.log(chalk.gray(' ' + '─'.repeat(48)));
|
|
318
|
+
for (const v of allPolicyViolations) {
|
|
319
|
+
const dot =
|
|
320
|
+
v.severity === 'critical' ? chalk.red('●') :
|
|
321
|
+
v.severity === 'high' ? chalk.yellow('●') :
|
|
322
|
+
v.severity === 'medium' ? chalk.blue('●') :
|
|
323
|
+
chalk.gray('●');
|
|
324
|
+
const lineInfo = v.line != null ? chalk.gray(` [line ${v.line}]`) : '';
|
|
325
|
+
const ruleInfo = v.ruleId ? chalk.gray(` (${v.ruleId})`) : '';
|
|
326
|
+
console.log(` ${dot} ${chalk.white(v.message)}${lineInfo}${ruleInfo}`);
|
|
327
|
+
}
|
|
328
|
+
console.log('');
|
|
329
|
+
}
|
|
330
|
+
|
|
285
331
|
// Calculate totals for telemetry and display
|
|
286
332
|
const totalCritical = results.reduce((sum, r) => sum + r.critical, 0);
|
|
287
333
|
const totalHigh = results.reduce((sum, r) => sum + r.high, 0);
|
|
@@ -363,9 +409,19 @@ export async function scanCommand(args: ScanArgs): Promise<void> {
|
|
|
363
409
|
}
|
|
364
410
|
}
|
|
365
411
|
|
|
412
|
+
// SR2-3: Policy gate — only active when .codeslick.yml is present and valid
|
|
413
|
+
let policyBlocks = false;
|
|
414
|
+
if (!policyUsingDefaults && policyThresholds) {
|
|
415
|
+
const allFindingsFlat = results.flatMap(r => {
|
|
416
|
+
const vulns = (r.result as any)?.security?.vulnerabilities ?? [];
|
|
417
|
+
return (vulns as Array<{ severity: string }>).map(v => ({ severity: v.severity }));
|
|
418
|
+
});
|
|
419
|
+
policyBlocks = exceedsPolicyThreshold(allFindingsFlat, allPolicyViolations, policyThresholds, 'cli');
|
|
420
|
+
}
|
|
421
|
+
|
|
366
422
|
// Determine final exit code
|
|
367
|
-
// Both security scan and tests must pass
|
|
368
|
-
const finalSuccess = !shouldBlock && testsPassed;
|
|
423
|
+
// Both security scan and tests must pass, and policy must not block
|
|
424
|
+
const finalSuccess = !shouldBlock && !policyBlocks && testsPassed;
|
|
369
425
|
|
|
370
426
|
if (!finalSuccess) {
|
|
371
427
|
if (!args.json) {
|