codegate-ai 0.3.1 → 0.4.0
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/dist/commands/scan-command/helpers.js +9 -13
- package/dist/commands/scan-command.js +2 -0
- package/dist/commands/skills-wrapper.js +15 -1
- package/dist/report/requested-target-findings.d.ts +8 -0
- package/dist/report/requested-target-findings.js +51 -0
- package/dist/reporter/terminal.js +58 -34
- package/dist/tui/views/dashboard.js +13 -3
- package/package.json +1 -1
|
@@ -4,31 +4,27 @@ import { renderJsonReport } from "../../reporter/json.js";
|
|
|
4
4
|
import { renderMarkdownReport } from "../../reporter/markdown.js";
|
|
5
5
|
import { renderSarifReport } from "../../reporter/sarif.js";
|
|
6
6
|
import { renderTerminalReport } from "../../reporter/terminal.js";
|
|
7
|
+
import { partitionRequestedTargetFindings } from "../../report/requested-target-findings.js";
|
|
7
8
|
function isRecord(value) {
|
|
8
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
10
|
}
|
|
10
|
-
function isHttpLikeTarget(target) {
|
|
11
|
-
return /^https?:\/\//iu.test(target);
|
|
12
|
-
}
|
|
13
|
-
function isUserScopeFindingPath(path) {
|
|
14
|
-
return path === "~" || path.startsWith("~/");
|
|
15
|
-
}
|
|
16
11
|
export function summarizeRequestedTargetFindings(report, displayTarget) {
|
|
17
|
-
|
|
12
|
+
const groups = partitionRequestedTargetFindings(report, displayTarget);
|
|
13
|
+
if (!groups) {
|
|
18
14
|
return null;
|
|
19
15
|
}
|
|
20
|
-
const targetFindings =
|
|
21
|
-
const
|
|
16
|
+
const targetFindings = groups.targetFindings.length;
|
|
17
|
+
const localFindings = groups.localFindings.length;
|
|
22
18
|
if (targetFindings === 0) {
|
|
23
|
-
if (
|
|
19
|
+
if (localFindings === 0) {
|
|
24
20
|
return "Requested URL target result: no findings were detected in the URL content.";
|
|
25
21
|
}
|
|
26
|
-
return `Requested URL target result: no findings were detected in the URL content. ${
|
|
22
|
+
return `Requested URL target result: no findings were detected in the URL content. ${localFindings} finding(s) came from local host paths (~/* or absolute host paths).`;
|
|
27
23
|
}
|
|
28
|
-
if (
|
|
24
|
+
if (localFindings === 0) {
|
|
29
25
|
return `Requested URL target result: ${targetFindings} finding(s) were detected in the URL content.`;
|
|
30
26
|
}
|
|
31
|
-
return `Requested URL target result: ${targetFindings} finding(s) were detected in the URL content. ${
|
|
27
|
+
return `Requested URL target result: ${targetFindings} finding(s) were detected in the URL content. ${localFindings} additional finding(s) came from local host paths (~/* or absolute host paths).`;
|
|
32
28
|
}
|
|
33
29
|
export function metadataSummary(metadata) {
|
|
34
30
|
let raw;
|
|
@@ -7,6 +7,7 @@ import { buildPromptEvidenceText, supportsToollessLocalTextAnalysis, } from "../
|
|
|
7
7
|
import { buildLocalTextAnalysisPrompt, buildSecurityAnalysisPrompt, } from "../layer3-dynamic/meta-agent.js";
|
|
8
8
|
import { layer3OutcomesToFindings, mergeLayer3Findings, runDeepScanWithConsent, } from "../pipeline.js";
|
|
9
9
|
import { mergeMetaAgentMetadata, metadataSummary, noEligibleDeepResourceNotes, parseLocalTextFindings, parseMetaAgentOutput, remediationSummaryLines, renderByFormat, summarizeRequestedTargetFindings, withMetaAgentFinding, } from "./scan-command/helpers.js";
|
|
10
|
+
import { reorderRequestedTargetFindings } from "../report/requested-target-findings.js";
|
|
10
11
|
function toMetaAgentPreference(value) {
|
|
11
12
|
const normalized = value.trim().toLowerCase();
|
|
12
13
|
if (normalized === "claude" || normalized === "claude-code") {
|
|
@@ -289,6 +290,7 @@ export async function executeScanCommand(input, deps) {
|
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
report = applyConfigPolicy(report, input.config);
|
|
293
|
+
report = reorderRequestedTargetFindings(report, input.displayTarget);
|
|
292
294
|
const remediationRequested = input.options.remediate ||
|
|
293
295
|
input.options.fixSafe ||
|
|
294
296
|
input.options.dryRun ||
|
|
@@ -4,6 +4,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { applyConfigPolicy, OUTPUT_FORMATS, } from "../config.js";
|
|
6
6
|
import { renderByFormat, summarizeRequestedTargetFindings } from "./scan-command/helpers.js";
|
|
7
|
+
import { reorderRequestedTargetFindings } from "../report/requested-target-findings.js";
|
|
7
8
|
import { resolveScanTarget } from "../scan-target.js";
|
|
8
9
|
function parseWrapperOptionValue(args, index, flag) {
|
|
9
10
|
const current = args[index] ?? "";
|
|
@@ -16,7 +17,10 @@ function parseWrapperOptionValue(args, index, flag) {
|
|
|
16
17
|
return [value, index];
|
|
17
18
|
}
|
|
18
19
|
const nextValue = args[index + 1];
|
|
19
|
-
if (!nextValue ||
|
|
20
|
+
if (!nextValue ||
|
|
21
|
+
nextValue.trim().length === 0 ||
|
|
22
|
+
nextValue === "--" ||
|
|
23
|
+
nextValue.startsWith("-")) {
|
|
20
24
|
throw new Error(`${flag} requires a value`);
|
|
21
25
|
}
|
|
22
26
|
return [nextValue, index + 1];
|
|
@@ -74,6 +78,15 @@ function firstLikelySourceAfterAdd(args, addIndex, context) {
|
|
|
74
78
|
return null;
|
|
75
79
|
}
|
|
76
80
|
if (looksLikeSourceToken(token, context)) {
|
|
81
|
+
// Heuristic: if a source-looking token is immediately after an option flag and followed by
|
|
82
|
+
// another source-looking token, treat the first one as an option value and continue.
|
|
83
|
+
const previous = index > addIndex + 1 ? (args[index - 1] ?? "") : "";
|
|
84
|
+
const next = args[index + 1] ?? "";
|
|
85
|
+
if (previous.startsWith("-") &&
|
|
86
|
+
!previous.startsWith("--skill") &&
|
|
87
|
+
looksLikeSourceToken(next, context)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
77
90
|
return token;
|
|
78
91
|
}
|
|
79
92
|
}
|
|
@@ -327,6 +340,7 @@ export async function executeSkillsWrapper(input, deps) {
|
|
|
327
340
|
};
|
|
328
341
|
}
|
|
329
342
|
report = applyConfigPolicy(report, config);
|
|
343
|
+
report = reorderRequestedTargetFindings(report, resolvedTarget.displayTarget);
|
|
330
344
|
const shouldUseTui = config.tui.enabled && isTTY && deps.renderTui !== undefined && noTui !== true;
|
|
331
345
|
const targetSummaryNote = config.output_format === "terminal"
|
|
332
346
|
? summarizeRequestedTargetFindings(report, resolvedTarget.displayTarget)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from "../types/finding.js";
|
|
2
|
+
import type { CodeGateReport } from "../types/report.js";
|
|
3
|
+
export interface RequestedTargetFindingGroups {
|
|
4
|
+
targetFindings: Finding[];
|
|
5
|
+
localFindings: Finding[];
|
|
6
|
+
}
|
|
7
|
+
export declare function partitionRequestedTargetFindings(report: CodeGateReport, displayTarget?: string): RequestedTargetFindingGroups | null;
|
|
8
|
+
export declare function reorderRequestedTargetFindings(report: CodeGateReport, displayTarget?: string): CodeGateReport;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
const HTTP_LIKE_TARGET_PATTERN = /^https?:\/\//iu;
|
|
3
|
+
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[a-z]:[\\/]/iu;
|
|
4
|
+
const WINDOWS_USER_SCOPE_PATH_PATTERN = /^~[\\/]/u;
|
|
5
|
+
const UNC_PATH_PATTERN = /^[\\/]{2}[^\\/]/u;
|
|
6
|
+
const FILE_URI_PATH_PATTERN = /^file:\/\//iu;
|
|
7
|
+
function isHttpLikeTarget(target) {
|
|
8
|
+
return HTTP_LIKE_TARGET_PATTERN.test(target);
|
|
9
|
+
}
|
|
10
|
+
function isUserScopeFindingPath(path) {
|
|
11
|
+
return path === "~" || path.startsWith("~/") || WINDOWS_USER_SCOPE_PATH_PATTERN.test(path);
|
|
12
|
+
}
|
|
13
|
+
function isWindowsAbsolutePath(path) {
|
|
14
|
+
return WINDOWS_ABSOLUTE_PATH_PATTERN.test(path);
|
|
15
|
+
}
|
|
16
|
+
function isLocalHostFindingPath(path) {
|
|
17
|
+
return (isUserScopeFindingPath(path) ||
|
|
18
|
+
isAbsolute(path) ||
|
|
19
|
+
isWindowsAbsolutePath(path) ||
|
|
20
|
+
UNC_PATH_PATTERN.test(path) ||
|
|
21
|
+
FILE_URI_PATH_PATTERN.test(path));
|
|
22
|
+
}
|
|
23
|
+
export function partitionRequestedTargetFindings(report, displayTarget) {
|
|
24
|
+
const target = displayTarget ?? report.scan_target;
|
|
25
|
+
if (!isHttpLikeTarget(target)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const targetFindings = [];
|
|
29
|
+
const localFindings = [];
|
|
30
|
+
for (const finding of report.findings) {
|
|
31
|
+
if (isLocalHostFindingPath(finding.file_path)) {
|
|
32
|
+
localFindings.push(finding);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
targetFindings.push(finding);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
targetFindings,
|
|
39
|
+
localFindings,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function reorderRequestedTargetFindings(report, displayTarget) {
|
|
43
|
+
const groups = partitionRequestedTargetFindings(report, displayTarget);
|
|
44
|
+
if (!groups) {
|
|
45
|
+
return report;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...report,
|
|
49
|
+
findings: [...groups.targetFindings, ...groups.localFindings],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toAbsoluteDisplayPath } from "../path-display.js";
|
|
2
|
+
import { partitionRequestedTargetFindings } from "../report/requested-target-findings.js";
|
|
2
3
|
function appendLabeledList(lines, label, values) {
|
|
3
4
|
if (values.length === 0) {
|
|
4
5
|
return;
|
|
@@ -36,8 +37,46 @@ function formatLocation(location) {
|
|
|
36
37
|
}
|
|
37
38
|
return parts.length > 0 ? parts.join(" @ ") : null;
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
function appendFinding(lines, report, options, finding) {
|
|
40
41
|
const verbose = options.verbose === true;
|
|
42
|
+
lines.push(`[${finding.severity}] ${toAbsoluteDisplayPath(report.scan_target, finding.file_path)}`);
|
|
43
|
+
lines.push(` ${finding.description}`);
|
|
44
|
+
if (finding.incident_title) {
|
|
45
|
+
appendLabeledText(lines, "Incident", finding.incident_title);
|
|
46
|
+
}
|
|
47
|
+
if (finding.evidence && finding.evidence.length > 0) {
|
|
48
|
+
appendEvidence(lines, finding.evidence);
|
|
49
|
+
}
|
|
50
|
+
appendLabeledList(lines, "Observed", finding.observed ?? []);
|
|
51
|
+
if (finding.inference) {
|
|
52
|
+
appendLabeledText(lines, "Inference", finding.inference);
|
|
53
|
+
}
|
|
54
|
+
appendLabeledList(lines, "Not verified", finding.not_verified ?? []);
|
|
55
|
+
if (verbose) {
|
|
56
|
+
lines.push(` Rule: ${finding.rule_id}`);
|
|
57
|
+
lines.push(` Finding ID: ${finding.finding_id}`);
|
|
58
|
+
lines.push(` Category: ${finding.category} | Layer: ${finding.layer} | Confidence: ${finding.confidence}`);
|
|
59
|
+
const formattedLocation = formatLocation(finding.location);
|
|
60
|
+
if (formattedLocation) {
|
|
61
|
+
lines.push(` Location: ${formattedLocation}`);
|
|
62
|
+
}
|
|
63
|
+
if (finding.cve) {
|
|
64
|
+
lines.push(` CVE: ${finding.cve}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(` CWE: ${finding.cwe}`);
|
|
67
|
+
if (finding.owasp.length > 0) {
|
|
68
|
+
lines.push(` OWASP: ${finding.owasp.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
if (finding.remediation_actions.length > 0) {
|
|
71
|
+
lines.push(` Remediation: ${finding.remediation_actions.join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (finding.layer === "L3" && finding.source_config) {
|
|
75
|
+
const fieldSuffix = finding.source_config.field ? ` (${finding.source_config.field})` : "";
|
|
76
|
+
lines.push(` source config: ${finding.source_config.file_path}${fieldSuffix}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function renderTerminalReport(report, options = {}) {
|
|
41
80
|
const lines = [];
|
|
42
81
|
lines.push(`CodeGate v${report.version}`);
|
|
43
82
|
lines.push(`Target: ${report.scan_target}`);
|
|
@@ -52,43 +91,28 @@ export function renderTerminalReport(report, options = {}) {
|
|
|
52
91
|
lines.push("No findings.");
|
|
53
92
|
return lines.join("\n");
|
|
54
93
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lines.push(`
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
if (finding.evidence && finding.evidence.length > 0) {
|
|
62
|
-
appendEvidence(lines, finding.evidence);
|
|
94
|
+
const groups = partitionRequestedTargetFindings(report);
|
|
95
|
+
if (groups) {
|
|
96
|
+
lines.push(`Requested URL target findings (${groups.targetFindings.length}):`);
|
|
97
|
+
if (groups.targetFindings.length === 0) {
|
|
98
|
+
lines.push(" none");
|
|
63
99
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
appendLabeledList(lines, "Not verified", finding.not_verified ?? []);
|
|
69
|
-
if (verbose) {
|
|
70
|
-
lines.push(` Rule: ${finding.rule_id}`);
|
|
71
|
-
lines.push(` Finding ID: ${finding.finding_id}`);
|
|
72
|
-
lines.push(` Category: ${finding.category} | Layer: ${finding.layer} | Confidence: ${finding.confidence}`);
|
|
73
|
-
const formattedLocation = formatLocation(finding.location);
|
|
74
|
-
if (formattedLocation) {
|
|
75
|
-
lines.push(` Location: ${formattedLocation}`);
|
|
76
|
-
}
|
|
77
|
-
if (finding.cve) {
|
|
78
|
-
lines.push(` CVE: ${finding.cve}`);
|
|
79
|
-
}
|
|
80
|
-
lines.push(` CWE: ${finding.cwe}`);
|
|
81
|
-
if (finding.owasp.length > 0) {
|
|
82
|
-
lines.push(` OWASP: ${finding.owasp.join(", ")}`);
|
|
83
|
-
}
|
|
84
|
-
if (finding.remediation_actions.length > 0) {
|
|
85
|
-
lines.push(` Remediation: ${finding.remediation_actions.join(", ")}`);
|
|
100
|
+
else {
|
|
101
|
+
for (const finding of groups.targetFindings) {
|
|
102
|
+
appendFinding(lines, report, options, finding);
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
lines.push(`
|
|
105
|
+
if (groups.localFindings.length > 0) {
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(`Additional local host findings (${groups.localFindings.length}):`);
|
|
108
|
+
for (const finding of groups.localFindings) {
|
|
109
|
+
appendFinding(lines, report, options, finding);
|
|
110
|
+
}
|
|
91
111
|
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
for (const finding of report.findings) {
|
|
115
|
+
appendFinding(lines, report, options, finding);
|
|
92
116
|
}
|
|
93
117
|
return lines.join("\n");
|
|
94
118
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { toAbsoluteDisplayPath } from "../../path-display.js";
|
|
4
|
+
import { partitionRequestedTargetFindings } from "../../report/requested-target-findings.js";
|
|
4
5
|
import { defaultTheme } from "../theme.js";
|
|
6
|
+
const FINDINGS_PER_SECTION_LIMIT = 5;
|
|
7
|
+
function FindingBlock(props) {
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["[", props.finding.severity, "]", " ", toAbsoluteDisplayPath(props.report.scan_target, props.finding.file_path)] }), _jsx(Text, { children: props.finding.description }), props.finding.evidence ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Evidence:" }), props.finding.evidence.split("\n").map((line, index) => (_jsx(Text, { children: line }, `${props.finding.finding_id}-evidence-${index}`)))] })) : null] }, props.finding.finding_id));
|
|
9
|
+
}
|
|
10
|
+
function FindingsSection(props) {
|
|
11
|
+
const visibleFindings = props.findings.slice(0, FINDINGS_PER_SECTION_LIMIT);
|
|
12
|
+
const remaining = props.findings.length - visibleFindings.length;
|
|
13
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: defaultTheme.title, children: [props.title, " (", props.findings.length, "):"] }), visibleFindings.length === 0 ? (_jsx(Text, { color: defaultTheme.muted, children: "none" })) : (visibleFindings.map((finding) => (_jsx(FindingBlock, { report: props.report, finding: finding }, finding.finding_id)))), remaining > 0 ? (_jsxs(Text, { color: defaultTheme.muted, children: ["...and ", remaining, " more findings"] })) : null] }));
|
|
14
|
+
}
|
|
5
15
|
export function DashboardView(props) {
|
|
6
|
-
const
|
|
7
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Text, { color: defaultTheme.title, children: ["CodeGate v", props.report.version] }), _jsxs(Text, { color: defaultTheme.muted, children: ["Target: ", props.report.scan_target] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Installed tools: ", props.report.tools_detected.join(", ") || "none"] }), _jsxs(Text, { children: ["Findings: ", props.report.summary.total, " (CRITICAL", " ", props.report.summary.by_severity.CRITICAL ?? 0, ", HIGH", " ", props.report.summary.by_severity.HIGH ?? 0, ", MEDIUM", " ", props.report.summary.by_severity.MEDIUM ?? 0, ", LOW", " ", props.report.summary.by_severity.LOW ?? 0, ", INFO", " ", props.report.summary.by_severity.INFO ?? 0, ")"] }), props.notices && props.notices.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Notes:" }), props.notices.map((notice, index) => (_jsx(Text, { color: defaultTheme.muted, children: notice }, `notice-${index}`)))] })) : null] }),
|
|
16
|
+
const groupedFindings = partitionRequestedTargetFindings(props.report);
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Text, { color: defaultTheme.title, children: ["CodeGate v", props.report.version] }), _jsxs(Text, { color: defaultTheme.muted, children: ["Target: ", props.report.scan_target] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Installed tools: ", props.report.tools_detected.join(", ") || "none"] }), _jsxs(Text, { children: ["Findings: ", props.report.summary.total, " (CRITICAL", " ", props.report.summary.by_severity.CRITICAL ?? 0, ", HIGH", " ", props.report.summary.by_severity.HIGH ?? 0, ", MEDIUM", " ", props.report.summary.by_severity.MEDIUM ?? 0, ", LOW", " ", props.report.summary.by_severity.LOW ?? 0, ", INFO", " ", props.report.summary.by_severity.INFO ?? 0, ")"] }), props.notices && props.notices.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Notes:" }), props.notices.map((notice, index) => (_jsx(Text, { color: defaultTheme.muted, children: notice }, `notice-${index}`)))] })) : null] }), props.report.findings.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Findings detail:" }), groupedFindings ? (_jsxs(_Fragment, { children: [_jsx(FindingsSection, { title: "Requested URL target findings", report: props.report, findings: groupedFindings.targetFindings }), groupedFindings.localFindings.length > 0 ? (_jsx(FindingsSection, { title: "Additional local host findings", report: props.report, findings: groupedFindings.localFindings })) : null] })) : (_jsx(FindingsSection, { title: "Findings", report: props.report, findings: props.report.findings }))] })) : null] }));
|
|
8
18
|
}
|