kodingo-cli 1.0.38 → 1.1.1

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.
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ /**
3
+ * graph-builder.ts
4
+ *
5
+ * Builds a structural relationship graph (KortexGraph) for a repository.
6
+ *
7
+ * Nodes are keyed by fingerprint (stable across renames/moves — same mechanism
8
+ * used for memory symbols). Edges represent "calls" relationships between symbols.
9
+ *
10
+ * Resolution has two tiers:
11
+ * - Tier 1 (TypeScript/JavaScript): uses the TypeScript compiler's type checker
12
+ * to resolve call expressions to their actual declaration, giving near-100%
13
+ * accurate edges. Confidence = 1.0.
14
+ * - Tier 2 (all other languages): heuristic name-based matching against the
15
+ * repo-wide symbol table. Confidence is scored by name uniqueness.
16
+ *
17
+ * This module performs zero LLM calls. Pure static analysis.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ var __importDefault = (this && this.__importDefault) || function (mod) {
53
+ return (mod && mod.__esModule) ? mod : { "default": mod };
54
+ };
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.buildGraph = buildGraph;
57
+ exports.diffGraphs = diffGraphs;
58
+ exports.saveGraph = saveGraph;
59
+ exports.loadGraph = loadGraph;
60
+ const fs_1 = __importDefault(require("fs"));
61
+ const path_1 = __importDefault(require("path"));
62
+ const ts = __importStar(require("typescript"));
63
+ const ast_parser_1 = require("./ast-parser");
64
+ const SUPPORTED_EXTENSIONS = [
65
+ ".ts", ".tsx", ".js", ".jsx",
66
+ ".py", ".go", ".rs", ".php",
67
+ ".java", ".kt", ".rb", ".c", ".cpp",
68
+ ];
69
+ const TS_JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
70
+ const IGNORED_DIRS = [
71
+ "node_modules", ".git", ".kortex", "dist", "build",
72
+ "out", ".next", "coverage", "__pycache__", "vendor",
73
+ ];
74
+ function walkFiles(dir) {
75
+ const results = [];
76
+ try {
77
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
78
+ for (const entry of entries) {
79
+ if (IGNORED_DIRS.includes(entry.name))
80
+ continue;
81
+ const fullPath = path_1.default.join(dir, entry.name);
82
+ if (entry.isDirectory()) {
83
+ results.push(...walkFiles(fullPath));
84
+ }
85
+ else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
86
+ results.push(fullPath);
87
+ }
88
+ }
89
+ }
90
+ catch { }
91
+ return results;
92
+ }
93
+ function languageForFile(file) {
94
+ const ext = path_1.default.extname(file).toLowerCase();
95
+ const map = {
96
+ ".ts": "typescript", ".tsx": "typescript",
97
+ ".js": "javascript", ".jsx": "javascript",
98
+ ".py": "python", ".go": "go", ".rs": "rust",
99
+ ".php": "php", ".java": "java", ".kt": "kotlin",
100
+ ".rb": "ruby", ".c": "c", ".cpp": "cpp",
101
+ };
102
+ return map[ext] ?? "unknown";
103
+ }
104
+ async function collectAllSymbols(repoRoot) {
105
+ const files = walkFiles(repoRoot);
106
+ const all = [];
107
+ for (const file of files) {
108
+ try {
109
+ const source = fs_1.default.readFileSync(file, "utf-8");
110
+ const nodes = await (0, ast_parser_1.parseFile)(file, source);
111
+ for (const n of nodes) {
112
+ all.push({ ...n, language: languageForFile(file) });
113
+ }
114
+ }
115
+ catch { }
116
+ }
117
+ return all;
118
+ }
119
+ function buildNameIndex(symbols) {
120
+ const index = new Map();
121
+ for (const sym of symbols) {
122
+ const shortName = sym.name.includes(".") ? sym.name.split(".").pop() : sym.name;
123
+ const existing = index.get(shortName) ?? [];
124
+ existing.push(sym.fingerprint);
125
+ index.set(shortName, existing);
126
+ }
127
+ return index;
128
+ }
129
+ function resolveTier2Edges(symbols, nameIndex) {
130
+ const edges = [];
131
+ for (const sym of symbols) {
132
+ if (TS_JS_EXTENSIONS.has(path_1.default.extname(sym.file)))
133
+ continue;
134
+ for (const calledName of sym.calls) {
135
+ const candidates = nameIndex.get(calledName);
136
+ if (!candidates || candidates.length === 0)
137
+ continue;
138
+ const uniqueCandidates = Array.from(new Set(candidates));
139
+ const confidence = uniqueCandidates.length === 1 ? 0.8 : Math.max(0.2, 0.6 / uniqueCandidates.length);
140
+ for (const targetFingerprint of uniqueCandidates) {
141
+ if (targetFingerprint === sym.fingerprint)
142
+ continue;
143
+ edges.push({
144
+ from: sym.fingerprint,
145
+ to: targetFingerprint,
146
+ type: "calls",
147
+ confidence,
148
+ file: sym.file,
149
+ line: sym.startLine,
150
+ });
151
+ }
152
+ }
153
+ }
154
+ return edges;
155
+ }
156
+ function resolveTier1Edges(repoRoot, tsFiles, symbols) {
157
+ if (tsFiles.length === 0)
158
+ return [];
159
+ const edges = [];
160
+ const symbolsByFile = new Map();
161
+ for (const sym of symbols) {
162
+ if (!TS_JS_EXTENSIONS.has(path_1.default.extname(sym.file)))
163
+ continue;
164
+ const list = symbolsByFile.get(sym.file) ?? [];
165
+ list.push(sym);
166
+ symbolsByFile.set(sym.file, list);
167
+ }
168
+ function findEnclosingSymbol(file, line) {
169
+ const candidates = symbolsByFile.get(file);
170
+ if (!candidates)
171
+ return null;
172
+ let best = null;
173
+ for (const c of candidates) {
174
+ if (line >= c.startLine && line <= c.endLine) {
175
+ if (!best || (c.endLine - c.startLine) < (best.endLine - best.startLine)) {
176
+ best = c;
177
+ }
178
+ }
179
+ }
180
+ return best;
181
+ }
182
+ let tsconfigPath;
183
+ try {
184
+ tsconfigPath = ts.findConfigFile(repoRoot, ts.sys.fileExists, "tsconfig.json") ?? undefined;
185
+ }
186
+ catch {
187
+ tsconfigPath = undefined;
188
+ }
189
+ let compilerOptions = {
190
+ allowJs: true,
191
+ jsx: ts.JsxEmit.Preserve,
192
+ module: ts.ModuleKind.CommonJS,
193
+ target: ts.ScriptTarget.ES2020,
194
+ noEmit: true,
195
+ };
196
+ if (tsconfigPath) {
197
+ try {
198
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
199
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path_1.default.dirname(tsconfigPath));
200
+ compilerOptions = { ...compilerOptions, ...parsed.options, noEmit: true };
201
+ }
202
+ catch { }
203
+ }
204
+ let program;
205
+ try {
206
+ program = ts.createProgram(tsFiles, compilerOptions);
207
+ }
208
+ catch {
209
+ return [];
210
+ }
211
+ const checker = program.getTypeChecker();
212
+ for (const sourceFile of program.getSourceFiles()) {
213
+ if (sourceFile.isDeclarationFile)
214
+ continue;
215
+ if (!tsFiles.includes(sourceFile.fileName))
216
+ continue;
217
+ function recordEdge(identifierNode, edgeType) {
218
+ try {
219
+ let symbol = checker.getSymbolAtLocation(identifierNode);
220
+ // Resolve import aliases to the real declaration — otherwise imported
221
+ // symbols point at the import statement itself, not the function body.
222
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
223
+ symbol = checker.getAliasedSymbol(symbol);
224
+ }
225
+ const declarations = symbol?.getDeclarations();
226
+ if (!declarations || declarations.length === 0)
227
+ return;
228
+ const decl = declarations[0];
229
+ const declFile = decl.getSourceFile().fileName;
230
+ const declLine = decl.getSourceFile().getLineAndCharacterOfPosition(decl.getStart()).line + 1;
231
+ const targetSymbol = findEnclosingSymbol(declFile, declLine);
232
+ const refLine = sourceFile.getLineAndCharacterOfPosition(identifierNode.getStart()).line + 1;
233
+ const callerSymbol = findEnclosingSymbol(sourceFile.fileName, refLine);
234
+ if (targetSymbol && callerSymbol && targetSymbol.fingerprint !== callerSymbol.fingerprint) {
235
+ edges.push({
236
+ from: callerSymbol.fingerprint,
237
+ to: targetSymbol.fingerprint,
238
+ type: edgeType,
239
+ confidence: edgeType === "calls" ? 1.0 : 0.9,
240
+ file: sourceFile.fileName,
241
+ line: refLine,
242
+ });
243
+ }
244
+ }
245
+ catch { }
246
+ }
247
+ function visit(node) {
248
+ if (ts.isCallExpression(node)) {
249
+ // The call target itself — e.g. authMiddleware() — direct invocation
250
+ recordEdge(node.expression, "calls");
251
+ // Arguments passed to this call that resolve to known function symbols —
252
+ // e.g. app.addHook("preHandler", authMiddleware) — registration/callback pattern.
253
+ // This is where middleware, event handlers, and hooks get missed if we only
254
+ // look at direct calls.
255
+ for (const arg of node.arguments) {
256
+ if (ts.isIdentifier(arg)) {
257
+ recordEdge(arg, "references");
258
+ }
259
+ }
260
+ }
261
+ // Also catch bare identifier references outside call arguments —
262
+ // e.g. `export const handler = someFunction;` or object literal values
263
+ // like `{ preHandler: authMiddleware }`.
264
+ if (ts.isIdentifier(node) &&
265
+ !ts.isCallExpression(node.parent) &&
266
+ node.parent &&
267
+ (ts.isPropertyAssignment(node.parent) || ts.isVariableDeclaration(node.parent) || ts.isBinaryExpression(node.parent))) {
268
+ recordEdge(node, "references");
269
+ }
270
+ ts.forEachChild(node, visit);
271
+ }
272
+ visit(sourceFile);
273
+ }
274
+ return edges;
275
+ }
276
+ function computeCentrality(nodes, edges) {
277
+ for (const edge of edges) {
278
+ if (nodes[edge.to])
279
+ nodes[edge.to].fanIn += edge.confidence;
280
+ if (nodes[edge.from])
281
+ nodes[edge.from].fanOut += edge.confidence;
282
+ }
283
+ for (const node of Object.values(nodes)) {
284
+ node.centrality = node.fanIn * 2 + node.fanOut;
285
+ }
286
+ }
287
+ async function buildGraph(repoRoot) {
288
+ const symbols = await collectAllSymbols(repoRoot);
289
+ const nodes = {};
290
+ for (const sym of symbols) {
291
+ const isTsJs = TS_JS_EXTENSIONS.has(path_1.default.extname(sym.file));
292
+ nodes[sym.fingerprint] = {
293
+ fingerprint: sym.fingerprint,
294
+ name: sym.name,
295
+ file: sym.file,
296
+ type: sym.type,
297
+ startLine: sym.startLine,
298
+ endLine: sym.endLine,
299
+ language: sym.language,
300
+ resolutionTier: isTsJs ? 1 : 2,
301
+ fanIn: 0,
302
+ fanOut: 0,
303
+ centrality: 0,
304
+ rawCalls: sym.calls ?? [],
305
+ };
306
+ }
307
+ const tsFiles = symbols
308
+ .map(s => s.file)
309
+ .filter(f => TS_JS_EXTENSIONS.has(path_1.default.extname(f)))
310
+ .filter((f, i, arr) => arr.indexOf(f) === i);
311
+ const tier1Edges = resolveTier1Edges(repoRoot, tsFiles, symbols);
312
+ const nameIndex = buildNameIndex(symbols);
313
+ const tier2Edges = resolveTier2Edges(symbols, nameIndex);
314
+ const edges = [...tier1Edges, ...tier2Edges];
315
+ computeCentrality(nodes, edges);
316
+ return {
317
+ builtAt: new Date().toISOString(),
318
+ repoRoot,
319
+ nodes,
320
+ edges,
321
+ };
322
+ }
323
+ function edgeKey(e) {
324
+ return `${e.from}::${e.to}::${e.type}`;
325
+ }
326
+ /**
327
+ * Diffs the freshly built graph against the previously saved one.
328
+ * Nodes are compared by fingerprint. Edges are compared by (from, to, type) —
329
+ * this prevents re-inserting the same edge on every sync while still allowing
330
+ * confidence values to be refreshed via the "added" path (upsert semantics).
331
+ */
332
+ function diffGraphs(previous, current) {
333
+ const prevNodeKeys = new Set(previous ? Object.keys(previous.nodes) : []);
334
+ const currNodeKeys = new Set(Object.keys(current.nodes));
335
+ const added = [];
336
+ for (const key of currNodeKeys) {
337
+ // Always include changed nodes — cheap to upsert, and centrality/fanIn/fanOut
338
+ // change frequently even when the symbol itself hasn't moved.
339
+ added.push(current.nodes[key]);
340
+ }
341
+ const removed = [];
342
+ for (const key of prevNodeKeys) {
343
+ if (!currNodeKeys.has(key))
344
+ removed.push(key);
345
+ }
346
+ const prevEdgeMap = new Map();
347
+ for (const e of previous?.edges ?? []) {
348
+ prevEdgeMap.set(edgeKey(e), e);
349
+ }
350
+ const currEdgeMap = new Map();
351
+ for (const e of current.edges) {
352
+ currEdgeMap.set(edgeKey(e), e);
353
+ }
354
+ const edgesAdded = [];
355
+ for (const [key, edge] of currEdgeMap) {
356
+ if (!prevEdgeMap.has(key))
357
+ edgesAdded.push(edge);
358
+ }
359
+ const edgesRemoved = [];
360
+ for (const [key, edge] of prevEdgeMap) {
361
+ if (!currEdgeMap.has(key)) {
362
+ edgesRemoved.push({ from: edge.from, to: edge.to, type: edge.type });
363
+ }
364
+ }
365
+ return { added, removed, edgesAdded, edgesRemoved };
366
+ }
367
+ function saveGraph(repoRoot, graph) {
368
+ const kortexDir = path_1.default.join(repoRoot, ".kortex");
369
+ if (!fs_1.default.existsSync(kortexDir)) {
370
+ fs_1.default.mkdirSync(kortexDir, { recursive: true });
371
+ }
372
+ const graphPath = path_1.default.join(kortexDir, "graph.json");
373
+ fs_1.default.writeFileSync(graphPath, JSON.stringify(graph, null, 2), "utf-8");
374
+ }
375
+ function loadGraph(repoRoot) {
376
+ const graphPath = path_1.default.join(repoRoot, ".kortex", "graph.json");
377
+ if (!fs_1.default.existsSync(graphPath))
378
+ return null;
379
+ try {
380
+ return JSON.parse(fs_1.default.readFileSync(graphPath, "utf-8"));
381
+ }
382
+ catch {
383
+ return null;
384
+ }
385
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * security-code-analyzer.ts
4
+ *
5
+ * Layer 1 orchestrator — runs entirely client-side, where the actual source
6
+ * code and the fully-resolved local graph both exist. Combines:
7
+ * - Deterministic code pattern scanning (SQL injection, hardcoded secrets, etc)
8
+ * - Missing-auth-on-route detection, using the RESOLVED local graph (more
9
+ * precise than the raw_calls fallback, since resolved edges already
10
+ * correctly handle object-literal patterns like { preHandler: authMiddleware })
11
+ *
12
+ * For every flagged candidate, assembles full code context (target body +
13
+ * 1-hop callers/callees) via context-assembler.ts, ready to send to the
14
+ * server for Layer 2 — an LLM judgment with full visibility, which is what
15
+ * actually adjudicates real risk vs false positive.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.findMissingAuthCandidatesLocal = findMissingAuthCandidatesLocal;
19
+ exports.analyzeForSecurityCandidates = analyzeForSecurityCandidates;
20
+ const context_assembler_1 = require("./context-assembler");
21
+ const code_pattern_scanner_1 = require("./code-pattern-scanner");
22
+ const ROUTE_REGISTRATION_METHOD_NAMES = new Set(["get", "post", "put", "delete", "patch", "addhook", "all"]);
23
+ const AUTH_FUNCTION_HINTS = [
24
+ "auth", "requireauth", "requireorg", "requireadmin", "requirelogin",
25
+ "verifytoken", "checkauth", "isauthenticated", "authenticate", "authorize",
26
+ "ensureauth", "authmiddleware", "authguard", "protect", "jwt",
27
+ ];
28
+ const PUBLIC_ENDPOINT_HINTS = ["health", "login", "signup", "register", "public", "webhook", "ping", "status"];
29
+ function includesRouteRegistration(names) {
30
+ return names.some(name => ROUTE_REGISTRATION_METHOD_NAMES.has(name.toLowerCase()));
31
+ }
32
+ function includesAuthHint(names) {
33
+ return names.some(name => {
34
+ const lower = name.toLowerCase();
35
+ return AUTH_FUNCTION_HINTS.some(hint => lower.includes(hint));
36
+ });
37
+ }
38
+ function looksLikePublicEndpoint(symbolName, file) {
39
+ const lower = (symbolName + " " + file).toLowerCase();
40
+ return PUBLIC_ENDPOINT_HINTS.some(hint => lower.includes(hint));
41
+ }
42
+ /**
43
+ * Finds route-registering functions with no resolved auth reference.
44
+ * Uses BOTH the resolved local graph edges (most precise — handles object-
45
+ * literal patterns) AND the raw call/reference list (catches library calls
46
+ * that never resolve as edges, like app.get itself).
47
+ */
48
+ function findMissingAuthCandidatesLocal(graph, symbolsByFingerprint) {
49
+ const candidates = [];
50
+ for (const node of Object.values(graph.nodes)) {
51
+ if (node.type !== "function" && node.type !== "method")
52
+ continue;
53
+ const sym = symbolsByFingerprint.get(node.fingerprint);
54
+ const rawCallNames = sym?.calls ?? [];
55
+ // Names reachable via resolved graph edges (calls + references) — this
56
+ // correctly includes object-literal-nested identifiers like
57
+ // { preHandler: authMiddleware }, which raw call extraction alone misses.
58
+ const resolvedEdgeNames = graph.edges
59
+ .filter(e => e.from === node.fingerprint)
60
+ .map(e => graph.nodes[e.to]?.name)
61
+ .filter((n) => !!n);
62
+ const allNames = [...rawCallNames, ...resolvedEdgeNames];
63
+ if (!includesRouteRegistration(allNames))
64
+ continue;
65
+ if (includesAuthHint(allNames))
66
+ continue;
67
+ if (looksLikePublicEndpoint(node.name, node.file))
68
+ continue;
69
+ candidates.push({ symbol: node.name, file: node.file, fingerprint: node.fingerprint });
70
+ }
71
+ return candidates;
72
+ }
73
+ /**
74
+ * Runs both Layer 1 detectors and assembles full code context for every
75
+ * flagged candidate — ready to send to the server for Layer 2 LLM judgment.
76
+ */
77
+ function analyzeForSecurityCandidates(repoRoot, graph, symbols) {
78
+ const symbolsByFingerprint = new Map(symbols.map(s => [s.fingerprint, s]));
79
+ const candidates = [];
80
+ // ── Code pattern findings ────────────────────────────────────────────────
81
+ const patternFindings = (0, code_pattern_scanner_1.scanAllSymbolsForPatterns)(symbols);
82
+ for (const finding of patternFindings) {
83
+ const sym = symbols.find(s => s.name === finding.symbol && s.file === finding.file);
84
+ const fingerprint = sym?.fingerprint ?? "";
85
+ const assembled = fingerprint ? (0, context_assembler_1.assembleContext)(repoRoot, graph, fingerprint) : null;
86
+ candidates.push({
87
+ kind: "code_pattern",
88
+ symbol: finding.symbol,
89
+ file: finding.file,
90
+ fingerprint,
91
+ patternType: finding.patternType,
92
+ severity: finding.severity,
93
+ snippet: finding.snippet,
94
+ description: finding.description,
95
+ context: assembled ? {
96
+ targetBody: assembled.targetBody,
97
+ callers: assembled.callers.map(c => ({ name: c.name, snippet: c.snippet })),
98
+ callees: assembled.callees.map(c => ({ name: c.name, snippet: c.snippet })),
99
+ } : null,
100
+ });
101
+ }
102
+ // ── Missing-auth candidates ───────────────────────────────────────────────
103
+ const authCandidates = findMissingAuthCandidatesLocal(graph, symbolsByFingerprint);
104
+ for (const cand of authCandidates) {
105
+ const assembled = (0, context_assembler_1.assembleContext)(repoRoot, graph, cand.fingerprint);
106
+ candidates.push({
107
+ kind: "missing_auth",
108
+ symbol: cand.symbol,
109
+ file: cand.file,
110
+ fingerprint: cand.fingerprint,
111
+ context: assembled ? {
112
+ targetBody: assembled.targetBody,
113
+ callers: assembled.callers.map(c => ({ name: c.name, snippet: c.snippet })),
114
+ callees: assembled.callees.map(c => ({ name: c.name, snippet: c.snippet })),
115
+ } : null,
116
+ });
117
+ }
118
+ return candidates;
119
+ }