codeant-cli 0.4.6 → 0.4.8

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.
Files changed (39) hide show
  1. package/package.json +8 -2
  2. package/src/commands/scans/dismissed.js +26 -0
  3. package/src/commands/scans/formatters/csv.js +24 -0
  4. package/src/commands/scans/formatters/index.js +12 -0
  5. package/src/commands/scans/formatters/json.js +8 -0
  6. package/src/commands/scans/formatters/md.js +63 -0
  7. package/src/commands/scans/formatters/sarif.js +105 -0
  8. package/src/commands/scans/formatters/table.js +83 -0
  9. package/src/commands/scans/get.js +64 -0
  10. package/src/commands/scans/history.js +43 -0
  11. package/src/commands/scans/index.js +146 -0
  12. package/src/commands/scans/lib/categories.js +51 -0
  13. package/src/commands/scans/lib/dismissMatch.js +32 -0
  14. package/src/commands/scans/lib/emit.js +25 -0
  15. package/src/commands/scans/lib/filters.js +72 -0
  16. package/src/commands/scans/lib/log.js +25 -0
  17. package/src/commands/scans/lib/normalize.js +155 -0
  18. package/src/commands/scans/lib/paginate.js +22 -0
  19. package/src/commands/scans/lib/resolveScan.js +50 -0
  20. package/src/commands/scans/lib/sort.js +19 -0
  21. package/src/commands/scans/orgs.js +15 -0
  22. package/src/commands/scans/repos.js +41 -0
  23. package/src/commands/scans/results.js +156 -0
  24. package/src/commands/scans/start-scan.js +90 -0
  25. package/src/components/ScanCenter.js +337 -0
  26. package/src/components/SelectList.js +80 -0
  27. package/src/index.js +12 -0
  28. package/src/scanCenter/handleSelectConnection.js +14 -0
  29. package/src/scanCenter/handleSelectRepo.js +19 -0
  30. package/src/scanCenter/handleSelectResultType.js +34 -0
  31. package/src/scanCenter/handleSelectScan.js +4 -0
  32. package/src/scanCenter/validateConnectionOnMount.js +15 -0
  33. package/src/scans/connectionHandler.js +52 -0
  34. package/src/scans/fetchAdvancedScanResults.js +325 -0
  35. package/src/scans/fetchDismissedAlerts.js +57 -0
  36. package/src/scans/fetchScanResults.js +115 -0
  37. package/src/scans/getScanHistory.js +38 -0
  38. package/src/scans/listRepos.js +38 -0
  39. package/src/scans/startScan.js +22 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,13 @@
27
27
  ".": "./src/reviewHeadless.js",
28
28
  "./review": "./src/reviewHeadless.js",
29
29
  "./push-protection": "./src/utils/installPushProtectionHook.js",
30
- "./config": "./src/utils/config.js"
30
+ "./config": "./src/utils/config.js",
31
+ "./scans/connection": "./src/scans/connectionHandler.js",
32
+ "./scans/list-repos": "./src/scans/listRepos.js",
33
+ "./scans/scan-history": "./src/scans/getScanHistory.js",
34
+ "./scans/fetch-results": "./src/scans/fetchScanResults.js",
35
+ "./scans/fetch-advanced-results": "./src/scans/fetchAdvancedScanResults.js",
36
+ "./scans/dismissed-alerts": "./src/scans/fetchDismissedAlerts.js"
31
37
  },
