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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
const DIRECT_OVERRIDE_PHRASES = [
|
|
3
|
+
"ignore previous instructions",
|
|
4
|
+
"skip permissions",
|
|
5
|
+
"bypass permissions",
|
|
6
|
+
];
|
|
7
|
+
const NEGATION_PATTERN = /\b(?:must not|should not|do not|don't|never)\b/iu;
|
|
8
|
+
const SENSITIVE_READ_PATTERN = /\b(?:read|cat)\s+(?:~\/\.ssh(?:\/[^\s]+)?|\.env\b|~\/\.[a-z0-9._-]+(?:\/[^\s]+)*)/iu;
|
|
9
|
+
const OUTBOUND_TRANSFER_PATTERN = /\b(?:upload externally|send to (?:an |a )?(?:external )?(?:webhook|endpoint|server)|curl\b|wget\b|invoke-webrequest\b|post to\b|https?:\/\/|exfiltrat(?:e|ion|ing))\b/iu;
|
|
10
|
+
const SUSPICIOUS_LONG_LINE_PATTERN = /\b(?:ignore previous instructions|skip permissions|bypass permissions|upload externally|curl\b|wget\b|https?:\/\/|bash\s+-lc|sh\s+-c|powershell\b|base64\b|~\/\.ssh|\.env\b)\b/iu;
|
|
11
|
+
const REMOTE_SHELL_PATTERN = /\b(?:curl|wget)\b[^\n|]{0,240}\|\s*(?:bash|sh)\b|\b(?:invoke-webrequest|iwr)\b[^\n|]{0,240}\|\s*(?:iex|invoke-expression)\b/iu;
|
|
12
|
+
const HTML_COMMENT_PATTERN = /<!--([\s\S]*?)-->/gu;
|
|
13
|
+
const COMMENT_PAYLOAD_PATTERN = /\b(?:secret instructions|ignore previous instructions|curl\b|wget\b|invoke-webrequest\b|bash\b|powershell\b|session share\b|profile sync\b)\b/iu;
|
|
14
|
+
const COOKIE_EXPORT_PATTERN = /\bcookies?\s+(?:export|import|get)\b/iu;
|
|
15
|
+
const SESSION_SHARE_PATTERN = /\bsession\s+share\b|\blive url\b/iu;
|
|
16
|
+
const PROFILE_SYNC_PATTERN = /\bprofile\s+sync\b|\breal chrome\b|\blogin sessions\b|\bsession tokens?\b|--profile\b/iu;
|
|
17
|
+
const BOOTSTRAP_INSTALL_PATTERN = /\b(?:npm|pnpm|yarn|bun)\s+install\s+-g\b|\bbrew\s+install\b|\bpipx\s+install\b|\bgo\s+install\b|\b(?:npx|pnpx|uvx)\b[^\n`]{0,160}@latest\b/iu;
|
|
18
|
+
const AGENT_CONTROL_POINT_PATTERN = /\.claude\/hooks\/|\.claude\/settings\.json|\.claude\/agents\/|\bclaude\.md\b|\bagents\.md\b|\bmcp configuration\b/iu;
|
|
19
|
+
const RESTART_LOAD_PATTERN = /\brestart\b.*\b(?:load|take effect|activate|reload|work)\b|\bonly load after restart\b|\bafter restarting\b/iu;
|
|
20
|
+
function makeFinding(filePath, field, ruleId, description, evidence, severity = "HIGH", narrative = {}) {
|
|
21
|
+
const location = { field };
|
|
22
|
+
if (typeof evidence?.line === "number") {
|
|
23
|
+
location.line = evidence.line;
|
|
24
|
+
}
|
|
25
|
+
if (typeof evidence?.column === "number") {
|
|
26
|
+
location.column = evidence.column;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
rule_id: ruleId,
|
|
30
|
+
finding_id: `RULE_INJECTION-${filePath}-${field}`,
|
|
31
|
+
severity,
|
|
32
|
+
category: "RULE_INJECTION",
|
|
33
|
+
layer: "L2",
|
|
34
|
+
file_path: filePath,
|
|
35
|
+
location,
|
|
36
|
+
description,
|
|
37
|
+
affected_tools: [
|
|
38
|
+
"claude-code",
|
|
39
|
+
"codex-cli",
|
|
40
|
+
"opencode",
|
|
41
|
+
"cursor",
|
|
42
|
+
"windsurf",
|
|
43
|
+
"github-copilot",
|
|
44
|
+
],
|
|
45
|
+
cve: null,
|
|
46
|
+
owasp: ["ASI01"],
|
|
47
|
+
cwe: "CWE-116",
|
|
48
|
+
confidence: "HIGH",
|
|
49
|
+
fixable: true,
|
|
50
|
+
remediation_actions: ["strip_unicode", "remove_block", "quarantine_file"],
|
|
51
|
+
evidence: evidence?.evidence ?? null,
|
|
52
|
+
observed: narrative.observed ?? null,
|
|
53
|
+
inference: narrative.inference ?? null,
|
|
54
|
+
not_verified: narrative.notVerified ?? null,
|
|
55
|
+
incident_id: narrative.incidentId ?? null,
|
|
56
|
+
incident_title: narrative.incidentTitle ?? null,
|
|
57
|
+
incident_primary: narrative.incidentPrimary ?? null,
|
|
58
|
+
suppressed: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function buildLineEvidence(line, lineNumber, column) {
|
|
62
|
+
return {
|
|
63
|
+
evidence: `line ${lineNumber}\n${lineNumber} | ${line}`,
|
|
64
|
+
line: lineNumber,
|
|
65
|
+
column,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function buildMultilineEvidence(lines, lineNumbers) {
|
|
69
|
+
const uniqueLines = Array.from(new Set(lineNumbers)).sort((left, right) => left - right);
|
|
70
|
+
const snippets = uniqueLines.map((lineNumber) => `${lineNumber} | ${lines[lineNumber - 1] ?? ""}`);
|
|
71
|
+
return {
|
|
72
|
+
evidence: `lines ${uniqueLines.join(", ")}\n${snippets.join("\n")}`,
|
|
73
|
+
line: uniqueLines[0] ?? 1,
|
|
74
|
+
column: 1,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function hasNearbyNegation(line, matchIndex) {
|
|
78
|
+
const prefix = line.slice(Math.max(0, matchIndex - 24), matchIndex);
|
|
79
|
+
return NEGATION_PATTERN.test(prefix);
|
|
80
|
+
}
|
|
81
|
+
function detectSuspiciousInstruction(lines) {
|
|
82
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
83
|
+
const line = lines[index] ?? "";
|
|
84
|
+
const lower = line.toLowerCase();
|
|
85
|
+
for (const phrase of DIRECT_OVERRIDE_PHRASES) {
|
|
86
|
+
const matchIndex = lower.indexOf(phrase);
|
|
87
|
+
if (matchIndex < 0 || hasNearbyNegation(lower, matchIndex)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
phrase,
|
|
92
|
+
evidence: buildLineEvidence(line, index + 1, matchIndex + 1),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const sensitiveReadMatch = line.match(SENSITIVE_READ_PATTERN);
|
|
96
|
+
const outboundMatch = line.match(OUTBOUND_TRANSFER_PATTERN);
|
|
97
|
+
if (!sensitiveReadMatch || !outboundMatch) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const outboundIndex = outboundMatch.index ?? line.length;
|
|
101
|
+
if (hasNearbyNegation(lower, outboundIndex)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const phrase = outboundMatch[0].toLowerCase();
|
|
105
|
+
return {
|
|
106
|
+
phrase,
|
|
107
|
+
evidence: buildLineEvidence(line, index + 1, outboundIndex + 1),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function detectRemoteShell(lines) {
|
|
113
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
114
|
+
const line = lines[index] ?? "";
|
|
115
|
+
const match = line.match(REMOTE_SHELL_PATTERN);
|
|
116
|
+
if (!match) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const matchIndex = match.index ?? 0;
|
|
120
|
+
if (hasNearbyNegation(line.toLowerCase(), matchIndex)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
return buildLineEvidence(line, index + 1, matchIndex + 1);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function shellLabelFromEvidence(evidence) {
|
|
128
|
+
const raw = evidence?.evidence?.toLowerCase() ?? "";
|
|
129
|
+
if (raw.includes("| bash")) {
|
|
130
|
+
return "bash";
|
|
131
|
+
}
|
|
132
|
+
if (raw.includes("| sh")) {
|
|
133
|
+
return "sh";
|
|
134
|
+
}
|
|
135
|
+
if (raw.includes("| iex") || raw.includes("| invoke-expression")) {
|
|
136
|
+
return "PowerShell";
|
|
137
|
+
}
|
|
138
|
+
return "a shell";
|
|
139
|
+
}
|
|
140
|
+
function detectHiddenCommentPayload(input, lines) {
|
|
141
|
+
HTML_COMMENT_PATTERN.lastIndex = 0;
|
|
142
|
+
let match = HTML_COMMENT_PATTERN.exec(input.textContent);
|
|
143
|
+
while (match) {
|
|
144
|
+
const commentBody = match[1] ?? "";
|
|
145
|
+
if (!COMMENT_PAYLOAD_PATTERN.test(commentBody)) {
|
|
146
|
+
match = HTML_COMMENT_PATTERN.exec(input.textContent);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const startLine = input.textContent.slice(0, match.index ?? 0).split(/\r?\n/u).length;
|
|
150
|
+
const endLine = startLine + match[0].split(/\r?\n/u).length - 1;
|
|
151
|
+
const lineNumbers = Array.from({ length: endLine - startLine + 1 }, (_, index) => startLine + index);
|
|
152
|
+
return buildMultilineEvidence(lines, lineNumbers);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function detectSessionTransfer(lines) {
|
|
157
|
+
const matchedLines = [];
|
|
158
|
+
const categories = new Set();
|
|
159
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
160
|
+
const line = lines[index] ?? "";
|
|
161
|
+
const lower = line.toLowerCase();
|
|
162
|
+
if (NEGATION_PATTERN.test(lower)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
let matched = false;
|
|
166
|
+
if (COOKIE_EXPORT_PATTERN.test(line)) {
|
|
167
|
+
categories.add("cookies");
|
|
168
|
+
matched = true;
|
|
169
|
+
}
|
|
170
|
+
if (SESSION_SHARE_PATTERN.test(line)) {
|
|
171
|
+
categories.add("session_share");
|
|
172
|
+
matched = true;
|
|
173
|
+
}
|
|
174
|
+
if (PROFILE_SYNC_PATTERN.test(line)) {
|
|
175
|
+
categories.add("profile");
|
|
176
|
+
matched = true;
|
|
177
|
+
}
|
|
178
|
+
if (matched) {
|
|
179
|
+
matchedLines.push(index + 1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (categories.size < 2 || matchedLines.length < 2) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return buildMultilineEvidence(lines, matchedLines.slice(0, 4));
|
|
186
|
+
}
|
|
187
|
+
function detectBootstrapControlPoints(lines) {
|
|
188
|
+
const installLines = [];
|
|
189
|
+
const controlPointLines = [];
|
|
190
|
+
const restartLines = [];
|
|
191
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
192
|
+
const line = lines[index] ?? "";
|
|
193
|
+
const lineNumber = index + 1;
|
|
194
|
+
if (BOOTSTRAP_INSTALL_PATTERN.test(line)) {
|
|
195
|
+
installLines.push(lineNumber);
|
|
196
|
+
}
|
|
197
|
+
if (AGENT_CONTROL_POINT_PATTERN.test(line)) {
|
|
198
|
+
controlPointLines.push(lineNumber);
|
|
199
|
+
}
|
|
200
|
+
if (RESTART_LOAD_PATTERN.test(line)) {
|
|
201
|
+
restartLines.push(lineNumber);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (installLines.length === 0 || controlPointLines.length === 0 || restartLines.length === 0) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return buildMultilineEvidence(lines, [
|
|
208
|
+
...installLines.slice(0, 2),
|
|
209
|
+
...controlPointLines.slice(0, 2),
|
|
210
|
+
restartLines[0],
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
export function detectRuleFileIssues(input) {
|
|
214
|
+
const findings = [];
|
|
215
|
+
const hiddenUnicodeRegex = /(?:\u200B|\u200C|\u200D|\u2060|\uFEFF|[\u202A-\u202E])/u;
|
|
216
|
+
const hiddenRemoteShellIncident = {
|
|
217
|
+
incidentId: "hidden-remote-shell-payload",
|
|
218
|
+
incidentTitle: "Hidden remote shell payload in skill file",
|
|
219
|
+
};
|
|
220
|
+
const hiddenMatch = input.unicodeAnalysis === false ? null : input.textContent.match(hiddenUnicodeRegex);
|
|
221
|
+
if (hiddenMatch?.[0]) {
|
|
222
|
+
const evidence = buildFindingEvidence({
|
|
223
|
+
textContent: input.textContent,
|
|
224
|
+
searchTerms: [hiddenMatch[0]],
|
|
225
|
+
fallbackValue: "hidden Unicode character detected",
|
|
226
|
+
});
|
|
227
|
+
findings.push(makeFinding(input.filePath, "hidden_unicode", "rule-file-hidden-unicode", "Rule file contains hidden Unicode characters", evidence));
|
|
228
|
+
}
|
|
229
|
+
const lines = input.textContent.split(/\r?\n/u);
|
|
230
|
+
const hiddenCommentPayload = detectHiddenCommentPayload(input, lines);
|
|
231
|
+
if (hiddenCommentPayload) {
|
|
232
|
+
findings.push(makeFinding(input.filePath, "hidden_comment_payload", "rule-file-hidden-comment-payload", "Rule file contains a hidden comment payload with executable or override instructions", hiddenCommentPayload, "CRITICAL", {
|
|
233
|
+
...hiddenRemoteShellIncident,
|
|
234
|
+
incidentPrimary: true,
|
|
235
|
+
observed: [
|
|
236
|
+
"A hidden HTML comment block contains agent-directed instructions.",
|
|
237
|
+
"The hidden block includes a secret instruction directive aimed at the agent.",
|
|
238
|
+
],
|
|
239
|
+
inference: "The skill conceals instructions from the human reader while attempting to steer agent behavior.",
|
|
240
|
+
notVerified: [
|
|
241
|
+
"CodeGate did not execute any instruction from the hidden block.",
|
|
242
|
+
"CodeGate did not fetch or inspect any referenced remote content.",
|
|
243
|
+
],
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
const suspiciousInstruction = detectSuspiciousInstruction(lines);
|
|
247
|
+
if (suspiciousInstruction) {
|
|
248
|
+
findings.push(makeFinding(input.filePath, "suspicious_instruction", "rule-file-suspicious-instruction", `Rule file contains suspicious instruction pattern: ${suspiciousInstruction.phrase}`, suspiciousInstruction.evidence));
|
|
249
|
+
}
|
|
250
|
+
const remoteShell = detectRemoteShell(lines);
|
|
251
|
+
if (remoteShell) {
|
|
252
|
+
const remoteShellNarrative = {
|
|
253
|
+
observed: [
|
|
254
|
+
"The file instructs the agent to download remote content with curl.",
|
|
255
|
+
`The downloaded content is piped directly into ${shellLabelFromEvidence(remoteShell)}.`,
|
|
256
|
+
],
|
|
257
|
+
inference: "Following this instruction would execute remote code supplied by the referenced URL.",
|
|
258
|
+
notVerified: [
|
|
259
|
+
"CodeGate did not fetch the referenced URL.",
|
|
260
|
+
"CodeGate did not execute the piped shell command.",
|
|
261
|
+
],
|
|
262
|
+
...(hiddenCommentPayload ? hiddenRemoteShellIncident : {}),
|
|
263
|
+
};
|
|
264
|
+
findings.push(makeFinding(input.filePath, "remote_shell", "rule-file-remote-shell", "Rule file instructs fetching remote content and piping it into a shell", remoteShell, "CRITICAL", remoteShellNarrative));
|
|
265
|
+
}
|
|
266
|
+
const sessionTransfer = detectSessionTransfer(lines);
|
|
267
|
+
if (sessionTransfer) {
|
|
268
|
+
findings.push(makeFinding(input.filePath, "session_transfer", "rule-file-session-transfer", "Rule file describes transferring authenticated browser cookies, profiles, or shared sessions", sessionTransfer, "HIGH"));
|
|
269
|
+
}
|
|
270
|
+
const bootstrapControlPoints = detectBootstrapControlPoints(lines);
|
|
271
|
+
if (bootstrapControlPoints) {
|
|
272
|
+
findings.push(makeFinding(input.filePath, "bootstrap_control_points", "rule-file-bootstrap-control-points", "Rule file bootstraps persistent agent hooks or settings and requires restart to activate them", bootstrapControlPoints, "HIGH", {
|
|
273
|
+
incidentId: "bootstrap-control-points",
|
|
274
|
+
incidentTitle: "Persistent agent bootstrap via hooks and settings",
|
|
275
|
+
incidentPrimary: true,
|
|
276
|
+
observed: [
|
|
277
|
+
"The file instructs installing or bootstrapping tooling with global or latest-version commands.",
|
|
278
|
+
"The bootstrap flow writes persistent agent control points such as hooks, settings, or agent instructions.",
|
|
279
|
+
"The file states that a restart is required for the new control points to take effect.",
|
|
280
|
+
],
|
|
281
|
+
inference: "Following this skill would create persistent agent behavior changes that survive the current task and expand future execution control.",
|
|
282
|
+
notVerified: [
|
|
283
|
+
"CodeGate did not run the bootstrap or installer commands.",
|
|
284
|
+
"CodeGate did not modify any local hooks, settings, or agent instruction files.",
|
|
285
|
+
],
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
const longLineIndex = lines.findIndex((line) => line.length > 300 && SUSPICIOUS_LONG_LINE_PATTERN.test(line) && !NEGATION_PATTERN.test(line));
|
|
289
|
+
if (longLineIndex >= 0) {
|
|
290
|
+
const lineNumber = longLineIndex + 1;
|
|
291
|
+
const evidence = {
|
|
292
|
+
evidence: `line ${lineNumber}\n${lineNumber} | ${lines[longLineIndex]}`,
|
|
293
|
+
line: lineNumber,
|
|
294
|
+
column: 1,
|
|
295
|
+
};
|
|
296
|
+
findings.push(makeFinding(input.filePath, "long_line", "rule-file-long-line", "Rule file contains unusually long lines that may hide payloads", evidence));
|
|
297
|
+
}
|
|
298
|
+
return findings;
|
|
299
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface SymlinkEscapeEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
target: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SymlinkInput {
|
|
7
|
+
symlinkEscapes: SymlinkEscapeEntry[];
|
|
8
|
+
}
|
|
9
|
+
export declare function detectSymlinkEscapes(input: SymlinkInput): Finding[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function makeFinding(path, target, severity) {
|
|
2
|
+
return {
|
|
3
|
+
rule_id: "symlink-escape",
|
|
4
|
+
finding_id: `SYMLINK_ESCAPE-${path}`,
|
|
5
|
+
severity,
|
|
6
|
+
category: "SYMLINK_ESCAPE",
|
|
7
|
+
layer: "L2",
|
|
8
|
+
file_path: path,
|
|
9
|
+
location: { field: "symlink_target" },
|
|
10
|
+
description: `Symlink resolves outside project root: ${target}`,
|
|
11
|
+
affected_tools: [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"codex-cli",
|
|
14
|
+
"opencode",
|
|
15
|
+
"cursor",
|
|
16
|
+
"windsurf",
|
|
17
|
+
"github-copilot",
|
|
18
|
+
],
|
|
19
|
+
cve: null,
|
|
20
|
+
owasp: ["ASI06"],
|
|
21
|
+
cwe: "CWE-59",
|
|
22
|
+
confidence: "HIGH",
|
|
23
|
+
fixable: true,
|
|
24
|
+
remediation_actions: ["remove_symlink", "quarantine_file"],
|
|
25
|
+
suppressed: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function detectSymlinkEscapes(input) {
|
|
29
|
+
const sensitiveIndicators = [
|
|
30
|
+
"/.ssh/",
|
|
31
|
+
"/.aws/",
|
|
32
|
+
"/.kube/",
|
|
33
|
+
"/.docker/",
|
|
34
|
+
"/.npmrc",
|
|
35
|
+
"/.git-credentials",
|
|
36
|
+
"/etc/passwd",
|
|
37
|
+
"/etc/shadow",
|
|
38
|
+
];
|
|
39
|
+
return input.symlinkEscapes.map((entry) => {
|
|
40
|
+
const severity = sensitiveIndicators.some((token) => entry.target.includes(token))
|
|
41
|
+
? "HIGH"
|
|
42
|
+
: "MEDIUM";
|
|
43
|
+
return makeFinding(entry.path, entry.target, severity);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type GitHookEntry } from "./detectors/git-hooks.js";
|
|
2
|
+
import { type SymlinkEscapeEntry } from "./detectors/symlink.js";
|
|
3
|
+
import type { Finding } from "../types/finding.js";
|
|
4
|
+
import type { DiscoveryFormat } from "../types/discovery.js";
|
|
5
|
+
export interface StaticFileInput {
|
|
6
|
+
filePath: string;
|
|
7
|
+
format: DiscoveryFormat;
|
|
8
|
+
parsed: unknown;
|
|
9
|
+
textContent: string;
|
|
10
|
+
}
|
|
11
|
+
export interface StaticEngineConfig {
|
|
12
|
+
knownSafeMcpServers: string[];
|
|
13
|
+
knownSafeFormatters: string[];
|
|
14
|
+
knownSafeLspServers: string[];
|
|
15
|
+
knownSafeHooks: string[];
|
|
16
|
+
blockedCommands: string[];
|
|
17
|
+
trustedApiDomains: string[];
|
|
18
|
+
unicodeAnalysis: boolean;
|
|
19
|
+
checkIdeSettings: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface StaticEngineInput {
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
files: StaticFileInput[];
|
|
24
|
+
symlinkEscapes: SymlinkEscapeEntry[];
|
|
25
|
+
hooks: GitHookEntry[];
|
|
26
|
+
config: StaticEngineConfig;
|
|
27
|
+
}
|
|
28
|
+
export declare function runStaticEngine(input: StaticEngineInput): Finding[];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { detectCommandExecution } from "./detectors/command-exec.js";
|
|
2
|
+
import { detectConsentBypass } from "./detectors/consent-bypass.js";
|
|
3
|
+
import { detectEnvOverrides } from "./detectors/env-override.js";
|
|
4
|
+
import { detectGitHookIssues } from "./detectors/git-hooks.js";
|
|
5
|
+
import { detectIdeSettingsIssues } from "./detectors/ide-settings.js";
|
|
6
|
+
import { detectPluginManifestIssues } from "./detectors/plugin-manifest.js";
|
|
7
|
+
import { detectRuleFileIssues } from "./detectors/rule-file.js";
|
|
8
|
+
import { detectSymlinkEscapes } from "./detectors/symlink.js";
|
|
9
|
+
function dedupeFindings(findings) {
|
|
10
|
+
const deduped = new Map();
|
|
11
|
+
for (const finding of findings) {
|
|
12
|
+
const key = `${finding.category}:${finding.rule_id}:${finding.description}`;
|
|
13
|
+
const existing = deduped.get(key);
|
|
14
|
+
if (!existing) {
|
|
15
|
+
deduped.set(key, {
|
|
16
|
+
...finding,
|
|
17
|
+
affected_locations: [{ file_path: finding.file_path, location: finding.location }],
|
|
18
|
+
});
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const nextLocation = { file_path: finding.file_path, location: finding.location };
|
|
22
|
+
const locations = existing.affected_locations ?? [];
|
|
23
|
+
const alreadyIncluded = locations.some((location) => location.file_path === nextLocation.file_path &&
|
|
24
|
+
location.location?.field === nextLocation.location?.field &&
|
|
25
|
+
location.location?.line === nextLocation.location?.line);
|
|
26
|
+
if (!alreadyIncluded) {
|
|
27
|
+
locations.push(nextLocation);
|
|
28
|
+
}
|
|
29
|
+
existing.affected_locations = locations;
|
|
30
|
+
}
|
|
31
|
+
return Array.from(deduped.values());
|
|
32
|
+
}
|
|
33
|
+
export function runStaticEngine(input) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
for (const file of input.files) {
|
|
36
|
+
findings.push(...detectEnvOverrides({
|
|
37
|
+
filePath: file.filePath,
|
|
38
|
+
parsed: file.parsed,
|
|
39
|
+
textContent: file.textContent,
|
|
40
|
+
trustedApiDomains: input.config.trustedApiDomains,
|
|
41
|
+
}));
|
|
42
|
+
findings.push(...detectConsentBypass({
|
|
43
|
+
filePath: file.filePath,
|
|
44
|
+
parsed: file.parsed,
|
|
45
|
+
textContent: file.textContent,
|
|
46
|
+
trustedApiDomains: input.config.trustedApiDomains,
|
|
47
|
+
}));
|
|
48
|
+
findings.push(...detectCommandExecution({
|
|
49
|
+
filePath: file.filePath,
|
|
50
|
+
parsed: file.parsed,
|
|
51
|
+
textContent: file.textContent,
|
|
52
|
+
knownSafeMcpServers: input.config.knownSafeMcpServers,
|
|
53
|
+
knownSafeFormatters: input.config.knownSafeFormatters,
|
|
54
|
+
knownSafeLspServers: input.config.knownSafeLspServers,
|
|
55
|
+
blockedCommands: input.config.blockedCommands,
|
|
56
|
+
}));
|
|
57
|
+
if (input.config.checkIdeSettings) {
|
|
58
|
+
findings.push(...detectIdeSettingsIssues({
|
|
59
|
+
filePath: file.filePath,
|
|
60
|
+
parsed: file.parsed,
|
|
61
|
+
textContent: file.textContent,
|
|
62
|
+
projectRoot: input.projectRoot,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
findings.push(...detectPluginManifestIssues({
|
|
66
|
+
filePath: file.filePath,
|
|
67
|
+
parsed: file.parsed,
|
|
68
|
+
textContent: file.textContent,
|
|
69
|
+
trustedApiDomains: input.config.trustedApiDomains,
|
|
70
|
+
blockedCommands: input.config.blockedCommands,
|
|
71
|
+
}));
|
|
72
|
+
if (file.format === "text" || file.format === "markdown") {
|
|
73
|
+
findings.push(...detectRuleFileIssues({
|
|
74
|
+
filePath: file.filePath,
|
|
75
|
+
textContent: file.textContent,
|
|
76
|
+
unicodeAnalysis: input.config.unicodeAnalysis,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
findings.push(...detectSymlinkEscapes({ symlinkEscapes: input.symlinkEscapes }));
|
|
81
|
+
findings.push(...detectGitHookIssues({ hooks: input.hooks, knownSafeHooks: input.config.knownSafeHooks }));
|
|
82
|
+
return dedupeFindings(findings);
|
|
83
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface FindingEvidence {
|
|
2
|
+
evidence: string;
|
|
3
|
+
line?: number;
|
|
4
|
+
column?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface BuildFindingEvidenceInput {
|
|
7
|
+
textContent: string;
|
|
8
|
+
jsonPaths?: string[];
|
|
9
|
+
searchTerms?: string[];
|
|
10
|
+
fallbackValue?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildFindingEvidence(input: BuildFindingEvidenceInput): FindingEvidence | null;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { findNodeAtLocation, parseTree } from "jsonc-parser";
|
|
2
|
+
function toLineStarts(textContent) {
|
|
3
|
+
const starts = [0];
|
|
4
|
+
for (let index = 0; index < textContent.length; index += 1) {
|
|
5
|
+
if (textContent.charCodeAt(index) === 10) {
|
|
6
|
+
starts.push(index + 1);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return starts;
|
|
10
|
+
}
|
|
11
|
+
function offsetToLineAndColumn(lineStarts, offset) {
|
|
12
|
+
if (lineStarts.length === 0) {
|
|
13
|
+
return { line: 1, column: 1 };
|
|
14
|
+
}
|
|
15
|
+
let low = 0;
|
|
16
|
+
let high = lineStarts.length - 1;
|
|
17
|
+
let lineIndex = 0;
|
|
18
|
+
while (low <= high) {
|
|
19
|
+
const mid = Math.floor((low + high) / 2);
|
|
20
|
+
if (lineStarts[mid] <= offset) {
|
|
21
|
+
lineIndex = mid;
|
|
22
|
+
low = mid + 1;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
high = mid - 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
line: lineIndex + 1,
|
|
30
|
+
column: offset - lineStarts[lineIndex] + 1,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function formatLineBlock(textContent, startLine, endLine, startColumn) {
|
|
34
|
+
const lines = textContent.split(/\r?\n/u);
|
|
35
|
+
const snippetLines = lines.slice(startLine - 1, endLine);
|
|
36
|
+
const numberedLines = snippetLines.map((line, index) => `${startLine + index} | ${line}`);
|
|
37
|
+
const header = startLine === endLine ? `line ${startLine}` : `lines ${startLine}-${endLine}`;
|
|
38
|
+
return {
|
|
39
|
+
evidence: [header, ...numberedLines].join("\n"),
|
|
40
|
+
line: startLine,
|
|
41
|
+
column: startColumn,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function splitJsonPath(path) {
|
|
45
|
+
return path
|
|
46
|
+
.split(".")
|
|
47
|
+
.map((segment) => segment.trim())
|
|
48
|
+
.filter((segment) => segment.length > 0)
|
|
49
|
+
.map((segment) => {
|
|
50
|
+
if (/^[0-9]+$/u.test(segment)) {
|
|
51
|
+
return Number(segment);
|
|
52
|
+
}
|
|
53
|
+
return segment;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function normalizeSnippetNode(node) {
|
|
57
|
+
if (node.parent?.type === "property") {
|
|
58
|
+
return node.parent;
|
|
59
|
+
}
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
function extractEvidenceFromJsonPath(textContent, path) {
|
|
63
|
+
if (path.length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const root = parseTree(textContent);
|
|
67
|
+
if (!root) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const node = findNodeAtLocation(root, splitJsonPath(path));
|
|
71
|
+
if (!node) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const snippetNode = normalizeSnippetNode(node);
|
|
75
|
+
const startOffset = snippetNode.offset;
|
|
76
|
+
const endOffset = snippetNode.offset + snippetNode.length;
|
|
77
|
+
const lineStarts = toLineStarts(textContent);
|
|
78
|
+
const start = offsetToLineAndColumn(lineStarts, startOffset);
|
|
79
|
+
const end = offsetToLineAndColumn(lineStarts, Math.max(startOffset, endOffset - 1));
|
|
80
|
+
return formatLineBlock(textContent, start.line, end.line, start.column);
|
|
81
|
+
}
|
|
82
|
+
function extractEvidenceFromSearchTerm(textContent, term) {
|
|
83
|
+
if (term.length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const offset = textContent.indexOf(term);
|
|
87
|
+
if (offset < 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const lineStarts = toLineStarts(textContent);
|
|
91
|
+
const position = offsetToLineAndColumn(lineStarts, offset);
|
|
92
|
+
return formatLineBlock(textContent, position.line, position.line, position.column);
|
|
93
|
+
}
|
|
94
|
+
function uniqueValues(values) {
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const unique = [];
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
const normalized = value.trim();
|
|
99
|
+
if (normalized.length === 0 || seen.has(normalized)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
seen.add(normalized);
|
|
103
|
+
unique.push(normalized);
|
|
104
|
+
}
|
|
105
|
+
return unique;
|
|
106
|
+
}
|
|
107
|
+
export function buildFindingEvidence(input) {
|
|
108
|
+
if (input.textContent.length > 0) {
|
|
109
|
+
const jsonPaths = uniqueValues(input.jsonPaths ?? []);
|
|
110
|
+
for (const path of jsonPaths) {
|
|
111
|
+
const extracted = extractEvidenceFromJsonPath(input.textContent, path);
|
|
112
|
+
if (extracted) {
|
|
113
|
+
return extracted;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const searchTerms = uniqueValues(input.searchTerms ?? []);
|
|
117
|
+
for (const term of searchTerms) {
|
|
118
|
+
const extracted = extractEvidenceFromSearchTerm(input.textContent, term);
|
|
119
|
+
if (extracted) {
|
|
120
|
+
return extracted;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (input.fallbackValue && input.fallbackValue.length > 0) {
|
|
125
|
+
return { evidence: input.fallbackValue };
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type RuleQueryType = "json_path" | "toml_path" | "env_key" | "text_pattern";
|
|
2
|
+
export type RuleCondition = "equals_true" | "equals_false" | "exists" | "not_empty" | "matches_regex" | "not_in_allowlist" | "regex_match" | "contains" | "line_length_exceeds";
|
|
3
|
+
export interface DetectionRule {
|
|
4
|
+
id: string;
|
|
5
|
+
severity: string;
|
|
6
|
+
category: string;
|
|
7
|
+
description: string;
|
|
8
|
+
tool: string;
|
|
9
|
+
file_pattern: string;
|
|
10
|
+
query_type: RuleQueryType;
|
|
11
|
+
query: string;
|
|
12
|
+
condition: RuleCondition;
|
|
13
|
+
cve?: string;
|
|
14
|
+
owasp: string[];
|
|
15
|
+
cwe: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RuleEvaluationInput {
|
|
18
|
+
filePath: string;
|
|
19
|
+
format: string;
|
|
20
|
+
parsed: unknown;
|
|
21
|
+
textContent: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function evaluateRule(rule: DetectionRule, input: RuleEvaluationInput): boolean;
|
|
24
|
+
export declare function loadRulePacks(baseDir?: string): DetectionRule[];
|