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.
- package/package.json +8 -2
- package/src/commands/scans/dismissed.js +26 -0
- package/src/commands/scans/formatters/csv.js +24 -0
- package/src/commands/scans/formatters/index.js +12 -0
- package/src/commands/scans/formatters/json.js +8 -0
- package/src/commands/scans/formatters/md.js +63 -0
- package/src/commands/scans/formatters/sarif.js +105 -0
- package/src/commands/scans/formatters/table.js +83 -0
- package/src/commands/scans/get.js +64 -0
- package/src/commands/scans/history.js +43 -0
- package/src/commands/scans/index.js +146 -0
- package/src/commands/scans/lib/categories.js +51 -0
- package/src/commands/scans/lib/dismissMatch.js +32 -0
- package/src/commands/scans/lib/emit.js +25 -0
- package/src/commands/scans/lib/filters.js +72 -0
- package/src/commands/scans/lib/log.js +25 -0
- package/src/commands/scans/lib/normalize.js +155 -0
- package/src/commands/scans/lib/paginate.js +22 -0
- package/src/commands/scans/lib/resolveScan.js +50 -0
- package/src/commands/scans/lib/sort.js +19 -0
- package/src/commands/scans/orgs.js +15 -0
- package/src/commands/scans/repos.js +41 -0
- package/src/commands/scans/results.js +156 -0
- package/src/commands/scans/start-scan.js +90 -0
- package/src/components/ScanCenter.js +337 -0
- package/src/components/SelectList.js +80 -0
- package/src/index.js +12 -0
- package/src/scanCenter/handleSelectConnection.js +14 -0
- package/src/scanCenter/handleSelectRepo.js +19 -0
- package/src/scanCenter/handleSelectResultType.js +34 -0
- package/src/scanCenter/handleSelectScan.js +4 -0
- package/src/scanCenter/validateConnectionOnMount.js +15 -0
- package/src/scans/connectionHandler.js +52 -0
- package/src/scans/fetchAdvancedScanResults.js +325 -0
- package/src/scans/fetchDismissedAlerts.js +57 -0
- package/src/scans/fetchScanResults.js +115 -0
- package/src/scans/getScanHistory.js +38 -0
- package/src/scans/listRepos.js +38 -0
- 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.
|
|
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,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
|
+
}
|