32
38
  "files": [
33
39
  "src"
@@ -0,0 +1,26 @@
1
+ import { fetchDismissedAlerts } from '../../scans/fetchDismissedAlerts.js';
2
+
3
+ /**
4
+ * codeant scans dismissed --repo <repo> [--analysis-type security|secrets]
5
+ */
6
+ export async function runDismissed({ repo, analysisType = 'security' } = {}) {
7
+ if (!repo) {
8
+ const err = new Error('--repo is required');
9
+ err.exitCode = 1;
10
+ throw err;
11
+ }
12
+
13
+ const result = await fetchDismissedAlerts(repo, analysisType);
14
+ if (!result.success) {
15
+ const err = new Error(result.error || 'Failed to fetch dismissed alerts');
16
+ err.exitCode = 1;
17
+ throw err;
18
+ }
19
+
20
+ return {
21
+ repo,
22
+ analysis_type: analysisType,
23
+ total: result.dismissedAlerts.length,
24
+ dismissed_alerts: result.dismissedAlerts,
25
+ };
26
+ }
@@ -0,0 +1,24 @@
1
+ const HEADERS = ['id', 'category', 'severity', 'file_path', 'line_number', 'check_id', 'check_name', 'message', 'cwe', 'cve', 'dismissed'];
2
+
3
+ function csvCell(val) {
4
+ if (val === null || val === undefined) return '';
5
+ const s = String(val);
6
+ if (s.includes(',') || s.includes('"') || s.includes('\n')) {
7
+ return '"' + s.replace(/"/g, '""') + '"';
8
+ }
9
+ return s;
10
+ }
11
+
12
+ export default {
13
+ name: 'csv',
14
+ mime: 'text/csv',
15
+ extension: '.csv',
16
+ render(envelope) {
17
+ const { findings = [] } = envelope;
18
+ const rows = [HEADERS.join(',')];
19
+ for (const f of findings) {
20
+ rows.push(HEADERS.map((h) => csvCell(f[h])).join(','));
21
+ }
22
+ return rows.join('\n');
23
+ },
24
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Formatter registry.
3
+ * Contract: { name, mime, extension, render(envelope) → string }
4
+ * Add a new format = drop a file in formatters/ + one line here.
5
+ */
6
+ import json from './json.js';
7
+ import sarif from './sarif.js';
8
+ import csv from './csv.js';
9
+ import md from './md.js';
10
+ import table from './table.js';
11
+
12
+ export const FORMATTERS = { json, sarif, csv, md, table };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ name: 'json',
3
+ mime: 'application/json',
4
+ extension: '.json',
5
+ render(envelope) {
6
+ return JSON.stringify(envelope, null, 2);
7
+ },
8
+ };
@@ -0,0 +1,63 @@
1
+ const SEV_EMOJI = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵', info: '⚪', unknown: '⚫' };
2
+
3
+ export default {
4
+ name: 'md',
5
+ mime: 'text/markdown',
6
+ extension: '.md',
7
+ render(envelope) {
8
+ const { findings = [], repo, scan, summary, generated_at, errors = [] } = envelope;
9
+ const lines = [];
10
+
11
+ lines.push(`# CodeAnt Scan Results`);
12
+ lines.push('');
13
+ lines.push(`**Repo:** \`${repo}\``);
14
+ if (scan) {
15
+ lines.push(`**Branch:** \`${scan.branch || 'unknown'}\` `);
16
+ lines.push(`**Commit:** \`${scan.commit_id || 'unknown'}\` `);
17
+ }
18
+ lines.push(`**Generated:** ${generated_at}`);
19
+ lines.push('');
20
+
21
+ // Summary
22
+ lines.push('## Summary');
23
+ lines.push('');
24
+ lines.push(`| Severity | Count |`);
25
+ lines.push(`|----------|-------|`);
26
+ for (const [sev, count] of Object.entries(summary.by_severity || {})) {
27
+ if (count > 0) {
28
+ lines.push(`| ${SEV_EMOJI[sev] || ''} ${sev} | ${count} |`);
29
+ }
30
+ }
31
+ lines.push('');
32
+
33
+ if (errors.length > 0) {
34
+ lines.push('## Errors');
35
+ lines.push('');
36
+ for (const e of errors) {
37
+ lines.push(`- **${e.category}**: ${e.error}`);
38
+ }
39
+ lines.push('');
40
+ }
41
+
42
+ if (findings.length === 0) {
43
+ lines.push('*No findings.*');
44
+ return lines.join('\n');
45
+ }
46
+
47
+ lines.push('## Findings');
48
+ lines.push('');
49
+ lines.push('| Severity | Category | File | Line | Check | Message |');
50
+ lines.push('|----------|----------|------|------|-------|---------|');
51
+
52
+ for (const f of findings) {
53
+ const sev = `${SEV_EMOJI[f.severity] || ''} ${f.severity}`;
54
+ const file = f.file_path;
55
+ const line = f.line_number;
56
+ const check = f.check_id || f.check_name || '';
57
+ const msg = (f.message || '').replace(/\|/g, '\\|').slice(0, 120);
58
+ lines.push(`| ${sev} | ${f.category} | \`${file}\` | ${line} | \`${check}\` | ${msg} |`);
59
+ }
60
+
61
+ return lines.join('\n');
62
+ },
63
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * SARIF 2.1.0 formatter.
3
+ * Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
4
+ */
5
+
6
+ const SEV_TO_SARIF = {
7
+ critical: 'error',
8
+ high: 'error',
9
+ medium: 'warning',
10
+ low: 'note',
11
+ info: 'none',
12
+ unknown: 'none',
13
+ };
14
+
15
+ export default {
16
+ name: 'sarif',
17
+ mime: 'application/sarif+json',
18
+ extension: '.sarif',
19
+ render(envelope) {
20
+ const { findings = [], repo, scan, tool_version } = envelope;
21
+
22
+ // Build per-category rule sets
23
+ const ruleMap = new Map();
24
+ for (const f of findings) {
25
+ const key = `${f.category}/${f.check_id || f.check_name}`;
26
+ if (!ruleMap.has(key)) {
27
+ ruleMap.set(key, {
28
+ id: key,
29
+ name: f.check_name || f.check_id || 'issue',
30
+ shortDescription: { text: f.check_name || f.check_id || 'issue' },
31
+ properties: { category: f.category, severity: f.severity },
32
+ });
33
+ }
34
+ }
35
+
36
+ const rules = [...ruleMap.values()];
37
+
38
+ const results = findings.map((f) => {
39
+ const ruleId = `${f.category}/${f.check_id || f.check_name}`;
40
+ const result = {
41
+ ruleId,
42
+ level: SEV_TO_SARIF[f.severity] ?? 'warning',
43
+ message: { text: f.message || f.check_name || 'issue' },
44
+ locations: [
45
+ {
46
+ physicalLocation: {
47
+ artifactLocation: { uri: f.file_path, uriBaseId: '%SRCROOT%' },
48
+ region: {
49
+ startLine: f.line_number || 1,
50
+ endLine: (f.line_range && f.line_range.length > 1)
51
+ ? f.line_range[f.line_range.length - 1]
52
+ : f.line_number || 1,
53
+ },
54
+ },
55
+ },
56
+ ],
57
+ properties: {
58
+ findingId: f.id,
59
+ category: f.category,
60
+ severity: f.severity,
61
+ dismissed: f.dismissed,
62
+ },
63
+ };
64
+
65
+ if (f.cwe) result.taxa = [{ toolComponent: { name: 'CWE' }, id: f.cwe }];
66
+ if (f.package) result.properties.package = f.package;
67
+
68
+ return result;
69
+ });
70
+
71
+ const sarif = {
72
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
73
+ version: '2.1.0',
74
+ runs: [
75
+ {
76
+ tool: {
77
+ driver: {
78
+ name: 'codeant-cli',
79
+ version: tool_version || '0.0.0',
80
+ informationUri: 'https://www.codeant.ai',
81
+ rules,
82
+ },
83
+ },
84
+ versionControlProvenance: scan
85
+ ? [
86
+ {
87
+ repositoryUri: repo,
88
+ revisionId: scan.commit_id,
89
+ branch: scan.branch || undefined,
90
+ },
91
+ ]
92
+ : undefined,
93
+ results,
94
+ properties: {
95
+ repo,
96
+ generatedAt: envelope.generated_at,
97
+ schemaVersion: envelope.schema_version,
98
+ },
99
+ },
100
+ ],
101
+ };
102
+
103
+ return JSON.stringify(sarif, null, 2);
104
+ },
105
+ };
@@ -0,0 +1,83 @@
1
+ // Terminal ASCII table — no external dependencies
2
+
3
+ let noColor = false;
4
+ export function setNoColor(v) { noColor = v; }
5
+
6
+ const SEV_COLOR = {
7
+ critical: '\x1b[35m', // magenta
8
+ high: '\x1b[31m', // red
9
+ medium: '\x1b[33m', // yellow
10
+ low: '\x1b[34m', // blue
11
+ info: '\x1b[37m', // white
12
+ unknown: '\x1b[90m', // dark gray
13
+ };
14
+ const RESET = '\x1b[0m';
15
+
16
+ function color(text, code) {
17
+ if (noColor || !process.stdout.isTTY) return text;
18
+ return `${code}${text}${RESET}`;
19
+ }
20
+
21
+ function pad(str, len) {
22
+ const s = String(str ?? '');
23
+ return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
24
+ }
25
+
26
+ const COLS = [
27
+ { key: 'severity', label: 'SEVERITY', width: 10 },
28
+ { key: 'category', label: 'CATEGORY', width: 17 },
29
+ { key: 'file_path', label: 'FILE', width: 40 },
30
+ { key: 'line_number', label: 'LINE', width: 6 },
31
+ { key: 'check_id', label: 'CHECK', width: 20 },
32
+ { key: 'message', label: 'MESSAGE', width: 60 },
33
+ ];
34
+
35
+ export default {
36
+ name: 'table',
37
+ mime: 'text/plain',
38
+ extension: '.txt',
39
+ render(envelope) {
40
+ const { findings = [], summary, errors = [] } = envelope;
41
+ const lines = [];
42
+
43
+ // Header
44
+ const sep = COLS.map((c) => '-'.repeat(c.width)).join('-+-');
45
+ const header = COLS.map((c) => pad(c.label, c.width)).join(' | ');
46
+ lines.push(sep);
47
+ lines.push(header);
48
+ lines.push(sep);
49
+
50
+ for (const f of findings) {
51
+ const sev = f.severity;
52
+ const cols = COLS.map((c) => {
53
+ let val = String(f[c.key] ?? '');
54
+ if (c.key === 'file_path' && val.length > c.width) {
55
+ val = '…' + val.slice(-(c.width - 1));
56
+ }
57
+ val = pad(val, c.width);
58
+ if (c.key === 'severity') {
59
+ val = color(val, SEV_COLOR[sev] ?? '');
60
+ }
61
+ return val;
62
+ });
63
+ lines.push(cols.join(' | '));
64
+ }
65
+
66
+ lines.push(sep);
67
+ lines.push(`Total: ${summary?.total ?? findings.length} ` +
68
+ Object.entries(summary?.by_severity ?? {})
69
+ .filter(([, n]) => n > 0)
70
+ .map(([s, n]) => color(`${s}:${n}`, SEV_COLOR[s] ?? ''))
71
+ .join(' ')
72
+ );
73
+
74
+ if (errors.length > 0) {
75
+ lines.push('');
76
+ for (const e of errors) {
77
+ lines.push(color(`[error] ${e.category}: ${e.error}`, '\x1b[31m'));
78
+ }
79
+ }
80
+
81
+ return lines.join('\n');
82
+ },
83
+ };
@@ -0,0 +1,64 @@
1
+ import { resolveScan } from './lib/resolveScan.js';
2
+ import { parseTypes } from './lib/categories.js';
3
+ import { normalizeIssue, normalizeSeverity } from './lib/normalize.js';
4
+ import { progress } from './lib/log.js';
5
+
6
+ const SEV_KEYS = ['critical', 'high', 'medium', 'low', 'info', 'unknown'];
7
+
8
+ /**
9
+ * codeant scans get --repo <repo> [--scan <sha>] [--branch <name>] [--types <list>]
10
+ * Returns metadata + summary (severity/category counts). No findings array.
11
+ */
12
+ export async function runGet({ repo, scan, branch, types } = {}) {
13
+ if (!repo) {
14
+ const err = new Error('--repo is required');
15
+ err.exitCode = 1;
16
+ throw err;
17
+ }
18
+
19
+ progress(`resolving scan for ${repo}…`);
20
+ const scanMeta = await resolveScan({ repo, scan, branch });
21
+
22
+ const categories = parseTypes(types);
23
+ progress(`fetching ${categories.map((c) => c.key).join(', ')}…`);
24
+
25
+ const settled = await Promise.allSettled(
26
+ categories.map((c) => c.fetcher(repo, scanMeta.commit_id))
27
+ );
28
+
29
+ const bySeverity = Object.fromEntries(SEV_KEYS.map((k) => [k, 0]));
30
+ const byCategory = {};
31
+ const errors = [];
32
+ let total = 0;
33
+
34
+ for (let i = 0; i < settled.length; i++) {
35
+ const cat = categories[i];
36
+ const s = settled[i];
37
+
38
+ if (s.status === 'rejected' || !s.value?.success) {
39
+ const msg = s.status === 'rejected' ? s.reason?.message : s.value?.error;
40
+ errors.push({ category: cat.key, error: msg || 'unknown error' });
41
+ byCategory[cat.key] = 0;
42
+ continue;
43
+ }
44
+
45
+ const issues = s.value.issues || [];
46
+ byCategory[cat.key] = issues.length;
47
+ total += issues.length;
48
+
49
+ for (const issue of issues) {
50
+ const normalized = normalizeIssue(issue, cat.key);
51
+ if (normalized) {
52
+ bySeverity[normalized.severity] = (bySeverity[normalized.severity] ?? 0) + 1;
53
+ }
54
+ }
55
+ }
56
+
57
+ return {
58
+ repo,
59
+ scan: scanMeta,
60
+ categories: categories.map((c) => c.key),
61
+ summary: { total, by_severity: bySeverity, by_category: byCategory },
62
+ errors,
63
+ };
64
+ }
@@ -0,0 +1,43 @@
1
+ import { getScanHistory } from '../../scans/getScanHistory.js';
2
+
3
+ /**
4
+ * codeant scans history --repo <repo> [--branch <name>] [--since <iso>] [--limit <n>]
5
+ */
6
+ export async function runHistory({ repo, branch, since, limit = 20 } = {}) {
7
+ if (!repo) {
8
+ const err = new Error('--repo is required');
9
+ err.exitCode = 1;
10
+ throw err;
11
+ }
12
+
13
+ const result = await getScanHistory(repo);
14
+ if (!result.success) {
15
+ const err = new Error(result.error || 'Failed to fetch scan history');
16
+ err.exitCode = 1;
17
+ throw err;
18
+ }
19
+
20
+ let history = [...(result.scanHistory || [])].sort(
21
+ (a, b) => new Date(b.timestamp) - new Date(a.timestamp)
22
+ );
23
+
24
+ if (branch) {
25
+ history = history.filter((s) => s.branch === branch);
26
+ }
27
+
28
+ if (since) {
29
+ const sinceDate = new Date(since);
30
+ if (isNaN(sinceDate)) {
31
+ const err = new Error(`Invalid --since date: ${since}`);
32
+ err.exitCode = 1;
33
+ throw err;
34
+ }
35
+ history = history.filter((s) => new Date(s.timestamp) >= sinceDate);
36
+ }
37
+
38
+ if (limit > 0) {
39
+ history = history.slice(0, limit);
40
+ }
41
+
42
+ return { repo, branch: branch || null, total: history.length, scans: history };
43
+ }
@@ -0,0 +1,146 @@
1
+ import { runOrgs } from './orgs.js';
2
+ import { runRepos } from './repos.js';
3
+ import { runHistory } from './history.js';
4
+ import { runGet } from './get.js';
5
+ import { runResults } from './results.js';
6
+ import { runDismissed } from './dismissed.js';
7
+ import { runStartScan } from './start-scan.js';
8
+ import { setQuiet, setNoColor } from './lib/log.js';
9
+ import { setNoColor as tableSetNoColor } from './formatters/table.js';
10
+
11
+ /**
12
+ * Register all `codeant scans <verb>` subcommands.
13
+ *
14
+ * @param {import('commander').Command} program
15
+ * @param {{ runCmd: Function }} helpers
16
+ */
17
+ export default function registerScansCommands(program, { runCmd }) {
18
+ const scans = program.command('scans').description('Fetch and explore scan results');
19
+
20
+ // ── orgs ───────────────────────────────────────────────────────────────────
21
+ scans
22
+ .command('orgs')
23
+ .description('List authenticated organizations')
24
+ .action(() => runCmd(() => runOrgs()));
25
+
26
+ // ── repos ──────────────────────────────────────────────────────────────────
27
+ scans
28
+ .command('repos')
29
+ .description('List repositories')
30
+ .option('--org <org>', 'Organization name (auto-picked when only one is authenticated)')
31
+ .action((opts) => runCmd(() => runRepos({ org: opts.org })));
32
+
33
+ // ── history ────────────────────────────────────────────────────────────────
34
+ scans
35
+ .command('history')
36
+ .description('Show scan history for a repository')
37
+ .requiredOption('--repo <repo>', 'Repository (owner/repo)')
38
+ .option('--branch <name>', 'Filter by branch name')
39
+ .option('--since <iso>', 'Show scans since ISO date')
40
+ .option('--limit <n>', 'Max results (default: 20)', parseInt, 20)
41
+ .action((opts) =>
42
+ runCmd(() => runHistory({ repo: opts.repo, branch: opts.branch, since: opts.since, limit: opts.limit }))
43
+ );
44
+
45
+ // ── get ────────────────────────────────────────────────────────────────────
46
+ scans
47
+ .command('get')
48
+ .description('Scan metadata + severity/category summary (no findings)')
49
+ .requiredOption('--repo <repo>', 'Repository (owner/repo)')
50
+ .option('--scan <sha>', 'Specific commit SHA to use')
51
+ .option('--branch <name>', 'Resolve latest scan on this branch')
52
+ .option('--types <list>', 'Comma-separated scan types (default: all)', 'all')
53
+ .option('--quiet', 'Suppress progress output')
54
+ .action((opts) => {
55
+ setQuiet(opts.quiet);
56
+ runCmd(() => runGet({ repo: opts.repo, scan: opts.scan, branch: opts.branch, types: opts.types }));
57
+ });
58
+
59
+ // ── results ────────────────────────────────────────────────────────────────
60
+ scans
61
+ .command('results')
62
+ .description('Fetch full scan findings')
63
+ .requiredOption('--repo <repo>', 'Repository (owner/repo)')
64
+ .option('--scan <sha>', 'Specific commit SHA to use')
65
+ .option('--branch <name>', 'Resolve latest scan on this branch')
66
+ .option(
67
+ '--types <list>',
68
+ 'Comma-separated types: sast,sca,secrets,iac,dead_code,sbom,anti_patterns,docstring,complex_functions,all',
69
+ 'all'
70
+ )
71
+ .option('--severity <list>', 'Filter by severity (e.g. critical,high)')
72
+ .option('--path <glob>', 'Filter by file path glob')
73
+ .option('--check <regex>', 'Filter by check ID or name (regex)')
74
+ .option('--include-dismissed', 'Include dismissed findings (excluded by default)')
75
+ .option('--format <fmt>', 'Output format: json|sarif|csv|md|table (default: json)', 'json')
76
+ .option('--output <path>', 'Write output to file instead of stdout')
77
+ .option('--fields <list>', 'Project findings to subset of fields (comma-separated)')
78
+ .option('--limit <n>', 'Max findings per page (default: 100)', parseInt, 100)
79
+ .option('--offset <n>', 'Pagination offset (default: 0)', parseInt, 0)
80
+ .option('--fail-fast', 'Exit 3 on first category fetch failure')
81
+ .option('--no-color', 'Disable ANSI color (auto-disabled when not a TTY)')
82
+ .option('--quiet', 'Suppress progress output on stderr')
83
+ .action(async (opts) => {
84
+ setQuiet(opts.quiet);
85
+ if (opts.noColor) {
86
+ setNoColor(true);
87
+ tableSetNoColor(true);
88
+ }
89
+
90
+ try {
91
+ await runResults({
92
+ repo: opts.repo,
93
+ scan: opts.scan,
94
+ branch: opts.branch,
95
+ types: opts.types,
96
+ severity: opts.severity,
97
+ path: opts.path,
98
+ check: opts.check,
99
+ includeDismissed: opts.includeDismissed || false,
100
+ format: opts.format,
101
+ output: opts.output,
102
+ fields: opts.fields,
103
+ limit: opts.limit,
104
+ offset: opts.offset,
105
+ failFast: opts.failFast || false,
106
+ });
107
+ } catch (err) {
108
+ process.stderr.write(JSON.stringify({ error: err.message }) + '\n');
109
+ process.exit(err.exitCode ?? 1);
110
+ }
111
+ });
112
+
113
+ // ── dismissed ──────────────────────────────────────────────────────────────
114
+ scans
115
+ .command('dismissed')
116
+ .description('List dismissed alerts for a repository')
117
+ .requiredOption('--repo <repo>', 'Repository (owner/repo)')
118
+ .option('--analysis-type <type>', 'Analysis type: security|secrets (default: security)', 'security')
119
+ .action((opts) =>
120
+ runCmd(() => runDismissed({ repo: opts.repo, analysisType: opts.analysisType }))
121
+ );
122
+
123
+ // ── start-scan ─────────────────────────────────────────────────────────────
124
+ scans
125
+ .command('start-scan')
126
+ .description('Trigger a new analysis run for a repository')
127
+ .option('--repo <repo>', 'Repository (owner/repo, auto-detected from git remote)')
128
+ .option('--branch <name>', 'Branch to scan (auto-detected from current checkout)')
129
+ .option('--commit <sha>', 'Commit SHA to scan (resolved from remote if omitted)')
130
+ .option('--include <paths>', 'Comma-separated file path glob patterns to include')
131
+ .option('--exclude <paths>', 'Comma-separated file path glob patterns to exclude')
132
+ .action(async (opts) => {
133
+ try {
134
+ await runStartScan({
135
+ repo: opts.repo,
136
+ branch: opts.branch,
137
+ commit: opts.commit,
138
+ include: opts.include,
139
+ exclude: opts.exclude,
140
+ });
141
+ } catch (err) {
142
+ process.stderr.write(JSON.stringify({ error: err.message }) + '\n');
143
+ process.exit(err.exitCode ?? 1);
144
+ }
145
+ });
146
+ }
@@ -0,0 +1,51 @@
1
+ import {
2
+ fetchSastResults,
3
+ fetchAntiPatternsResults,
4
+ fetchDocstringResults,
5
+ fetchComplexFunctionsResults,
6
+ } from '../../../scans/fetchScanResults.js';
7
+ import {
8
+ fetchScaResults,
9
+ fetchSbomResults,
10
+ fetchSecretsResults,
11
+ fetchIacResults,
12
+ fetchDeadCodeResults,
13
+ } from '../../../scans/fetchAdvancedScanResults.js';
14
+
15
+ /**
16
+ * Category registry. Add one row to support a new scan type throughout the CLI.
17
+ * kind: 'code' | 'package' | 'inventory' | 'secret' | 'config'
18
+ */
19
+ export const CATEGORIES = {
20
+ sast: { fetcher: fetchSastResults, kind: 'code' },
21
+ anti_patterns: { fetcher: fetchAntiPatternsResults, kind: 'code' },
22
+ docstring: { fetcher: fetchDocstringResults, kind: 'code' },
23
+ complex_functions: { fetcher: fetchComplexFunctionsResults, kind: 'code' },
24
+ sca: { fetcher: fetchScaResults, kind: 'package' },
25
+ sbom: { fetcher: fetchSbomResults, kind: 'inventory' },
26
+ secrets: { fetcher: fetchSecretsResults, kind: 'secret' },
27
+ iac: { fetcher: fetchIacResults, kind: 'config' },
28
+ dead_code: { fetcher: fetchDeadCodeResults, kind: 'code' },
29
+ };
30
+
31
+ /**
32
+ * Parse comma-separated --types value. 'all' expands to every key.
33
+ * Returns array of { key, fetcher, kind }.
34
+ * Throws with exit code 1 on unknown type name.
35
+ */
36
+ export function parseTypes(typesStr) {
37
+ const keys = Object.keys(CATEGORIES);
38
+ const raw = typesStr ? typesStr.split(',').map((s) => s.trim()).filter(Boolean) : ['all'];
39
+
40
+ const expanded = raw.includes('all') ? keys : raw;
41
+
42
+ const unknown = expanded.filter((k) => !CATEGORIES[k]);
43
+ if (unknown.length > 0) {
44
+ const err = new Error(`Unknown type(s): ${unknown.join(', ')}. Valid: ${keys.join(', ')}`);
45
+ err.exitCode = 1;
46
+ err.detail = { error: `Unknown type(s): ${unknown.join(', ')}`, valid: keys };
47
+ throw err;
48
+ }
49
+
50
+ return expanded.map((k) => ({ key: k, ...CATEGORIES[k] }));
51
+ }