codeant-cli 0.4.6 → 0.4.7
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 +121 -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/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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { fetchApi } from '../utils/fetchApi.js';
|
|
2
|
+
|
|
3
|
+
export const ADVANCED_RESULT_TYPES = {
|
|
4
|
+
SCA: 'sca',
|
|
5
|
+
SBOM: 'sbom',
|
|
6
|
+
SECRETS: 'secrets',
|
|
7
|
+
IAC: 'iac',
|
|
8
|
+
DEAD_CODE: 'dead_code',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const EXTRA_DEAD_CODE_MESSAGES = {
|
|
12
|
+
S1481: 'Unused local variable',
|
|
13
|
+
S1854: 'Unused assignment',
|
|
14
|
+
S1172: 'Unused function parameter',
|
|
15
|
+
S1144: 'Unused private method',
|
|
16
|
+
S1763: 'Unreachable code',
|
|
17
|
+
S5603: 'Unused scope-limited definition',
|
|
18
|
+
S3985: 'Unused private nested class',
|
|
19
|
+
S1128: 'Unnecessary import',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Strip /mnt/lambda/code/.../commitId/ prefix from a full Lambda path.
|
|
24
|
+
*/
|
|
25
|
+
function extractRelativeFilePath(fullPath) {
|
|
26
|
+
if (!fullPath) return fullPath;
|
|
27
|
+
const match = fullPath.match(/\/([a-f0-9]{40})\//i);
|
|
28
|
+
if (match) {
|
|
29
|
+
const idx = fullPath.indexOf(match[1]);
|
|
30
|
+
return fullPath.substring(idx + match[1].length + 1);
|
|
31
|
+
}
|
|
32
|
+
return fullPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cleanLeadingSlash(p) {
|
|
36
|
+
return typeof p === 'string' && p.startsWith('/') ? p.slice(1) : (p || 'unknown');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Flatten the dead_code nested structure into a flat array of issues.
|
|
41
|
+
*/
|
|
42
|
+
function flattenDeadCode(deadCodeData) {
|
|
43
|
+
if (!deadCodeData || typeof deadCodeData !== 'object' || Array.isArray(deadCodeData)) {
|
|
44
|
+
return deadCodeData;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hasNestedKeys =
|
|
48
|
+
deadCodeData.python_dead_code !== undefined ||
|
|
49
|
+
deadCodeData.js_dead_code !== undefined ||
|
|
50
|
+
deadCodeData.extra_dead_code !== undefined;
|
|
51
|
+
|
|
52
|
+
if (!hasNestedKeys) return deadCodeData;
|
|
53
|
+
|
|
54
|
+
const flat = [];
|
|
55
|
+
|
|
56
|
+
// Python dead code
|
|
57
|
+
(deadCodeData.python_dead_code || []).forEach((item) => {
|
|
58
|
+
if (!item) return;
|
|
59
|
+
const filePath = extractRelativeFilePath(item.file_path || item.path || 'unknown');
|
|
60
|
+
(item.issues || []).forEach((issue) => {
|
|
61
|
+
if (!issue) return;
|
|
62
|
+
const typeMatch = issue.issue?.match(/unused (\w+)/i);
|
|
63
|
+
const issueType = typeMatch ? typeMatch[1] : 'code';
|
|
64
|
+
const nameMatch = issue.issue?.match(/'([^']*)'/);
|
|
65
|
+
const name = nameMatch ? nameMatch[1] : '';
|
|
66
|
+
const confMatch = issue.issue?.match(/\((\d+)% confidence\)/);
|
|
67
|
+
flat.push({
|
|
68
|
+
file_path: filePath,
|
|
69
|
+
line_number: issue.line_number || 0,
|
|
70
|
+
issue_text: issue.issue || `Unused ${issueType}: ${name}`,
|
|
71
|
+
message: issue.issue || `Unused ${issueType}: ${name}`,
|
|
72
|
+
severity: 'warning',
|
|
73
|
+
type: `unused_${issueType}`,
|
|
74
|
+
name,
|
|
75
|
+
confidence: confMatch ? confMatch[1] : '90',
|
|
76
|
+
check_name: `Unused ${issueType}: ${name}`,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// JS dead code — unused files
|
|
82
|
+
const jsDeadCode = deadCodeData.js_dead_code || {};
|
|
83
|
+
(jsDeadCode.unused_files || []).forEach((filePath) => {
|
|
84
|
+
if (!filePath) return;
|
|
85
|
+
flat.push({
|
|
86
|
+
file_path: extractRelativeFilePath(cleanLeadingSlash(filePath)),
|
|
87
|
+
line_number: 1,
|
|
88
|
+
issue_text: 'Unused file - this file is not imported anywhere',
|
|
89
|
+
message: 'Unused file - this file is not imported anywhere',
|
|
90
|
+
severity: 'warning',
|
|
91
|
+
type: 'unused_file',
|
|
92
|
+
check_name: 'Unused file',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// JS dead code — unused exports
|
|
97
|
+
(jsDeadCode.unused_exports || []).forEach((exportData) => {
|
|
98
|
+
if (!Array.isArray(exportData) || exportData.length !== 2) return;
|
|
99
|
+
let [filePath, exportIssues] = exportData;
|
|
100
|
+
if (!filePath || !Array.isArray(exportIssues)) return;
|
|
101
|
+
filePath = extractRelativeFilePath(cleanLeadingSlash(filePath));
|
|
102
|
+
exportIssues.forEach((exp) => {
|
|
103
|
+
if (!exp) return;
|
|
104
|
+
flat.push({
|
|
105
|
+
file_path: filePath,
|
|
106
|
+
line_number: exp.line || 0,
|
|
107
|
+
issue_text: `Unused export: ${exp.name || 'unknown'}`,
|
|
108
|
+
message: `Unused export: ${exp.name || 'unknown'}`,
|
|
109
|
+
severity: 'warning',
|
|
110
|
+
type: 'unused_export',
|
|
111
|
+
name: exp.name,
|
|
112
|
+
check_name: `Unused export: ${exp.name || 'unknown'}`,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Extra dead code (Sonar-style rules)
|
|
118
|
+
const extra = deadCodeData.extra_dead_code?.results || deadCodeData.extra_dead_code || {};
|
|
119
|
+
if (typeof extra === 'object' && !Array.isArray(extra)) {
|
|
120
|
+
Object.entries(extra).forEach(([filePath, issues]) => {
|
|
121
|
+
if (!filePath || !Array.isArray(issues)) return;
|
|
122
|
+
issues.forEach((issue) => {
|
|
123
|
+
if (!issue) return;
|
|
124
|
+
const messageId = issue['message-id'] || '';
|
|
125
|
+
const msgKey = messageId.includes(':') ? messageId.split(':')[1] : messageId;
|
|
126
|
+
flat.push({
|
|
127
|
+
file_path: extractRelativeFilePath(filePath),
|
|
128
|
+
line_number: issue.line_number || 0,
|
|
129
|
+
issue_text: issue.issue_text || EXTRA_DEAD_CODE_MESSAGES[msgKey] || 'Dead code detected',
|
|
130
|
+
message: issue.issue_text || EXTRA_DEAD_CODE_MESSAGES[msgKey] || 'Dead code detected',
|
|
131
|
+
severity: 'warning',
|
|
132
|
+
type: msgKey || 'dead_code',
|
|
133
|
+
rule_id: msgKey,
|
|
134
|
+
confidence: issue.confidence || '90',
|
|
135
|
+
check_name: EXTRA_DEAD_CODE_MESSAGES[msgKey] || 'Dead code detected',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return flat;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Normalize a single advanced issue to a consistent shape.
|
|
146
|
+
*/
|
|
147
|
+
function normalizeAdvancedIssue(item, resultType) {
|
|
148
|
+
if (!item) {
|
|
149
|
+
return { file_path: 'unknown', line_number: 1, file_line_range: [1], check_name: 'Unknown issue', severity: 'medium' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const normalized = { ...item };
|
|
153
|
+
normalized.file_path = extractRelativeFilePath(item.file_path || item.path || item.filename || 'unknown');
|
|
154
|
+
normalized.line_number = item.line_number || item.start_line || item.line || 1;
|
|
155
|
+
normalized.file_line_range =
|
|
156
|
+
Array.isArray(item.file_line_range) && item.file_line_range.length > 0
|
|
157
|
+
? item.file_line_range
|
|
158
|
+
: [normalized.line_number];
|
|
159
|
+
|
|
160
|
+
switch (resultType) {
|
|
161
|
+
case ADVANCED_RESULT_TYPES.SCA:
|
|
162
|
+
normalized.check_name =
|
|
163
|
+
item.vulnerability_id || item.cve_id || item.advisory_id ||
|
|
164
|
+
(item.package_name ? `Vulnerability in ${item.package_name}` : null) ||
|
|
165
|
+
item.title || item.description || 'Package vulnerability detected';
|
|
166
|
+
normalized.severity = item.severity || 'medium';
|
|
167
|
+
break;
|
|
168
|
+
case ADVANCED_RESULT_TYPES.SBOM:
|
|
169
|
+
normalized.check_name = item.package_name
|
|
170
|
+
? `${item.package_name}${item.version ? '@' + item.version : ''}`
|
|
171
|
+
: (item.name || item.description || 'Software component');
|
|
172
|
+
normalized.severity = item.severity || 'info';
|
|
173
|
+
break;
|
|
174
|
+
case ADVANCED_RESULT_TYPES.SECRETS:
|
|
175
|
+
normalized.check_name =
|
|
176
|
+
item.type || item.secret_type || item.rule_id || item.description || 'Secret detected';
|
|
177
|
+
normalized.severity = item.severity || 'high';
|
|
178
|
+
break;
|
|
179
|
+
case ADVANCED_RESULT_TYPES.IAC:
|
|
180
|
+
normalized.check_name =
|
|
181
|
+
item.check_id || item.rule_id || item.policy_id ||
|
|
182
|
+
item.description || item.title || 'Infrastructure misconfiguration';
|
|
183
|
+
normalized.severity = item.severity || 'medium';
|
|
184
|
+
break;
|
|
185
|
+
case ADVANCED_RESULT_TYPES.DEAD_CODE:
|
|
186
|
+
normalized.check_name =
|
|
187
|
+
item.check_name || item.name || item.function_name || item.symbol_name || item.description || 'Unused code detected';
|
|
188
|
+
normalized.severity = item.severity || 'low';
|
|
189
|
+
break;
|
|
190
|
+
default:
|
|
191
|
+
normalized.check_name = item.description || item.message || item.name || 'Issue detected';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return normalized;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Fetch advanced scan results (SCA, SBOM, secrets, IaC, dead code).
|
|
199
|
+
*
|
|
200
|
+
* @param {string} repo - "org/repo-name"
|
|
201
|
+
* @param {string} commitId - 40-char commit SHA
|
|
202
|
+
* @param {string} resultType - one of ADVANCED_RESULT_TYPES values
|
|
203
|
+
* @returns {Promise<{ success: boolean, issues?: Array, healthyPackages?: Array, status?: string, error?: string }>}
|
|
204
|
+
*/
|
|
205
|
+
export async function fetchAdvancedScanResults(repo, commitId, resultType) {
|
|
206
|
+
if (!Object.values(ADVANCED_RESULT_TYPES).includes(resultType)) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: `Invalid result_type. Must be one of: ${Object.values(ADVANCED_RESULT_TYPES).join(', ')}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const response = await fetchApi('/extension/scans2/fetch-advanced-results', 'POST', {
|
|
215
|
+
repo,
|
|
216
|
+
commit_id: commitId,
|
|
217
|
+
result_type: resultType,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!response) {
|
|
221
|
+
return { success: false, error: 'Failed to connect to CodeAnt server' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (response.status === 'error') {
|
|
225
|
+
return { success: false, error: response.message || `Failed to fetch ${resultType} results` };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let resultsData;
|
|
229
|
+
let healthyPackages = [];
|
|
230
|
+
|
|
231
|
+
if (resultType === ADVANCED_RESULT_TYPES.SECRETS) {
|
|
232
|
+
resultsData = response.secrets;
|
|
233
|
+
} else if (resultType === ADVANCED_RESULT_TYPES.DEAD_CODE) {
|
|
234
|
+
resultsData = flattenDeadCode(response.dead_code);
|
|
235
|
+
} else if (resultType === ADVANCED_RESULT_TYPES.SCA) {
|
|
236
|
+
const scaResults = response.results;
|
|
237
|
+
if (scaResults && typeof scaResults === 'object' && !Array.isArray(scaResults) && scaResults.all_vulnerabilities !== undefined) {
|
|
238
|
+
resultsData = scaResults.all_vulnerabilities || [];
|
|
239
|
+
healthyPackages = scaResults.healthy_packages || [];
|
|
240
|
+
} else {
|
|
241
|
+
resultsData = scaResults;
|
|
242
|
+
}
|
|
243
|
+
} else if (resultType === ADVANCED_RESULT_TYPES.IAC) {
|
|
244
|
+
const iacData = response.results || response.result;
|
|
245
|
+
if (Array.isArray(iacData)) {
|
|
246
|
+
const valid = iacData.filter(Boolean);
|
|
247
|
+
const hasNested = valid.some((item) => item?.results && Array.isArray(item.results.failed_checks));
|
|
248
|
+
if (hasNested) {
|
|
249
|
+
const flat = [];
|
|
250
|
+
valid.forEach((resultItem) => {
|
|
251
|
+
(resultItem?.results?.failed_checks || []).forEach((check) => {
|
|
252
|
+
if (!check) return;
|
|
253
|
+
const fp = cleanLeadingSlash(extractRelativeFilePath(check.file_path || 'unknown'));
|
|
254
|
+
flat.push({
|
|
255
|
+
file_path: fp,
|
|
256
|
+
line_number: check.file_line_range?.[0] || 1,
|
|
257
|
+
file_line_range: check.file_line_range || [1],
|
|
258
|
+
check_id: check.check_id || '',
|
|
259
|
+
check_name: check.check_name || 'Infrastructure misconfiguration',
|
|
260
|
+
issue_text: check.check_name || 'Infrastructure misconfiguration',
|
|
261
|
+
message: check.check_name || 'Infrastructure misconfiguration',
|
|
262
|
+
severity: check.severity || 'medium',
|
|
263
|
+
guideline: check.guideline || '',
|
|
264
|
+
code_block: check.code_block || [],
|
|
265
|
+
resource: check.resource || '',
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
resultsData = flat;
|
|
270
|
+
} else {
|
|
271
|
+
resultsData = valid;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
resultsData = iacData;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
resultsData = response.results;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Normalize to array
|
|
281
|
+
let issues = [];
|
|
282
|
+
if (Array.isArray(resultsData)) {
|
|
283
|
+
issues = resultsData.filter(Boolean).map((item) => normalizeAdvancedIssue(item, resultType));
|
|
284
|
+
} else if (resultsData && typeof resultsData === 'object') {
|
|
285
|
+
for (const [filePath, fileItems] of Object.entries(resultsData)) {
|
|
286
|
+
if (!filePath) continue;
|
|
287
|
+
if (Array.isArray(fileItems)) {
|
|
288
|
+
fileItems.forEach((item) => {
|
|
289
|
+
if (item) issues.push(normalizeAdvancedIssue({ ...item, file_path: filePath }, resultType));
|
|
290
|
+
});
|
|
291
|
+
} else if (fileItems && typeof fileItems === 'object') {
|
|
292
|
+
issues.push(normalizeAdvancedIssue({ ...fileItems, file_path: filePath }, resultType));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Filter secrets false positives
|
|
298
|
+
if (resultType === ADVANCED_RESULT_TYPES.SECRETS) {
|
|
299
|
+
issues = issues.filter((issue) => String(issue.confidence_score || '').toLowerCase() !== 'false_positive');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (resultType === ADVANCED_RESULT_TYPES.SCA) {
|
|
303
|
+
return { success: true, issues, healthyPackages, status: response.status || 'done' };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { success: true, issues, status: response.status || 'done' };
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return { success: false, error: error.message || `Failed to fetch ${resultType} results` };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const fetchScaResults = (repo, commitId) =>
|
|
313
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SCA);
|
|
314
|
+
|
|
315
|
+
export const fetchSbomResults = (repo, commitId) =>
|
|
316
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SBOM);
|
|
317
|
+
|
|
318
|
+
export const fetchSecretsResults = (repo, commitId) =>
|
|
319
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SECRETS);
|
|
320
|
+
|
|
321
|
+
export const fetchIacResults = (repo, commitId) =>
|
|
322
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.IAC);
|
|
323
|
+
|
|
324
|
+
export const fetchDeadCodeResults = (repo, commitId) =>
|
|
325
|
+
fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.DEAD_CODE);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fetchApi } from '../utils/fetchApi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch dismissed alerts for a repository.
|
|
5
|
+
*
|
|
6
|
+
* Issue keys use the format: "file_path||::||context_code_block||::||test_id"
|
|
7
|
+
* For secrets the format is: "file_path||::||line_number||::||type"
|
|
8
|
+
*
|
|
9
|
+
* @param {string} repo - "org/repo-name"
|
|
10
|
+
* @param {string} analysisType - e.g. "security" (default)
|
|
11
|
+
* @returns {Promise<{ success: boolean, dismissedAlerts?: Array, error?: string }>}
|
|
12
|
+
*/
|
|
13
|
+
export async function fetchDismissedAlerts(repo, analysisType = 'security') {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetchApi('/extension/scans2/dismiss-alerts/get', 'POST', {
|
|
16
|
+
repo,
|
|
17
|
+
analysis_type: analysisType,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!response) {
|
|
21
|
+
return { success: false, error: 'Failed to connect to CodeAnt server' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (response.status === 'error') {
|
|
25
|
+
return { success: false, error: response.message || 'Failed to fetch dismissed alerts' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const dismissData = response.data || {};
|
|
29
|
+
const dismissedAlerts = [];
|
|
30
|
+
|
|
31
|
+
for (const [issueKey, dismissInfo] of Object.entries(dismissData)) {
|
|
32
|
+
if (!issueKey.includes('||::||')) continue;
|
|
33
|
+
|
|
34
|
+
const parts = issueKey.split('||::||');
|
|
35
|
+
let file_path = parts[0] || '';
|
|
36
|
+
const part1 = parts[1] || '';
|
|
37
|
+
const part2 = parts[2] || '';
|
|
38
|
+
|
|
39
|
+
file_path = file_path.replace(/\/(security_issues|anti_patterns|docstring|complex_functions)\.json$/, '');
|
|
40
|
+
|
|
41
|
+
dismissedAlerts.push({
|
|
42
|
+
file_path,
|
|
43
|
+
context_code_block: part1,
|
|
44
|
+
test_id: part2,
|
|
45
|
+
line_number: parseInt(part1, 10) || 0,
|
|
46
|
+
type: part2,
|
|
47
|
+
issue_key: issueKey,
|
|
48
|
+
reason_for_dismiss: dismissInfo?.reason_for_dismiss || '',
|
|
49
|
+
comment_for_dismiss: dismissInfo?.comment_for_dismiss || '',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { success: true, dismissedAlerts };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { success: false, error: error.message || 'Failed to fetch dismissed alerts' };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { fetchApi } from '../utils/fetchApi.js';
|
|
2
|
+
|
|
3
|
+
export const VALID_RESULT_TYPES = {
|
|
4
|
+
SECURITY_ISSUES: 'security_issues',
|
|
5
|
+
ANTI_PATTERNS: 'anti_patterns',
|
|
6
|
+
DOCSTRING: 'docstring',
|
|
7
|
+
COMPLEX_FUNCTIONS: 'complex_functions',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const RESULT_FILE_SUFFIXES = {
|
|
11
|
+
security_issues: '/security_issues.json',
|
|
12
|
+
anti_patterns: '/anti_patterns.json',
|
|
13
|
+
docstring: '/docstring.json',
|
|
14
|
+
complex_functions: '/complex_functions.json',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Strip org/repo/commitId prefix from a full server-side path.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} fullPath
|
|
21
|
+
* @param {string} commitId
|
|
22
|
+
* @param {string|null} suffix - e.g. '/security_issues.json'
|
|
23
|
+
*/
|
|
24
|
+
function extractCleanPath(fullPath, commitId, suffix = null) {
|
|
25
|
+
let p = fullPath;
|
|
26
|
+
|
|
27
|
+
if (suffix && p.endsWith(suffix)) {
|
|
28
|
+
p = p.slice(0, -suffix.length);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (commitId && p.includes(commitId)) {
|
|
32
|
+
const idx = p.indexOf(commitId);
|
|
33
|
+
return p.substring(idx + commitId.length + 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const match = p.match(/\/([a-f0-9]{40})\//i);
|
|
37
|
+
if (match) {
|
|
38
|
+
const idx = p.indexOf(match[1]);
|
|
39
|
+
return p.substring(idx + match[1].length + 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parts = p.split('/');
|
|
43
|
+
return parts.length > 3 ? parts.slice(3).join('/') : p;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch scan results (SAST, anti-patterns, docstring, complex-functions).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} repo - "org/repo-name"
|
|
50
|
+
* @param {string} commitId - 40-char commit SHA
|
|
51
|
+
* @param {string} resultType - one of VALID_RESULT_TYPES values
|
|
52
|
+
* @returns {Promise<{ success: boolean, issues?: Array, status?: string, error?: string }>}
|
|
53
|
+
*/
|
|
54
|
+
export async function fetchScanResults(repo, commitId, resultType) {
|
|
55
|
+
if (!Object.values(VALID_RESULT_TYPES).includes(resultType)) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
error: `Invalid result_type. Must be one of: ${Object.values(VALID_RESULT_TYPES).join(', ')}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetchApi('/extension/scans2/fetch-results', 'POST', {
|
|
64
|
+
repo,
|
|
65
|
+
commit_id: commitId,
|
|
66
|
+
result_type: resultType,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!response) {
|
|
70
|
+
return { success: false, error: 'Failed to connect to CodeAnt server' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (response.status === 'error') {
|
|
74
|
+
return { success: false, error: response.message || `Failed to fetch ${resultType} results` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fileSuffix = RESULT_FILE_SUFFIXES[resultType] || '';
|
|
78
|
+
const resultsData = response.results || response;
|
|
79
|
+
const issues = [];
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(resultsData)) {
|
|
82
|
+
issues.push(...resultsData);
|
|
83
|
+
} else if (resultsData && typeof resultsData === 'object') {
|
|
84
|
+
for (const [fullPath, fileIssues] of Object.entries(resultsData)) {
|
|
85
|
+
if (!Array.isArray(fileIssues) || fileIssues.length === 0) continue;
|
|
86
|
+
|
|
87
|
+
const cleanPath = extractCleanPath(fullPath, commitId, fileSuffix);
|
|
88
|
+
for (const issue of fileIssues) {
|
|
89
|
+
issues.push({
|
|
90
|
+
...issue,
|
|
91
|
+
file_path: cleanPath,
|
|
92
|
+
file_line_range: [issue.line_number || issue.start_line || issue.line || 1],
|
|
93
|
+
check_name: issue.issue_text || issue.message || issue.description || issue.name,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { success: true, issues, status: response.status || 'done' };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return { success: false, error: error.message || `Failed to fetch ${resultType} results` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const fetchSastResults = (repo, commitId) =>
|
|
106
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.SECURITY_ISSUES);
|
|
107
|
+
|
|
108
|
+
export const fetchAntiPatternsResults = (repo, commitId) =>
|
|
109
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.ANTI_PATTERNS);
|
|
110
|
+
|
|
111
|
+
export const fetchDocstringResults = (repo, commitId) =>
|
|
112
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.DOCSTRING);
|
|
113
|
+
|
|
114
|
+
export const fetchComplexFunctionsResults = (repo, commitId) =>
|
|
115
|
+
fetchScanResults(repo, commitId, VALID_RESULT_TYPES.COMPLEX_FUNCTIONS);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fetchApi } from '../utils/fetchApi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch scan history for a repository.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} repo - Repository full name (e.g. "org/repo-name")
|
|
7
|
+
* @returns {Promise<Object>}
|
|
8
|
+
* {
|
|
9
|
+
* success: true,
|
|
10
|
+
* repo: "org/repo-name",
|
|
11
|
+
* scanHistory: [...]
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
export async function getScanHistory(repo) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetchApi('/extension/scans2/get-scan-history', 'POST', { repo });
|
|
17
|
+
|
|
18
|
+
if (!response) {
|
|
19
|
+
return { success: false, error: 'Failed to connect to CodeAnt server' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (response.last_analysis_results !== undefined) {
|
|
23
|
+
return {
|
|
24
|
+
success: true,
|
|
25
|
+
repo: response.repo || repo,
|
|
26
|
+
scanHistory: response.last_analysis_results || [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (response.status === 'error') {
|
|
31
|
+
return { success: false, error: response.message || 'Failed to fetch scan history' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { success: false, error: 'Unexpected response from server' };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return { success: false, error: error.message || 'Failed to fetch scan history' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fetchApi } from '../utils/fetchApi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch repositories for the authenticated user from the CodeAnt backend.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} organizationName - The organization name to filter repositories by.
|
|
7
|
+
* @returns {Promise<Object>}
|
|
8
|
+
* {
|
|
9
|
+
* success: true,
|
|
10
|
+
* repos: [{ name, full_name, pushed_at, ... }]
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
export async function listRepos(organizationName) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetchApi('/extension/scans2/listrepos', 'POST', {
|
|
16
|
+
org: organizationName,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!response) {
|
|
20
|
+
return { success: false, error: 'Failed to connect to CodeAnt server' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (response.repos) {
|
|
24
|
+
const sortedRepos = (response.repos || []).sort(
|
|
25
|
+
(a, b) => (Date.parse(b.pushed_at) || 0) - (Date.parse(a.pushed_at) || 0)
|
|
26
|
+
);
|
|
27
|
+
return { success: true, repos: sortedRepos };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (response.status === 'error') {
|
|
31
|
+
return { success: false, error: response.message || 'Failed to fetch repositories' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { success: false, error: 'Unexpected response from server' };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return { success: false, error: error.message || 'Failed to fetch repositories' };
|
|
37
|
+
}
|
|
38
|
+
}
|