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.
@@ -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
- if (!displayTarget || !isHttpLikeTarget(displayTarget)) {
12
+ const groups = partitionRequestedTargetFindings(report, displayTarget);
13
+ if (!groups) {
18
14
  return null;
19
15
  }
20
- const targetFindings = report.findings.filter((finding) => !isUserScopeFindingPath(finding.file_path)).length;
21
- const userScopeFindings = report.findings.length - targetFindings;
16
+ const targetFindings = groups.targetFindings.length;
17
+ const localFindings = groups.localFindings.length;
22
18
  if (targetFindings === 0) {
23
- if (userScopeFindings === 0) {
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. ${userScopeFindings} finding(s) came from enabled user-scope paths (~/*).`;
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 (userScopeFindings === 0) {
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. ${userScopeFindings} additional finding(s) came from enabled user-scope paths (~/*).`;
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 || nextValue.trim().length === 0) {
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
- export function renderTerminalReport(report, options = {}) {
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
- for (const finding of report.findings) {
56
- lines.push(`[${finding.severity}] ${toAbsoluteDisplayPath(report.scan_target, finding.file_path)}`);
57
- lines.push(` ${finding.description}`);
58
- if (finding.incident_title) {
59
- appendLabeledText(lines, "Incident", finding.incident_title);
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
- appendLabeledList(lines, "Observed", finding.observed ?? []);
65
- if (finding.inference) {
66
- appendLabeledText(lines, "Inference", finding.inference);
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 (finding.layer === "L3" && finding.source_config) {
89
- const fieldSuffix = finding.source_config.field ? ` (${finding.source_config.field})` : "";
90
- lines.push(` source config: ${finding.source_config.file_path}${fieldSuffix}`);
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 visibleFindings = props.report.findings.slice(0, 5);
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] }), visibleFindings.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Findings detail:" }), visibleFindings.map((finding) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["[", finding.severity, "]", " ", toAbsoluteDisplayPath(props.report.scan_target, finding.file_path)] }), _jsx(Text, { children: finding.description }), finding.evidence ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Evidence:" }), finding.evidence.split("\n").map((line, index) => (_jsx(Text, { children: line }, `${finding.finding_id}-evidence-${index}`)))] })) : null] }, finding.finding_id))), props.report.findings.length > visibleFindings.length ? (_jsxs(Text, { color: defaultTheme.muted, children: ["...and ", props.report.findings.length - visibleFindings.length, " more findings"] })) : null] })) : 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegate-ai",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Pre-flight security scanner for AI coding tool configurations.",
5
5
  "license": "MIT",
6
6
  "type": "module",