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.
- package/dist/cli.js +33 -0
- package/dist/commands/init.js +18 -1
- package/dist/commands/install-hook.js +2 -0
- package/dist/commands/scan-repo.js +48 -9
- package/dist/commands/security-scan.js +217 -0
- package/dist/commands/sync-dependencies.js +69 -0
- package/dist/commands/sync-graph.js +105 -0
- package/dist/test-buildgraph-debug.js +10 -0
- package/dist/test-calls-debug.js +16 -0
- package/dist/utils/ast-parser.js +105 -0
- package/dist/utils/code-pattern-scanner.js +128 -0
- package/dist/utils/context-assembler.js +91 -0
- package/dist/utils/dependency-parser.js +229 -0
- package/dist/utils/graph-builder.js +385 -0
- package/dist/utils/security-code-analyzer.js +119 -0
- package/dist/utils/tree-sitter-parser.js +281 -0
- package/package.json +6 -4
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,9 @@ const deny_1 = require("./commands/deny");
|
|
|
14
14
|
const scan_git_1 = require("./commands/scan-git");
|
|
15
15
|
const scan_claude_1 = require("./commands/scan-claude");
|
|
16
16
|
const scan_repo_1 = require("./commands/scan-repo");
|
|
17
|
+
const sync_graph_1 = require("./commands/sync-graph");
|
|
18
|
+
const sync_dependencies_1 = require("./commands/sync-dependencies");
|
|
19
|
+
const security_scan_1 = require("./commands/security-scan");
|
|
17
20
|
const init_1 = require("./commands/init");
|
|
18
21
|
const install_hook_1 = require("./commands/install-hook");
|
|
19
22
|
const update_1 = require("./commands/update");
|
|
@@ -231,6 +234,36 @@ program
|
|
|
231
234
|
payload.repo = String(options.repo);
|
|
232
235
|
await (0, scan_repo_1.scanRepoCommand)(payload);
|
|
233
236
|
});
|
|
237
|
+
program
|
|
238
|
+
.command("sync-graph")
|
|
239
|
+
.description("Build and sync the structural relationship graph for the current repo")
|
|
240
|
+
.option("--repo <path>", "repo path to scan")
|
|
241
|
+
.action(async (options) => {
|
|
242
|
+
const payload = {};
|
|
243
|
+
if (options.repo)
|
|
244
|
+
payload.repo = String(options.repo);
|
|
245
|
+
await (0, sync_graph_1.syncGraphCommand)(payload);
|
|
246
|
+
});
|
|
247
|
+
program
|
|
248
|
+
.command("sync-dependencies")
|
|
249
|
+
.description("Extract and sync declared dependencies for security scanning")
|
|
250
|
+
.option("--repo <path>", "repo path to scan")
|
|
251
|
+
.action(async (options) => {
|
|
252
|
+
const payload = {};
|
|
253
|
+
if (options.repo)
|
|
254
|
+
payload.repo = String(options.repo);
|
|
255
|
+
await (0, sync_dependencies_1.syncDependenciesCommand)(payload);
|
|
256
|
+
});
|
|
257
|
+
program
|
|
258
|
+
.command("security-scan")
|
|
259
|
+
.description("Scan synced dependencies for known vulnerabilities")
|
|
260
|
+
.option("--repo <path>", "repo path to scan")
|
|
261
|
+
.action(async (options) => {
|
|
262
|
+
const payload = {};
|
|
263
|
+
if (options.repo)
|
|
264
|
+
payload.repo = String(options.repo);
|
|
265
|
+
await (0, security_scan_1.securityScanCommand)(payload);
|
|
266
|
+
});
|
|
234
267
|
program
|
|
235
268
|
.command("context")
|
|
236
269
|
.description("Get affirmed memories for a file — designed for AI agents")
|
package/dist/commands/init.js
CHANGED
|
@@ -289,7 +289,24 @@ function registerInitCommand(program) {
|
|
|
289
289
|
catch { }
|
|
290
290
|
console.log(`✔ Connected. Token saved to ${persistence_config_1.CONFIG_PATH}`);
|
|
291
291
|
console.log("\nKortex is now watching this repo.");
|
|
292
|
-
|
|
292
|
+
// Install the git hook automatically — no manual step required.
|
|
293
|
+
// This is what triggers scan-git and sync-dependencies on every future
|
|
294
|
+
// commit, silently in the background.
|
|
295
|
+
try {
|
|
296
|
+
const { installHookCommand } = await Promise.resolve().then(() => __importStar(require("./install-hook")));
|
|
297
|
+
await installHookCommand({ repo: repoRoot });
|
|
298
|
+
console.log("✔ Git hook installed — memories and dependency data will stay current automatically on every commit.\n");
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
console.log(`⚠ Could not auto-install git hook (${err.message}). Run \`kodingo install-hook\` manually to enable automatic capture.\n`);
|
|
302
|
+
}
|
|
303
|
+
// Initial dependency sync — so security scanning has data immediately,
|
|
304
|
+
// rather than waiting for the next commit to trigger the hook.
|
|
305
|
+
try {
|
|
306
|
+
const { syncDependenciesCommand } = await Promise.resolve().then(() => __importStar(require("./sync-dependencies")));
|
|
307
|
+
await syncDependenciesCommand({ repo: repoRoot, silent: true });
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
293
310
|
// Auto-scan existing codebase
|
|
294
311
|
try {
|
|
295
312
|
const { scanRepoCommand } = await Promise.resolve().then(() => __importStar(require("./scan-repo")));
|
|
@@ -18,6 +18,8 @@ async function installHookCommand(options) {
|
|
|
18
18
|
"# <kodingo>",
|
|
19
19
|
"# Kodingo: auto-infer decision memory from git commits",
|
|
20
20
|
`"${kodingoBin}" scan-git --repo "$(git rev-parse --show-toplevel)" &`,
|
|
21
|
+
"# Kodingo Security: keep dependency data current for vulnerability scanning",
|
|
22
|
+
`"${kodingoBin}" sync-dependencies --repo "$(git rev-parse --show-toplevel)" &`,
|
|
21
23
|
"# </kodingo>",
|
|
22
24
|
].join("\n");
|
|
23
25
|
// Pre-commit hook — runs synchronously to enforce guardrails
|
|
@@ -55,6 +55,8 @@ const path_1 = __importDefault(require("path"));
|
|
|
55
55
|
const persistence_config_1 = require("../utils/persistence-config");
|
|
56
56
|
const os = __importStar(require("os"));
|
|
57
57
|
const ast_parser_1 = require("../utils/ast-parser");
|
|
58
|
+
const graph_builder_1 = require("../utils/graph-builder");
|
|
59
|
+
const context_assembler_1 = require("../utils/context-assembler");
|
|
58
60
|
const API_BASE = "https://kodingo-api.onrender.com";
|
|
59
61
|
const SUPPORTED_EXTENSIONS = [
|
|
60
62
|
".ts", ".tsx", ".js", ".jsx",
|
|
@@ -124,7 +126,8 @@ async function fetchExistingSymbols(token, repo, apiUrl) {
|
|
|
124
126
|
if (!res.ok)
|
|
125
127
|
break;
|
|
126
128
|
const data = await res.json();
|
|
127
|
-
|
|
129
|
+
const page = data.data ?? [];
|
|
130
|
+
for (const m of page) {
|
|
128
131
|
if (m.symbol) {
|
|
129
132
|
// Store both symbol-only and symbol::repo keys for accurate matching
|
|
130
133
|
known.add(m.symbol);
|
|
@@ -133,8 +136,8 @@ async function fetchExistingSymbols(token, repo, apiUrl) {
|
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
offset += limit;
|
|
136
|
-
if (
|
|
137
|
-
break;
|
|
139
|
+
if (page.length < limit)
|
|
140
|
+
break; // fewer than a full page means we've reached the end
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
catch { }
|
|
@@ -150,12 +153,29 @@ async function sendSignal(token, apiUrl, memoryId, weightClass) {
|
|
|
150
153
|
}
|
|
151
154
|
catch { }
|
|
152
155
|
}
|
|
153
|
-
async function inferAndCapture(token, symbol, repo) {
|
|
156
|
+
async function inferAndCapture(token, symbol, repo, graph, repoRoot) {
|
|
154
157
|
try {
|
|
155
|
-
|
|
158
|
+
// Use contextual inference when the graph knows this symbol — richer, structure-grounded reasoning.
|
|
159
|
+
// Falls back to isolated /infer for symbols the graph didn't resolve (e.g. parse failures).
|
|
160
|
+
const useContextual = graph && symbol.fingerprint && graph.nodes[symbol.fingerprint];
|
|
161
|
+
const endpoint = useContextual ? "/infer/contextual" : "/infer";
|
|
162
|
+
let body = { symbol: symbol.name, code: symbol.code };
|
|
163
|
+
if (useContextual && graph && symbol.fingerprint) {
|
|
164
|
+
const assembled = (0, context_assembler_1.assembleContext)(repoRoot, graph, symbol.fingerprint);
|
|
165
|
+
if (assembled) {
|
|
166
|
+
body = {
|
|
167
|
+
symbol: symbol.name,
|
|
168
|
+
code: symbol.code,
|
|
169
|
+
callers: assembled.callers.map(c => ({ name: c.name, snippet: c.snippet, confidence: c.confidence })),
|
|
170
|
+
callees: assembled.callees.map(c => ({ name: c.name, snippet: c.snippet, confidence: c.confidence })),
|
|
171
|
+
weakestConfidence: assembled.weakestConfidence,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const inferRes = await fetch(`${API_BASE}${endpoint}`, {
|
|
156
176
|
method: "POST",
|
|
157
177
|
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
158
|
-
body: JSON.stringify(
|
|
178
|
+
body: JSON.stringify(body),
|
|
159
179
|
});
|
|
160
180
|
if (!inferRes.ok)
|
|
161
181
|
return false;
|
|
@@ -217,8 +237,27 @@ async function scanRepoCommand(options = {}) {
|
|
|
217
237
|
}
|
|
218
238
|
// Fetch all existing memory symbols in one bulk call
|
|
219
239
|
const { known: existingSymbols, idMap: existingIds } = await fetchExistingSymbols(token, repoName, apiUrl);
|
|
220
|
-
//
|
|
221
|
-
|
|
240
|
+
// Build (or reuse) the structural graph — used to order symbols by
|
|
241
|
+
// centrality (most structurally critical first) instead of randomly,
|
|
242
|
+
// and to assemble context for richer, structure-grounded inference.
|
|
243
|
+
let graph = null;
|
|
244
|
+
try {
|
|
245
|
+
graph = await (0, graph_builder_1.buildGraph)(repoRoot);
|
|
246
|
+
(0, graph_builder_1.saveGraph)(repoRoot, graph);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
graph = null;
|
|
250
|
+
}
|
|
251
|
+
const ordered = graph
|
|
252
|
+
? [...allSymbols].sort((a, b) => {
|
|
253
|
+
const centralityA = a.fingerprint && graph.nodes[a.fingerprint] ? graph.nodes[a.fingerprint].centrality : -1;
|
|
254
|
+
const centralityB = b.fingerprint && graph.nodes[b.fingerprint] ? graph.nodes[b.fingerprint].centrality : -1;
|
|
255
|
+
if (centralityA !== centralityB)
|
|
256
|
+
return centralityB - centralityA;
|
|
257
|
+
return Math.random() - 0.5;
|
|
258
|
+
})
|
|
259
|
+
: [...allSymbols].sort(() => Math.random() - 0.5);
|
|
260
|
+
const shuffled = ordered;
|
|
222
261
|
// Filter locally — no per-symbol API calls needed
|
|
223
262
|
const toCapture = [];
|
|
224
263
|
const seen = new Set();
|
|
@@ -253,7 +292,7 @@ async function scanRepoCommand(options = {}) {
|
|
|
253
292
|
let captured = 0;
|
|
254
293
|
for (let i = 0; i < toCapture.length; i += BATCH_SIZE) {
|
|
255
294
|
const batch = toCapture.slice(i, i + BATCH_SIZE);
|
|
256
|
-
const results = await Promise.all(batch.map(sym => inferAndCapture(token, sym, repoName)));
|
|
295
|
+
const results = await Promise.all(batch.map(sym => inferAndCapture(token, sym, repoName, graph, repoRoot)));
|
|
257
296
|
const batchCaptured = results.filter(Boolean).length;
|
|
258
297
|
captured += batchCaptured;
|
|
259
298
|
if (!options.silent) {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo security-scan
|
|
4
|
+
*
|
|
5
|
+
* Triggers a vulnerability scan against whatever dependencies were last
|
|
6
|
+
* synced via 'kodingo sync-dependencies', then prints a ranked report.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.securityScanCommand = securityScanCommand;
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
16
|
+
const repo_scope_1 = require("../utils/repo-scope");
|
|
17
|
+
const graph_builder_1 = require("../utils/graph-builder");
|
|
18
|
+
const security_code_analyzer_1 = require("../utils/security-code-analyzer");
|
|
19
|
+
const context_assembler_1 = require("../utils/context-assembler");
|
|
20
|
+
const ast_parser_1 = require("../utils/ast-parser");
|
|
21
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", ".kortex", "dist", "build", "vendor", "out", ".next"]);
|
|
22
|
+
const SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".php", ".java", ".kt", ".rb", ".c", ".cpp"];
|
|
23
|
+
function walkFiles(dir) {
|
|
24
|
+
const results = [];
|
|
25
|
+
try {
|
|
26
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (IGNORED_DIRS.has(entry.name))
|
|
29
|
+
continue;
|
|
30
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
results.push(...walkFiles(fullPath));
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
|
|
35
|
+
results.push(fullPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
const SEVERITY_ICON = {
|
|
43
|
+
critical: "🔴", high: "🟠", moderate: "🟡", low: "🔵", unknown: "⚪",
|
|
44
|
+
};
|
|
45
|
+
async function securityScanCommand(options = {}) {
|
|
46
|
+
const repoRoot = (0, repo_scope_1.resolveRepoScope)(options.repo ? { repo: options.repo } : {}) ?? process.cwd();
|
|
47
|
+
const config = (0, persistence_config_1.readConfig)();
|
|
48
|
+
const wsConfig = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
|
|
49
|
+
const token = wsConfig?.token ?? "";
|
|
50
|
+
const apiUrl = wsConfig?.apiUrl ?? config.apiUrl ?? "https://kodingo-api.onrender.com";
|
|
51
|
+
if (!token) {
|
|
52
|
+
if (!options.silent)
|
|
53
|
+
console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const repoName = path_1.default.basename(repoRoot);
|
|
57
|
+
if (!options.silent)
|
|
58
|
+
console.log(`\n🔒 Kortex Security — scanning ${repoName} for known vulnerabilities...\n`);
|
|
59
|
+
try {
|
|
60
|
+
const scanRes = await fetch(`${apiUrl}/security/scan`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
63
|
+
body: JSON.stringify({ repo: repoName }),
|
|
64
|
+
});
|
|
65
|
+
if (!scanRes.ok) {
|
|
66
|
+
if (!options.silent)
|
|
67
|
+
console.error(` ✖ Scan failed: ${scanRes.status} ${await scanRes.text()}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const scanData = await scanRes.json();
|
|
71
|
+
if (scanData.message) {
|
|
72
|
+
if (!options.silent)
|
|
73
|
+
console.log(` ${scanData.message}`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (!options.silent)
|
|
77
|
+
console.log(` Checked ${scanData.scanned} dependencies against known vulnerability databases\n`);
|
|
78
|
+
const findingsRes = await fetch(`${apiUrl}/security/findings?repo=${encodeURIComponent(repoName)}`, {
|
|
79
|
+
headers: { "X-Kodingo-Token": token },
|
|
80
|
+
});
|
|
81
|
+
if (!findingsRes.ok)
|
|
82
|
+
return true; // scan itself succeeded even if the report fetch fails
|
|
83
|
+
const { findings } = await findingsRes.json();
|
|
84
|
+
if (findings.length === 0) {
|
|
85
|
+
if (!options.silent)
|
|
86
|
+
console.log(" ✔ No known vulnerabilities found in your dependencies.");
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (!options.silent) {
|
|
90
|
+
console.log(` ⚠ ${findings.length} vulnerabilit${findings.length === 1 ? "y" : "ies"} found:\n`);
|
|
91
|
+
for (const f of findings) {
|
|
92
|
+
const icon = SEVERITY_ICON[f.severity] ?? "⚪";
|
|
93
|
+
console.log(` ${icon} ${f.severity.toUpperCase()} — ${f.package_name}@${f.version}`);
|
|
94
|
+
console.log(` ${f.summary}`);
|
|
95
|
+
if (f.fixed_version)
|
|
96
|
+
console.log(` Fix: upgrade to ${f.fixed_version} or later`);
|
|
97
|
+
if (f.alsoAffectsRepoCount && f.alsoAffectsRepoCount > 0) {
|
|
98
|
+
console.log(` ⚠ Also present in ${f.alsoAffectsRepoCount} other repo${f.alsoAffectsRepoCount === 1 ? "" : "s"} in this project`);
|
|
99
|
+
}
|
|
100
|
+
console.log(` ${f.vulnerability_id}\n`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Code-level analysis — Layer 1 (local) + Layer 2 (server LLM judgment) ──
|
|
104
|
+
if (!options.silent)
|
|
105
|
+
console.log(`\n🔍 Analysing code for vulnerable patterns and missing auth...\n`);
|
|
106
|
+
try {
|
|
107
|
+
const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
|
|
108
|
+
(0, graph_builder_1.saveGraph)(repoRoot, graph);
|
|
109
|
+
const files = walkFiles(repoRoot);
|
|
110
|
+
const symbols = [];
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
try {
|
|
113
|
+
const source = fs_1.default.readFileSync(file, "utf-8");
|
|
114
|
+
symbols.push(...(await (0, ast_parser_1.parseFile)(file, source)));
|
|
115
|
+
}
|
|
116
|
+
catch { }
|
|
117
|
+
}
|
|
118
|
+
const candidates = (0, security_code_analyzer_1.analyzeForSecurityCandidates)(repoRoot, graph, symbols);
|
|
119
|
+
if (candidates.length === 0) {
|
|
120
|
+
if (!options.silent)
|
|
121
|
+
console.log(" ✔ No suspicious patterns detected.");
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const codeScanRes = await fetch(`${apiUrl}/security/scan-code`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
127
|
+
body: JSON.stringify({ repo: repoName, candidates }),
|
|
128
|
+
});
|
|
129
|
+
if (codeScanRes.ok) {
|
|
130
|
+
const codeResult = await codeScanRes.json();
|
|
131
|
+
if (!options.silent) {
|
|
132
|
+
console.log(` Checked ${codeResult.scanned} candidates — ${codeResult.findingsCreated} confirmed, ${codeResult.dismissed} dismissed as false positives after review\n`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (!options.silent) {
|
|
136
|
+
console.error(` ✖ Code analysis failed: ${codeScanRes.status}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (!options.silent)
|
|
142
|
+
console.error(` ✖ Code analysis failed: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
// ── Decision drift — does current code still honour past decisions? ──────
|
|
145
|
+
if (!options.silent)
|
|
146
|
+
console.log(`\n📜 Checking for drift from past decisions...\n`);
|
|
147
|
+
try {
|
|
148
|
+
const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
|
|
149
|
+
const files = walkFiles(repoRoot);
|
|
150
|
+
const symbols = [];
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
try {
|
|
153
|
+
const source = fs_1.default.readFileSync(file, "utf-8");
|
|
154
|
+
symbols.push(...(await (0, ast_parser_1.parseFile)(file, source)));
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
157
|
+
}
|
|
158
|
+
const symbolNames = Array.from(new Set(symbols.map(s => s.name)));
|
|
159
|
+
if (symbolNames.length > 0) {
|
|
160
|
+
const candidatesRes = await fetch(`${apiUrl}/security/decision-drift-candidates`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
163
|
+
body: JSON.stringify({ repo: repoName, symbols: symbolNames.slice(0, 3000) }),
|
|
164
|
+
});
|
|
165
|
+
if (candidatesRes.ok) {
|
|
166
|
+
const { matches } = await candidatesRes.json();
|
|
167
|
+
if (matches.length === 0) {
|
|
168
|
+
if (!options.silent)
|
|
169
|
+
console.log(" No symbols with affirmed decisions found yet.");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const checks = [];
|
|
173
|
+
for (const match of matches) {
|
|
174
|
+
const sym = symbols.find(s => s.name === match.symbol);
|
|
175
|
+
if (!sym)
|
|
176
|
+
continue;
|
|
177
|
+
const assembled = (0, context_assembler_1.assembleContext)(repoRoot, graph, sym.fingerprint);
|
|
178
|
+
if (!assembled)
|
|
179
|
+
continue;
|
|
180
|
+
checks.push({
|
|
181
|
+
symbol: match.symbol,
|
|
182
|
+
file: sym.file,
|
|
183
|
+
fingerprint: sym.fingerprint,
|
|
184
|
+
currentBody: assembled.targetBody,
|
|
185
|
+
decisionTitle: match.decisionTitle,
|
|
186
|
+
decisionContent: match.decisionContent,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (checks.length > 0) {
|
|
190
|
+
const driftRes = await fetch(`${apiUrl}/security/scan-decision-drift`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
193
|
+
body: JSON.stringify({ repo: repoName, checks }),
|
|
194
|
+
});
|
|
195
|
+
if (driftRes.ok) {
|
|
196
|
+
const driftResult = await driftRes.json();
|
|
197
|
+
if (!options.silent) {
|
|
198
|
+
console.log(` Checked ${driftResult.scanned} symbols against past decisions — ${driftResult.findingsCreated} drifted, ${driftResult.noDrift} still honoured\n`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (!options.silent)
|
|
208
|
+
console.error(` ✖ Decision drift check failed: ${err.message}`);
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
if (!options.silent)
|
|
214
|
+
console.error(` ✖ Scan failed: ${err.message}`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo sync-dependencies
|
|
4
|
+
*
|
|
5
|
+
* Extracts declared dependencies from the current repo (package.json/lock,
|
|
6
|
+
* composer.json/lock) and syncs them to the server for security scanning.
|
|
7
|
+
* Run this before 'kodingo security-scan' — the scan works off whatever was
|
|
8
|
+
* last synced here.
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.syncDependenciesCommand = syncDependenciesCommand;
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
17
|
+
const repo_scope_1 = require("../utils/repo-scope");
|
|
18
|
+
const dependency_parser_1 = require("../utils/dependency-parser");
|
|
19
|
+
async function syncDependenciesCommand(options = {}) {
|
|
20
|
+
const repoRoot = (0, repo_scope_1.resolveRepoScope)(options.repo ? { repo: options.repo } : {}) ?? process.cwd();
|
|
21
|
+
const config = (0, persistence_config_1.readConfig)();
|
|
22
|
+
const wsConfig = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
|
|
23
|
+
const token = wsConfig?.token ?? "";
|
|
24
|
+
const apiUrl = wsConfig?.apiUrl ?? config.apiUrl ?? "https://kodingo-api.onrender.com";
|
|
25
|
+
if (!token) {
|
|
26
|
+
if (!options.silent)
|
|
27
|
+
console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const repoName = path_1.default.basename(repoRoot);
|
|
31
|
+
if (!options.silent)
|
|
32
|
+
console.log(`\n🔒 Kortex Security — scanning dependencies for ${repoName}...\n`);
|
|
33
|
+
const deps = (0, dependency_parser_1.extractDependencies)(repoRoot);
|
|
34
|
+
if (deps.length === 0) {
|
|
35
|
+
if (!options.silent)
|
|
36
|
+
console.log(" No package.json or composer.json found. Nothing to sync.");
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (!options.silent)
|
|
40
|
+
console.log(` Found ${deps.length} dependencies`);
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(`${apiUrl}/security/sync`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
repo: repoName,
|
|
47
|
+
dependencies: deps.map(d => ({
|
|
48
|
+
ecosystem: d.ecosystem,
|
|
49
|
+
packageName: d.packageName,
|
|
50
|
+
version: d.version,
|
|
51
|
+
manifestFile: d.manifestFile,
|
|
52
|
+
})),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
if (!options.silent)
|
|
57
|
+
console.error(` ✖ Sync failed: ${res.status} ${await res.text()}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!options.silent)
|
|
61
|
+
console.log(`\n✔ Synced ${deps.length} dependencies for ${repoName}`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (!options.silent)
|
|
66
|
+
console.error(` ✖ Sync failed: ${err.message}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo sync-graph
|
|
4
|
+
*
|
|
5
|
+
* Builds the structural relationship graph for the current repo, diffs it
|
|
6
|
+
* against the previously saved local graph, and syncs only the diff to the
|
|
7
|
+
* server. Safe to run repeatedly — nodes are upserted, edges are additive
|
|
8
|
+
* with soft-delete on removal.
|
|
9
|
+
*
|
|
10
|
+
* Designed to be fast on repeat runs since only changed files need re-parsing
|
|
11
|
+
* in a future incremental version; for now it does a full local rebuild but
|
|
12
|
+
* only transmits the diff over the network.
|
|
13
|
+
*/
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.syncGraphCommand = syncGraphCommand;
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
21
|
+
const repo_scope_1 = require("../utils/repo-scope");
|
|
22
|
+
const graph_builder_1 = require("../utils/graph-builder");
|
|
23
|
+
async function syncGraphCommand(options = {}) {
|
|
24
|
+
const repoRoot = (0, repo_scope_1.resolveRepoScope)(options.repo ? { repo: options.repo } : {}) ?? process.cwd();
|
|
25
|
+
const config = (0, persistence_config_1.readConfig)();
|
|
26
|
+
const wsConfig = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
|
|
27
|
+
const token = wsConfig?.token ?? "";
|
|
28
|
+
const apiUrl = wsConfig?.apiUrl ?? config.apiUrl ?? "https://kodingo-api.onrender.com";
|
|
29
|
+
if (!token) {
|
|
30
|
+
if (!options.silent)
|
|
31
|
+
console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const repoName = path_1.default.basename(repoRoot);
|
|
35
|
+
if (!options.silent)
|
|
36
|
+
console.log(`\n🧠 Kortex — building structural graph for ${repoName}...\n`);
|
|
37
|
+
const previous = (0, graph_builder_1.loadGraph)(repoRoot);
|
|
38
|
+
const current = await (0, graph_builder_1.buildGraph)(repoRoot);
|
|
39
|
+
const diff = (0, graph_builder_1.diffGraphs)(previous, current);
|
|
40
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.edgesAdded.length === 0 && diff.edgesRemoved.length === 0) {
|
|
41
|
+
if (!options.silent)
|
|
42
|
+
console.log(" ✔ No structural changes detected. Graph is up to date.");
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (!options.silent) {
|
|
46
|
+
console.log(` ${diff.added.length} symbols, ${diff.edgesAdded.length} new relationships detected`);
|
|
47
|
+
}
|
|
48
|
+
// Chunk large syncs to respect the server's per-request caps (2000 nodes / 10000 edges)
|
|
49
|
+
const NODE_CHUNK = 1500;
|
|
50
|
+
const EDGE_CHUNK = 8000;
|
|
51
|
+
try {
|
|
52
|
+
for (let i = 0; i < diff.added.length || i === 0; i += NODE_CHUNK) {
|
|
53
|
+
const nodeChunk = diff.added.slice(i, i + NODE_CHUNK);
|
|
54
|
+
const edgeChunk = i === 0 ? diff.edgesAdded.slice(0, EDGE_CHUNK) : [];
|
|
55
|
+
const res = await fetch(`${apiUrl}/graph/sync`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": token },
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
repo: repoName,
|
|
60
|
+
added: nodeChunk.map(n => ({
|
|
61
|
+
fingerprint: n.fingerprint,
|
|
62
|
+
name: n.name,
|
|
63
|
+
file: path_1.default.relative(repoRoot, n.file),
|
|
64
|
+
type: n.type,
|
|
65
|
+
language: n.language,
|
|
66
|
+
startLine: n.startLine,
|
|
67
|
+
endLine: n.endLine,
|
|
68
|
+
resolutionTier: n.resolutionTier,
|
|
69
|
+
fanIn: n.fanIn,
|
|
70
|
+
fanOut: n.fanOut,
|
|
71
|
+
centrality: n.centrality,
|
|
72
|
+
rawCalls: n.rawCalls,
|
|
73
|
+
})),
|
|
74
|
+
removed: i === 0 ? diff.removed : [],
|
|
75
|
+
edgesAdded: edgeChunk.map(e => ({
|
|
76
|
+
fromFingerprint: e.from,
|
|
77
|
+
toFingerprint: e.to,
|
|
78
|
+
edgeType: e.type,
|
|
79
|
+
confidence: e.confidence,
|
|
80
|
+
file: path_1.default.relative(repoRoot, e.file),
|
|
81
|
+
line: e.line,
|
|
82
|
+
})),
|
|
83
|
+
edgesRemoved: i === 0 ? diff.edgesRemoved.map(e => ({ fromFingerprint: e.from, toFingerprint: e.to })) : [],
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
if (!options.silent)
|
|
88
|
+
console.error(` ✖ Sync failed: ${res.status} ${await res.text()}`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (diff.added.length === 0)
|
|
92
|
+
break; // ensure single-iteration exit when nothing to chunk
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (!options.silent)
|
|
97
|
+
console.error(` ✖ Sync failed: ${err.message}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
(0, graph_builder_1.saveGraph)(repoRoot, current);
|
|
101
|
+
if (!options.silent) {
|
|
102
|
+
console.log(`\n✔ Graph synced — ${Object.keys(current.nodes).length} symbols, ${current.edges.length} relationships tracked for ${repoName}`);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const graph_builder_1 = require("./utils/graph-builder");
|
|
4
|
+
async function main() {
|
|
5
|
+
const repoRoot = process.argv[2];
|
|
6
|
+
const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
|
|
7
|
+
const node = Object.values(graph.nodes).find(n => n.name === "projectRoutes");
|
|
8
|
+
console.log(node);
|
|
9
|
+
}
|
|
10
|
+
main();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const ast_parser_1 = require("./utils/ast-parser");
|
|
8
|
+
async function main() {
|
|
9
|
+
const filePath = process.argv[2];
|
|
10
|
+
const source = fs_1.default.readFileSync(filePath, "utf-8");
|
|
11
|
+
const symbols = await (0, ast_parser_1.parseFile)(filePath, source);
|
|
12
|
+
for (const s of symbols) {
|
|
13
|
+
console.log(`${s.name}: calls = [${s.calls.join(", ")}]`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
main();
|