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.
@@ -115,7 +115,22 @@ async function securityScanCommand(options = {}) {
115
115
  }
116
116
  catch { }
117
117
  }
118
- const candidates = (0, security_code_analyzer_1.analyzeForSecurityCandidates)(repoRoot, graph, symbols);
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.");
@@ -70,6 +70,7 @@ async function syncGraphCommand(options = {}) {
70
70
  fanOut: n.fanOut,
71
71
  centrality: n.centrality,
72
72
  rawCalls: n.rawCalls,
73
+ rawRoutes: n.rawRoutes,
73
74
  })),
74
75
  removed: i === 0 ? diff.removed : [],
75
76
  edgesAdded: edgeChunk.map(e => ({
@@ -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 —
@@ -302,6 +302,7 @@ async function buildGraph(repoRoot) {
302
302
  fanOut: 0,
303
303
  centrality: 0,
304
304
  rawCalls: sym.calls ?? [],
305
+ rawRoutes: sym.routes ?? [],
305
306
  };
306
307
  }
307
308
  const tsFiles = symbols
@@ -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 ?? "";
@@ -260,6 +260,7 @@ async function parseWithTreeSitter(filePath, source) {
260
260
  body,
261
261
  fingerprint: (0, ast_parser_1.computeFingerprint)(body),
262
262
  calls: extractCallNamesTs(node, config),
263
+ routes: [],
263
264
  imports: fileImports,
264
265
  });
265
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodingo-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Kodingo CLI",
5
5
  "license": "MIT",
6
6
  "private": false,