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/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "codeslick-cli",
3
- "version": "1.5.2",
4
- "description": "CodeSlick CLI tool for pre-commit security scanning with Terraform IaC support",
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": "tsc",
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
- "terraform",
24
- "iac",
25
- "infrastructure-as-code"
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/codeslick2",
32
- "directory": "packages/cli"
32
+ "url": "https://github.com/VitorLourenco/codeslick-cli.git"
33
33
  },
34
34
  "bugs": {
35
- "url": "https://github.com/VitorLourenco/codeslick2/issues"
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
  }
@@ -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();
@@ -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) {