depwire-cli 0.9.25 → 0.9.27
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/README.md +32 -2
- package/dist/{chunk-QHVWDUSX.js → chunk-DA5LWNJ4.js} +1484 -16
- package/dist/{chunk-ORGAO3HT.js → chunk-RGD3YJYQ.js} +79 -13
- package/dist/index.js +232 -39
- package/dist/mcpb-entry.js +2 -2
- package/dist/sdk.d.ts +49 -1
- package/dist/sdk.js +3 -1
- package/package.json +6 -6
|
@@ -14,8 +14,9 @@ import {
|
|
|
14
14
|
loadMetadata,
|
|
15
15
|
parseProject,
|
|
16
16
|
parseTypeScriptFile,
|
|
17
|
+
scanSecurity,
|
|
17
18
|
searchSymbols
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-DA5LWNJ4.js";
|
|
19
20
|
|
|
20
21
|
// src/viz/data.ts
|
|
21
22
|
import { basename } from "path";
|
|
@@ -170,14 +171,14 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
|
170
171
|
const net = await import("net");
|
|
171
172
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
172
173
|
const testPort = startPort + attempt;
|
|
173
|
-
const isAvailable = await new Promise((
|
|
174
|
+
const isAvailable = await new Promise((resolve4) => {
|
|
174
175
|
const server = net.createServer();
|
|
175
176
|
server.once("error", () => {
|
|
176
|
-
|
|
177
|
+
resolve4(false);
|
|
177
178
|
});
|
|
178
179
|
server.once("listening", () => {
|
|
179
180
|
server.close();
|
|
180
|
-
|
|
181
|
+
resolve4(true);
|
|
181
182
|
});
|
|
182
183
|
server.listen(testPort, "127.0.0.1");
|
|
183
184
|
});
|
|
@@ -376,7 +377,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
376
377
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
377
378
|
|
|
378
379
|
// src/mcp/tools.ts
|
|
379
|
-
import { dirname as dirname2, join as join5 } from "path";
|
|
380
|
+
import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
|
|
380
381
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
381
382
|
|
|
382
383
|
// src/mcp/connect.ts
|
|
@@ -625,8 +626,8 @@ async function getCurrentBranch(dir) {
|
|
|
625
626
|
}
|
|
626
627
|
}
|
|
627
628
|
async function checkoutCommit(dir, hash) {
|
|
628
|
-
if (!/^[a-
|
|
629
|
-
throw new Error(`Invalid
|
|
629
|
+
if (!/^[a-f0-9]+$/.test(hash)) {
|
|
630
|
+
throw new Error(`Invalid commit hash: ${hash}`);
|
|
630
631
|
}
|
|
631
632
|
try {
|
|
632
633
|
execSync(`git checkout -q ${hash}`, { cwd: dir, stdio: "ignore" });
|
|
@@ -635,11 +636,12 @@ async function checkoutCommit(dir, hash) {
|
|
|
635
636
|
}
|
|
636
637
|
}
|
|
637
638
|
async function restoreOriginal(dir, originalBranch) {
|
|
638
|
-
if (!/^[a-zA-Z0-
|
|
639
|
-
throw new Error(`Invalid
|
|
639
|
+
if (!/^[a-zA-Z0-9/_.\-]+$/.test(originalBranch)) {
|
|
640
|
+
throw new Error(`Invalid branch name: ${originalBranch}`);
|
|
640
641
|
}
|
|
641
642
|
try {
|
|
642
643
|
execSync(`git checkout -q ${originalBranch}`, {
|
|
644
|
+
// depwire-security-reviewed: branch validated above
|
|
643
645
|
cwd: dir,
|
|
644
646
|
stdio: "ignore"
|
|
645
647
|
});
|
|
@@ -787,19 +789,22 @@ function getWeekNumber(date) {
|
|
|
787
789
|
|
|
788
790
|
// src/temporal/snapshots.ts
|
|
789
791
|
import { writeFileSync, readFileSync, mkdirSync, existsSync as existsSync2, readdirSync } from "fs";
|
|
790
|
-
import {
|
|
792
|
+
import { resolve as resolve2 } from "path";
|
|
791
793
|
function saveSnapshot(snapshot, outputDir) {
|
|
792
794
|
if (!existsSync2(outputDir)) {
|
|
793
795
|
mkdirSync(outputDir, { recursive: true });
|
|
794
796
|
}
|
|
795
797
|
const filename = `${snapshot.commitHash.substring(0, 8)}.json`;
|
|
796
|
-
const filepath =
|
|
798
|
+
const filepath = resolve2(outputDir, filename);
|
|
799
|
+
if (!filepath.startsWith(resolve2(outputDir))) {
|
|
800
|
+
throw new Error(`Path traversal attempt blocked: ${filepath}`);
|
|
801
|
+
}
|
|
797
802
|
writeFileSync(filepath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
798
803
|
}
|
|
799
804
|
function loadSnapshot(commitHash, outputDir) {
|
|
800
805
|
const shortHash = commitHash.substring(0, 8);
|
|
801
|
-
const filepath =
|
|
802
|
-
if (!existsSync2(filepath)) {
|
|
806
|
+
const filepath = resolve2(outputDir, `${shortHash}.json`);
|
|
807
|
+
if (!filepath.startsWith(resolve2(outputDir)) || !existsSync2(filepath)) {
|
|
803
808
|
return null;
|
|
804
809
|
}
|
|
805
810
|
try {
|
|
@@ -1127,6 +1132,50 @@ Always run this before any refactor that touches file structure.`,
|
|
|
1127
1132
|
},
|
|
1128
1133
|
required: ["operation", "target"]
|
|
1129
1134
|
}
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
name: "security_scan",
|
|
1138
|
+
description: `Scan the codebase for security vulnerabilities using deterministic checks + graph-aware severity scoring. No API key required.
|
|
1139
|
+
|
|
1140
|
+
Checks: dependency CVEs, shell injection, hardcoded secrets, path traversal, auth bypass, input validation, information disclosure, cryptography weaknesses, frontend XSS, architecture-level risks.
|
|
1141
|
+
|
|
1142
|
+
Graph-aware severity: vulnerabilities reachable from MCP tools or HTTP routes are automatically elevated. A medium shell injection reachable from connect_repo becomes Critical.
|
|
1143
|
+
|
|
1144
|
+
Returns ranked findings (Critical \u2192 Low) with attack scenarios and suggested fixes. Use --target for single-file scan.`,
|
|
1145
|
+
inputSchema: {
|
|
1146
|
+
type: "object",
|
|
1147
|
+
properties: {
|
|
1148
|
+
target: {
|
|
1149
|
+
type: "string",
|
|
1150
|
+
description: "Relative file path to scan. Omit to scan entire repo."
|
|
1151
|
+
},
|
|
1152
|
+
classes: {
|
|
1153
|
+
type: "array",
|
|
1154
|
+
items: {
|
|
1155
|
+
type: "string",
|
|
1156
|
+
enum: [
|
|
1157
|
+
"dependency-cve",
|
|
1158
|
+
"shell-injection",
|
|
1159
|
+
"code-injection",
|
|
1160
|
+
"secrets",
|
|
1161
|
+
"path-traversal",
|
|
1162
|
+
"auth",
|
|
1163
|
+
"input-validation",
|
|
1164
|
+
"information-disclosure",
|
|
1165
|
+
"architecture",
|
|
1166
|
+
"cryptography",
|
|
1167
|
+
"supply-chain",
|
|
1168
|
+
"frontend-xss"
|
|
1169
|
+
]
|
|
1170
|
+
},
|
|
1171
|
+
description: "Vulnerability classes to check. Omit for all."
|
|
1172
|
+
},
|
|
1173
|
+
graphAware: {
|
|
1174
|
+
type: "boolean",
|
|
1175
|
+
description: "Enable graph-aware severity elevation (recommended). Default: true."
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1130
1179
|
}
|
|
1131
1180
|
];
|
|
1132
1181
|
}
|
|
@@ -1209,6 +1258,19 @@ async function handleToolCall(name, args, state) {
|
|
|
1209
1258
|
} else {
|
|
1210
1259
|
result = handleSimulateChange(args, state);
|
|
1211
1260
|
}
|
|
1261
|
+
} else if (name === "security_scan") {
|
|
1262
|
+
if (!isProjectLoaded(state)) {
|
|
1263
|
+
result = {
|
|
1264
|
+
error: "No project loaded",
|
|
1265
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
1266
|
+
};
|
|
1267
|
+
} else {
|
|
1268
|
+
result = await scanSecurity(state.projectRoot, state.graph, {
|
|
1269
|
+
target: args.target,
|
|
1270
|
+
classes: args.classes,
|
|
1271
|
+
graphAware: args.graphAware !== false
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1212
1274
|
} else {
|
|
1213
1275
|
if (!isProjectLoaded(state)) {
|
|
1214
1276
|
result = {
|
|
@@ -1679,6 +1741,10 @@ Available document types:
|
|
|
1679
1741
|
continue;
|
|
1680
1742
|
}
|
|
1681
1743
|
const filePath = join5(docsDir, metadata.documents[doc].file);
|
|
1744
|
+
if (!resolve3(filePath).startsWith(resolve3(docsDir))) {
|
|
1745
|
+
missing.push(doc);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1682
1748
|
if (!existsSync3(filePath)) {
|
|
1683
1749
|
missing.push(doc);
|
|
1684
1750
|
continue;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
stashChanges,
|
|
18
18
|
updateFileInGraph,
|
|
19
19
|
watchProject
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-RGD3YJYQ.js";
|
|
21
21
|
import {
|
|
22
22
|
SimulationEngine,
|
|
23
23
|
analyzeDeadCode,
|
|
@@ -29,14 +29,15 @@ import {
|
|
|
29
29
|
getHealthTrend,
|
|
30
30
|
getImpact,
|
|
31
31
|
parseProject,
|
|
32
|
+
scanSecurity,
|
|
32
33
|
searchSymbols
|
|
33
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-DA5LWNJ4.js";
|
|
34
35
|
|
|
35
36
|
// src/index.ts
|
|
36
37
|
import { Command } from "commander";
|
|
37
|
-
import { resolve as
|
|
38
|
-
import { writeFileSync, readFileSync as
|
|
39
|
-
import { fileURLToPath as
|
|
38
|
+
import { resolve as resolve4, dirname as dirname4, join as join5 } from "path";
|
|
39
|
+
import { writeFileSync, readFileSync as readFileSync3, existsSync } from "fs";
|
|
40
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
40
41
|
|
|
41
42
|
// src/graph/serializer.ts
|
|
42
43
|
import { DirectedGraph } from "graphology";
|
|
@@ -230,7 +231,7 @@ import { join as join2 } from "path";
|
|
|
230
231
|
import express from "express";
|
|
231
232
|
import { readFileSync } from "fs";
|
|
232
233
|
import { fileURLToPath } from "url";
|
|
233
|
-
import { dirname,
|
|
234
|
+
import { dirname, resolve } from "path";
|
|
234
235
|
import open from "open";
|
|
235
236
|
|
|
236
237
|
// src/viz/temporal-data.ts
|
|
@@ -305,10 +306,10 @@ async function findAvailablePort(startPort) {
|
|
|
305
306
|
const net = await import("net");
|
|
306
307
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
307
308
|
const testPort = startPort + attempt;
|
|
308
|
-
const isAvailable = await new Promise((
|
|
309
|
-
const server = net.createServer().once("error", () =>
|
|
309
|
+
const isAvailable = await new Promise((resolve5) => {
|
|
310
|
+
const server = net.createServer().once("error", () => resolve5(false)).once("listening", () => {
|
|
310
311
|
server.close();
|
|
311
|
-
|
|
312
|
+
resolve5(true);
|
|
312
313
|
}).listen(testPort, "127.0.0.1");
|
|
313
314
|
});
|
|
314
315
|
if (isAvailable) {
|
|
@@ -324,19 +325,22 @@ async function startTemporalServer(snapshots, projectRoot, preferredPort = 3334)
|
|
|
324
325
|
app.get("/api/data", (_req, res) => {
|
|
325
326
|
res.json(vizData);
|
|
326
327
|
});
|
|
327
|
-
const publicDir =
|
|
328
|
+
const publicDir = resolve(__dirname, "viz", "public");
|
|
328
329
|
app.get("/", (_req, res) => {
|
|
329
|
-
const htmlPath =
|
|
330
|
+
const htmlPath = resolve(publicDir, "temporal.html");
|
|
331
|
+
if (!htmlPath.startsWith(publicDir)) return res.status(403).send("Forbidden");
|
|
330
332
|
const html = readFileSync(htmlPath, "utf-8");
|
|
331
333
|
res.send(html);
|
|
332
334
|
});
|
|
333
335
|
app.get("/temporal.js", (_req, res) => {
|
|
334
|
-
const jsPath =
|
|
336
|
+
const jsPath = resolve(publicDir, "temporal.js");
|
|
337
|
+
if (!jsPath.startsWith(publicDir)) return res.status(403).send("Forbidden");
|
|
335
338
|
const js = readFileSync(jsPath, "utf-8");
|
|
336
339
|
res.type("application/javascript").send(js);
|
|
337
340
|
});
|
|
338
341
|
app.get("/temporal.css", (_req, res) => {
|
|
339
|
-
const cssPath =
|
|
342
|
+
const cssPath = resolve(publicDir, "temporal.css");
|
|
343
|
+
if (!cssPath.startsWith(publicDir)) return res.status(403).send("Forbidden");
|
|
340
344
|
const css = readFileSync(cssPath, "utf-8");
|
|
341
345
|
res.type("text/css").send(css);
|
|
342
346
|
});
|
|
@@ -349,13 +353,13 @@ async function startTemporalServer(snapshots, projectRoot, preferredPort = 3334)
|
|
|
349
353
|
console.log(" (Could not open browser automatically)");
|
|
350
354
|
});
|
|
351
355
|
});
|
|
352
|
-
await new Promise((
|
|
356
|
+
await new Promise((resolve5, reject) => {
|
|
353
357
|
server.on("error", reject);
|
|
354
358
|
process.on("SIGINT", () => {
|
|
355
359
|
console.log("\n\nShutting down temporal server...");
|
|
356
360
|
server.close(() => {
|
|
357
361
|
console.log("Server stopped");
|
|
358
|
-
|
|
362
|
+
resolve5();
|
|
359
363
|
process.exit(0);
|
|
360
364
|
});
|
|
361
365
|
});
|
|
@@ -500,7 +504,7 @@ async function trackCommand(command, version = "unknown") {
|
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
// src/commands/whatif.ts
|
|
503
|
-
import { resolve } from "path";
|
|
507
|
+
import { resolve as resolve2 } from "path";
|
|
504
508
|
import chalk from "chalk";
|
|
505
509
|
|
|
506
510
|
// src/viz/whatif-server.ts
|
|
@@ -689,14 +693,14 @@ async function findAvailablePort2(startPort, maxAttempts = 10) {
|
|
|
689
693
|
const net = await import("net");
|
|
690
694
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
691
695
|
const testPort = startPort + attempt;
|
|
692
|
-
const isAvailable = await new Promise((
|
|
696
|
+
const isAvailable = await new Promise((resolve5) => {
|
|
693
697
|
const server = net.createServer();
|
|
694
698
|
server.once("error", () => {
|
|
695
|
-
|
|
699
|
+
resolve5(false);
|
|
696
700
|
});
|
|
697
701
|
server.once("listening", () => {
|
|
698
702
|
server.close();
|
|
699
|
-
|
|
703
|
+
resolve5(true);
|
|
700
704
|
});
|
|
701
705
|
server.listen(testPort, "127.0.0.1");
|
|
702
706
|
});
|
|
@@ -751,7 +755,7 @@ Opening What If UI at ${url}`);
|
|
|
751
755
|
// src/commands/whatif.ts
|
|
752
756
|
async function whatif(dir, options) {
|
|
753
757
|
if (!options.simulate) {
|
|
754
|
-
const projectRoot2 = dir === "." ? findProjectRoot() :
|
|
758
|
+
const projectRoot2 = dir === "." ? findProjectRoot() : resolve2(dir);
|
|
755
759
|
console.error(`Parsing project: ${projectRoot2}`);
|
|
756
760
|
const parsedFiles2 = await parseProject(projectRoot2);
|
|
757
761
|
const graph2 = buildGraph(parsedFiles2);
|
|
@@ -777,7 +781,7 @@ async function whatif(dir, options) {
|
|
|
777
781
|
process.exit(1);
|
|
778
782
|
}
|
|
779
783
|
const action = buildAction(options);
|
|
780
|
-
const projectRoot = dir === "." ? findProjectRoot() :
|
|
784
|
+
const projectRoot = dir === "." ? findProjectRoot() : resolve2(dir);
|
|
781
785
|
console.error(`Parsing project: ${projectRoot}`);
|
|
782
786
|
const parsedFiles = await parseProject(projectRoot);
|
|
783
787
|
const graph = buildGraph(parsedFiles);
|
|
@@ -887,18 +891,198 @@ function formatAction(action) {
|
|
|
887
891
|
}
|
|
888
892
|
}
|
|
889
893
|
|
|
890
|
-
// src/
|
|
894
|
+
// src/commands/security.ts
|
|
895
|
+
import { resolve as resolve3, dirname as dirname3, join as join4 } from "path";
|
|
896
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
897
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
898
|
+
|
|
899
|
+
// src/security/reporter.ts
|
|
900
|
+
import chalk2 from "chalk";
|
|
901
|
+
var SEVERITY_COLORS = {
|
|
902
|
+
critical: chalk2.red.bold,
|
|
903
|
+
high: chalk2.red,
|
|
904
|
+
medium: chalk2.yellow,
|
|
905
|
+
low: chalk2.blue,
|
|
906
|
+
info: chalk2.dim
|
|
907
|
+
};
|
|
908
|
+
var SEVERITY_LABELS = {
|
|
909
|
+
critical: "CRITICAL",
|
|
910
|
+
high: "HIGH",
|
|
911
|
+
medium: "MEDIUM",
|
|
912
|
+
low: "LOW",
|
|
913
|
+
info: "INFO"
|
|
914
|
+
};
|
|
915
|
+
function formatTable(result, elapsedMs) {
|
|
916
|
+
const lines = [];
|
|
917
|
+
const sep = "\u2500".repeat(62);
|
|
918
|
+
lines.push("");
|
|
919
|
+
lines.push(chalk2.bold("Depwire Security Scan"));
|
|
920
|
+
lines.push("");
|
|
921
|
+
const summaryParts = [
|
|
922
|
+
result.summary.critical > 0 ? chalk2.red.bold(`${result.summary.critical} Critical`) : null,
|
|
923
|
+
result.summary.high > 0 ? chalk2.red(`${result.summary.high} High`) : null,
|
|
924
|
+
result.summary.medium > 0 ? chalk2.yellow(`${result.summary.medium} Medium`) : null,
|
|
925
|
+
result.summary.low > 0 ? chalk2.blue(`${result.summary.low} Low`) : null,
|
|
926
|
+
result.summary.info > 0 ? chalk2.dim(`${result.summary.info} Info`) : null
|
|
927
|
+
].filter(Boolean);
|
|
928
|
+
if (summaryParts.length > 0) {
|
|
929
|
+
lines.push(`\u250C${sep}\u2510`);
|
|
930
|
+
lines.push(`\u2502 ${summaryParts.join(" \u2502 ")} \u2502`);
|
|
931
|
+
lines.push(`\u2514${sep}\u2518`);
|
|
932
|
+
} else {
|
|
933
|
+
lines.push(chalk2.green.bold(" No security findings detected."));
|
|
934
|
+
}
|
|
935
|
+
lines.push("");
|
|
936
|
+
const severityOrder = ["critical", "high", "medium", "low", "info"];
|
|
937
|
+
for (const severity of severityOrder) {
|
|
938
|
+
const group = result.findings.filter((f) => f.severity === severity);
|
|
939
|
+
if (group.length === 0) continue;
|
|
940
|
+
const colorFn = SEVERITY_COLORS[severity];
|
|
941
|
+
lines.push(colorFn(SEVERITY_LABELS[severity]));
|
|
942
|
+
for (const finding of group) {
|
|
943
|
+
lines.push(` ${colorFn(`[${finding.id}]`)} ${finding.title}`);
|
|
944
|
+
lines.push(` File: ${finding.file}${finding.line ? `:${finding.line}` : ""}`);
|
|
945
|
+
lines.push(` ${chalk2.dim(finding.description)}`);
|
|
946
|
+
lines.push(` ${chalk2.dim("Fix:")} ${finding.suggestedFix}`);
|
|
947
|
+
if (finding.graphReachability?.elevatedBy) {
|
|
948
|
+
lines.push(` ${chalk2.magenta("\u2191 Elevated:")} ${finding.graphReachability.elevatedBy}`);
|
|
949
|
+
}
|
|
950
|
+
lines.push("");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const elapsed = (elapsedMs / 1e3).toFixed(1);
|
|
954
|
+
lines.push(chalk2.dim(`Scanned ${result.filesScanned} files in ${elapsed}s`));
|
|
955
|
+
lines.push(chalk2.dim("Run with --format json for machine output"));
|
|
956
|
+
lines.push(chalk2.dim("Run with --format sarif for GitHub Security integration"));
|
|
957
|
+
lines.push("");
|
|
958
|
+
return lines.join("\n");
|
|
959
|
+
}
|
|
960
|
+
function formatJSON(result) {
|
|
961
|
+
return JSON.stringify(result, null, 2);
|
|
962
|
+
}
|
|
963
|
+
function formatSARIF(result, version) {
|
|
964
|
+
const rules = result.findings.map((f) => ({
|
|
965
|
+
id: f.id,
|
|
966
|
+
shortDescription: { text: f.title },
|
|
967
|
+
fullDescription: { text: f.description },
|
|
968
|
+
help: { text: f.suggestedFix },
|
|
969
|
+
properties: {
|
|
970
|
+
severity: f.severity,
|
|
971
|
+
vulnerabilityClass: f.vulnerabilityClass
|
|
972
|
+
}
|
|
973
|
+
}));
|
|
974
|
+
const uniqueRules = Array.from(
|
|
975
|
+
new Map(rules.map((r) => [r.id, r])).values()
|
|
976
|
+
);
|
|
977
|
+
const results = result.findings.map((f) => {
|
|
978
|
+
let level;
|
|
979
|
+
if (f.severity === "critical" || f.severity === "high") level = "error";
|
|
980
|
+
else if (f.severity === "medium") level = "warning";
|
|
981
|
+
else level = "note";
|
|
982
|
+
const sarifResult = {
|
|
983
|
+
ruleId: f.id,
|
|
984
|
+
level,
|
|
985
|
+
message: { text: `${f.title}: ${f.description}` },
|
|
986
|
+
locations: [
|
|
987
|
+
{
|
|
988
|
+
physicalLocation: {
|
|
989
|
+
artifactLocation: { uri: f.file },
|
|
990
|
+
region: f.line ? { startLine: f.line } : void 0
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
]
|
|
994
|
+
};
|
|
995
|
+
return sarifResult;
|
|
996
|
+
});
|
|
997
|
+
const sarif = {
|
|
998
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
999
|
+
version: "2.1.0",
|
|
1000
|
+
runs: [
|
|
1001
|
+
{
|
|
1002
|
+
tool: {
|
|
1003
|
+
driver: {
|
|
1004
|
+
name: "depwire",
|
|
1005
|
+
version,
|
|
1006
|
+
rules: uniqueRules
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
results
|
|
1010
|
+
}
|
|
1011
|
+
]
|
|
1012
|
+
};
|
|
1013
|
+
return JSON.stringify(sarif, null, 2);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/commands/security.ts
|
|
891
1017
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
892
1018
|
var __dirname3 = dirname3(__filename3);
|
|
893
|
-
|
|
894
|
-
|
|
1019
|
+
function getVersion() {
|
|
1020
|
+
try {
|
|
1021
|
+
let dir = __dirname3;
|
|
1022
|
+
for (let i = 0; i < 5; i++) {
|
|
1023
|
+
const pkgPath = join4(dir, "package.json");
|
|
1024
|
+
try {
|
|
1025
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
1026
|
+
if (pkg.name === "depwire-cli") return pkg.version;
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
dir = dirname3(dir);
|
|
1030
|
+
}
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
return "0.0.0";
|
|
1034
|
+
}
|
|
1035
|
+
var SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
|
|
1036
|
+
async function securityCommand(dir, options) {
|
|
1037
|
+
const projectRoot = dir === "." ? findProjectRoot() : resolve3(dir);
|
|
1038
|
+
console.error(`Scanning: ${projectRoot}`);
|
|
1039
|
+
const startTime = Date.now();
|
|
1040
|
+
const parsedFiles = await parseProject(projectRoot);
|
|
1041
|
+
console.error(`Parsed ${parsedFiles.length} files`);
|
|
1042
|
+
const graph = buildGraph(parsedFiles);
|
|
1043
|
+
console.error(`Built graph: ${graph.order} symbols, ${graph.size} edges`);
|
|
1044
|
+
const result = await scanSecurity(projectRoot, graph, {
|
|
1045
|
+
target: options.target,
|
|
1046
|
+
classes: options.class,
|
|
1047
|
+
format: options.format || "table",
|
|
1048
|
+
graphAware: true
|
|
1049
|
+
});
|
|
1050
|
+
const elapsedMs = Date.now() - startTime;
|
|
1051
|
+
const format = options.format || "table";
|
|
1052
|
+
if (format === "json") {
|
|
1053
|
+
console.log(formatJSON(result));
|
|
1054
|
+
} else if (format === "sarif") {
|
|
1055
|
+
console.log(formatSARIF(result, getVersion()));
|
|
1056
|
+
} else {
|
|
1057
|
+
console.log(formatTable(result, elapsedMs));
|
|
1058
|
+
}
|
|
1059
|
+
if (options.failOn) {
|
|
1060
|
+
const threshold = options.failOn;
|
|
1061
|
+
const thresholdIdx = SEVERITY_ORDER.indexOf(threshold);
|
|
1062
|
+
if (thresholdIdx >= 0) {
|
|
1063
|
+
const hasFindings = result.findings.some(
|
|
1064
|
+
(f) => SEVERITY_ORDER.indexOf(f.severity) <= thresholdIdx
|
|
1065
|
+
);
|
|
1066
|
+
if (hasFindings) {
|
|
1067
|
+
console.error(`Findings at or above ${threshold} severity detected \u2014 exiting with code 1`);
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/index.ts
|
|
1075
|
+
var __filename4 = fileURLToPath4(import.meta.url);
|
|
1076
|
+
var __dirname4 = dirname4(__filename4);
|
|
1077
|
+
var packageJsonPath = join5(__dirname4, "../package.json");
|
|
1078
|
+
var packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
895
1079
|
var program = new Command();
|
|
896
1080
|
program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
|
|
897
1081
|
program.command("parse").description("Parse a TypeScript project and build dependency graph").argument("[directory]", "Project directory to parse (defaults to current directory or auto-detected project root)").option("-o, --output <path>", "Output JSON file path", "depwire-output.json").option("--pretty", "Pretty-print JSON output").option("--stats", "Print summary statistics").option("--exclude <patterns...>", 'Glob patterns to exclude (e.g., "**/*.test.*" "dist/**")').option("--verbose", "Show detailed parsing progress").action(async (directory, options) => {
|
|
898
1082
|
trackCommand("parse", packageJson.version);
|
|
899
1083
|
const startTime = Date.now();
|
|
900
1084
|
try {
|
|
901
|
-
const projectRoot = directory ?
|
|
1085
|
+
const projectRoot = directory ? resolve4(directory) : findProjectRoot();
|
|
902
1086
|
console.log(`Parsing project: ${projectRoot}`);
|
|
903
1087
|
const parsedFiles = await parseProject(projectRoot, {
|
|
904
1088
|
exclude: options.exclude,
|
|
@@ -937,12 +1121,12 @@ Orphan Files (no cross-references): ${summary.orphanFiles.length}`);
|
|
|
937
1121
|
program.command("query").description("Query impact analysis for a symbol").argument("<directory>", "Project directory").argument("<symbol-name>", "Symbol name to query").action(async (directory, symbolName) => {
|
|
938
1122
|
trackCommand("query", packageJson.version);
|
|
939
1123
|
try {
|
|
940
|
-
const projectRoot =
|
|
941
|
-
const cacheFile = "depwire-output.json";
|
|
1124
|
+
const projectRoot = resolve4(directory);
|
|
1125
|
+
const cacheFile = resolve4("depwire-output.json");
|
|
942
1126
|
let graph;
|
|
943
1127
|
if (existsSync(cacheFile)) {
|
|
944
1128
|
console.log("Loading from cache...");
|
|
945
|
-
const json = JSON.parse(
|
|
1129
|
+
const json = JSON.parse(readFileSync3(cacheFile, "utf-8"));
|
|
946
1130
|
graph = importFromJSON(json);
|
|
947
1131
|
} else {
|
|
948
1132
|
console.log("Parsing project...");
|
|
@@ -986,7 +1170,7 @@ Total Transitive Dependents: ${impact.transitiveDependents.length}`);
|
|
|
986
1170
|
program.command("viz").description("Launch interactive arc diagram visualization").argument("[directory]", "Project directory to visualize (defaults to current directory or auto-detected project root)").option("-p, --port <number>", "Server port", "3333").option("--no-open", "Don't auto-open browser").option("--exclude <patterns...>", 'Glob patterns to exclude (e.g., "**/*.test.*" "dist/**")').option("--verbose", "Show detailed parsing progress").action(async (directory, options) => {
|
|
987
1171
|
trackCommand("viz", packageJson.version);
|
|
988
1172
|
try {
|
|
989
|
-
const projectRoot = directory ?
|
|
1173
|
+
const projectRoot = directory ? resolve4(directory) : findProjectRoot();
|
|
990
1174
|
console.log(`Parsing project: ${projectRoot}`);
|
|
991
1175
|
const parsedFiles = await parseProject(projectRoot, {
|
|
992
1176
|
exclude: options.exclude,
|
|
@@ -1009,7 +1193,7 @@ program.command("viz").description("Launch interactive arc diagram visualization
|
|
|
1009
1193
|
program.command("temporal").description("Visualize how the dependency graph evolved over git history").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--commits <number>", "Number of commits to sample", "20").option("--strategy <type>", "Sampling strategy: even, weekly, monthly", "even").option("-p, --port <number>", "Server port", "3334").option("--output <path>", "Save snapshots to custom path (default: .depwire/temporal/)").option("--verbose", "Show progress for each commit being parsed").option("--stats", "Show summary statistics at end").action(async (directory, options) => {
|
|
1010
1194
|
trackCommand("temporal", packageJson.version);
|
|
1011
1195
|
try {
|
|
1012
|
-
const projectRoot = directory ?
|
|
1196
|
+
const projectRoot = directory ? resolve4(directory) : findProjectRoot();
|
|
1013
1197
|
await runTemporalAnalysis(projectRoot, {
|
|
1014
1198
|
commits: parseInt(options.commits, 10),
|
|
1015
1199
|
strategy: options.strategy,
|
|
@@ -1029,11 +1213,11 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
|
|
|
1029
1213
|
const state = createEmptyState();
|
|
1030
1214
|
let projectRootToConnect = null;
|
|
1031
1215
|
if (directory) {
|
|
1032
|
-
projectRootToConnect =
|
|
1216
|
+
projectRootToConnect = resolve4(directory);
|
|
1033
1217
|
} else {
|
|
1034
1218
|
const detectedRoot = findProjectRoot();
|
|
1035
1219
|
const cwd = process.cwd();
|
|
1036
|
-
if (detectedRoot !== cwd || existsSync(
|
|
1220
|
+
if (detectedRoot !== cwd || existsSync(join5(cwd, "package.json")) || existsSync(join5(cwd, "tsconfig.json")) || existsSync(join5(cwd, "go.mod")) || existsSync(join5(cwd, "pyproject.toml")) || existsSync(join5(cwd, "setup.py")) || existsSync(join5(cwd, ".git"))) {
|
|
1037
1221
|
projectRootToConnect = detectedRoot;
|
|
1038
1222
|
}
|
|
1039
1223
|
}
|
|
@@ -1090,8 +1274,8 @@ program.command("docs").description("Generate comprehensive codebase documentati
|
|
|
1090
1274
|
trackCommand("docs", packageJson.version);
|
|
1091
1275
|
const startTime = Date.now();
|
|
1092
1276
|
try {
|
|
1093
|
-
const projectRoot = directory ?
|
|
1094
|
-
const outputDir = options.output ?
|
|
1277
|
+
const projectRoot = directory ? resolve4(directory) : findProjectRoot();
|
|
1278
|
+
const outputDir = options.output ? resolve4(options.output) : join5(projectRoot, ".depwire");
|
|
1095
1279
|
const includeList = options.include.split(",").map((s) => s.trim());
|
|
1096
1280
|
const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
|
|
1097
1281
|
if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
|
|
@@ -1153,16 +1337,16 @@ async function promptGitignore() {
|
|
|
1153
1337
|
input: process.stdin,
|
|
1154
1338
|
output: process.stdout
|
|
1155
1339
|
});
|
|
1156
|
-
return new Promise((
|
|
1340
|
+
return new Promise((resolve5) => {
|
|
1157
1341
|
rl.question("Add .depwire/ to .gitignore? [Y/n] ", (answer) => {
|
|
1158
1342
|
rl.close();
|
|
1159
1343
|
const normalized = answer.trim().toLowerCase();
|
|
1160
|
-
|
|
1344
|
+
resolve5(normalized === "" || normalized === "y" || normalized === "yes");
|
|
1161
1345
|
});
|
|
1162
1346
|
});
|
|
1163
1347
|
}
|
|
1164
1348
|
function addToGitignore(projectRoot, pattern) {
|
|
1165
|
-
const gitignorePath =
|
|
1349
|
+
const gitignorePath = join5(projectRoot, ".gitignore");
|
|
1166
1350
|
try {
|
|
1167
1351
|
let content = "";
|
|
1168
1352
|
if (existsSyncNode(gitignorePath)) {
|
|
@@ -1187,7 +1371,7 @@ ${pattern}
|
|
|
1187
1371
|
program.command("health").description("Analyze dependency architecture health (0-100 score)").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--json", "Output as JSON").option("--verbose", "Show detailed breakdown").action(async (directory, options) => {
|
|
1188
1372
|
trackCommand("health", packageJson.version);
|
|
1189
1373
|
try {
|
|
1190
|
-
const projectRoot = directory ?
|
|
1374
|
+
const projectRoot = directory ? resolve4(directory) : findProjectRoot();
|
|
1191
1375
|
const startTime = Date.now();
|
|
1192
1376
|
const parsedFiles = await parseProject(projectRoot);
|
|
1193
1377
|
const graph = buildGraph(parsedFiles);
|
|
@@ -1211,7 +1395,7 @@ program.command("health").description("Analyze dependency architecture health (0
|
|
|
1211
1395
|
program.command("dead-code").description("Identify dead code - symbols defined but never referenced").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--confidence <level>", "Minimum confidence level to show: high, medium, low (default: medium)", "medium").option("--json", "Output as JSON (for CI/automation)").option("--verbose", "Show detailed info for each dead symbol").option("--stats", "Show summary statistics").option("--include-tests", "Include test files in analysis").option("--include-low", "Shortcut for --confidence low").option("--debug", "Show debug information (exclusion stats)").action(async (directory, options) => {
|
|
1212
1396
|
trackCommand("dead-code", packageJson.version);
|
|
1213
1397
|
try {
|
|
1214
|
-
const projectRoot = directory ?
|
|
1398
|
+
const projectRoot = directory ? resolve4(directory) : findProjectRoot();
|
|
1215
1399
|
const startTime = Date.now();
|
|
1216
1400
|
const parsedFiles = await parseProject(projectRoot);
|
|
1217
1401
|
const graph = buildGraph(parsedFiles);
|
|
@@ -1247,4 +1431,13 @@ program.command("whatif").description("Simulate architectural changes before tou
|
|
|
1247
1431
|
process.exit(1);
|
|
1248
1432
|
}
|
|
1249
1433
|
});
|
|
1434
|
+
program.command("security").description("Scan codebase for security vulnerabilities (deterministic, no API key required)").argument("[directory]", "Project directory to scan (defaults to current directory or auto-detected project root)").option("--target <file>", "Scan a single file instead of the whole repo").option("--class <classes...>", "Only run specific vulnerability class checks").option("--format <format>", "Output format: table (default), json, sarif", "table").option("--fail-on <level>", "Exit with code 1 if findings at this severity or above").action(async (directory, options) => {
|
|
1435
|
+
trackCommand("security", packageJson.version);
|
|
1436
|
+
try {
|
|
1437
|
+
await securityCommand(directory || ".", options);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
console.error("Error running security scan:", err);
|
|
1440
|
+
process.exit(1);
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1250
1443
|
program.parse();
|
package/dist/mcpb-entry.js
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
startMcpServer,
|
|
5
5
|
updateFileInGraph,
|
|
6
6
|
watchProject
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-RGD3YJYQ.js";
|
|
8
8
|
import {
|
|
9
9
|
buildGraph,
|
|
10
10
|
parseProject
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-DA5LWNJ4.js";
|
|
12
12
|
|
|
13
13
|
// src/mcpb-entry.ts
|
|
14
14
|
import { resolve } from "path";
|