codegate-ai 0.6.1 → 0.8.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/README.md +61 -25
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +59 -41
- package/dist/commands/scan-command/helpers.d.ts +6 -1
- package/dist/commands/scan-command/helpers.js +46 -1
- package/dist/commands/scan-command.js +49 -55
- package/dist/commands/scan-content-command.d.ts +16 -0
- package/dist/commands/scan-content-command.js +61 -0
- package/dist/config/suppression-policy.d.ts +14 -0
- package/dist/config/suppression-policy.js +81 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +29 -3
- package/dist/layer2-static/advisories/agent-components.json +62 -0
- package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
- package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
- package/dist/layer2-static/detectors/command-exec.js +6 -0
- package/dist/layer2-static/detectors/rule-file.js +5 -0
- package/dist/layer2-static/engine.d.ts +4 -1
- package/dist/layer2-static/engine.js +97 -0
- package/dist/layer2-static/rule-engine.d.ts +1 -1
- package/dist/layer2-static/rule-engine.js +1 -13
- package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
- package/dist/layer2-static/rule-pack-loader.js +187 -0
- package/dist/layer3-dynamic/command-builder.d.ts +1 -0
- package/dist/layer3-dynamic/command-builder.js +44 -2
- package/dist/layer3-dynamic/local-text-analysis.d.ts +9 -1
- package/dist/layer3-dynamic/local-text-analysis.js +12 -27
- package/dist/layer3-dynamic/meta-agent.d.ts +1 -2
- package/dist/layer3-dynamic/meta-agent.js +3 -6
- package/dist/layer3-dynamic/prompt-templates/local-text-analysis.md +33 -21
- package/dist/layer3-dynamic/prompt-templates/security-analysis.md +11 -1
- package/dist/layer3-dynamic/prompt-templates/tool-poisoning.md +9 -1
- package/dist/layer3-dynamic/toxic-flow.js +6 -0
- package/dist/pipeline.js +9 -8
- package/dist/report/finding-fingerprint.d.ts +5 -0
- package/dist/report/finding-fingerprint.js +47 -0
- package/dist/reporter/markdown.js +25 -3
- package/dist/reporter/sarif.js +2 -0
- package/dist/reporter/terminal.js +25 -0
- package/dist/scan-target/fetch-plan.d.ts +8 -0
- package/dist/scan-target/fetch-plan.js +30 -0
- package/dist/scan-target/staging.js +60 -5
- package/dist/scan.js +3 -0
- package/dist/types/finding.d.ts +9 -0
- package/package.json +3 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function normalizeString(value) {
|
|
2
|
+
if (typeof value !== "string") {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
7
|
+
}
|
|
8
|
+
function escapeRegExp(value) {
|
|
9
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10
|
+
}
|
|
11
|
+
function globToRegExp(glob) {
|
|
12
|
+
const pattern = normalizeString(glob)?.replaceAll("\\", "/");
|
|
13
|
+
if (!pattern) {
|
|
14
|
+
return /^$/;
|
|
15
|
+
}
|
|
16
|
+
let regex = "^";
|
|
17
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
18
|
+
const char = pattern[index];
|
|
19
|
+
if (char === "*") {
|
|
20
|
+
if (pattern[index + 1] === "*") {
|
|
21
|
+
regex += ".*";
|
|
22
|
+
index++;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
regex += "[^/]*";
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === "?") {
|
|
30
|
+
regex += "[^/]";
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
regex += escapeRegExp(char);
|
|
34
|
+
}
|
|
35
|
+
regex += "$";
|
|
36
|
+
return new RegExp(regex);
|
|
37
|
+
}
|
|
38
|
+
function matchesGlob(value, glob) {
|
|
39
|
+
return globToRegExp(glob).test(value.replaceAll("\\", "/"));
|
|
40
|
+
}
|
|
41
|
+
function matchesSuppressionRule(finding, rule) {
|
|
42
|
+
const ruleId = normalizeString(rule.rule_id);
|
|
43
|
+
if (ruleId && finding.rule_id !== ruleId) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const filePath = normalizeString(rule.file_path);
|
|
47
|
+
if (filePath && !matchesGlob(finding.file_path, filePath)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const severity = rule.severity;
|
|
51
|
+
if (severity && finding.severity !== severity) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const category = normalizeString(rule.category);
|
|
55
|
+
if (category && finding.category !== category) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const cwe = normalizeString(rule.cwe);
|
|
59
|
+
if (cwe && finding.cwe !== cwe) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const fingerprint = normalizeString(rule.fingerprint);
|
|
63
|
+
if (fingerprint && finding.fingerprint !== fingerprint) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
export function applySuppressionPolicy(findings, policy) {
|
|
69
|
+
const legacySuppressions = new Set((policy.suppress_findings ?? [])
|
|
70
|
+
.map((findingId) => normalizeString(findingId))
|
|
71
|
+
.filter((findingId) => findingId !== undefined));
|
|
72
|
+
const rules = policy.suppression_rules ?? [];
|
|
73
|
+
return findings.map((finding) => {
|
|
74
|
+
const ruleMatch = rules.some((rule) => matchesSuppressionRule(finding, rule));
|
|
75
|
+
const legacyMatch = legacySuppressions.has(finding.finding_id);
|
|
76
|
+
return {
|
|
77
|
+
...finding,
|
|
78
|
+
suppressed: finding.suppressed || legacyMatch || ruleMatch,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Finding } from "./types/finding.js";
|
|
2
2
|
import type { CodeGateReport } from "./types/report.js";
|
|
3
|
+
import { type SuppressionRule } from "./config/suppression-policy.js";
|
|
3
4
|
export declare const OUTPUT_FORMATS: readonly ["terminal", "json", "sarif", "markdown", "html"];
|
|
4
5
|
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
5
6
|
export declare const SEVERITY_THRESHOLDS: readonly ["critical", "high", "medium", "low", "info"];
|
|
@@ -32,7 +33,11 @@ export interface CodeGateConfig {
|
|
|
32
33
|
check_ide_settings: boolean;
|
|
33
34
|
owasp_mapping: boolean;
|
|
34
35
|
trusted_api_domains: string[];
|
|
36
|
+
rule_pack_paths?: string[];
|
|
37
|
+
allowed_rules?: string[];
|
|
38
|
+
skip_rules?: string[];
|
|
35
39
|
suppress_findings: string[];
|
|
40
|
+
suppression_rules?: SuppressionRule[];
|
|
36
41
|
}
|
|
37
42
|
export interface CliConfigOverrides {
|
|
38
43
|
format?: OutputFormat;
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
5
|
import { applyReportSummary, computeExitCode as computeReportExitCode } from "./report-summary.js";
|
|
6
|
+
import { applySuppressionPolicy } from "./config/suppression-policy.js";
|
|
6
7
|
export const OUTPUT_FORMATS = ["terminal", "json", "sarif", "markdown", "html"];
|
|
7
8
|
export const SEVERITY_THRESHOLDS = ["critical", "high", "medium", "low", "info"];
|
|
8
9
|
export const DEFAULT_CONFIG = {
|
|
@@ -34,7 +35,11 @@ export const DEFAULT_CONFIG = {
|
|
|
34
35
|
check_ide_settings: true,
|
|
35
36
|
owasp_mapping: true,
|
|
36
37
|
trusted_api_domains: [],
|
|
38
|
+
rule_pack_paths: [],
|
|
39
|
+
allowed_rules: [],
|
|
40
|
+
skip_rules: [],
|
|
37
41
|
suppress_findings: [],
|
|
42
|
+
suppression_rules: [],
|
|
38
43
|
};
|
|
39
44
|
function normalizeOutputFormat(value) {
|
|
40
45
|
if (!value) {
|
|
@@ -163,22 +168,43 @@ export function resolveEffectiveConfig(options) {
|
|
|
163
168
|
globalConfig.trusted_api_domains,
|
|
164
169
|
projectConfig.trusted_api_domains,
|
|
165
170
|
]),
|
|
171
|
+
rule_pack_paths: unique([
|
|
172
|
+
DEFAULT_CONFIG.rule_pack_paths,
|
|
173
|
+
globalConfig.rule_pack_paths,
|
|
174
|
+
projectConfig.rule_pack_paths,
|
|
175
|
+
]),
|
|
176
|
+
allowed_rules: unique([
|
|
177
|
+
DEFAULT_CONFIG.allowed_rules,
|
|
178
|
+
globalConfig.allowed_rules,
|
|
179
|
+
projectConfig.allowed_rules,
|
|
180
|
+
]),
|
|
181
|
+
skip_rules: unique([
|
|
182
|
+
DEFAULT_CONFIG.skip_rules,
|
|
183
|
+
globalConfig.skip_rules,
|
|
184
|
+
projectConfig.skip_rules,
|
|
185
|
+
]),
|
|
166
186
|
suppress_findings: unique([
|
|
167
187
|
DEFAULT_CONFIG.suppress_findings,
|
|
168
188
|
globalConfig.suppress_findings,
|
|
169
189
|
projectConfig.suppress_findings,
|
|
170
190
|
]),
|
|
191
|
+
suppression_rules: [
|
|
192
|
+
...(DEFAULT_CONFIG.suppression_rules ?? []),
|
|
193
|
+
...(globalConfig.suppression_rules ?? []),
|
|
194
|
+
...(projectConfig.suppression_rules ?? []),
|
|
195
|
+
],
|
|
171
196
|
};
|
|
172
197
|
}
|
|
173
198
|
export function computeExitCode(findings, threshold) {
|
|
174
199
|
return computeReportExitCode(findings, threshold);
|
|
175
200
|
}
|
|
176
201
|
export function applyConfigPolicy(report, config) {
|
|
177
|
-
const
|
|
178
|
-
|
|
202
|
+
const findings = applySuppressionPolicy(report.findings, {
|
|
203
|
+
suppress_findings: config.suppress_findings,
|
|
204
|
+
suppression_rules: config.suppression_rules,
|
|
205
|
+
}).map((finding) => ({
|
|
179
206
|
...finding,
|
|
180
207
|
owasp: config.owasp_mapping ? finding.owasp : [],
|
|
181
|
-
suppressed: finding.suppressed || suppressionSet.has(finding.finding_id),
|
|
182
208
|
}));
|
|
183
209
|
return applyReportSummary({
|
|
184
210
|
...report,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "filesystem",
|
|
4
|
+
"rule_id": "advisory-agent-component-filesystem",
|
|
5
|
+
"severity": "MEDIUM",
|
|
6
|
+
"category": "CONFIG_PRESENT",
|
|
7
|
+
"description": "Filesystem agent components can expose broad local file access and should be reviewed before approval.",
|
|
8
|
+
"signatures": ["@anthropic/mcp-server-filesystem", "mcp-server-filesystem"],
|
|
9
|
+
"file_patterns": ["**/.mcp.json", "**/mcp.json", "**/settings.json"],
|
|
10
|
+
"remediation_actions": ["remove_field", "review_component"],
|
|
11
|
+
"metadata": {
|
|
12
|
+
"sources": ["mcpServers.*.command", "mcpServers.*.args"],
|
|
13
|
+
"sinks": ["local_filesystem"],
|
|
14
|
+
"referenced_secrets": [],
|
|
15
|
+
"risk_tags": ["sensitive_access"],
|
|
16
|
+
"origin": "agent-components.json"
|
|
17
|
+
},
|
|
18
|
+
"owasp": ["ASI03", "ASI07"],
|
|
19
|
+
"cwe": "CWE-200",
|
|
20
|
+
"confidence": "HIGH"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "github",
|
|
24
|
+
"rule_id": "advisory-agent-component-github",
|
|
25
|
+
"severity": "MEDIUM",
|
|
26
|
+
"category": "CONFIG_PRESENT",
|
|
27
|
+
"description": "GitHub agent components can ingest untrusted repository content and deserve manual review.",
|
|
28
|
+
"signatures": ["@modelcontextprotocol/server-github", "server-github"],
|
|
29
|
+
"file_patterns": ["**/.mcp.json", "**/mcp.json", "**/settings.json"],
|
|
30
|
+
"remediation_actions": ["remove_field", "review_component"],
|
|
31
|
+
"metadata": {
|
|
32
|
+
"sources": ["mcpServers.*.command", "mcpServers.*.args"],
|
|
33
|
+
"sinks": ["repository_content"],
|
|
34
|
+
"referenced_secrets": [],
|
|
35
|
+
"risk_tags": ["untrusted_input"],
|
|
36
|
+
"origin": "agent-components.json"
|
|
37
|
+
},
|
|
38
|
+
"owasp": ["ASI01", "ASI07"],
|
|
39
|
+
"cwe": "CWE-74",
|
|
40
|
+
"confidence": "HIGH"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "slack",
|
|
44
|
+
"rule_id": "advisory-agent-component-slack",
|
|
45
|
+
"severity": "HIGH",
|
|
46
|
+
"category": "CONFIG_PRESENT",
|
|
47
|
+
"description": "Slack agent components can become exfiltration sinks and should be approved deliberately.",
|
|
48
|
+
"signatures": ["@modelcontextprotocol/server-slack", "server-slack"],
|
|
49
|
+
"file_patterns": ["**/.mcp.json", "**/mcp.json", "**/settings.json"],
|
|
50
|
+
"remediation_actions": ["remove_field", "review_component"],
|
|
51
|
+
"metadata": {
|
|
52
|
+
"sources": ["mcpServers.*.command", "mcpServers.*.args"],
|
|
53
|
+
"sinks": ["message_exfiltration"],
|
|
54
|
+
"referenced_secrets": [],
|
|
55
|
+
"risk_tags": ["exfiltration_sink"],
|
|
56
|
+
"origin": "agent-components.json"
|
|
57
|
+
},
|
|
58
|
+
"owasp": ["ASI07", "ASI09"],
|
|
59
|
+
"cwe": "CWE-200",
|
|
60
|
+
"confidence": "HIGH"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
5
|
+
const COMPONENTS_PATH = join(dirname(fileURLToPath(import.meta.url)), "../advisories/agent-components.json");
|
|
6
|
+
const GENERIC_AFFECTED_TOOLS = [
|
|
7
|
+
"claude-code",
|
|
8
|
+
"codex-cli",
|
|
9
|
+
"opencode",
|
|
10
|
+
"cursor",
|
|
11
|
+
"windsurf",
|
|
12
|
+
"github-copilot",
|
|
13
|
+
];
|
|
14
|
+
function globToRegExp(glob) {
|
|
15
|
+
const pattern = glob.replaceAll("\\", "/").trim();
|
|
16
|
+
if (pattern.length === 0) {
|
|
17
|
+
return /^$/u;
|
|
18
|
+
}
|
|
19
|
+
let regex = "^";
|
|
20
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
21
|
+
const char = pattern[index];
|
|
22
|
+
if (char === "*") {
|
|
23
|
+
if (pattern[index + 1] === "*") {
|
|
24
|
+
regex += ".*";
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
regex += "[^/]*";
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (char === "?") {
|
|
33
|
+
regex += "[^/]";
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if ("\\^$.*+?()[]{}|".includes(char)) {
|
|
37
|
+
regex += `\\${char}`;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
regex += char;
|
|
41
|
+
}
|
|
42
|
+
regex += "$";
|
|
43
|
+
return new RegExp(regex, "u");
|
|
44
|
+
}
|
|
45
|
+
function matchesGlob(value, glob) {
|
|
46
|
+
const normalizedValue = value.replaceAll("\\", "/");
|
|
47
|
+
const normalizedGlob = glob.replaceAll("\\", "/").trim();
|
|
48
|
+
if (normalizedGlob.startsWith("**/")) {
|
|
49
|
+
const suffix = normalizedGlob.slice(3);
|
|
50
|
+
return normalizedValue === suffix || normalizedValue.endsWith(`/${suffix}`);
|
|
51
|
+
}
|
|
52
|
+
return globToRegExp(normalizedGlob).test(normalizedValue);
|
|
53
|
+
}
|
|
54
|
+
function loadComponents() {
|
|
55
|
+
const raw = readFileSync(COMPONENTS_PATH, "utf8");
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
const ADVISORY_COMPONENTS = loadComponents();
|
|
60
|
+
function normalizeToken(value) {
|
|
61
|
+
return value.trim().toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
function collectStringCandidates(value, path = [], output = []) {
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
output.push({ path: path.join("."), value });
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
value.forEach((entry, index) => {
|
|
70
|
+
collectStringCandidates(entry, [...path, String(index)], output);
|
|
71
|
+
});
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
if (!value || typeof value !== "object") {
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
for (const [key, child] of Object.entries(value)) {
|
|
78
|
+
collectStringCandidates(child, [...path, key], output);
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
function makeFinding(filePath, matchedPath, component, signature, evidence) {
|
|
83
|
+
const location = { field: matchedPath };
|
|
84
|
+
if (typeof evidence?.line === "number") {
|
|
85
|
+
location.line = evidence.line;
|
|
86
|
+
}
|
|
87
|
+
if (typeof evidence?.column === "number") {
|
|
88
|
+
location.column = evidence.column;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
rule_id: component.rule_id,
|
|
92
|
+
finding_id: `${component.rule_id}-${filePath}-${matchedPath}`,
|
|
93
|
+
severity: component.severity,
|
|
94
|
+
category: component.category,
|
|
95
|
+
layer: "L2",
|
|
96
|
+
file_path: filePath,
|
|
97
|
+
location,
|
|
98
|
+
description: component.description,
|
|
99
|
+
affected_tools: GENERIC_AFFECTED_TOOLS,
|
|
100
|
+
cve: null,
|
|
101
|
+
owasp: component.owasp,
|
|
102
|
+
cwe: component.cwe,
|
|
103
|
+
confidence: component.confidence,
|
|
104
|
+
fixable: true,
|
|
105
|
+
remediation_actions: component.remediation_actions,
|
|
106
|
+
metadata: {
|
|
107
|
+
sources: component.metadata?.sources ?? [],
|
|
108
|
+
sinks: component.metadata?.sinks ?? [],
|
|
109
|
+
referenced_secrets: component.metadata?.referenced_secrets ?? [],
|
|
110
|
+
risk_tags: component.metadata?.risk_tags ?? [],
|
|
111
|
+
origin: component.metadata?.origin ?? "agent-components.json",
|
|
112
|
+
},
|
|
113
|
+
evidence: evidence?.evidence ?? null,
|
|
114
|
+
suppressed: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function componentMatchesFile(component, filePath) {
|
|
118
|
+
return component.file_patterns.some((pattern) => matchesGlob(filePath, pattern));
|
|
119
|
+
}
|
|
120
|
+
function signatureMatches(value, signatures) {
|
|
121
|
+
const normalizedValue = normalizeToken(value);
|
|
122
|
+
for (const signature of signatures) {
|
|
123
|
+
const normalizedSignature = normalizeToken(signature);
|
|
124
|
+
if (normalizedSignature.length > 0 && normalizedValue.includes(normalizedSignature)) {
|
|
125
|
+
return signature;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function detectComponentMatch(input, component) {
|
|
131
|
+
if (!componentMatchesFile(component, input.filePath)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const stringCandidates = collectStringCandidates(input.parsed);
|
|
135
|
+
for (const candidate of stringCandidates) {
|
|
136
|
+
const signature = signatureMatches(candidate.value, component.signatures);
|
|
137
|
+
if (!signature) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const evidence = buildFindingEvidence({
|
|
141
|
+
textContent: input.textContent,
|
|
142
|
+
jsonPaths: candidate.path.length > 0 ? [candidate.path] : [],
|
|
143
|
+
searchTerms: [signature],
|
|
144
|
+
fallbackValue: `${candidate.path} = ${candidate.value}`,
|
|
145
|
+
});
|
|
146
|
+
return makeFinding(input.filePath, candidate.path || component.id, component, signature, evidence);
|
|
147
|
+
}
|
|
148
|
+
for (const signature of component.signatures) {
|
|
149
|
+
if (!signatureMatches(input.textContent, [signature])) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const evidence = buildFindingEvidence({
|
|
153
|
+
textContent: input.textContent,
|
|
154
|
+
searchTerms: [signature],
|
|
155
|
+
fallbackValue: signature,
|
|
156
|
+
});
|
|
157
|
+
return makeFinding(input.filePath, component.id, component, signature, evidence);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
export function detectAdvisoryIntelligence(input) {
|
|
162
|
+
const findings = [];
|
|
163
|
+
for (const component of ADVISORY_COMPONENTS) {
|
|
164
|
+
const finding = detectComponentMatch(input, component);
|
|
165
|
+
if (finding) {
|
|
166
|
+
findings.push(finding);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return findings;
|
|
170
|
+
}
|
|
@@ -95,6 +95,12 @@ function makeFinding(filePath, field, ruleId, severity, description, evidence) {
|
|
|
95
95
|
confidence: "HIGH",
|
|
96
96
|
fixable: true,
|
|
97
97
|
remediation_actions: ["remove_field", "replace_with_default"],
|
|
98
|
+
metadata: {
|
|
99
|
+
sources: [filePath, field],
|
|
100
|
+
sinks: ["process-execution"],
|
|
101
|
+
risk_tags: ["command-execution", "shell-pipeline"],
|
|
102
|
+
origin: "command-exec",
|
|
103
|
+
},
|
|
98
104
|
evidence: evidence?.evidence ?? null,
|
|
99
105
|
suppressed: false,
|
|
100
106
|
};
|
|
@@ -48,6 +48,11 @@ function makeFinding(filePath, field, ruleId, description, evidence, severity =
|
|
|
48
48
|
confidence: "HIGH",
|
|
49
49
|
fixable: true,
|
|
50
50
|
remediation_actions: ["strip_unicode", "remove_block", "quarantine_file"],
|
|
51
|
+
metadata: {
|
|
52
|
+
sources: [filePath, field],
|
|
53
|
+
risk_tags: ["rule-injection", "prompt-injection"],
|
|
54
|
+
origin: "rule-file",
|
|
55
|
+
},
|
|
51
56
|
evidence: evidence?.evidence ?? null,
|
|
52
57
|
observed: narrative.observed ?? null,
|
|
53
58
|
inference: narrative.inference ?? null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type GitHookEntry } from "./detectors/git-hooks.js";
|
|
2
2
|
import { type SymlinkEscapeEntry } from "./detectors/symlink.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type Finding } from "../types/finding.js";
|
|
4
4
|
import type { DiscoveryFormat } from "../types/discovery.js";
|
|
5
5
|
export interface StaticFileInput {
|
|
6
6
|
filePath: string;
|
|
@@ -17,6 +17,9 @@ export interface StaticEngineConfig {
|
|
|
17
17
|
trustedApiDomains: string[];
|
|
18
18
|
unicodeAnalysis: boolean;
|
|
19
19
|
checkIdeSettings: boolean;
|
|
20
|
+
rulePackPaths?: string[];
|
|
21
|
+
allowedRules?: string[];
|
|
22
|
+
skipRules?: string[];
|
|
20
23
|
}
|
|
21
24
|
export interface StaticEngineInput {
|
|
22
25
|
projectRoot: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { detectCommandExecution } from "./detectors/command-exec.js";
|
|
2
|
+
import { detectAdvisoryIntelligence } from "./detectors/advisory-intelligence.js";
|
|
2
3
|
import { detectConsentBypass } from "./detectors/consent-bypass.js";
|
|
3
4
|
import { detectEnvOverrides } from "./detectors/env-override.js";
|
|
4
5
|
import { detectGitHookIssues } from "./detectors/git-hooks.js";
|
|
@@ -6,6 +7,78 @@ import { detectIdeSettingsIssues } from "./detectors/ide-settings.js";
|
|
|
6
7
|
import { detectPluginManifestIssues } from "./detectors/plugin-manifest.js";
|
|
7
8
|
import { detectRuleFileIssues } from "./detectors/rule-file.js";
|
|
8
9
|
import { detectSymlinkEscapes } from "./detectors/symlink.js";
|
|
10
|
+
import { FINDING_CATEGORIES } from "../types/finding.js";
|
|
11
|
+
import { buildFindingEvidence } from "./evidence.js";
|
|
12
|
+
import { evaluateRule, loadRulePacks } from "./rule-engine.js";
|
|
13
|
+
const GENERIC_AFFECTED_TOOLS = [
|
|
14
|
+
"claude-code",
|
|
15
|
+
"codex-cli",
|
|
16
|
+
"opencode",
|
|
17
|
+
"cursor",
|
|
18
|
+
"windsurf",
|
|
19
|
+
"github-copilot",
|
|
20
|
+
];
|
|
21
|
+
function parseRuleSeverity(value) {
|
|
22
|
+
const normalized = value.trim().toUpperCase();
|
|
23
|
+
if (normalized === "CRITICAL" ||
|
|
24
|
+
normalized === "HIGH" ||
|
|
25
|
+
normalized === "MEDIUM" ||
|
|
26
|
+
normalized === "LOW") {
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
return "INFO";
|
|
30
|
+
}
|
|
31
|
+
function parseRuleCategory(value) {
|
|
32
|
+
const normalized = value.trim().toUpperCase();
|
|
33
|
+
if (FINDING_CATEGORIES.some((category) => category === normalized)) {
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
return "CONFIG_PRESENT";
|
|
37
|
+
}
|
|
38
|
+
function remediationActionsForRule(rule) {
|
|
39
|
+
if (rule.query_type === "text_pattern") {
|
|
40
|
+
return ["quarantine_file", "remove_block"];
|
|
41
|
+
}
|
|
42
|
+
return ["remove_field", "replace_with_default"];
|
|
43
|
+
}
|
|
44
|
+
function findingFromRulePackMatch(file, rule) {
|
|
45
|
+
const locationField = rule.query_type === "text_pattern" ? "content" : rule.query;
|
|
46
|
+
const evidence = buildFindingEvidence({
|
|
47
|
+
textContent: file.textContent,
|
|
48
|
+
searchTerms: [rule.query],
|
|
49
|
+
fallbackValue: `${locationField} matched rule ${rule.id}`,
|
|
50
|
+
});
|
|
51
|
+
const affectedTools = rule.tool === "*" ? GENERIC_AFFECTED_TOOLS : [rule.tool];
|
|
52
|
+
return {
|
|
53
|
+
rule_id: rule.id,
|
|
54
|
+
finding_id: `RULE_PACK-${rule.id}-${file.filePath}-${locationField}`,
|
|
55
|
+
severity: parseRuleSeverity(rule.severity),
|
|
56
|
+
category: parseRuleCategory(rule.category),
|
|
57
|
+
layer: "L2",
|
|
58
|
+
file_path: file.filePath,
|
|
59
|
+
location: { field: locationField },
|
|
60
|
+
description: rule.description,
|
|
61
|
+
affected_tools: affectedTools,
|
|
62
|
+
cve: rule.cve ?? null,
|
|
63
|
+
owasp: rule.owasp,
|
|
64
|
+
cwe: rule.cwe,
|
|
65
|
+
confidence: "HIGH",
|
|
66
|
+
fixable: true,
|
|
67
|
+
remediation_actions: remediationActionsForRule(rule),
|
|
68
|
+
metadata: {
|
|
69
|
+
sources: [file.filePath, locationField],
|
|
70
|
+
risk_tags: ["rule-pack"],
|
|
71
|
+
origin: "rule-pack",
|
|
72
|
+
},
|
|
73
|
+
evidence: evidence?.evidence ?? null,
|
|
74
|
+
suppressed: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function hasEquivalentFinding(findings, candidate) {
|
|
78
|
+
return findings.some((finding) => finding.rule_id === candidate.rule_id &&
|
|
79
|
+
finding.file_path === candidate.file_path &&
|
|
80
|
+
(finding.location.field ?? "") === (candidate.location.field ?? ""));
|
|
81
|
+
}
|
|
9
82
|
function dedupeFindings(findings) {
|
|
10
83
|
const deduped = new Map();
|
|
11
84
|
for (const finding of findings) {
|
|
@@ -32,6 +105,11 @@ function dedupeFindings(findings) {
|
|
|
32
105
|
}
|
|
33
106
|
export function runStaticEngine(input) {
|
|
34
107
|
const findings = [];
|
|
108
|
+
const rulePackRules = loadRulePacks({
|
|
109
|
+
rule_pack_paths: input.config.rulePackPaths ?? [],
|
|
110
|
+
allowed_rules: input.config.allowedRules ?? [],
|
|
111
|
+
skip_rules: input.config.skipRules ?? [],
|
|
112
|
+
});
|
|
35
113
|
for (const file of input.files) {
|
|
36
114
|
findings.push(...detectEnvOverrides({
|
|
37
115
|
filePath: file.filePath,
|
|
@@ -69,6 +147,11 @@ export function runStaticEngine(input) {
|
|
|
69
147
|
trustedApiDomains: input.config.trustedApiDomains,
|
|
70
148
|
blockedCommands: input.config.blockedCommands,
|
|
71
149
|
}));
|
|
150
|
+
findings.push(...detectAdvisoryIntelligence({
|
|
151
|
+
filePath: file.filePath,
|
|
152
|
+
parsed: file.parsed,
|
|
153
|
+
textContent: file.textContent,
|
|
154
|
+
}));
|
|
72
155
|
if (file.format === "text" || file.format === "markdown") {
|
|
73
156
|
findings.push(...detectRuleFileIssues({
|
|
74
157
|
filePath: file.filePath,
|
|
@@ -76,6 +159,20 @@ export function runStaticEngine(input) {
|
|
|
76
159
|
unicodeAnalysis: input.config.unicodeAnalysis,
|
|
77
160
|
}));
|
|
78
161
|
}
|
|
162
|
+
for (const rule of rulePackRules) {
|
|
163
|
+
if (!evaluateRule(rule, {
|
|
164
|
+
filePath: file.filePath,
|
|
165
|
+
format: file.format,
|
|
166
|
+
parsed: file.parsed,
|
|
167
|
+
textContent: file.textContent,
|
|
168
|
+
})) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const candidate = findingFromRulePackMatch(file, rule);
|
|
172
|
+
if (!hasEquivalentFinding(findings, candidate)) {
|
|
173
|
+
findings.push(candidate);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
79
176
|
}
|
|
80
177
|
findings.push(...detectSymlinkEscapes({ symlinkEscapes: input.symlinkEscapes }));
|
|
81
178
|
findings.push(...detectGitHookIssues({ hooks: input.hooks, knownSafeHooks: input.config.knownSafeHooks }));
|
|
@@ -21,4 +21,4 @@ export interface RuleEvaluationInput {
|
|
|
21
21
|
textContent: string;
|
|
22
22
|
}
|
|
23
23
|
export declare function evaluateRule(rule: DetectionRule, input: RuleEvaluationInput): boolean;
|
|
24
|
-
export
|
|
24
|
+
export { loadRulePacks } from "./rule-pack-loader.js";
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, extname, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
const rulesDir = resolve(dirname(fileURLToPath(import.meta.url)), "rules");
|
|
5
1
|
function escapeRegex(value) {
|
|
6
2
|
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
7
3
|
}
|
|
@@ -127,12 +123,4 @@ export function evaluateRule(rule, input) {
|
|
|
127
123
|
}
|
|
128
124
|
return false;
|
|
129
125
|
}
|
|
130
|
-
export
|
|
131
|
-
const files = readdirSync(baseDir)
|
|
132
|
-
.filter((file) => extname(file) === ".json")
|
|
133
|
-
.sort();
|
|
134
|
-
return files.flatMap((file) => {
|
|
135
|
-
const raw = readFileSync(join(baseDir, file), "utf8");
|
|
136
|
-
return JSON.parse(raw);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
126
|
+
export { loadRulePacks } from "./rule-pack-loader.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DetectionRule } from "./rule-engine.js";
|
|
2
|
+
export interface RulePackLoaderOptions {
|
|
3
|
+
baseDir?: string;
|
|
4
|
+
rule_pack_paths?: string[];
|
|
5
|
+
allowed_rules?: string[];
|
|
6
|
+
skip_rules?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function loadRulePacks(): DetectionRule[];
|
|
9
|
+
export declare function loadRulePacks(baseDir: string): DetectionRule[];
|
|
10
|
+
export declare function loadRulePacks(options: RulePackLoaderOptions): DetectionRule[];
|