Skip to content

Commit 18250c3

Browse files
committed
scans agent
1 parent f343909 commit 18250c3

23 files changed

Lines changed: 1216 additions & 0 deletions

‎src/commands/scans/dismissed.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 };
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};

‎src/commands/scans/get.js‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)