kodingo-cli 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/security-scan.js +16 -1
- package/dist/commands/sync-graph.js +1 -0
- package/dist/utils/ast-parser.js +45 -0
- package/dist/utils/code-pattern-scanner.js +103 -0
- package/dist/utils/graph-builder.js +1 -0
- package/dist/utils/security-code-analyzer.js +29 -2
- package/dist/utils/tree-sitter-parser.js +1 -0
- package/package.json +1 -1
|
@@ -115,7 +115,22 @@ async function securityScanCommand(options = {}) {
|
|
|
115
115
|
}
|
|
116
116
|
catch { }
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
// Fetch dynamically generated rules from OSV-driven rule generation
|
|
119
|
+
let dynamicRules = [];
|
|
120
|
+
try {
|
|
121
|
+
const rulesRes = await fetch(`${apiUrl}/security/rules`, {
|
|
122
|
+
headers: { "X-Kodingo-Token": token },
|
|
123
|
+
});
|
|
124
|
+
if (rulesRes.ok) {
|
|
125
|
+
const rulesData = await rulesRes.json();
|
|
126
|
+
dynamicRules = rulesData.rules ?? [];
|
|
127
|
+
if (!options.silent && dynamicRules.length > 0) {
|
|
128
|
+
console.log(` Loaded ${dynamicRules.length} dynamically generated detection rule${dynamicRules.length === 1 ? "" : "s"} from OSV intelligence\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
const candidates = (0, security_code_analyzer_1.analyzeForSecurityCandidates)(repoRoot, graph, symbols, dynamicRules);
|
|
119
134
|
if (candidates.length === 0) {
|
|
120
135
|
if (!options.silent)
|
|
121
136
|
console.log(" ✔ No suspicious patterns detected.");
|
package/dist/utils/ast-parser.js
CHANGED
|
@@ -142,6 +142,45 @@ function extractCallNames(node, acc = new Set()) {
|
|
|
142
142
|
walk(node);
|
|
143
143
|
return Array.from(acc);
|
|
144
144
|
}
|
|
145
|
+
const ROUTE_HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "all"]);
|
|
146
|
+
function extractRoutes(node) {
|
|
147
|
+
const routes = [];
|
|
148
|
+
function walk(n) {
|
|
149
|
+
if (!n || typeof n !== "object")
|
|
150
|
+
return;
|
|
151
|
+
if (n.type === "CallExpression") {
|
|
152
|
+
const callee = n.callee;
|
|
153
|
+
if (callee?.type === "MemberExpression" &&
|
|
154
|
+
ROUTE_HTTP_METHODS.has(callee.property?.name?.toLowerCase())) {
|
|
155
|
+
const firstArg = n.arguments?.[0];
|
|
156
|
+
if (firstArg?.type === "Literal" && typeof firstArg.value === "string" &&
|
|
157
|
+
firstArg.value.startsWith("/")) {
|
|
158
|
+
routes.push({ method: callee.property.name.toUpperCase(), path: firstArg.value });
|
|
159
|
+
}
|
|
160
|
+
if (firstArg?.type === "TemplateLiteral") {
|
|
161
|
+
const raw = firstArg.quasis?.map((q) => q.value?.raw ?? "").join("*") ?? "";
|
|
162
|
+
if (raw.startsWith("/")) {
|
|
163
|
+
routes.push({ method: callee.property.name.toUpperCase(), path: raw });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const key of Object.keys(n)) {
|
|
169
|
+
if (key === "parent")
|
|
170
|
+
continue;
|
|
171
|
+
const child = n[key];
|
|
172
|
+
if (Array.isArray(child)) {
|
|
173
|
+
for (const c of child)
|
|
174
|
+
walk(c);
|
|
175
|
+
}
|
|
176
|
+
else if (child && typeof child === "object" && child.type) {
|
|
177
|
+
walk(child);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
walk(node);
|
|
182
|
+
return routes;
|
|
183
|
+
}
|
|
145
184
|
async function parseTypeScript(filePath, source) {
|
|
146
185
|
try {
|
|
147
186
|
const parse = tsParse;
|
|
@@ -173,6 +212,7 @@ async function parseTypeScript(filePath, source) {
|
|
|
173
212
|
body,
|
|
174
213
|
fingerprint: computeFingerprint(body),
|
|
175
214
|
calls: extractCallNames(node.body),
|
|
215
|
+
routes: extractRoutes(node.body),
|
|
176
216
|
imports: fileImports,
|
|
177
217
|
});
|
|
178
218
|
}
|
|
@@ -190,6 +230,7 @@ async function parseTypeScript(filePath, source) {
|
|
|
190
230
|
body,
|
|
191
231
|
fingerprint: computeFingerprint(body),
|
|
192
232
|
calls: extractCallNames(node.init.body),
|
|
233
|
+
routes: extractRoutes(node.init.body),
|
|
193
234
|
imports: fileImports,
|
|
194
235
|
});
|
|
195
236
|
}
|
|
@@ -207,6 +248,7 @@ async function parseTypeScript(filePath, source) {
|
|
|
207
248
|
body,
|
|
208
249
|
fingerprint: computeFingerprint(body),
|
|
209
250
|
calls: extractCallNames(node.body),
|
|
251
|
+
routes: extractRoutes(node.body),
|
|
210
252
|
imports: fileImports,
|
|
211
253
|
});
|
|
212
254
|
// Visit methods inside the class
|
|
@@ -225,6 +267,7 @@ async function parseTypeScript(filePath, source) {
|
|
|
225
267
|
body: mBody,
|
|
226
268
|
fingerprint: computeFingerprint(mBody),
|
|
227
269
|
calls: extractCallNames(member.value?.body),
|
|
270
|
+
routes: extractRoutes(member.value?.body),
|
|
228
271
|
imports: fileImports,
|
|
229
272
|
});
|
|
230
273
|
}
|
|
@@ -299,6 +342,7 @@ function parseStructural(filePath, source, patterns) {
|
|
|
299
342
|
body,
|
|
300
343
|
fingerprint: computeFingerprint(body),
|
|
301
344
|
calls: extractCallNamesRegex(body),
|
|
345
|
+
routes: [],
|
|
302
346
|
imports: [],
|
|
303
347
|
});
|
|
304
348
|
break;
|
|
@@ -344,6 +388,7 @@ function parsePython(filePath, source) {
|
|
|
344
388
|
body,
|
|
345
389
|
fingerprint: computeFingerprint(body),
|
|
346
390
|
calls: extractCallNamesRegex(body),
|
|
391
|
+
routes: [],
|
|
347
392
|
imports: [],
|
|
348
393
|
});
|
|
349
394
|
}
|
|
@@ -93,6 +93,109 @@ const RULES = [
|
|
|
93
93
|
regex: /\b(fetch|axios\.get|axios\.post|request)\s*\(\s*[^)]*\+\s*\w+/,
|
|
94
94
|
description: "Outbound HTTP request built via concatenation with a variable — potential SSRF if that variable is user-controlled.",
|
|
95
95
|
},
|
|
96
|
+
// ── Insecure deserialization ───────────────────────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
type: "insecure_deserialization",
|
|
99
|
+
severity: "critical",
|
|
100
|
+
regex: /\b(unserialize|pickle\.loads|deserialize)\s*\(/,
|
|
101
|
+
description: "Unvalidated data passed to a deserialization function — can enable remote code execution if input is attacker-controlled.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "insecure_deserialization",
|
|
105
|
+
severity: "critical",
|
|
106
|
+
regex: /yaml\.load\s*\([^,)]+\)(?!.*Loader)/,
|
|
107
|
+
description: "yaml.load() without a safe Loader — enables arbitrary code execution via crafted YAML input.",
|
|
108
|
+
languages: ["python"],
|
|
109
|
+
},
|
|
110
|
+
// ── XXE injection ──────────────────────────────────────────────────────────
|
|
111
|
+
{
|
|
112
|
+
type: "xxe_injection",
|
|
113
|
+
severity: "high",
|
|
114
|
+
regex: /resolve_entities\s*=\s*True|load_dtd\s*=\s*True/,
|
|
115
|
+
description: "XML parser configured with external entity resolution or DTD loading enabled — XXE injection vulnerability.",
|
|
116
|
+
languages: ["python"],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: "xxe_injection",
|
|
120
|
+
severity: "high",
|
|
121
|
+
regex: /libxml_disable_entity_loader\s*\(\s*false\s*\)/,
|
|
122
|
+
description: "PHP XML external entity loading explicitly enabled — XXE injection vulnerability.",
|
|
123
|
+
languages: ["php"],
|
|
124
|
+
},
|
|
125
|
+
// ── Open redirect ─────────────────────────────────────────────────────────
|
|
126
|
+
{
|
|
127
|
+
type: "open_redirect",
|
|
128
|
+
severity: "high",
|
|
129
|
+
regex: /\bres\.redirect\s*\([^)]*(?:req\.|params\.|query\.)/,
|
|
130
|
+
description: "Redirect target built from user-supplied request data — potential open redirect enabling phishing attacks.",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: "open_redirect",
|
|
134
|
+
severity: "high",
|
|
135
|
+
regex: /header\s*\(\s*['"]Location.*(?:\$_GET|\$_POST|\$_REQUEST)/,
|
|
136
|
+
description: "Location header set from user-supplied input — open redirect vulnerability.",
|
|
137
|
+
languages: ["php"],
|
|
138
|
+
},
|
|
139
|
+
// ── Mass assignment ───────────────────────────────────────────────────────
|
|
140
|
+
{
|
|
141
|
+
type: "mass_assignment",
|
|
142
|
+
severity: "high",
|
|
143
|
+
regex: /Object\.assign\s*\([^,)]+,\s*req\.body/,
|
|
144
|
+
description: "User-supplied request body merged directly into an object via Object.assign — potential mass assignment.",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
type: "mass_assignment",
|
|
148
|
+
severity: "high",
|
|
149
|
+
regex: /\$request->all\(\)|\$request->input\(\)/,
|
|
150
|
+
description: "All user input bound directly to model without allowlist — mass assignment vulnerability in Laravel/PHP.",
|
|
151
|
+
languages: ["php"],
|
|
152
|
+
},
|
|
153
|
+
// ── Prototype pollution ───────────────────────────────────────────────────
|
|
154
|
+
{
|
|
155
|
+
type: "prototype_pollution",
|
|
156
|
+
severity: "critical",
|
|
157
|
+
regex: /\[['"]__proto__['"]\]|__proto__\s*=/,
|
|
158
|
+
description: "Prototype pollution — modification of __proto__ through user-controlled input can corrupt the global object prototype.",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: "prototype_pollution",
|
|
162
|
+
severity: "critical",
|
|
163
|
+
regex: /constructor\.prototype\s*=/,
|
|
164
|
+
description: "Direct modification of constructor.prototype — potential prototype pollution vulnerability.",
|
|
165
|
+
},
|
|
166
|
+
// ── CORS misconfiguration ─────────────────────────────────────────────────
|
|
167
|
+
{
|
|
168
|
+
type: "cors_misconfiguration",
|
|
169
|
+
severity: "high",
|
|
170
|
+
regex: /['"]Access-Control-Allow-Origin['"]\s*[,:]\s*['"]\*['"]/,
|
|
171
|
+
description: "Wildcard CORS policy — allows any origin to make cross-origin requests to this endpoint.",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: "cors_misconfiguration",
|
|
175
|
+
severity: "moderate",
|
|
176
|
+
regex: /origin\s*[:=]\s*req\.headers(?:\.origin|\.get\s*\(\s*['"]origin)/,
|
|
177
|
+
description: "CORS origin reflected directly from request headers without validation — allows attacker-controlled origins.",
|
|
178
|
+
},
|
|
179
|
+
// ── Timing attacks ────────────────────────────────────────────────────────
|
|
180
|
+
{
|
|
181
|
+
type: "timing_attack",
|
|
182
|
+
severity: "moderate",
|
|
183
|
+
regex: /(?:token|secret|password|apiKey|api_key)\s*===\s*(?:req|request|input|provided)/,
|
|
184
|
+
description: "Non-constant-time comparison in a security-sensitive context — vulnerable to timing attacks that can leak secret values.",
|
|
185
|
+
},
|
|
186
|
+
// ── Insecure session management ───────────────────────────────────────────
|
|
187
|
+
{
|
|
188
|
+
type: "insecure_session",
|
|
189
|
+
severity: "high",
|
|
190
|
+
regex: /(?:session|sessionId|session_id)\s*[:=][^;\n]*Math\.random/,
|
|
191
|
+
description: "Session ID generated using Math.random() — not cryptographically secure, session IDs are predictable.",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: "insecure_session",
|
|
195
|
+
severity: "moderate",
|
|
196
|
+
regex: /res\.cookie\s*\([^)]+\)(?!.*httpOnly)(?!.*secure)/,
|
|
197
|
+
description: "Cookie set without httpOnly or Secure flags — session cookie may be accessible to JavaScript or transmitted over HTTP.",
|
|
198
|
+
},
|
|
96
199
|
];
|
|
97
200
|
/**
|
|
98
201
|
* Scans a single symbol's body against all pattern rules. Returns every match —
|
|
@@ -73,12 +73,39 @@ function findMissingAuthCandidatesLocal(graph, symbolsByFingerprint) {
|
|
|
73
73
|
/**
|
|
74
74
|
* Runs both Layer 1 detectors and assembles full code context for every
|
|
75
75
|
* flagged candidate — ready to send to the server for Layer 2 LLM judgment.
|
|
76
|
+
* Dynamic rules from OSV-driven generation are applied alongside static rules.
|
|
76
77
|
*/
|
|
77
|
-
function analyzeForSecurityCandidates(repoRoot, graph, symbols) {
|
|
78
|
+
function analyzeForSecurityCandidates(repoRoot, graph, symbols, dynamicRules = []) {
|
|
78
79
|
const symbolsByFingerprint = new Map(symbols.map(s => [s.fingerprint, s]));
|
|
79
80
|
const candidates = [];
|
|
80
|
-
// ── Code pattern findings
|
|
81
|
+
// ── Code pattern findings — static + dynamic rules ────────────────────────
|
|
81
82
|
const patternFindings = (0, code_pattern_scanner_1.scanAllSymbolsForPatterns)(symbols);
|
|
83
|
+
// Apply dynamic rules generated from OSV vulnerability data
|
|
84
|
+
for (const sym of symbols) {
|
|
85
|
+
const language = sym.language ?? "unknown";
|
|
86
|
+
for (const rule of dynamicRules) {
|
|
87
|
+
if (rule.languages && !rule.languages.includes(language))
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
const regex = new RegExp(rule.regexPattern);
|
|
91
|
+
const match = sym.body.match(regex);
|
|
92
|
+
if (match && match.index !== undefined) {
|
|
93
|
+
patternFindings.push({
|
|
94
|
+
patternType: rule.patternType,
|
|
95
|
+
symbol: sym.name,
|
|
96
|
+
file: sym.file,
|
|
97
|
+
startLine: sym.startLine,
|
|
98
|
+
severity: rule.severity,
|
|
99
|
+
snippet: sym.body.slice(Math.max(0, match.index - 20), match.index + 100).trim(),
|
|
100
|
+
description: rule.description,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
82
109
|
for (const finding of patternFindings) {
|
|
83
110
|
const sym = symbols.find(s => s.name === finding.symbol && s.file === finding.file);
|
|
84
111
|
const fingerprint = sym?.fingerprint ?? "";
|