codegate-ai 0.1.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/LICENSE +22 -0
- package/README.md +390 -0
- package/dist/cli-prompts.d.ts +6 -0
- package/dist/cli-prompts.js +94 -0
- package/dist/cli.d.ts +64 -0
- package/dist/cli.js +443 -0
- package/dist/commands/run-policy.d.ts +27 -0
- package/dist/commands/run-policy.js +39 -0
- package/dist/commands/scan-command/helpers.d.ts +28 -0
- package/dist/commands/scan-command/helpers.js +233 -0
- package/dist/commands/scan-command.d.ts +90 -0
- package/dist/commands/scan-command.js +403 -0
- package/dist/commands/undo.d.ts +5 -0
- package/dist/commands/undo.js +14 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +187 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/knowledge-base/claude-code.json +152 -0
- package/dist/knowledge-base/cline.json +224 -0
- package/dist/knowledge-base/codex.json +162 -0
- package/dist/knowledge-base/copilot.json +132 -0
- package/dist/knowledge-base/cursor.json +134 -0
- package/dist/knowledge-base/gemini-cli.json +112 -0
- package/dist/knowledge-base/jetbrains-junie.json +208 -0
- package/dist/knowledge-base/kiro.json +102 -0
- package/dist/knowledge-base/opencode.json +128 -0
- package/dist/knowledge-base/roo-code.json +116 -0
- package/dist/knowledge-base/schema.json +77 -0
- package/dist/knowledge-base/windsurf.json +80 -0
- package/dist/knowledge-base/zed.json +88 -0
- package/dist/layer1-discovery/config-parser.d.ts +12 -0
- package/dist/layer1-discovery/config-parser.js +52 -0
- package/dist/layer1-discovery/file-walker.d.ts +13 -0
- package/dist/layer1-discovery/file-walker.js +77 -0
- package/dist/layer1-discovery/knowledge-base.d.ts +36 -0
- package/dist/layer1-discovery/knowledge-base.js +58 -0
- package/dist/layer1-discovery/tool-detector.d.ts +20 -0
- package/dist/layer1-discovery/tool-detector.js +138 -0
- package/dist/layer2-static/detectors/command-exec.d.ts +11 -0
- package/dist/layer2-static/detectors/command-exec.js +343 -0
- package/dist/layer2-static/detectors/consent-bypass.d.ts +8 -0
- package/dist/layer2-static/detectors/consent-bypass.js +330 -0
- package/dist/layer2-static/detectors/env-override.d.ts +8 -0
- package/dist/layer2-static/detectors/env-override.js +132 -0
- package/dist/layer2-static/detectors/git-hooks.d.ts +11 -0
- package/dist/layer2-static/detectors/git-hooks.js +61 -0
- package/dist/layer2-static/detectors/ide-settings.d.ts +8 -0
- package/dist/layer2-static/detectors/ide-settings.js +66 -0
- package/dist/layer2-static/detectors/plugin-manifest.d.ts +9 -0
- package/dist/layer2-static/detectors/plugin-manifest.js +1943 -0
- package/dist/layer2-static/detectors/rule-file.d.ts +7 -0
- package/dist/layer2-static/detectors/rule-file.js +299 -0
- package/dist/layer2-static/detectors/symlink.d.ts +9 -0
- package/dist/layer2-static/detectors/symlink.js +45 -0
- package/dist/layer2-static/engine.d.ts +28 -0
- package/dist/layer2-static/engine.js +83 -0
- package/dist/layer2-static/evidence.d.ts +12 -0
- package/dist/layer2-static/evidence.js +128 -0
- package/dist/layer2-static/rule-engine.d.ts +24 -0
- package/dist/layer2-static/rule-engine.js +138 -0
- package/dist/layer2-static/state/scan-state.d.ts +32 -0
- package/dist/layer2-static/state/scan-state.js +296 -0
- package/dist/layer3-dynamic/command-builder.d.ts +15 -0
- package/dist/layer3-dynamic/command-builder.js +39 -0
- package/dist/layer3-dynamic/local-text-analysis.d.ts +19 -0
- package/dist/layer3-dynamic/local-text-analysis.js +73 -0
- package/dist/layer3-dynamic/meta-agent.d.ts +17 -0
- package/dist/layer3-dynamic/meta-agent.js +33 -0
- package/dist/layer3-dynamic/prompt-templates/local-text-analysis.md +32 -0
- package/dist/layer3-dynamic/prompt-templates/security-analysis.md +13 -0
- package/dist/layer3-dynamic/prompt-templates/tool-poisoning.md +15 -0
- package/dist/layer3-dynamic/resource-fetcher.d.ts +25 -0
- package/dist/layer3-dynamic/resource-fetcher.js +119 -0
- package/dist/layer3-dynamic/sandbox.d.ts +13 -0
- package/dist/layer3-dynamic/sandbox.js +40 -0
- package/dist/layer3-dynamic/tool-description-acquisition.d.ts +22 -0
- package/dist/layer3-dynamic/tool-description-acquisition.js +76 -0
- package/dist/layer3-dynamic/tool-description-scanner.d.ts +11 -0
- package/dist/layer3-dynamic/tool-description-scanner.js +53 -0
- package/dist/layer3-dynamic/toxic-flow.d.ts +12 -0
- package/dist/layer3-dynamic/toxic-flow.js +57 -0
- package/dist/layer4-remediation/actions/quarantine.d.ts +1 -0
- package/dist/layer4-remediation/actions/quarantine.js +8 -0
- package/dist/layer4-remediation/actions/remove-field.d.ts +5 -0
- package/dist/layer4-remediation/actions/remove-field.js +53 -0
- package/dist/layer4-remediation/actions/replace-value.d.ts +5 -0
- package/dist/layer4-remediation/actions/replace-value.js +26 -0
- package/dist/layer4-remediation/actions/strip-unicode.d.ts +5 -0
- package/dist/layer4-remediation/actions/strip-unicode.js +8 -0
- package/dist/layer4-remediation/backup-manager.d.ts +32 -0
- package/dist/layer4-remediation/backup-manager.js +138 -0
- package/dist/layer4-remediation/diff-generator.d.ts +6 -0
- package/dist/layer4-remediation/diff-generator.js +29 -0
- package/dist/layer4-remediation/remediation-runner.d.ts +36 -0
- package/dist/layer4-remediation/remediation-runner.js +230 -0
- package/dist/layer4-remediation/remediator.d.ts +36 -0
- package/dist/layer4-remediation/remediator.js +117 -0
- package/dist/path-display.d.ts +1 -0
- package/dist/path-display.js +20 -0
- package/dist/pipeline.d.ts +34 -0
- package/dist/pipeline.js +259 -0
- package/dist/report-summary.d.ts +6 -0
- package/dist/report-summary.js +48 -0
- package/dist/reporter/html.d.ts +2 -0
- package/dist/reporter/html.js +103 -0
- package/dist/reporter/json.d.ts +2 -0
- package/dist/reporter/json.js +3 -0
- package/dist/reporter/markdown.d.ts +2 -0
- package/dist/reporter/markdown.js +52 -0
- package/dist/reporter/sarif.d.ts +2 -0
- package/dist/reporter/sarif.js +84 -0
- package/dist/reporter/terminal.d.ts +5 -0
- package/dist/reporter/terminal.js +94 -0
- package/dist/runtime/signal-handlers.d.ts +10 -0
- package/dist/runtime/signal-handlers.js +17 -0
- package/dist/scan-target/helpers.d.ts +20 -0
- package/dist/scan-target/helpers.js +268 -0
- package/dist/scan-target/staging.d.ts +5 -0
- package/dist/scan-target/staging.js +114 -0
- package/dist/scan-target/types.d.ts +18 -0
- package/dist/scan-target/types.js +1 -0
- package/dist/scan-target.d.ts +3 -0
- package/dist/scan-target.js +31 -0
- package/dist/scan.d.ts +54 -0
- package/dist/scan.js +593 -0
- package/dist/tui/app.d.ts +10 -0
- package/dist/tui/app.js +21 -0
- package/dist/tui/theme.d.ts +8 -0
- package/dist/tui/theme.js +7 -0
- package/dist/tui/views/dashboard.d.ts +6 -0
- package/dist/tui/views/dashboard.js +8 -0
- package/dist/tui/views/deep-scan-consent.d.ts +5 -0
- package/dist/tui/views/deep-scan-consent.js +6 -0
- package/dist/tui/views/progress.d.ts +4 -0
- package/dist/tui/views/progress.js +6 -0
- package/dist/tui/views/summary.d.ts +5 -0
- package/dist/tui/views/summary.js +16 -0
- package/dist/types/discovery.d.ts +12 -0
- package/dist/types/discovery.js +1 -0
- package/dist/types/finding.d.ts +46 -0
- package/dist/types/finding.js +15 -0
- package/dist/types/report.d.ts +25 -0
- package/dist/types/report.js +23 -0
- package/dist/wrapper.d.ts +35 -0
- package/dist/wrapper.js +220 -0
- package/package.json +97 -0
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { runStaticEngine, } from "./layer2-static/engine.js";
|
|
2
|
+
import { createEmptyReport } from "./types/report.js";
|
|
3
|
+
import { scanToolDescriptions, } from "./layer3-dynamic/tool-description-scanner.js";
|
|
4
|
+
import { detectToxicFlows } from "./layer3-dynamic/toxic-flow.js";
|
|
5
|
+
import { applyReportSummary } from "./report-summary.js";
|
|
6
|
+
export function runStaticPipeline(input) {
|
|
7
|
+
const findings = runStaticEngine({
|
|
8
|
+
projectRoot: input.projectRoot,
|
|
9
|
+
files: input.files,
|
|
10
|
+
symlinkEscapes: input.symlinkEscapes,
|
|
11
|
+
hooks: input.hooks,
|
|
12
|
+
config: input.config,
|
|
13
|
+
});
|
|
14
|
+
const report = createEmptyReport({
|
|
15
|
+
version: input.version,
|
|
16
|
+
kbVersion: input.kbVersion,
|
|
17
|
+
scanTarget: input.scanTarget,
|
|
18
|
+
toolsDetected: input.toolsDetected,
|
|
19
|
+
exitCode: 0,
|
|
20
|
+
});
|
|
21
|
+
return applyReportSummary({
|
|
22
|
+
...report,
|
|
23
|
+
findings,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function parseSeverity(value) {
|
|
27
|
+
if (value === "CRITICAL" || value === "HIGH" || value === "MEDIUM" || value === "LOW") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return "INFO";
|
|
31
|
+
}
|
|
32
|
+
function parseConfidence(value) {
|
|
33
|
+
if (value === "HIGH" || value === "MEDIUM") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return "LOW";
|
|
37
|
+
}
|
|
38
|
+
function parseCategory(value) {
|
|
39
|
+
if (value === "ENV_OVERRIDE" ||
|
|
40
|
+
value === "COMMAND_EXEC" ||
|
|
41
|
+
value === "CONSENT_BYPASS" ||
|
|
42
|
+
value === "RULE_INJECTION" ||
|
|
43
|
+
value === "IDE_SETTINGS" ||
|
|
44
|
+
value === "SYMLINK_ESCAPE" ||
|
|
45
|
+
value === "GIT_HOOK" ||
|
|
46
|
+
value === "CONFIG_PRESENT" ||
|
|
47
|
+
value === "CONFIG_CHANGE" ||
|
|
48
|
+
value === "NEW_SERVER" ||
|
|
49
|
+
value === "TOXIC_FLOW") {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
return "PARSE_ERROR";
|
|
53
|
+
}
|
|
54
|
+
function parseLayer3Response(resourceId, metadata) {
|
|
55
|
+
if (!metadata || typeof metadata !== "object") {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const root = metadata;
|
|
59
|
+
if (!Array.isArray(root.findings)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return root.findings
|
|
63
|
+
.filter((item) => typeof item === "object" && item !== null)
|
|
64
|
+
.map((item, index) => {
|
|
65
|
+
const findingId = item.id ?? `L3-${resourceId}-${index}`;
|
|
66
|
+
return {
|
|
67
|
+
rule_id: item.id ?? "layer3-analysis-finding",
|
|
68
|
+
finding_id: findingId,
|
|
69
|
+
severity: parseSeverity(item.severity),
|
|
70
|
+
category: parseCategory(item.category),
|
|
71
|
+
layer: "L3",
|
|
72
|
+
file_path: item.file_path ?? resourceId,
|
|
73
|
+
location: { field: item.field },
|
|
74
|
+
description: item.description ?? "Layer 3 analysis finding",
|
|
75
|
+
affected_tools: [],
|
|
76
|
+
cve: null,
|
|
77
|
+
owasp: Array.isArray(item.owasp) ? item.owasp : [],
|
|
78
|
+
cwe: item.cwe ?? "CWE-20",
|
|
79
|
+
confidence: parseConfidence(item.confidence),
|
|
80
|
+
evidence: typeof item.evidence === "string" ? item.evidence : null,
|
|
81
|
+
fixable: item.fixable ?? false,
|
|
82
|
+
remediation_actions: item.remediation_actions ?? [],
|
|
83
|
+
source_config: item.source_config ?? null,
|
|
84
|
+
suppressed: false,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function asRecord(value) {
|
|
89
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
function parseToxicClass(value) {
|
|
95
|
+
if (value === "untrusted_input" ||
|
|
96
|
+
value === "sensitive_access" ||
|
|
97
|
+
value === "exfiltration_sink") {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function parseToxicClasses(value) {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
const parsed = parseToxicClass(value);
|
|
105
|
+
return parsed ? [parsed] : [];
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(value)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const classes = [];
|
|
111
|
+
for (const item of value) {
|
|
112
|
+
if (typeof item !== "string") {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const parsed = parseToxicClass(item);
|
|
116
|
+
if (parsed) {
|
|
117
|
+
classes.push(parsed);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return classes;
|
|
121
|
+
}
|
|
122
|
+
function parseToolEntries(metadata) {
|
|
123
|
+
const root = asRecord(metadata);
|
|
124
|
+
if (!root || !Array.isArray(root.tools)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return root.tools
|
|
128
|
+
.map((entry) => asRecord(entry))
|
|
129
|
+
.filter((entry) => entry !== null)
|
|
130
|
+
.map((entry) => {
|
|
131
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
132
|
+
const description = typeof entry.description === "string" ? entry.description : "";
|
|
133
|
+
const classifications = parseToxicClasses(entry.classifications ?? entry.classification);
|
|
134
|
+
return { name, description, classifications };
|
|
135
|
+
})
|
|
136
|
+
.filter((entry) => entry.name.length > 0 && entry.description.length > 0);
|
|
137
|
+
}
|
|
138
|
+
function parseToolClassifications(metadata, tools) {
|
|
139
|
+
const map = {};
|
|
140
|
+
const root = asRecord(metadata);
|
|
141
|
+
for (const tool of tools) {
|
|
142
|
+
if (!tool.classifications || tool.classifications.length === 0) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
map[tool.name] = tool.classifications;
|
|
146
|
+
}
|
|
147
|
+
const classificationRoot = asRecord(root?.tool_classifications ?? root?.toolClassifications);
|
|
148
|
+
if (!classificationRoot) {
|
|
149
|
+
return map;
|
|
150
|
+
}
|
|
151
|
+
for (const [toolName, value] of Object.entries(classificationRoot)) {
|
|
152
|
+
const parsed = parseToxicClasses(value);
|
|
153
|
+
if (parsed.length === 0) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
map[toolName] = parsed;
|
|
157
|
+
}
|
|
158
|
+
return map;
|
|
159
|
+
}
|
|
160
|
+
function deriveLayer3ToolFindings(resourceId, metadata, options = {}) {
|
|
161
|
+
const toolEntries = parseToolEntries(metadata);
|
|
162
|
+
if (toolEntries.length === 0) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const toolDescriptions = toolEntries.map((entry) => ({
|
|
166
|
+
name: entry.name,
|
|
167
|
+
description: entry.description,
|
|
168
|
+
}));
|
|
169
|
+
const knownClassifications = parseToolClassifications(metadata, toolEntries);
|
|
170
|
+
return [
|
|
171
|
+
...scanToolDescriptions({
|
|
172
|
+
serverId: resourceId,
|
|
173
|
+
tools: toolDescriptions,
|
|
174
|
+
unicodeAnalysis: options.unicodeAnalysis,
|
|
175
|
+
}),
|
|
176
|
+
...detectToxicFlows({
|
|
177
|
+
scopeId: resourceId,
|
|
178
|
+
tools: toolDescriptions,
|
|
179
|
+
knownClassifications,
|
|
180
|
+
}),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
function layer3ErrorFinding(resourceId, status, description) {
|
|
184
|
+
const severity = status === "timeout" ? "MEDIUM" : status === "skipped_without_consent" ? "INFO" : "LOW";
|
|
185
|
+
return {
|
|
186
|
+
rule_id: `layer3-${status}`,
|
|
187
|
+
finding_id: `L3-${status}-${resourceId}`,
|
|
188
|
+
severity,
|
|
189
|
+
category: "PARSE_ERROR",
|
|
190
|
+
layer: "L3",
|
|
191
|
+
file_path: resourceId,
|
|
192
|
+
location: { field: "layer3" },
|
|
193
|
+
description,
|
|
194
|
+
affected_tools: [],
|
|
195
|
+
cve: null,
|
|
196
|
+
owasp: [],
|
|
197
|
+
cwe: "CWE-20",
|
|
198
|
+
confidence: "HIGH",
|
|
199
|
+
fixable: false,
|
|
200
|
+
remediation_actions: [],
|
|
201
|
+
suppressed: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function isRegistryMetadataResource(resourceId) {
|
|
205
|
+
return (resourceId.startsWith("npm:") || resourceId.startsWith("pypi:") || resourceId.startsWith("git:"));
|
|
206
|
+
}
|
|
207
|
+
export function layer3OutcomesToFindings(outcomes, options = {}) {
|
|
208
|
+
const findings = [];
|
|
209
|
+
for (const outcome of outcomes) {
|
|
210
|
+
if (!outcome.approved || outcome.status === "skipped_without_consent") {
|
|
211
|
+
findings.push(layer3ErrorFinding(outcome.resourceId, "skipped_without_consent", "Deep scan skipped because consent was not granted"));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (outcome.status !== "ok" || !outcome.result) {
|
|
215
|
+
findings.push(layer3ErrorFinding(outcome.resourceId, outcome.status, `Deep scan failed with status: ${outcome.status}`));
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const parsed = parseLayer3Response(outcome.resourceId, outcome.result.metadata);
|
|
219
|
+
const derived = deriveLayer3ToolFindings(outcome.resourceId, outcome.result.metadata, options);
|
|
220
|
+
const combined = [...parsed, ...derived];
|
|
221
|
+
if (combined.length === 0) {
|
|
222
|
+
if (isRegistryMetadataResource(outcome.resourceId)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
findings.push(layer3ErrorFinding(outcome.resourceId, "network_error", "Deep scan response schema mismatch: expected metadata.findings[] or metadata.tools[]"));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
findings.push(...combined);
|
|
229
|
+
}
|
|
230
|
+
return findings;
|
|
231
|
+
}
|
|
232
|
+
export function mergeLayer3Findings(baseReport, layer3Findings) {
|
|
233
|
+
return applyReportSummary({
|
|
234
|
+
...baseReport,
|
|
235
|
+
findings: [...baseReport.findings, ...layer3Findings],
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
export async function runDeepScanWithConsent(resources, requestConsent, execute) {
|
|
239
|
+
const outcomes = [];
|
|
240
|
+
for (const resource of resources) {
|
|
241
|
+
const approved = await requestConsent(resource);
|
|
242
|
+
if (!approved) {
|
|
243
|
+
outcomes.push({
|
|
244
|
+
resourceId: resource.id,
|
|
245
|
+
approved: false,
|
|
246
|
+
status: "skipped_without_consent",
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const result = await execute(resource);
|
|
251
|
+
outcomes.push({
|
|
252
|
+
resourceId: resource.id,
|
|
253
|
+
approved: true,
|
|
254
|
+
status: result.status,
|
|
255
|
+
result,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return outcomes;
|
|
259
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Finding } from "./types/finding.js";
|
|
2
|
+
import type { CodeGateReport, ReportSummary } from "./types/report.js";
|
|
3
|
+
export type ReportThreshold = "critical" | "high" | "medium" | "low" | "info";
|
|
4
|
+
export declare function computeExitCode(findings: Finding[], threshold?: ReportThreshold): number;
|
|
5
|
+
export declare function summarizeFindings(findings: Finding[], threshold?: ReportThreshold): ReportSummary;
|
|
6
|
+
export declare function applyReportSummary(report: CodeGateReport, threshold?: ReportThreshold): CodeGateReport;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const SEVERITY_LEVEL = {
|
|
2
|
+
CRITICAL: 4,
|
|
3
|
+
HIGH: 3,
|
|
4
|
+
MEDIUM: 2,
|
|
5
|
+
LOW: 1,
|
|
6
|
+
INFO: 0,
|
|
7
|
+
};
|
|
8
|
+
const THRESHOLD_LEVEL = {
|
|
9
|
+
critical: 4,
|
|
10
|
+
high: 3,
|
|
11
|
+
medium: 2,
|
|
12
|
+
low: 1,
|
|
13
|
+
info: 0,
|
|
14
|
+
};
|
|
15
|
+
export function computeExitCode(findings, threshold = "high") {
|
|
16
|
+
const unsuppressed = findings.filter((finding) => !finding.suppressed);
|
|
17
|
+
if (unsuppressed.length === 0) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const thresholdLevel = THRESHOLD_LEVEL[threshold];
|
|
21
|
+
const hasDangerous = unsuppressed.some((finding) => SEVERITY_LEVEL[finding.severity] >= thresholdLevel);
|
|
22
|
+
return hasDangerous ? 2 : 1;
|
|
23
|
+
}
|
|
24
|
+
export function summarizeFindings(findings, threshold = "high") {
|
|
25
|
+
const bySeverity = {
|
|
26
|
+
CRITICAL: 0,
|
|
27
|
+
HIGH: 0,
|
|
28
|
+
MEDIUM: 0,
|
|
29
|
+
LOW: 0,
|
|
30
|
+
INFO: 0,
|
|
31
|
+
};
|
|
32
|
+
for (const finding of findings) {
|
|
33
|
+
bySeverity[finding.severity] = (bySeverity[finding.severity] ?? 0) + 1;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
total: findings.length,
|
|
37
|
+
by_severity: bySeverity,
|
|
38
|
+
fixable: findings.filter((finding) => finding.fixable).length,
|
|
39
|
+
suppressed: findings.filter((finding) => finding.suppressed).length,
|
|
40
|
+
exit_code: computeExitCode(findings, threshold),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function applyReportSummary(report, threshold = "high") {
|
|
44
|
+
return {
|
|
45
|
+
...report,
|
|
46
|
+
summary: summarizeFindings(report.findings, threshold),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function escapeHtml(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replaceAll("&", "&")
|
|
4
|
+
.replaceAll("<", "<")
|
|
5
|
+
.replaceAll(">", ">")
|
|
6
|
+
.replaceAll('"', """)
|
|
7
|
+
.replaceAll("'", "'");
|
|
8
|
+
}
|
|
9
|
+
function formatLocation(location) {
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (location.field) {
|
|
12
|
+
parts.push(location.field);
|
|
13
|
+
}
|
|
14
|
+
if (typeof location.line === "number") {
|
|
15
|
+
parts.push(`line ${location.line}`);
|
|
16
|
+
}
|
|
17
|
+
if (typeof location.column === "number") {
|
|
18
|
+
parts.push(`col ${location.column}`);
|
|
19
|
+
}
|
|
20
|
+
return parts.join(", ") || "-";
|
|
21
|
+
}
|
|
22
|
+
function renderSummary(report) {
|
|
23
|
+
return `
|
|
24
|
+
<ul>
|
|
25
|
+
<li><strong>Total findings:</strong> ${report.summary.total}</li>
|
|
26
|
+
<li><strong>CRITICAL:</strong> ${report.summary.by_severity.CRITICAL ?? 0}</li>
|
|
27
|
+
<li><strong>HIGH:</strong> ${report.summary.by_severity.HIGH ?? 0}</li>
|
|
28
|
+
<li><strong>MEDIUM:</strong> ${report.summary.by_severity.MEDIUM ?? 0}</li>
|
|
29
|
+
<li><strong>LOW:</strong> ${report.summary.by_severity.LOW ?? 0}</li>
|
|
30
|
+
<li><strong>INFO:</strong> ${report.summary.by_severity.INFO ?? 0}</li>
|
|
31
|
+
<li><strong>Fixable:</strong> ${report.summary.fixable}</li>
|
|
32
|
+
<li><strong>Suppressed:</strong> ${report.summary.suppressed}</li>
|
|
33
|
+
<li><strong>Exit code:</strong> ${report.summary.exit_code}</li>
|
|
34
|
+
</ul>
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
function renderFindings(report) {
|
|
38
|
+
if (report.findings.length === 0) {
|
|
39
|
+
return "<p>No findings.</p>";
|
|
40
|
+
}
|
|
41
|
+
const rows = report.findings
|
|
42
|
+
.map((finding) => {
|
|
43
|
+
const location = formatLocation(finding.location);
|
|
44
|
+
return `
|
|
45
|
+
<tr>
|
|
46
|
+
<td>${escapeHtml(finding.severity)}</td>
|
|
47
|
+
<td>${escapeHtml(finding.category)}</td>
|
|
48
|
+
<td>${escapeHtml(finding.file_path)}</td>
|
|
49
|
+
<td>${escapeHtml(location)}</td>
|
|
50
|
+
<td>${escapeHtml(finding.description)}</td>
|
|
51
|
+
</tr>
|
|
52
|
+
`;
|
|
53
|
+
})
|
|
54
|
+
.join("\n");
|
|
55
|
+
return `
|
|
56
|
+
<table>
|
|
57
|
+
<thead>
|
|
58
|
+
<tr>
|
|
59
|
+
<th>Severity</th>
|
|
60
|
+
<th>Category</th>
|
|
61
|
+
<th>File</th>
|
|
62
|
+
<th>Location</th>
|
|
63
|
+
<th>Description</th>
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
<tbody>
|
|
67
|
+
${rows}
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
export function renderHtmlReport(report) {
|
|
73
|
+
return `<!doctype html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<head>
|
|
76
|
+
<meta charset="utf-8" />
|
|
77
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
78
|
+
<title>CodeGate Report</title>
|
|
79
|
+
<style>
|
|
80
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 24px; color: #0f172a; }
|
|
81
|
+
h1, h2 { margin: 0 0 12px 0; }
|
|
82
|
+
.meta { margin: 0 0 20px 0; color: #334155; }
|
|
83
|
+
table { border-collapse: collapse; width: 100%; margin-top: 12px; }
|
|
84
|
+
th, td { border: 1px solid #cbd5e1; padding: 8px; text-align: left; vertical-align: top; }
|
|
85
|
+
th { background: #f1f5f9; }
|
|
86
|
+
code { background: #f8fafc; padding: 2px 4px; border-radius: 4px; }
|
|
87
|
+
</style>
|
|
88
|
+
</head>
|
|
89
|
+
<body>
|
|
90
|
+
<h1>CodeGate Report</h1>
|
|
91
|
+
<p class="meta">
|
|
92
|
+
Version <code>${escapeHtml(report.version)}</code> |
|
|
93
|
+
Target <code>${escapeHtml(report.scan_target)}</code> |
|
|
94
|
+
KB <code>${escapeHtml(report.kb_version)}</code> |
|
|
95
|
+
Generated <code>${escapeHtml(report.timestamp)}</code>
|
|
96
|
+
</p>
|
|
97
|
+
<h2>Summary</h2>
|
|
98
|
+
${renderSummary(report)}
|
|
99
|
+
<h2>Findings</h2>
|
|
100
|
+
${renderFindings(report)}
|
|
101
|
+
</body>
|
|
102
|
+
</html>`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function escapePipes(value) {
|
|
2
|
+
return value.replaceAll("|", "\\|");
|
|
3
|
+
}
|
|
4
|
+
function formatLocation(location) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
if (location.field) {
|
|
7
|
+
parts.push(location.field);
|
|
8
|
+
}
|
|
9
|
+
if (typeof location.line === "number") {
|
|
10
|
+
parts.push(`line ${location.line}`);
|
|
11
|
+
}
|
|
12
|
+
if (typeof location.column === "number") {
|
|
13
|
+
parts.push(`col ${location.column}`);
|
|
14
|
+
}
|
|
15
|
+
return parts.join(", ") || "-";
|
|
16
|
+
}
|
|
17
|
+
export function renderMarkdownReport(report) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
lines.push("# CodeGate Report");
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push(`- Version: \`${report.version}\``);
|
|
22
|
+
lines.push(`- Target: \`${report.scan_target}\``);
|
|
23
|
+
lines.push(`- Timestamp: \`${report.timestamp}\``);
|
|
24
|
+
lines.push(`- KB Version: \`${report.kb_version}\``);
|
|
25
|
+
lines.push(`- Exit Code: \`${report.summary.exit_code}\``);
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push("## Summary");
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push("| Metric | Value |");
|
|
30
|
+
lines.push("| --- | --- |");
|
|
31
|
+
lines.push(`| Total findings | ${report.summary.total} |`);
|
|
32
|
+
lines.push(`| CRITICAL | ${report.summary.by_severity.CRITICAL ?? 0} |`);
|
|
33
|
+
lines.push(`| HIGH | ${report.summary.by_severity.HIGH ?? 0} |`);
|
|
34
|
+
lines.push(`| MEDIUM | ${report.summary.by_severity.MEDIUM ?? 0} |`);
|
|
35
|
+
lines.push(`| LOW | ${report.summary.by_severity.LOW ?? 0} |`);
|
|
36
|
+
lines.push(`| INFO | ${report.summary.by_severity.INFO ?? 0} |`);
|
|
37
|
+
lines.push(`| Fixable | ${report.summary.fixable} |`);
|
|
38
|
+
lines.push(`| Suppressed | ${report.summary.suppressed} |`);
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("## Findings");
|
|
41
|
+
lines.push("");
|
|
42
|
+
if (report.findings.length === 0) {
|
|
43
|
+
lines.push("No findings.");
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
lines.push("| Severity | Category | File | Location | Description |");
|
|
47
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
48
|
+
for (const finding of report.findings) {
|
|
49
|
+
lines.push(`| ${finding.severity} | ${finding.category} | \`${escapePipes(finding.file_path)}\` | ${escapePipes(formatLocation(finding.location))} | ${escapePipes(finding.description)} |`);
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function toSarifLevel(severity) {
|
|
2
|
+
if (severity === "CRITICAL" || severity === "HIGH") {
|
|
3
|
+
return "error";
|
|
4
|
+
}
|
|
5
|
+
if (severity === "MEDIUM") {
|
|
6
|
+
return "warning";
|
|
7
|
+
}
|
|
8
|
+
return "note";
|
|
9
|
+
}
|
|
10
|
+
function findingToLocation(finding) {
|
|
11
|
+
const region = {};
|
|
12
|
+
if (typeof finding.location.line === "number") {
|
|
13
|
+
region.startLine = finding.location.line;
|
|
14
|
+
}
|
|
15
|
+
if (typeof finding.location.column === "number") {
|
|
16
|
+
region.startColumn = finding.location.column;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
physicalLocation: {
|
|
20
|
+
artifactLocation: {
|
|
21
|
+
uri: finding.file_path,
|
|
22
|
+
},
|
|
23
|
+
region: Object.keys(region).length > 0 ? region : undefined,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function buildRules(findings) {
|
|
28
|
+
const byRuleId = new Map();
|
|
29
|
+
for (const finding of findings) {
|
|
30
|
+
if (byRuleId.has(finding.rule_id)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
byRuleId.set(finding.rule_id, {
|
|
34
|
+
id: finding.rule_id,
|
|
35
|
+
shortDescription: { text: finding.description },
|
|
36
|
+
properties: {
|
|
37
|
+
category: finding.category,
|
|
38
|
+
layer: finding.layer,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return Array.from(byRuleId.values());
|
|
43
|
+
}
|
|
44
|
+
function findingToResult(finding) {
|
|
45
|
+
return {
|
|
46
|
+
ruleId: finding.rule_id,
|
|
47
|
+
level: toSarifLevel(finding.severity),
|
|
48
|
+
message: { text: finding.description },
|
|
49
|
+
locations: [findingToLocation(finding)],
|
|
50
|
+
properties: {
|
|
51
|
+
finding_id: finding.finding_id,
|
|
52
|
+
severity: finding.severity,
|
|
53
|
+
category: finding.category,
|
|
54
|
+
layer: finding.layer,
|
|
55
|
+
confidence: finding.confidence,
|
|
56
|
+
cve: finding.cve,
|
|
57
|
+
owasp: finding.owasp,
|
|
58
|
+
cwe: finding.cwe,
|
|
59
|
+
evidence: finding.evidence ?? null,
|
|
60
|
+
fixable: finding.fixable,
|
|
61
|
+
suppressed: finding.suppressed,
|
|
62
|
+
source_config: finding.source_config ?? null,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function renderSarifReport(report) {
|
|
67
|
+
const sarif = {
|
|
68
|
+
version: "2.1.0",
|
|
69
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
70
|
+
runs: [
|
|
71
|
+
{
|
|
72
|
+
tool: {
|
|
73
|
+
driver: {
|
|
74
|
+
name: "CodeGate",
|
|
75
|
+
semanticVersion: report.version,
|
|
76
|
+
rules: buildRules(report.findings),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
results: report.findings.map(findingToResult),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
return JSON.stringify(sarif, null, 2);
|
|
84
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { toAbsoluteDisplayPath } from "../path-display.js";
|
|
2
|
+
function appendLabeledList(lines, label, values) {
|
|
3
|
+
if (values.length === 0) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
lines.push(` ${label}:`);
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
lines.push(` - ${value}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function appendLabeledText(lines, label, value) {
|
|
12
|
+
if (value.length === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
lines.push(` ${label}: ${value}`);
|
|
16
|
+
}
|
|
17
|
+
function appendEvidence(lines, evidence) {
|
|
18
|
+
const evidenceLines = evidence.split("\n");
|
|
19
|
+
if (evidenceLines.length === 1) {
|
|
20
|
+
lines.push(` Evidence: ${evidenceLines[0]}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
lines.push(" Evidence:");
|
|
24
|
+
for (const evidenceLine of evidenceLines) {
|
|
25
|
+
lines.push(` ${evidenceLine}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function formatLocation(location) {
|
|
29
|
+
const parts = [];
|
|
30
|
+
if (location.field) {
|
|
31
|
+
parts.push(location.field);
|
|
32
|
+
}
|
|
33
|
+
if (typeof location.line === "number") {
|
|
34
|
+
const column = typeof location.column === "number" ? `:${location.column}` : "";
|
|
35
|
+
parts.push(`line ${location.line}${column}`);
|
|
36
|
+
}
|
|
37
|
+
return parts.length > 0 ? parts.join(" @ ") : null;
|
|
38
|
+
}
|
|
39
|
+
export function renderTerminalReport(report, options = {}) {
|
|
40
|
+
const verbose = options.verbose === true;
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push(`CodeGate v${report.version}`);
|
|
43
|
+
lines.push(`Target: ${report.scan_target}`);
|
|
44
|
+
lines.push(`Findings: ${report.summary.total}`);
|
|
45
|
+
lines.push(`CRITICAL: ${report.summary.by_severity.CRITICAL ?? 0}`);
|
|
46
|
+
lines.push(`HIGH: ${report.summary.by_severity.HIGH ?? 0}`);
|
|
47
|
+
lines.push(`MEDIUM: ${report.summary.by_severity.MEDIUM ?? 0}`);
|
|
48
|
+
lines.push(`LOW: ${report.summary.by_severity.LOW ?? 0}`);
|
|
49
|
+
lines.push(`INFO: ${report.summary.by_severity.INFO ?? 0}`);
|
|
50
|
+
lines.push("");
|
|
51
|
+
if (report.findings.length === 0) {
|
|
52
|
+
lines.push("No findings.");
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
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);
|
|
63
|
+
}
|
|
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(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
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}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SignalProcessLike {
|
|
2
|
+
on: (event: string, listener: () => void) => unknown;
|
|
3
|
+
off: (event: string, listener: () => void) => unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface RegisterSignalHandlersOptions {
|
|
6
|
+
processLike?: SignalProcessLike;
|
|
7
|
+
signals?: Array<"SIGINT" | "SIGTERM">;
|
|
8
|
+
onSignal: (signal: "SIGINT" | "SIGTERM") => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function registerSignalHandlers(options: RegisterSignalHandlersOptions): () => void;
|