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.
@@ -106,7 +106,7 @@ function findProjectRoot(startDir = process.cwd()) {
106
106
 
107
107
  // src/parser/index.ts
108
108
  import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
109
- import { join as join8 } from "path";
109
+ import { join as join8, resolve as resolve3 } from "path";
110
110
 
111
111
  // src/parser/detect.ts
112
112
  import { extname as extname3 } from "path";
@@ -1578,7 +1578,7 @@ var javascriptParser = {
1578
1578
 
1579
1579
  // src/parser/go.ts
1580
1580
  import { existsSync as existsSync5, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1581
- import { join as join5, dirname as dirname4 } from "path";
1581
+ import { join as join5, dirname as dirname4, resolve as resolve2 } from "path";
1582
1582
  function parseGoFile(filePath, sourceCode, projectRoot) {
1583
1583
  const parser = getParser("go");
1584
1584
  const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
@@ -1860,7 +1860,7 @@ function processCallExpression4(node, context) {
1860
1860
  function readGoModuleName(projectRoot) {
1861
1861
  let currentDir = projectRoot;
1862
1862
  for (let i = 0; i < 5; i++) {
1863
- const goModPath = join5(currentDir, "go.mod");
1863
+ const goModPath = resolve2(currentDir, "go.mod");
1864
1864
  if (existsSync5(goModPath)) {
1865
1865
  try {
1866
1866
  const content = readFileSync2(goModPath, "utf-8");
@@ -2822,6 +2822,10 @@ async function parseProject(projectRoot, options) {
2822
2822
  for (const file of files) {
2823
2823
  try {
2824
2824
  const fullPath = join8(projectRoot, file);
2825
+ if (!resolve3(fullPath).startsWith(resolve3(projectRoot))) {
2826
+ skippedFiles++;
2827
+ continue;
2828
+ }
2825
2829
  if (options?.exclude) {
2826
2830
  const shouldExclude2 = options.exclude.some(
2827
2831
  (pattern) => minimatch(file, pattern, { matchBase: true })
@@ -3508,7 +3512,7 @@ function calculateDepthScore(graph) {
3508
3512
 
3509
3513
  // src/health/index.ts
3510
3514
  import { readFileSync as readFileSync6, writeFileSync, existsSync as existsSync8, mkdirSync } from "fs";
3511
- import { join as join9, dirname as dirname8 } from "path";
3515
+ import { dirname as dirname8, resolve as resolve4 } from "path";
3512
3516
  function calculateHealthScore(graph, projectRoot) {
3513
3517
  const coupling = calculateCouplingScore(graph);
3514
3518
  const cohesion = calculateCohesionScore(graph);
@@ -3607,7 +3611,11 @@ function getHealthTrend(projectRoot, currentScore) {
3607
3611
  }
3608
3612
  }
3609
3613
  function saveHealthHistory(projectRoot, report) {
3610
- const historyFile = join9(projectRoot, ".depwire", "health-history.json");
3614
+ const resolvedRoot = resolve4(projectRoot);
3615
+ const historyFile = resolve4(resolvedRoot, ".depwire", "health-history.json");
3616
+ if (!historyFile.startsWith(resolvedRoot)) {
3617
+ return;
3618
+ }
3611
3619
  const entry = {
3612
3620
  timestamp: report.timestamp,
3613
3621
  score: report.overall,
@@ -3621,6 +3629,7 @@ function saveHealthHistory(projectRoot, report) {
3621
3629
  let history = [];
3622
3630
  if (existsSync8(historyFile)) {
3623
3631
  try {
3632
+ if (!historyFile.startsWith(resolvedRoot)) return;
3624
3633
  const content = readFileSync6(historyFile, "utf-8");
3625
3634
  history = JSON.parse(content);
3626
3635
  } catch {
@@ -3631,14 +3640,17 @@ function saveHealthHistory(projectRoot, report) {
3631
3640
  history = history.slice(-50);
3632
3641
  }
3633
3642
  mkdirSync(dirname8(historyFile), { recursive: true });
3643
+ if (!historyFile.startsWith(resolvedRoot)) return;
3634
3644
  writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf-8");
3635
3645
  }
3636
3646
  function loadHealthHistory(projectRoot) {
3637
- const historyFile = join9(projectRoot, ".depwire", "health-history.json");
3638
- if (!existsSync8(historyFile)) {
3647
+ const resolvedRoot = resolve4(projectRoot);
3648
+ const historyFile = resolve4(resolvedRoot, ".depwire", "health-history.json");
3649
+ if (!historyFile.startsWith(resolvedRoot) || !existsSync8(historyFile)) {
3639
3650
  return [];
3640
3651
  }
3641
3652
  try {
3653
+ if (!historyFile.startsWith(resolvedRoot)) return [];
3642
3654
  const content = readFileSync6(historyFile, "utf-8");
3643
3655
  return JSON.parse(content);
3644
3656
  } catch {
@@ -3777,8 +3789,9 @@ function isRelevantForDeadCodeDetection(attrs) {
3777
3789
  }
3778
3790
  function getPackageEntryPoints(projectRoot) {
3779
3791
  const entryPoints = /* @__PURE__ */ new Set();
3780
- const packageJsonPath = path2.join(projectRoot, "package.json");
3781
- if (!existsSync9(packageJsonPath)) {
3792
+ const resolvedRoot = path2.resolve(projectRoot);
3793
+ const packageJsonPath = path2.resolve(resolvedRoot, "package.json");
3794
+ if (!packageJsonPath.startsWith(resolvedRoot) || !existsSync9(packageJsonPath)) {
3782
3795
  return entryPoints;
3783
3796
  }
3784
3797
  try {
@@ -7441,7 +7454,7 @@ function getTopLevelDir2(filePath) {
7441
7454
 
7442
7455
  // src/docs/status.ts
7443
7456
  import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
7444
- import { join as join10 } from "path";
7457
+ import { resolve as resolve5 } from "path";
7445
7458
  function generateStatus(graph, projectRoot, version) {
7446
7459
  let output = "";
7447
7460
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -7474,7 +7487,11 @@ function getFileCount11(graph) {
7474
7487
  }
7475
7488
  function extractComments(projectRoot, filePath) {
7476
7489
  const comments = [];
7477
- const fullPath = join10(projectRoot, filePath);
7490
+ const resolvedRoot = resolve5(projectRoot);
7491
+ const fullPath = resolve5(resolvedRoot, filePath);
7492
+ if (!fullPath.startsWith(resolvedRoot)) {
7493
+ return comments;
7494
+ }
7478
7495
  if (!existsSync10(fullPath)) {
7479
7496
  return comments;
7480
7497
  }
@@ -8059,10 +8076,11 @@ function generateConfidenceSection(title, description, symbols, projectRoot) {
8059
8076
 
8060
8077
  // src/docs/metadata.ts
8061
8078
  import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
8062
- import { join as join11 } from "path";
8079
+ import { resolve as resolve6 } from "path";
8063
8080
  function loadMetadata(outputDir) {
8064
- const metadataPath = join11(outputDir, "metadata.json");
8065
- if (!existsSync11(metadataPath)) {
8081
+ const resolvedDir = resolve6(outputDir);
8082
+ const metadataPath = resolve6(resolvedDir, "metadata.json");
8083
+ if (!metadataPath.startsWith(resolvedDir) || !existsSync11(metadataPath)) {
8066
8084
  return null;
8067
8085
  }
8068
8086
  try {
@@ -8074,7 +8092,11 @@ function loadMetadata(outputDir) {
8074
8092
  }
8075
8093
  }
8076
8094
  function saveMetadata(outputDir, metadata) {
8077
- const metadataPath = join11(outputDir, "metadata.json");
8095
+ const resolvedDir = resolve6(outputDir);
8096
+ const metadataPath = resolve6(resolvedDir, "metadata.json");
8097
+ if (!metadataPath.startsWith(resolvedDir)) {
8098
+ throw new Error(`Path traversal attempt blocked: ${metadataPath}`);
8099
+ }
8078
8100
  writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
8079
8101
  }
8080
8102
  function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
@@ -8662,6 +8684,1451 @@ var SimulationEngine = class {
8662
8684
  }
8663
8685
  };
8664
8686
 
8687
+ // src/security/scanner.ts
8688
+ import { existsSync as existsSync14 } from "fs";
8689
+ import { join as join23 } from "path";
8690
+
8691
+ // src/security/checks/dependencies.ts
8692
+ import { execSync as execSync2 } from "child_process";
8693
+ import { existsSync as existsSync13, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
8694
+ import { join as join14 } from "path";
8695
+ function cvssToSeverity(score) {
8696
+ if (score >= 9) return "critical";
8697
+ if (score >= 7) return "high";
8698
+ if (score >= 4) return "medium";
8699
+ return "low";
8700
+ }
8701
+ async function checkDependencies(_files, projectRoot) {
8702
+ const findings = [];
8703
+ try {
8704
+ if (existsSync13(join14(projectRoot, "package.json"))) {
8705
+ findings.push(...checkNpmAudit(projectRoot));
8706
+ findings.push(...checkPackageJsonPatterns(projectRoot));
8707
+ findings.push(...checkPostinstallScripts(projectRoot));
8708
+ }
8709
+ if (existsSync13(join14(projectRoot, "requirements.txt")) || existsSync13(join14(projectRoot, "pyproject.toml"))) {
8710
+ findings.push(...checkPipAudit(projectRoot));
8711
+ }
8712
+ if (existsSync13(join14(projectRoot, "Cargo.toml"))) {
8713
+ findings.push(...checkCargoAudit(projectRoot));
8714
+ }
8715
+ if (existsSync13(join14(projectRoot, "go.mod"))) {
8716
+ findings.push(...checkGoVerify(projectRoot));
8717
+ }
8718
+ } catch (err) {
8719
+ findings.push({
8720
+ id: "",
8721
+ severity: "info",
8722
+ vulnerabilityClass: "dependency-cve",
8723
+ file: "package.json",
8724
+ title: "Dependency audit error",
8725
+ description: `Dependency audit encountered an error: ${String(err)}`,
8726
+ attackScenario: "N/A",
8727
+ suggestedFix: "Ensure audit tools are installed and try again."
8728
+ });
8729
+ }
8730
+ return findings;
8731
+ }
8732
+ function checkNpmAudit(projectRoot) {
8733
+ const findings = [];
8734
+ try {
8735
+ const output = execSync2("npm audit --json", {
8736
+ cwd: projectRoot,
8737
+ encoding: "utf-8",
8738
+ timeout: 3e4,
8739
+ stdio: ["pipe", "pipe", "pipe"]
8740
+ });
8741
+ const audit = JSON.parse(output);
8742
+ const vulnerabilities = audit.vulnerabilities || {};
8743
+ for (const [name, vuln] of Object.entries(vulnerabilities)) {
8744
+ const severity = vuln.severity === "critical" ? "critical" : vuln.severity === "high" ? "high" : vuln.severity === "moderate" ? "medium" : "low";
8745
+ findings.push({
8746
+ id: "",
8747
+ severity,
8748
+ vulnerabilityClass: "dependency-cve",
8749
+ file: "package.json",
8750
+ title: `Vulnerable dependency: ${name}`,
8751
+ description: `${name}@${vuln.range || "unknown"} has a known ${vuln.severity} vulnerability. ${vuln.title || ""}`.trim(),
8752
+ attackScenario: `An attacker could exploit the known vulnerability in ${name} to compromise the application.`,
8753
+ suggestedFix: vuln.fixAvailable ? `Update ${name} to a patched version.` : `No fix currently available. Consider replacing ${name}.`
8754
+ });
8755
+ }
8756
+ } catch (err) {
8757
+ if (err.stdout) {
8758
+ try {
8759
+ const audit = JSON.parse(err.stdout);
8760
+ const vulnerabilities = audit.vulnerabilities || {};
8761
+ for (const [name, vuln] of Object.entries(vulnerabilities)) {
8762
+ const severity = vuln.severity === "critical" ? "critical" : vuln.severity === "high" ? "high" : vuln.severity === "moderate" ? "medium" : "low";
8763
+ findings.push({
8764
+ id: "",
8765
+ severity,
8766
+ vulnerabilityClass: "dependency-cve",
8767
+ file: "package.json",
8768
+ title: `Vulnerable dependency: ${name}`,
8769
+ description: `${name}@${vuln.range || "unknown"} has a known ${vuln.severity} vulnerability.`,
8770
+ attackScenario: `An attacker could exploit the known vulnerability in ${name}.`,
8771
+ suggestedFix: vuln.fixAvailable ? `Update ${name} to a patched version.` : `No fix currently available.`
8772
+ });
8773
+ }
8774
+ } catch {
8775
+ findings.push({
8776
+ id: "",
8777
+ severity: "info",
8778
+ vulnerabilityClass: "dependency-cve",
8779
+ file: "package.json",
8780
+ title: "npm audit unavailable",
8781
+ description: "Could not parse npm audit output.",
8782
+ attackScenario: "N/A",
8783
+ suggestedFix: "Run npm audit manually to check for vulnerabilities."
8784
+ });
8785
+ }
8786
+ } else {
8787
+ findings.push({
8788
+ id: "",
8789
+ severity: "info",
8790
+ vulnerabilityClass: "dependency-cve",
8791
+ file: "package.json",
8792
+ title: "npm audit unavailable",
8793
+ description: "npm audit command failed or is not available.",
8794
+ attackScenario: "N/A",
8795
+ suggestedFix: "Ensure npm is installed and run npm audit manually."
8796
+ });
8797
+ }
8798
+ }
8799
+ return findings;
8800
+ }
8801
+ function checkPackageJsonPatterns(projectRoot) {
8802
+ const findings = [];
8803
+ try {
8804
+ const pkgPath = join14(projectRoot, "package.json");
8805
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
8806
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
8807
+ for (const [name, version] of Object.entries(allDeps)) {
8808
+ if (version.startsWith("^") || version.startsWith("~")) {
8809
+ findings.push({
8810
+ id: "",
8811
+ severity: "info",
8812
+ vulnerabilityClass: "supply-chain",
8813
+ file: "package.json",
8814
+ title: `Flexible version range: ${name}@${version}`,
8815
+ description: `${name} uses a ${version.startsWith("^") ? "caret" : "tilde"} version range which allows automatic minor/patch updates.`,
8816
+ attackScenario: "A compromised patch release could be automatically installed.",
8817
+ suggestedFix: `Pin to an exact version or use a lockfile to ensure reproducible builds.`
8818
+ });
8819
+ }
8820
+ }
8821
+ } catch {
8822
+ }
8823
+ return findings;
8824
+ }
8825
+ function checkPostinstallScripts(projectRoot) {
8826
+ const findings = [];
8827
+ const nodeModules = join14(projectRoot, "node_modules");
8828
+ if (!existsSync13(nodeModules)) return findings;
8829
+ try {
8830
+ const topLevelDeps = readdirSync5(nodeModules).filter((d) => !d.startsWith("."));
8831
+ for (const dep of topLevelDeps) {
8832
+ const depPkgPath = join14(nodeModules, dep, "package.json");
8833
+ if (!existsSync13(depPkgPath)) continue;
8834
+ try {
8835
+ const depPkg = JSON.parse(readFileSync10(depPkgPath, "utf-8"));
8836
+ const scripts = depPkg.scripts || {};
8837
+ if (scripts.postinstall || scripts.preinstall || scripts.install) {
8838
+ const scriptName = scripts.postinstall ? "postinstall" : scripts.preinstall ? "preinstall" : "install";
8839
+ const scriptContent = scripts[scriptName];
8840
+ findings.push({
8841
+ id: "",
8842
+ severity: "high",
8843
+ vulnerabilityClass: "supply-chain",
8844
+ file: `node_modules/${dep}/package.json`,
8845
+ title: `Supply chain risk: ${dep} has ${scriptName} script`,
8846
+ description: `The dependency ${dep} runs a ${scriptName} script on install: "${scriptContent}".`,
8847
+ attackScenario: `A compromised version of ${dep} could execute arbitrary code during npm install via its ${scriptName} script.`,
8848
+ suggestedFix: `Review the ${scriptName} script. Consider using --ignore-scripts or switching to a dependency without lifecycle scripts.`
8849
+ });
8850
+ }
8851
+ } catch {
8852
+ }
8853
+ }
8854
+ } catch {
8855
+ }
8856
+ return findings;
8857
+ }
8858
+ function checkPipAudit(projectRoot) {
8859
+ const findings = [];
8860
+ try {
8861
+ const output = execSync2("pip audit --format json", {
8862
+ cwd: projectRoot,
8863
+ encoding: "utf-8",
8864
+ timeout: 3e4,
8865
+ stdio: ["pipe", "pipe", "pipe"]
8866
+ });
8867
+ const audit = JSON.parse(output);
8868
+ for (const vuln of audit.vulnerabilities || []) {
8869
+ findings.push({
8870
+ id: "",
8871
+ severity: cvssToSeverity(vuln.cvss?.score || 5),
8872
+ vulnerabilityClass: "dependency-cve",
8873
+ file: existsSync13(join14(projectRoot, "requirements.txt")) ? "requirements.txt" : "pyproject.toml",
8874
+ title: `Vulnerable Python dependency: ${vuln.name}`,
8875
+ description: `${vuln.name}@${vuln.version} \u2014 ${vuln.id}: ${vuln.description || "Known vulnerability"}`,
8876
+ attackScenario: `An attacker could exploit the vulnerability in ${vuln.name}.`,
8877
+ suggestedFix: vuln.fix_versions?.length ? `Update to version ${vuln.fix_versions.join(" or ")}.` : "No fix available."
8878
+ });
8879
+ }
8880
+ } catch {
8881
+ findings.push({
8882
+ id: "",
8883
+ severity: "info",
8884
+ vulnerabilityClass: "dependency-cve",
8885
+ file: "requirements.txt",
8886
+ title: "pip audit unavailable",
8887
+ description: "pip audit command failed or is not installed.",
8888
+ attackScenario: "N/A",
8889
+ suggestedFix: "Install pip-audit: pip install pip-audit"
8890
+ });
8891
+ }
8892
+ return findings;
8893
+ }
8894
+ function checkCargoAudit(projectRoot) {
8895
+ const findings = [];
8896
+ try {
8897
+ const output = execSync2("cargo audit --json", {
8898
+ cwd: projectRoot,
8899
+ encoding: "utf-8",
8900
+ timeout: 3e4,
8901
+ stdio: ["pipe", "pipe", "pipe"]
8902
+ });
8903
+ const audit = JSON.parse(output);
8904
+ for (const advisory of audit.vulnerabilities?.list || []) {
8905
+ const a = advisory.advisory || {};
8906
+ findings.push({
8907
+ id: "",
8908
+ severity: cvssToSeverity(a.cvss?.score || 5),
8909
+ vulnerabilityClass: "dependency-cve",
8910
+ file: "Cargo.toml",
8911
+ title: `Vulnerable Rust crate: ${a.package || "unknown"}`,
8912
+ description: `${a.id || "RUSTSEC"}: ${a.title || "Known vulnerability"}`,
8913
+ attackScenario: `An attacker could exploit the vulnerability in the crate.`,
8914
+ suggestedFix: a.patched_versions?.length ? `Update to a patched version.` : "No fix available."
8915
+ });
8916
+ }
8917
+ } catch {
8918
+ findings.push({
8919
+ id: "",
8920
+ severity: "info",
8921
+ vulnerabilityClass: "dependency-cve",
8922
+ file: "Cargo.toml",
8923
+ title: "cargo audit unavailable",
8924
+ description: "cargo audit command failed or is not installed.",
8925
+ attackScenario: "N/A",
8926
+ suggestedFix: "Install cargo-audit: cargo install cargo-audit"
8927
+ });
8928
+ }
8929
+ return findings;
8930
+ }
8931
+ function checkGoVerify(projectRoot) {
8932
+ const findings = [];
8933
+ try {
8934
+ execSync2("go mod verify", {
8935
+ cwd: projectRoot,
8936
+ encoding: "utf-8",
8937
+ timeout: 3e4,
8938
+ stdio: ["pipe", "pipe", "pipe"]
8939
+ });
8940
+ } catch (err) {
8941
+ const output = err.stdout || err.stderr || "";
8942
+ if (output.includes("SECURITY")) {
8943
+ findings.push({
8944
+ id: "",
8945
+ severity: "high",
8946
+ vulnerabilityClass: "dependency-cve",
8947
+ file: "go.mod",
8948
+ title: "Go module verification failed",
8949
+ description: `go mod verify reported issues: ${output.substring(0, 200)}`,
8950
+ attackScenario: "Tampered modules could contain malicious code.",
8951
+ suggestedFix: "Run go mod verify and resolve integrity issues."
8952
+ });
8953
+ } else {
8954
+ findings.push({
8955
+ id: "",
8956
+ severity: "info",
8957
+ vulnerabilityClass: "dependency-cve",
8958
+ file: "go.mod",
8959
+ title: "go mod verify unavailable",
8960
+ description: "go mod verify command failed.",
8961
+ attackScenario: "N/A",
8962
+ suggestedFix: "Ensure Go is installed and run go mod verify manually."
8963
+ });
8964
+ }
8965
+ }
8966
+ return findings;
8967
+ }
8968
+
8969
+ // src/security/checks/injection.ts
8970
+ import { readFileSync as readFileSync11 } from "fs";
8971
+ import { join as join15 } from "path";
8972
+ var SKIP_DIRS = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
8973
+ var TEST_PATTERNS = ["test", "spec", "fixture", "mock", "__tests__", "__mocks__"];
8974
+ var USER_INPUT_NAMES = /(?:input|user|name|path|query|branch|hash|cmd|command|req\.|params|body|args|url|dir|file|subdirectory)/i;
8975
+ var PATTERNS = [
8976
+ {
8977
+ regex: /execSync\s*\(\s*`[^`]*\$\{/,
8978
+ title: "Shell Injection via execSync template literal",
8979
+ vulnClass: "shell-injection",
8980
+ baseSeverity: "high",
8981
+ description: "execSync called with a template literal containing interpolated values \u2014 potential RCE.",
8982
+ attackScenario: "An attacker could inject shell metacharacters through the interpolated variable to execute arbitrary commands.",
8983
+ suggestedFix: "Use execFileSync with an argument array instead of string interpolation, or validate input with a strict allowlist regex."
8984
+ },
8985
+ {
8986
+ regex: /exec\s*\(\s*`[^`]*\$\{/,
8987
+ title: "Shell Injection via exec template literal",
8988
+ vulnClass: "shell-injection",
8989
+ baseSeverity: "high",
8990
+ description: "exec called with a template literal containing interpolated values \u2014 potential RCE.",
8991
+ attackScenario: "An attacker could inject shell metacharacters through the interpolated variable.",
8992
+ suggestedFix: "Use execFile with an argument array instead of string interpolation."
8993
+ },
8994
+ {
8995
+ regex: /spawn\s*\([^)]*,\s*\[[^\]]*(?:input|user|path|query|cmd|command|args|req\.|params|body)/i,
8996
+ title: "Potentially unsafe spawn with user-controlled arguments",
8997
+ vulnClass: "shell-injection",
8998
+ baseSeverity: "medium",
8999
+ description: "spawn called with arguments that may originate from user input.",
9000
+ attackScenario: "An attacker could inject malicious arguments to the spawned process.",
9001
+ suggestedFix: "Validate all arguments against a strict allowlist before passing to spawn."
9002
+ },
9003
+ {
9004
+ regex: /subprocess\.run\s*\([^)]*shell\s*=\s*True/,
9005
+ title: "Python shell=True in subprocess.run",
9006
+ vulnClass: "shell-injection",
9007
+ baseSeverity: "high",
9008
+ description: "subprocess.run called with shell=True \u2014 command string is executed through the shell.",
9009
+ attackScenario: "An attacker could inject shell metacharacters if user input reaches the command string.",
9010
+ suggestedFix: "Use shell=False (default) and pass arguments as a list."
9011
+ },
9012
+ {
9013
+ regex: /os\.system\s*\(/,
9014
+ title: "Python os.system() call",
9015
+ vulnClass: "shell-injection",
9016
+ baseSeverity: "high",
9017
+ description: "os.system() executes a command string through the shell.",
9018
+ attackScenario: "An attacker could inject shell metacharacters if user input reaches the command string.",
9019
+ suggestedFix: "Use subprocess.run with shell=False and pass arguments as a list."
9020
+ },
9021
+ {
9022
+ regex: /eval\s*\(/,
9023
+ title: "eval() usage detected",
9024
+ vulnClass: "code-injection",
9025
+ baseSeverity: "high",
9026
+ description: "eval() executes arbitrary code from a string.",
9027
+ attackScenario: "An attacker could inject malicious code if user input reaches eval().",
9028
+ suggestedFix: "Remove eval() and use safe alternatives (JSON.parse for data, specific parsers for expressions)."
9029
+ },
9030
+ {
9031
+ regex: /new\s+Function\s*\(/,
9032
+ title: "new Function() constructor",
9033
+ vulnClass: "code-injection",
9034
+ baseSeverity: "high",
9035
+ description: "new Function() creates a function from a string \u2014 equivalent to eval().",
9036
+ attackScenario: "An attacker could inject malicious code if user input reaches the Function constructor.",
9037
+ suggestedFix: "Remove new Function() and use a safe alternative."
9038
+ },
9039
+ {
9040
+ regex: /fmt\.Sprintf\s*\([^)]*(?:SELECT|INSERT|UPDATE|DELETE)/i,
9041
+ title: "Go SQL injection via fmt.Sprintf",
9042
+ vulnClass: "code-injection",
9043
+ baseSeverity: "high",
9044
+ description: "SQL query built using fmt.Sprintf \u2014 vulnerable to SQL injection.",
9045
+ attackScenario: "An attacker could inject SQL through interpolated values to read or modify database data.",
9046
+ suggestedFix: "Use parameterized queries with ? or $1 placeholders instead of string formatting."
9047
+ },
9048
+ {
9049
+ regex: /db\.Query\s*\(\s*fmt\.Sprintf/,
9050
+ title: "Go SQL injection via db.Query with fmt.Sprintf",
9051
+ vulnClass: "code-injection",
9052
+ baseSeverity: "high",
9053
+ description: "Database query built using fmt.Sprintf directly passed to db.Query.",
9054
+ attackScenario: "An attacker could inject SQL through interpolated values.",
9055
+ suggestedFix: 'Use parameterized queries: db.Query("SELECT ... WHERE id = ?", id)'
9056
+ }
9057
+ ];
9058
+ function shouldSkip(filePath) {
9059
+ return SKIP_DIRS.some((d) => filePath.includes(d));
9060
+ }
9061
+ function isTestFile4(filePath) {
9062
+ const lower = filePath.toLowerCase();
9063
+ return TEST_PATTERNS.some((p) => lower.includes(p));
9064
+ }
9065
+ async function checkInjection(files, projectRoot) {
9066
+ const findings = [];
9067
+ try {
9068
+ for (const file of files) {
9069
+ if (shouldSkip(file.filePath) || isTestFile4(file.filePath)) continue;
9070
+ let content;
9071
+ try {
9072
+ content = readFileSync11(join15(projectRoot, file.filePath), "utf-8");
9073
+ } catch {
9074
+ continue;
9075
+ }
9076
+ const lines = content.split("\n");
9077
+ for (let i = 0; i < lines.length; i++) {
9078
+ const line = lines[i];
9079
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("#") || line.trimStart().startsWith("*")) {
9080
+ continue;
9081
+ }
9082
+ if (line.includes("depwire-security-reviewed")) continue;
9083
+ for (const pattern of PATTERNS) {
9084
+ if (pattern.regex.test(line)) {
9085
+ let severity = pattern.baseSeverity;
9086
+ if (severity === "medium" && USER_INPUT_NAMES.test(line)) {
9087
+ severity = "high";
9088
+ }
9089
+ findings.push({
9090
+ id: "",
9091
+ severity,
9092
+ vulnerabilityClass: pattern.vulnClass,
9093
+ file: file.filePath,
9094
+ line: i + 1,
9095
+ title: pattern.title,
9096
+ description: pattern.description,
9097
+ attackScenario: pattern.attackScenario,
9098
+ suggestedFix: pattern.suggestedFix
9099
+ });
9100
+ }
9101
+ }
9102
+ }
9103
+ }
9104
+ } catch {
9105
+ }
9106
+ return findings;
9107
+ }
9108
+
9109
+ // src/security/checks/secrets.ts
9110
+ import { readFileSync as readFileSync12 } from "fs";
9111
+ import { join as join16 } from "path";
9112
+ var SKIP_DIRS2 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
9113
+ var TEST_PATTERNS2 = ["test", "spec", "fixture", "mock", "__tests__", "__mocks__", ".example", ".sample"];
9114
+ var SECRET_PATTERNS = [
9115
+ // API Keys
9116
+ { pattern: /sk-[a-zA-Z0-9]{32,}/, title: "OpenAI API Key", severity: "critical" },
9117
+ { pattern: /AKIA[0-9A-Z]{16}/, title: "AWS Access Key", severity: "critical" },
9118
+ { pattern: /sk_live_[a-zA-Z0-9]{24,}/, title: "Stripe Live Key", severity: "critical" },
9119
+ { pattern: /ghp_[a-zA-Z0-9]{36}/, title: "GitHub Personal Token", severity: "critical" },
9120
+ { pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/, title: "Private Key", severity: "critical" },
9121
+ // Hardcoded passwords/secrets
9122
+ { pattern: /password\s*=\s*['"][^'"]{4,}['"]/, title: "Hardcoded Password", severity: "high" },
9123
+ { pattern: /secret\s*=\s*['"][^'"]{4,}['"]/, title: "Hardcoded Secret", severity: "high" },
9124
+ { pattern: /salt\s*=\s*['"][^'"]{4,}['"]/, title: "Hardcoded Salt", severity: "high" },
9125
+ { pattern: /api_key\s*=\s*['"][^'"]{4,}['"]/, title: "Hardcoded API Key", severity: "high" },
9126
+ { pattern: /token\s*=\s*['"][^'"]{8,}['"]/, title: "Hardcoded Token", severity: "high" },
9127
+ // Weak but not critical
9128
+ { pattern: /Math\.random\(\).*(?:token|session|id|key|secret)/i, title: "Math.random() for Security Value", severity: "high" }
9129
+ ];
9130
+ function shouldSkip2(filePath) {
9131
+ return SKIP_DIRS2.some((d) => filePath.includes(d));
9132
+ }
9133
+ function isTestFile5(filePath) {
9134
+ const lower = filePath.toLowerCase();
9135
+ return TEST_PATTERNS2.some((p) => lower.includes(p));
9136
+ }
9137
+ async function checkSecrets(files, projectRoot) {
9138
+ const findings = [];
9139
+ try {
9140
+ for (const file of files) {
9141
+ if (shouldSkip2(file.filePath) || isTestFile5(file.filePath)) continue;
9142
+ let content;
9143
+ try {
9144
+ content = readFileSync12(join16(projectRoot, file.filePath), "utf-8");
9145
+ } catch {
9146
+ continue;
9147
+ }
9148
+ const lines = content.split("\n");
9149
+ for (let i = 0; i < lines.length; i++) {
9150
+ const line = lines[i];
9151
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("#") || line.trimStart().startsWith("*")) {
9152
+ continue;
9153
+ }
9154
+ for (const sp of SECRET_PATTERNS) {
9155
+ if (sp.pattern.test(line)) {
9156
+ findings.push({
9157
+ id: "",
9158
+ severity: sp.severity,
9159
+ vulnerabilityClass: "secrets",
9160
+ file: file.filePath,
9161
+ line: i + 1,
9162
+ title: sp.title,
9163
+ description: `Potential ${sp.title.toLowerCase()} detected in source code.`,
9164
+ attackScenario: "An attacker with source code access could extract credentials and use them to access external services or escalate privileges.",
9165
+ suggestedFix: "Move secrets to environment variables or a secrets manager. Never commit secrets to source control."
9166
+ });
9167
+ }
9168
+ }
9169
+ }
9170
+ }
9171
+ } catch {
9172
+ }
9173
+ return findings;
9174
+ }
9175
+
9176
+ // src/security/checks/path-traversal.ts
9177
+ import { readFileSync as readFileSync13 } from "fs";
9178
+ import { join as join17 } from "path";
9179
+ var SKIP_DIRS3 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
9180
+ var USER_INPUT_VARS = /(?:req\.|params|query|body|input|path|dir|subdirectory|file|userInput|fileName|filePath)/i;
9181
+ var PATTERNS2 = [
9182
+ {
9183
+ regex: /path\.join\s*\(\s*(?:__dirname|root|base|projectRoot)[^)]*,/,
9184
+ title: "Potential path traversal via path.join",
9185
+ description: "path.join called with a root directory and a variable that may contain user input \u2014 without resolve() containment check.",
9186
+ suggestedFix: 'Use path.resolve() and verify the result starts with the expected root: if (!resolved.startsWith(root)) throw new Error("path traversal")'
9187
+ },
9188
+ {
9189
+ regex: /readFileSync\s*\([^)]*(?:input|user|path|dir|file|query|params|body|req\.)/i,
9190
+ title: "readFileSync with potentially user-controlled path",
9191
+ description: "readFileSync called with a variable that may originate from user input.",
9192
+ suggestedFix: "Validate and sanitize the file path. Use path.resolve() and verify it starts with the expected root directory."
9193
+ },
9194
+ {
9195
+ regex: /writeFileSync\s*\([^)]*(?:input|user|path|dir|file|query|params|body|req\.)/i,
9196
+ title: "writeFileSync with potentially user-controlled path",
9197
+ description: "writeFileSync called with a variable that may originate from user input.",
9198
+ suggestedFix: "Validate and sanitize the file path. Use path.resolve() and verify it starts with the expected root directory."
9199
+ },
9200
+ {
9201
+ regex: /createReadStream\s*\([^)]*(?:input|user|path|dir|file|query|params|body|req\.)/i,
9202
+ title: "createReadStream with potentially user-controlled path",
9203
+ description: "createReadStream called with a path that may originate from user input.",
9204
+ suggestedFix: "Validate and sanitize the file path before creating the stream."
9205
+ }
9206
+ ];
9207
+ function shouldSkip3(filePath) {
9208
+ if (SKIP_DIRS3.some((d) => filePath.includes(d))) return true;
9209
+ if (filePath.includes("wasm-init")) return true;
9210
+ return false;
9211
+ }
9212
+ function isRouteOrTool(filePath) {
9213
+ const lower = filePath.toLowerCase();
9214
+ return lower.includes("route") || lower.includes("api/") || lower.includes("mcp/") || lower.includes("handler") || lower.includes("controller");
9215
+ }
9216
+ var SAFE_OUTPUT_PATTERNS = /(?:output|outPath|outFile|dest|target|docPath).*\.(?:md|json|html|ts|js)['"]|['"][^'"]+\.(?:md|json|html|ts|js)['"]/;
9217
+ var SAFE_DIRNAME_ARGS = /(?:grammar|wasm|wasmPath|wasmFile|grammars)/i;
9218
+ async function checkPathTraversal(files, projectRoot) {
9219
+ const findings = [];
9220
+ try {
9221
+ for (const file of files) {
9222
+ if (shouldSkip3(file.filePath)) continue;
9223
+ let content;
9224
+ try {
9225
+ content = readFileSync13(join17(projectRoot, file.filePath), "utf-8");
9226
+ } catch {
9227
+ continue;
9228
+ }
9229
+ const lines = content.split("\n");
9230
+ const inRouteOrTool = isRouteOrTool(file.filePath);
9231
+ for (let i = 0; i < lines.length; i++) {
9232
+ const line = lines[i];
9233
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("#")) continue;
9234
+ for (const pattern of PATTERNS2) {
9235
+ if (pattern.regex.test(line)) {
9236
+ if (!USER_INPUT_VARS.test(line)) continue;
9237
+ if (/writeFileSync/.test(line) && SAFE_OUTPUT_PATTERNS.test(line)) continue;
9238
+ if (/(?:writeFileSync|readFileSync)/.test(line)) {
9239
+ const context = lines.slice(Math.max(0, i - 2), i + 1).join("\n");
9240
+ if (SAFE_OUTPUT_PATTERNS.test(context)) continue;
9241
+ }
9242
+ if (/__dirname/.test(line) && SAFE_DIRNAME_ARGS.test(line)) continue;
9243
+ const nearbyLines = lines.slice(Math.max(0, i - 15), Math.min(lines.length, i + 4)).join("\n");
9244
+ if (nearbyLines.includes("startsWith") && /resolve/.test(nearbyLines)) continue;
9245
+ const severity = inRouteOrTool ? "high" : "medium";
9246
+ findings.push({
9247
+ id: "",
9248
+ severity,
9249
+ vulnerabilityClass: "path-traversal",
9250
+ file: file.filePath,
9251
+ line: i + 1,
9252
+ title: pattern.title,
9253
+ description: pattern.description,
9254
+ attackScenario: "An attacker could use ../ sequences to traverse outside the intended directory and read or write arbitrary files on the server.",
9255
+ suggestedFix: pattern.suggestedFix
9256
+ });
9257
+ }
9258
+ }
9259
+ }
9260
+ }
9261
+ } catch {
9262
+ }
9263
+ return findings;
9264
+ }
9265
+
9266
+ // src/security/checks/auth.ts
9267
+ import { readFileSync as readFileSync14 } from "fs";
9268
+ import { join as join18 } from "path";
9269
+ var SKIP_DIRS4 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
9270
+ function shouldSkip4(filePath) {
9271
+ return SKIP_DIRS4.some((d) => filePath.includes(d));
9272
+ }
9273
+ function isAuthRelatedFile(filePath) {
9274
+ const lower = filePath.toLowerCase();
9275
+ return /(?:auth|session|token|jwt|oauth|login|passport)/.test(lower);
9276
+ }
9277
+ async function checkAuth(files, projectRoot) {
9278
+ const findings = [];
9279
+ try {
9280
+ for (const file of files) {
9281
+ if (shouldSkip4(file.filePath)) continue;
9282
+ let content;
9283
+ try {
9284
+ content = readFileSync14(join18(projectRoot, file.filePath), "utf-8");
9285
+ } catch {
9286
+ continue;
9287
+ }
9288
+ const lines = content.split("\n");
9289
+ const isAuthFile = isAuthRelatedFile(file.filePath);
9290
+ for (let i = 0; i < lines.length; i++) {
9291
+ const line = lines[i];
9292
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("#")) continue;
9293
+ if (/catch\s*\([^)]*\)\s*\{/.test(line)) {
9294
+ const catchBlock = lines.slice(i, Math.min(lines.length, i + 5)).join("\n");
9295
+ if (/(?:next\s*\(|return\s+true|resolve\s*\(\s*true\s*\))/.test(catchBlock)) {
9296
+ findings.push({
9297
+ id: "",
9298
+ severity: "medium",
9299
+ vulnerabilityClass: "auth",
9300
+ file: file.filePath,
9301
+ line: i + 1,
9302
+ title: "Fail-open catch block may bypass authentication",
9303
+ description: "A catch block that calls next(), returns true, or resolves true could bypass auth checks when an error occurs.",
9304
+ attackScenario: "An attacker could trigger an error condition (e.g., malformed token) to bypass authentication.",
9305
+ suggestedFix: "Ensure catch blocks deny access by default. Return false, call next(err), or throw."
9306
+ });
9307
+ }
9308
+ }
9309
+ if (/[?&](?:token|session|key|auth)=/.test(line)) {
9310
+ findings.push({
9311
+ id: "",
9312
+ severity: "high",
9313
+ vulnerabilityClass: "auth",
9314
+ file: file.filePath,
9315
+ line: i + 1,
9316
+ title: "Credential in URL query parameter",
9317
+ description: "Token, session, or auth key passed as a URL query parameter.",
9318
+ attackScenario: "URL query parameters are logged in server access logs, browser history, and referrer headers \u2014 exposing credentials.",
9319
+ suggestedFix: "Send credentials in Authorization headers or secure HTTP-only cookies instead."
9320
+ });
9321
+ }
9322
+ if (/Math\.random\(\)/.test(line) && isAuthFile) {
9323
+ findings.push({
9324
+ id: "",
9325
+ severity: "high",
9326
+ vulnerabilityClass: "auth",
9327
+ file: file.filePath,
9328
+ line: i + 1,
9329
+ title: "Math.random() used in auth-related file",
9330
+ description: "Math.random() is not cryptographically secure and should not be used for tokens, session IDs, or any security value.",
9331
+ attackScenario: "An attacker could predict Math.random() output and forge tokens or session IDs.",
9332
+ suggestedFix: "Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive values."
9333
+ });
9334
+ }
9335
+ if (/jwt\.verify\s*\(/.test(line)) {
9336
+ const nearbyLines = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 11)).join("\n");
9337
+ if (!/(?:expiresIn|exp\s*:|maxAge)/.test(nearbyLines)) {
9338
+ findings.push({
9339
+ id: "",
9340
+ severity: "medium",
9341
+ vulnerabilityClass: "auth",
9342
+ file: file.filePath,
9343
+ line: i + 1,
9344
+ title: "JWT verification without expiry check",
9345
+ description: "jwt.verify called without expiresIn or exp option nearby \u2014 tokens may never expire.",
9346
+ attackScenario: "A stolen JWT could be used indefinitely if it has no expiration.",
9347
+ suggestedFix: "Set expiresIn when signing and verify exp claim during verification."
9348
+ });
9349
+ }
9350
+ }
9351
+ if (/state.*cookie/i.test(line)) {
9352
+ const nearbyLines = lines.slice(i, Math.min(lines.length, i + 10)).join("\n");
9353
+ if (!/(?:maxAge.*0|clearCookie|delete.*state)/i.test(nearbyLines)) {
9354
+ findings.push({
9355
+ id: "",
9356
+ severity: "low",
9357
+ vulnerabilityClass: "auth",
9358
+ file: file.filePath,
9359
+ line: i + 1,
9360
+ title: "OAuth state cookie not cleared after use",
9361
+ description: "OAuth state parameter stored in cookie may not be cleared after consumption.",
9362
+ attackScenario: "A stale state cookie could be replayed in a CSRF attack against the OAuth flow.",
9363
+ suggestedFix: "Clear the state cookie immediately after successful validation."
9364
+ });
9365
+ }
9366
+ }
9367
+ }
9368
+ }
9369
+ } catch {
9370
+ }
9371
+ return findings;
9372
+ }
9373
+
9374
+ // src/security/checks/input-validation.ts
9375
+ import { readFileSync as readFileSync15 } from "fs";
9376
+ import { join as join19 } from "path";
9377
+ var SKIP_DIRS5 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
9378
+ function shouldSkip5(filePath) {
9379
+ return SKIP_DIRS5.some((d) => filePath.includes(d));
9380
+ }
9381
+ async function checkInputValidation(files, projectRoot) {
9382
+ const findings = [];
9383
+ try {
9384
+ for (const file of files) {
9385
+ if (shouldSkip5(file.filePath)) continue;
9386
+ let content;
9387
+ try {
9388
+ content = readFileSync15(join19(projectRoot, file.filePath), "utf-8");
9389
+ } catch {
9390
+ continue;
9391
+ }
9392
+ const lines = content.split("\n");
9393
+ const fullContent = content;
9394
+ for (let i = 0; i < lines.length; i++) {
9395
+ const line = lines[i];
9396
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("#")) continue;
9397
+ if (/cors\s*\(\s*\{\s*origin\s*:\s*['"]\*['"]/.test(line) || /Access-Control-Allow-Origin.*\*/.test(line)) {
9398
+ findings.push({
9399
+ id: "",
9400
+ severity: "medium",
9401
+ vulnerabilityClass: "input-validation",
9402
+ file: file.filePath,
9403
+ line: i + 1,
9404
+ title: "CORS wildcard origin",
9405
+ description: "CORS is configured to allow all origins (*), which permits any website to make requests to this API.",
9406
+ attackScenario: "An attacker could create a malicious website that makes authenticated requests to this API using the victim's cookies.",
9407
+ suggestedFix: "Restrict CORS origin to specific trusted domains instead of using wildcard."
9408
+ });
9409
+ }
9410
+ if (/express\.json\s*\(\s*\)/.test(line)) {
9411
+ if (!/limit/.test(line)) {
9412
+ findings.push({
9413
+ id: "",
9414
+ severity: "medium",
9415
+ vulnerabilityClass: "input-validation",
9416
+ file: file.filePath,
9417
+ line: i + 1,
9418
+ title: "No body size limit on JSON parser",
9419
+ description: "express.json() used without a size limit \u2014 the server may be vulnerable to large payload attacks.",
9420
+ attackScenario: "An attacker could send extremely large JSON payloads to exhaust server memory (denial of service).",
9421
+ suggestedFix: 'Set a body size limit: express.json({ limit: "1mb" })'
9422
+ });
9423
+ }
9424
+ }
9425
+ if (/req\.params\.id/.test(line)) {
9426
+ const nearbyLines = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 4)).join("\n");
9427
+ if (!/(?:isValidUUID|uuid|^[0-9a-f-]{36}|validate|isValid|parseInt)/.test(nearbyLines)) {
9428
+ findings.push({
9429
+ id: "",
9430
+ severity: "medium",
9431
+ vulnerabilityClass: "input-validation",
9432
+ file: file.filePath,
9433
+ line: i + 1,
9434
+ title: "req.params.id used without validation",
9435
+ description: "A route parameter (req.params.id) is used without apparent validation \u2014 could allow injection or invalid lookups.",
9436
+ attackScenario: "An attacker could pass malformed IDs to trigger unexpected behavior or SQL/NoSQL injection.",
9437
+ suggestedFix: "Validate req.params.id against expected format (e.g., UUID regex or parseInt) before use."
9438
+ });
9439
+ }
9440
+ }
9441
+ if (/(?:INSERT|db\.put|db\.create|\.save\(|\.insert\()/.test(line) && /req\.body/.test(line)) {
9442
+ const nearbyLines = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 3)).join("\n");
9443
+ if (!/\.length/.test(nearbyLines)) {
9444
+ findings.push({
9445
+ id: "",
9446
+ severity: "low",
9447
+ vulnerabilityClass: "input-validation",
9448
+ file: file.filePath,
9449
+ line: i + 1,
9450
+ title: "User input stored without length validation",
9451
+ description: "User input from req.body is stored to a database without apparent length validation.",
9452
+ attackScenario: "An attacker could store extremely long strings to waste storage or cause display issues.",
9453
+ suggestedFix: "Add length validation before storing user input: if (input.length > MAX_LENGTH) return res.status(400)..."
9454
+ });
9455
+ }
9456
+ }
9457
+ }
9458
+ }
9459
+ } catch {
9460
+ }
9461
+ return findings;
9462
+ }
9463
+
9464
+ // src/security/checks/information-disclosure.ts
9465
+ import { readFileSync as readFileSync16 } from "fs";
9466
+ import { join as join20 } from "path";
9467
+ var SKIP_DIRS6 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
9468
+ function shouldSkip6(filePath) {
9469
+ return SKIP_DIRS6.some((d) => filePath.includes(d));
9470
+ }
9471
+ async function checkInformationDisclosure(files, projectRoot) {
9472
+ const findings = [];
9473
+ try {
9474
+ for (const file of files) {
9475
+ if (shouldSkip6(file.filePath)) continue;
9476
+ let content;
9477
+ try {
9478
+ content = readFileSync16(join20(projectRoot, file.filePath), "utf-8");
9479
+ } catch {
9480
+ continue;
9481
+ }
9482
+ const lines = content.split("\n");
9483
+ for (let i = 0; i < lines.length; i++) {
9484
+ const line = lines[i];
9485
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("#")) continue;
9486
+ if (/res\.(?:json|send)\s*\(\s*\{[^}]*err\.stack/.test(line) || /res\.(?:json|send)\s*\(\s*\{[^}]*stack\s*:/.test(line)) {
9487
+ findings.push({
9488
+ id: "",
9489
+ severity: "medium",
9490
+ vulnerabilityClass: "information-disclosure",
9491
+ file: file.filePath,
9492
+ line: i + 1,
9493
+ title: "Stack trace in API response",
9494
+ description: "Error stack trace is included in an API response \u2014 exposes internal code paths and dependencies.",
9495
+ attackScenario: "An attacker could use stack traces to map internal code structure, identify frameworks, and find vulnerable code paths.",
9496
+ suggestedFix: 'Log stack traces to stderr and return a generic error message to clients: res.json({ error: "Internal server error" })'
9497
+ });
9498
+ }
9499
+ if (/console\.(?:log|error|warn)\s*\(\s*process\.env\s*\)/.test(line) || /Object\.keys\s*\(\s*process\.env\s*\)/.test(line)) {
9500
+ findings.push({
9501
+ id: "",
9502
+ severity: "low",
9503
+ vulnerabilityClass: "information-disclosure",
9504
+ file: file.filePath,
9505
+ line: i + 1,
9506
+ title: "Environment variable enumeration",
9507
+ description: "Entire process.env object is logged or enumerated \u2014 may expose secrets in log output.",
9508
+ attackScenario: "An attacker with log access could see all environment variables including API keys and database credentials.",
9509
+ suggestedFix: "Only log specific environment variable names (not values) when needed for debugging."
9510
+ });
9511
+ }
9512
+ if (/`[^`]*(?:clone|fetch|pull|push)[^`]*\$\{.*(?:url|token|key|auth).*\}`/i.test(line)) {
9513
+ findings.push({
9514
+ id: "",
9515
+ severity: "medium",
9516
+ vulnerabilityClass: "information-disclosure",
9517
+ file: file.filePath,
9518
+ line: i + 1,
9519
+ title: "Potential credential in error/log message",
9520
+ description: "A URL or token may be interpolated into an error or log message \u2014 could expose credentials.",
9521
+ attackScenario: "An attacker with log access could extract credentials from logged URLs containing embedded tokens.",
9522
+ suggestedFix: "Sanitize URLs before logging: strip query parameters and embedded credentials."
9523
+ });
9524
+ }
9525
+ if (/console\.(?:log|debug|info)\s*\(.*(?:token|password|secret|key|auth|credential)/i.test(line)) {
9526
+ if (!/['"].*(?:token|password|secret|key|auth).*['"]/.test(line)) {
9527
+ findings.push({
9528
+ id: "",
9529
+ severity: "low",
9530
+ vulnerabilityClass: "information-disclosure",
9531
+ file: file.filePath,
9532
+ line: i + 1,
9533
+ title: "Debug log may contain sensitive value",
9534
+ description: "A console.log statement references a variable with a sensitive name (token, password, secret, key, auth).",
9535
+ attackScenario: "An attacker with log access could extract sensitive values from debug output.",
9536
+ suggestedFix: "Remove debug logging of sensitive values, or use a structured logger that redacts sensitive fields."
9537
+ });
9538
+ }
9539
+ }
9540
+ }
9541
+ }
9542
+ } catch {
9543
+ }
9544
+ return findings;
9545
+ }
9546
+
9547
+ // src/security/checks/cryptography.ts
9548
+ import { readFileSync as readFileSync17 } from "fs";
9549
+ import { join as join21 } from "path";
9550
+ var SKIP_DIRS7 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
9551
+ function shouldSkip7(filePath) {
9552
+ return SKIP_DIRS7.some((d) => filePath.includes(d));
9553
+ }
9554
+ function isAuthOrCryptoFile(filePath) {
9555
+ const lower = filePath.toLowerCase();
9556
+ return /(?:auth|password|crypto|hash|session|token|jwt)/.test(lower);
9557
+ }
9558
+ async function checkCryptography(files, projectRoot) {
9559
+ const findings = [];
9560
+ try {
9561
+ for (const file of files) {
9562
+ if (shouldSkip7(file.filePath)) continue;
9563
+ let content;
9564
+ try {
9565
+ content = readFileSync17(join21(projectRoot, file.filePath), "utf-8");
9566
+ } catch {
9567
+ continue;
9568
+ }
9569
+ const lines = content.split("\n");
9570
+ const isCryptoFile = isAuthOrCryptoFile(file.filePath);
9571
+ for (let i = 0; i < lines.length; i++) {
9572
+ const line = lines[i];
9573
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("#")) continue;
9574
+ if (/createHash\s*\(\s*['"]md5['"]\s*\)/.test(line) || /hashlib\.md5\s*\(/.test(line)) {
9575
+ findings.push({
9576
+ id: "",
9577
+ severity: isCryptoFile ? "high" : "medium",
9578
+ vulnerabilityClass: "cryptography",
9579
+ file: file.filePath,
9580
+ line: i + 1,
9581
+ title: "Weak hash algorithm: MD5",
9582
+ description: "MD5 is cryptographically broken \u2014 collisions can be generated in seconds.",
9583
+ attackScenario: "An attacker could generate MD5 collisions to bypass integrity checks or forge password hashes.",
9584
+ suggestedFix: "Use SHA-256 or SHA-3 for integrity checks. Use bcrypt, scrypt, or argon2 for password hashing."
9585
+ });
9586
+ }
9587
+ if (/createHash\s*\(\s*['"]sha1['"]\s*\)/.test(line) || /hashlib\.sha1\s*\(/.test(line)) {
9588
+ findings.push({
9589
+ id: "",
9590
+ severity: isCryptoFile ? "high" : "medium",
9591
+ vulnerabilityClass: "cryptography",
9592
+ file: file.filePath,
9593
+ line: i + 1,
9594
+ title: "Weak hash algorithm: SHA-1",
9595
+ description: "SHA-1 has known collision attacks (SHAttered) \u2014 should not be used for security purposes.",
9596
+ attackScenario: "An attacker could generate SHA-1 collisions to bypass integrity checks.",
9597
+ suggestedFix: "Use SHA-256 or SHA-3 for integrity checks. Use bcrypt, scrypt, or argon2 for password hashing."
9598
+ });
9599
+ }
9600
+ if (/Math\.random\(\)/.test(line) && isCryptoFile) {
9601
+ findings.push({
9602
+ id: "",
9603
+ severity: "high",
9604
+ vulnerabilityClass: "cryptography",
9605
+ file: file.filePath,
9606
+ line: i + 1,
9607
+ title: "Math.random() in cryptography-related file",
9608
+ description: "Math.random() is not cryptographically secure \u2014 its output can be predicted.",
9609
+ attackScenario: "An attacker could predict Math.random() values to forge tokens, nonces, or other security-critical random values.",
9610
+ suggestedFix: "Use crypto.randomBytes() or crypto.getRandomValues() for cryptographic purposes."
9611
+ });
9612
+ }
9613
+ if (/(?:fetch|axios\.(?:get|post|put|delete|patch)|http\.request)\s*\(\s*['"]http:\/\/(?!(?:localhost|127\.))/i.test(line)) {
9614
+ findings.push({
9615
+ id: "",
9616
+ severity: "medium",
9617
+ vulnerabilityClass: "cryptography",
9618
+ file: file.filePath,
9619
+ line: i + 1,
9620
+ title: "HTTP used instead of HTTPS",
9621
+ description: "An HTTP (not HTTPS) URL is used for an external request \u2014 data is transmitted unencrypted.",
9622
+ attackScenario: "An attacker on the network path could intercept, read, or modify data in transit (man-in-the-middle).",
9623
+ suggestedFix: "Use HTTPS for all external requests to ensure data confidentiality and integrity."
9624
+ });
9625
+ }
9626
+ if (/pbkdf2/.test(line) && /['"][a-zA-Z0-9+/=]{8,}['"]/.test(line)) {
9627
+ findings.push({
9628
+ id: "",
9629
+ severity: "high",
9630
+ vulnerabilityClass: "cryptography",
9631
+ file: file.filePath,
9632
+ line: i + 1,
9633
+ title: "Hardcoded salt in key derivation",
9634
+ description: "A hardcoded salt is used with PBKDF2 \u2014 all users share the same salt.",
9635
+ attackScenario: "An attacker could precompute rainbow tables with the known salt to crack all passwords at once.",
9636
+ suggestedFix: "Generate a unique random salt per user using crypto.randomBytes(16)."
9637
+ });
9638
+ }
9639
+ }
9640
+ }
9641
+ } catch {
9642
+ }
9643
+ return findings;
9644
+ }
9645
+
9646
+ // src/security/checks/frontend.ts
9647
+ import { readFileSync as readFileSync18 } from "fs";
9648
+ import { join as join22 } from "path";
9649
+ var SKIP_DIRS8 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
9650
+ function shouldSkip8(filePath) {
9651
+ return SKIP_DIRS8.some((d) => filePath.includes(d));
9652
+ }
9653
+ function isFrontendFile(filePath) {
9654
+ return /\.(?:tsx|jsx|html)$/.test(filePath);
9655
+ }
9656
+ async function checkFrontend(files, projectRoot) {
9657
+ const findings = [];
9658
+ try {
9659
+ for (const file of files) {
9660
+ if (shouldSkip8(file.filePath)) continue;
9661
+ if (!isFrontendFile(file.filePath)) continue;
9662
+ let content;
9663
+ try {
9664
+ content = readFileSync18(join22(projectRoot, file.filePath), "utf-8");
9665
+ } catch {
9666
+ continue;
9667
+ }
9668
+ const lines = content.split("\n");
9669
+ for (let i = 0; i < lines.length; i++) {
9670
+ const line = lines[i];
9671
+ if (line.trimStart().startsWith("//") || line.trimStart().startsWith("{/*")) continue;
9672
+ if (/dangerouslySetInnerHTML/.test(line)) {
9673
+ findings.push({
9674
+ id: "",
9675
+ severity: "high",
9676
+ vulnerabilityClass: "frontend-xss",
9677
+ file: file.filePath,
9678
+ line: i + 1,
9679
+ title: "dangerouslySetInnerHTML usage",
9680
+ description: "dangerouslySetInnerHTML renders raw HTML \u2014 bypasses React's XSS protections.",
9681
+ attackScenario: "An attacker could inject malicious HTML/JavaScript if user input reaches dangerouslySetInnerHTML.",
9682
+ suggestedFix: "Sanitize HTML with DOMPurify before rendering, or use React components instead of raw HTML."
9683
+ });
9684
+ }
9685
+ if (/\.innerHTML\s*=/.test(line)) {
9686
+ findings.push({
9687
+ id: "",
9688
+ severity: "high",
9689
+ vulnerabilityClass: "frontend-xss",
9690
+ file: file.filePath,
9691
+ line: i + 1,
9692
+ title: "innerHTML assignment",
9693
+ description: "Direct innerHTML assignment renders raw HTML without sanitization.",
9694
+ attackScenario: "An attacker could inject malicious scripts through user-controlled content assigned to innerHTML.",
9695
+ suggestedFix: "Use textContent for plain text, or sanitize with DOMPurify before setting innerHTML."
9696
+ });
9697
+ }
9698
+ if (/document\.write\s*\(/.test(line)) {
9699
+ findings.push({
9700
+ id: "",
9701
+ severity: "medium",
9702
+ vulnerabilityClass: "frontend-xss",
9703
+ file: file.filePath,
9704
+ line: i + 1,
9705
+ title: "document.write() usage",
9706
+ description: "document.write() can introduce XSS vulnerabilities and degrades performance.",
9707
+ attackScenario: "An attacker could inject scripts through user input that reaches document.write().",
9708
+ suggestedFix: "Use DOM manipulation methods (createElement, appendChild) instead of document.write()."
9709
+ });
9710
+ }
9711
+ if (/target\s*=\s*["']_blank["']/.test(line)) {
9712
+ const fullLine = line;
9713
+ if (!/rel\s*=\s*["'][^"']*noopener[^"']*["']/.test(fullLine)) {
9714
+ findings.push({
9715
+ id: "",
9716
+ severity: "low",
9717
+ vulnerabilityClass: "frontend-xss",
9718
+ file: file.filePath,
9719
+ line: i + 1,
9720
+ title: 'Missing rel="noopener" on target="_blank"',
9721
+ description: 'Links with target="_blank" without rel="noopener noreferrer" give the opened page access to window.opener.',
9722
+ attackScenario: "The opened page could use window.opener to redirect the original page to a phishing site.",
9723
+ suggestedFix: 'Add rel="noopener noreferrer" to all links with target="_blank".'
9724
+ });
9725
+ }
9726
+ }
9727
+ if (/(?:localStorage|sessionStorage)\.setItem\s*\([^)]*(?:token|password|secret|key|auth)/i.test(line)) {
9728
+ findings.push({
9729
+ id: "",
9730
+ severity: "high",
9731
+ vulnerabilityClass: "frontend-xss",
9732
+ file: file.filePath,
9733
+ line: i + 1,
9734
+ title: "Sensitive data stored in browser storage",
9735
+ description: "A sensitive value (token, password, secret, key, auth) is stored in localStorage or sessionStorage.",
9736
+ attackScenario: "Any XSS vulnerability would allow an attacker to read all localStorage/sessionStorage data, including sensitive tokens.",
9737
+ suggestedFix: "Use secure HTTP-only cookies for sensitive tokens instead of browser storage."
9738
+ });
9739
+ }
9740
+ }
9741
+ }
9742
+ } catch {
9743
+ }
9744
+ return findings;
9745
+ }
9746
+
9747
+ // src/security/checks/architecture.ts
9748
+ var AUTH_KEYWORDS = /(?:auth|token|session|jwt|oauth|login|passport|credential)/i;
9749
+ var DATA_KEYWORDS = /(?:query|insert|fetch|get|find|select|update|delete|save|create|put|remove)/i;
9750
+ var DB_IMPORT_KEYWORDS = /(?:db|database|prisma|mongoose|d1|sql|knex|sequelize|typeorm|drizzle)/i;
9751
+ var CRYPTO_KEYWORDS = /(?:auth|crypto|token|session|jwt|password|hash)/i;
9752
+ function isSecurityFile(filePath) {
9753
+ return CRYPTO_KEYWORDS.test(filePath.toLowerCase());
9754
+ }
9755
+ function isRouteFile(filePath) {
9756
+ const lower = filePath.toLowerCase();
9757
+ return /(?:routes?\/|api\/|handler|controller|endpoint)/.test(lower);
9758
+ }
9759
+ async function checkArchitecture(files, projectRoot, graph) {
9760
+ const findings = [];
9761
+ try {
9762
+ findings.push(...checkGodFilesWithAuthAndData(graph));
9763
+ findings.push(...checkCircularAuthDeps(graph));
9764
+ findings.push(...checkDirectDbFromRoutes(graph));
9765
+ findings.push(...checkDeadAuthCode(graph));
9766
+ findings.push(...checkUnauthHighFanIn(graph));
9767
+ } catch {
9768
+ }
9769
+ return findings;
9770
+ }
9771
+ function checkGodFilesWithAuthAndData(graph) {
9772
+ const findings = [];
9773
+ const fileConnections = /* @__PURE__ */ new Map();
9774
+ const fileSymbolNames = /* @__PURE__ */ new Map();
9775
+ graph.forEachNode((_node, attrs) => {
9776
+ const fp = attrs.filePath;
9777
+ if (!fileSymbolNames.has(fp)) fileSymbolNames.set(fp, []);
9778
+ fileSymbolNames.get(fp).push(attrs.name);
9779
+ });
9780
+ graph.forEachEdge((_edge, _attrs, source, target) => {
9781
+ const sf = graph.getNodeAttributes(source).filePath;
9782
+ const tf = graph.getNodeAttributes(target).filePath;
9783
+ if (sf !== tf) {
9784
+ fileConnections.set(sf, (fileConnections.get(sf) || 0) + 1);
9785
+ fileConnections.set(tf, (fileConnections.get(tf) || 0) + 1);
9786
+ }
9787
+ });
9788
+ const connections = Array.from(fileConnections.values());
9789
+ const avg = connections.length > 0 ? connections.reduce((a, b) => a + b, 0) / connections.length : 0;
9790
+ const godThreshold = avg * 3;
9791
+ for (const [filePath, count] of fileConnections.entries()) {
9792
+ if (count <= godThreshold) continue;
9793
+ const symbols = fileSymbolNames.get(filePath) || [];
9794
+ const hasAuth = symbols.some((s) => AUTH_KEYWORDS.test(s));
9795
+ const hasData = symbols.some((s) => DATA_KEYWORDS.test(s));
9796
+ if (hasAuth && hasData) {
9797
+ findings.push({
9798
+ id: "",
9799
+ severity: "medium",
9800
+ vulnerabilityClass: "architecture",
9801
+ file: filePath,
9802
+ title: "God file mixes auth and data access logic",
9803
+ description: `${filePath} has ${count} connections and contains both auth-related and data-access symbols. This violates separation of concerns and makes security auditing difficult.`,
9804
+ attackScenario: "A bug in data access logic could inadvertently bypass auth checks when auth and data are tightly coupled in a single file.",
9805
+ suggestedFix: "Split auth logic and data access into separate modules with a clear service layer boundary."
9806
+ });
9807
+ }
9808
+ }
9809
+ return findings;
9810
+ }
9811
+ function checkCircularAuthDeps(graph) {
9812
+ const findings = [];
9813
+ const fileGraph = /* @__PURE__ */ new Map();
9814
+ graph.forEachEdge((_edge, _attrs, source, target) => {
9815
+ const sf = graph.getNodeAttributes(source).filePath;
9816
+ const tf = graph.getNodeAttributes(target).filePath;
9817
+ if (sf !== tf) {
9818
+ if (!fileGraph.has(sf)) fileGraph.set(sf, /* @__PURE__ */ new Set());
9819
+ fileGraph.get(sf).add(tf);
9820
+ }
9821
+ });
9822
+ const visited = /* @__PURE__ */ new Set();
9823
+ const recStack = /* @__PURE__ */ new Set();
9824
+ const cycles = [];
9825
+ function dfs(node, path6) {
9826
+ if (recStack.has(node)) {
9827
+ const cycleStart = path6.indexOf(node);
9828
+ if (cycleStart >= 0) cycles.push(path6.slice(cycleStart));
9829
+ return;
9830
+ }
9831
+ if (visited.has(node)) return;
9832
+ visited.add(node);
9833
+ recStack.add(node);
9834
+ path6.push(node);
9835
+ const neighbors = fileGraph.get(node);
9836
+ if (neighbors) {
9837
+ for (const neighbor of neighbors) {
9838
+ dfs(neighbor, [...path6]);
9839
+ }
9840
+ }
9841
+ recStack.delete(node);
9842
+ }
9843
+ for (const node of fileGraph.keys()) {
9844
+ if (!visited.has(node)) dfs(node, []);
9845
+ }
9846
+ const seen = /* @__PURE__ */ new Set();
9847
+ for (const cycle of cycles) {
9848
+ const key = [...cycle].sort().join(",");
9849
+ if (seen.has(key)) continue;
9850
+ seen.add(key);
9851
+ const hasSecurityFile = cycle.some((f) => isSecurityFile(f));
9852
+ if (hasSecurityFile) {
9853
+ findings.push({
9854
+ id: "",
9855
+ severity: "high",
9856
+ vulnerabilityClass: "architecture",
9857
+ file: cycle[0],
9858
+ title: "Circular dependency in auth/crypto module",
9859
+ description: `Circular dependency detected involving security-critical files: ${cycle.join(" \u2192 ")}`,
9860
+ attackScenario: "Circular dependencies in auth modules can lead to initialization order bugs where auth checks are bypassed during startup.",
9861
+ suggestedFix: "Break the circular dependency by extracting shared types/interfaces into a separate module."
9862
+ });
9863
+ }
9864
+ }
9865
+ return findings;
9866
+ }
9867
+ function checkDirectDbFromRoutes(graph) {
9868
+ const findings = [];
9869
+ const fileImports = /* @__PURE__ */ new Map();
9870
+ graph.forEachEdge((_edge, attrs, source, target) => {
9871
+ const sf = graph.getNodeAttributes(source).filePath;
9872
+ const tf = graph.getNodeAttributes(target).filePath;
9873
+ if (sf !== tf) {
9874
+ if (!fileImports.has(sf)) fileImports.set(sf, /* @__PURE__ */ new Set());
9875
+ fileImports.get(sf).add(tf);
9876
+ }
9877
+ });
9878
+ for (const [filePath, imports] of fileImports.entries()) {
9879
+ if (!isRouteFile(filePath)) continue;
9880
+ for (const importedFile of imports) {
9881
+ const importedName = importedFile.toLowerCase();
9882
+ if (DB_IMPORT_KEYWORDS.test(importedName)) {
9883
+ findings.push({
9884
+ id: "",
9885
+ severity: "medium",
9886
+ vulnerabilityClass: "architecture",
9887
+ file: filePath,
9888
+ title: "Direct DB access from route handler",
9889
+ description: `Route file ${filePath} imports directly from ${importedFile} (database client) without a service layer.`,
9890
+ attackScenario: "Direct DB access from routes makes it harder to enforce consistent authorization, validation, and audit logging.",
9891
+ suggestedFix: "Introduce a service layer between routes and database access for consistent security checks."
9892
+ });
9893
+ }
9894
+ }
9895
+ }
9896
+ return findings;
9897
+ }
9898
+ var SKIP_FILE_PATTERNS = ["test/", "tests/", "test/fixtures/", "__tests__/", "fixtures/", "spec/"];
9899
+ function checkDeadAuthCode(graph) {
9900
+ const findings = [];
9901
+ const seen = /* @__PURE__ */ new Set();
9902
+ graph.forEachNode((node, attrs) => {
9903
+ if (!attrs.exported) return;
9904
+ if (!isSecurityFile(attrs.filePath)) return;
9905
+ const lowerPath = attrs.filePath.toLowerCase();
9906
+ if (SKIP_FILE_PATTERNS.some((p) => lowerPath.includes(p))) return;
9907
+ if (!attrs.name || attrs.name.length < 4) return;
9908
+ const SKIP_NAMES = /* @__PURE__ */ new Set(["line", "lines", "content", "findings", "result", "results", "data", "options", "args", "config", "error", "catchBlock", "nearbyLines", "isCryptoFile", "isAuthFile", "isAuthRelatedFile"]);
9909
+ if (SKIP_NAMES.has(attrs.name)) return;
9910
+ if (graph.inDegree(node) === 0) {
9911
+ const dedupKey = `${attrs.filePath}:${attrs.startLine}:${attrs.name}`;
9912
+ if (seen.has(dedupKey)) return;
9913
+ seen.add(dedupKey);
9914
+ findings.push({
9915
+ id: "",
9916
+ severity: "info",
9917
+ vulnerabilityClass: "architecture",
9918
+ file: attrs.filePath,
9919
+ line: attrs.startLine,
9920
+ symbol: attrs.name,
9921
+ title: `Dead exported function in security file: ${attrs.name}`,
9922
+ description: `${attrs.name} in ${attrs.filePath} is exported but has zero dependents \u2014 may indicate an orphaned auth path.`,
9923
+ attackScenario: "Dead auth code may indicate incomplete security migration, leaving old vulnerable code paths accessible.",
9924
+ suggestedFix: "Review and remove dead auth code, or verify it is intentionally unused (e.g., SDK export)."
9925
+ });
9926
+ }
9927
+ });
9928
+ return findings;
9929
+ }
9930
+ function checkUnauthHighFanIn(graph) {
9931
+ const findings = [];
9932
+ const fileIncoming = /* @__PURE__ */ new Map();
9933
+ const fileImportedModules = /* @__PURE__ */ new Map();
9934
+ graph.forEachEdge((_edge, _attrs, source, target) => {
9935
+ const sf = graph.getNodeAttributes(source).filePath;
9936
+ const tf = graph.getNodeAttributes(target).filePath;
9937
+ if (sf !== tf) {
9938
+ fileIncoming.set(tf, (fileIncoming.get(tf) || 0) + 1);
9939
+ if (!fileImportedModules.has(sf)) fileImportedModules.set(sf, /* @__PURE__ */ new Set());
9940
+ fileImportedModules.get(sf).add(tf);
9941
+ }
9942
+ });
9943
+ for (const [filePath, count] of fileIncoming.entries()) {
9944
+ if (!isRouteFile(filePath)) continue;
9945
+ const imports = fileImportedModules.get(filePath) || /* @__PURE__ */ new Set();
9946
+ const hasAuthImport = Array.from(imports).some((imp) => AUTH_KEYWORDS.test(imp.toLowerCase()));
9947
+ if (hasAuthImport) continue;
9948
+ let severity;
9949
+ if (count > 10) severity = "high";
9950
+ else if (count > 5) severity = "medium";
9951
+ else if (count > 0) severity = "low";
9952
+ else continue;
9953
+ findings.push({
9954
+ id: "",
9955
+ severity,
9956
+ vulnerabilityClass: "architecture",
9957
+ file: filePath,
9958
+ title: `Unauthenticated route with high fan-in (${count})`,
9959
+ description: `${filePath} appears to be a route file with ${count} incoming references but imports no auth middleware.`,
9960
+ attackScenario: "A route without authentication that is widely depended upon could expose sensitive functionality to unauthorized users.",
9961
+ suggestedFix: "Add authentication middleware to this route or verify it is intentionally public."
9962
+ });
9963
+ }
9964
+ return findings;
9965
+ }
9966
+
9967
+ // src/security/graph-aware.ts
9968
+ var MCP_PATTERN = /(?:mcp\/|mcp-|\.mcp\.)/i;
9969
+ var ROUTE_PATTERN = /(?:routes?\/|api\/|handler|controller|endpoint|server)/i;
9970
+ var CLI_PATTERN = /(?:commands?\/|cli\/|bin\/)/i;
9971
+ var AUTH_PATTERN = /(?:auth|session|token|jwt|oauth|login|passport|middleware)/i;
9972
+ function classifyEntryPoint(filePath) {
9973
+ if (MCP_PATTERN.test(filePath)) return "mcp-tool";
9974
+ if (ROUTE_PATTERN.test(filePath)) return "http-route";
9975
+ if (CLI_PATTERN.test(filePath)) return "cli-command";
9976
+ return null;
9977
+ }
9978
+ function isUnauthenticatedRoute(filePath, graph) {
9979
+ if (!ROUTE_PATTERN.test(filePath)) return false;
9980
+ const routeNodes = [];
9981
+ graph.forEachNode((nodeId, attrs) => {
9982
+ if (attrs.filePath === filePath) routeNodes.push(nodeId);
9983
+ });
9984
+ for (const nodeId of routeNodes) {
9985
+ const outNeighbors = graph.outNeighbors(nodeId);
9986
+ for (const neighbor of outNeighbors) {
9987
+ const neighborAttrs = graph.getNodeAttributes(neighbor);
9988
+ if (AUTH_PATTERN.test(neighborAttrs.filePath) || AUTH_PATTERN.test(neighborAttrs.name)) {
9989
+ return false;
9990
+ }
9991
+ }
9992
+ }
9993
+ return true;
9994
+ }
9995
+ function findReachableEntryPoints(filePath, graph) {
9996
+ const entryPoints = [];
9997
+ const visited = /* @__PURE__ */ new Set();
9998
+ const queue = [];
9999
+ graph.forEachNode((nodeId, attrs) => {
10000
+ if (attrs.filePath === filePath) {
10001
+ queue.push(nodeId);
10002
+ visited.add(nodeId);
10003
+ }
10004
+ });
10005
+ while (queue.length > 0) {
10006
+ const current = queue.shift();
10007
+ const dependents = graph.inNeighbors(current);
10008
+ for (const dep of dependents) {
10009
+ if (visited.has(dep)) continue;
10010
+ visited.add(dep);
10011
+ queue.push(dep);
10012
+ const attrs = graph.getNodeAttributes(dep);
10013
+ const epType = classifyEntryPoint(attrs.filePath);
10014
+ if (epType && !entryPoints.some((ep) => ep.filePath === attrs.filePath)) {
10015
+ entryPoints.push({ filePath: attrs.filePath, type: epType });
10016
+ }
10017
+ }
10018
+ }
10019
+ return entryPoints;
10020
+ }
10021
+ function elevateByReachability(finding, graph, _projectRoot) {
10022
+ try {
10023
+ const entryPoints = findReachableEntryPoints(finding.file, graph);
10024
+ if (entryPoints.length === 0) return finding;
10025
+ const mcpEntryPoints = entryPoints.filter((ep) => ep.type === "mcp-tool");
10026
+ const httpEntryPoints = entryPoints.filter((ep) => ep.type === "http-route");
10027
+ const cliEntryPoints = entryPoints.filter((ep) => ep.type === "cli-command");
10028
+ let newSeverity = finding.severity;
10029
+ let elevationReason = "";
10030
+ if (finding.severity === "high") {
10031
+ const unauthRoutes = httpEntryPoints.filter((ep) => isUnauthenticatedRoute(ep.filePath, graph));
10032
+ if (unauthRoutes.length > 0) {
10033
+ newSeverity = "critical";
10034
+ elevationReason = `reachable from unauthenticated HTTP route: ${unauthRoutes[0].filePath}`;
10035
+ }
10036
+ }
10037
+ if (finding.severity === "medium" && httpEntryPoints.length > 0) {
10038
+ newSeverity = "high";
10039
+ elevationReason = `reachable from HTTP route: ${httpEntryPoints[0].filePath}`;
10040
+ }
10041
+ if (finding.severity === "medium" && mcpEntryPoints.length > 0) {
10042
+ if (newSeverity === "medium") {
10043
+ newSeverity = "high";
10044
+ elevationReason = `reachable from MCP tool: ${mcpEntryPoints[0].filePath}`;
10045
+ }
10046
+ }
10047
+ if (finding.severity === "low" && entryPoints.length > 0) {
10048
+ newSeverity = "medium";
10049
+ elevationReason = `reachable from ${entryPoints.length} external entry point(s)`;
10050
+ }
10051
+ const allEntryPointPaths = entryPoints.map((ep) => `${ep.type}: ${ep.filePath}`);
10052
+ return {
10053
+ ...finding,
10054
+ severity: newSeverity,
10055
+ graphReachability: {
10056
+ entryPoints: allEntryPointPaths,
10057
+ reachableFrom: entryPoints.length,
10058
+ elevatedBy: elevationReason
10059
+ }
10060
+ };
10061
+ } catch {
10062
+ return finding;
10063
+ }
10064
+ }
10065
+
10066
+ // src/security/scanner.ts
10067
+ var SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
10068
+ async function scanSecurity(projectRoot, graph, options = {}) {
10069
+ const startTime = Date.now();
10070
+ const parsedFiles = await parseProject(projectRoot);
10071
+ const filteredFiles = options.target ? parsedFiles.filter((f) => f.filePath === options.target || f.filePath.endsWith(options.target)) : parsedFiles;
10072
+ const hasFrontendFiles = filteredFiles.some((f) => /\.(?:tsx|jsx|html)$/.test(f.filePath));
10073
+ const checkResults = await Promise.all([
10074
+ // Skip dependency checks for single-file scans — they are repo-wide by nature
10075
+ options.target ? Promise.resolve([]) : checkDependencies(filteredFiles, projectRoot),
10076
+ checkInjection(filteredFiles, projectRoot),
10077
+ checkSecrets(filteredFiles, projectRoot),
10078
+ checkPathTraversal(filteredFiles, projectRoot),
10079
+ checkAuth(filteredFiles, projectRoot),
10080
+ checkInputValidation(filteredFiles, projectRoot),
10081
+ checkInformationDisclosure(filteredFiles, projectRoot),
10082
+ checkCryptography(filteredFiles, projectRoot),
10083
+ hasFrontendFiles ? checkFrontend(filteredFiles, projectRoot) : Promise.resolve([]),
10084
+ checkArchitecture(filteredFiles, projectRoot, graph)
10085
+ ]);
10086
+ let findings = checkResults.flat();
10087
+ if (options.classes && options.classes.length > 0) {
10088
+ const allowedClasses = new Set(options.classes);
10089
+ findings = findings.filter((f) => allowedClasses.has(f.vulnerabilityClass));
10090
+ }
10091
+ if (options.graphAware !== false) {
10092
+ findings = findings.map((f) => elevateByReachability(f, graph, projectRoot));
10093
+ }
10094
+ findings.sort((a, b) => {
10095
+ return SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity);
10096
+ });
10097
+ findings.forEach((f, i) => {
10098
+ f.id = `SEC-${String(i + 1).padStart(3, "0")}`;
10099
+ });
10100
+ const summary = {
10101
+ critical: findings.filter((f) => f.severity === "critical").length,
10102
+ high: findings.filter((f) => f.severity === "high").length,
10103
+ medium: findings.filter((f) => f.severity === "medium").length,
10104
+ low: findings.filter((f) => f.severity === "low").length,
10105
+ info: findings.filter((f) => f.severity === "info").length,
10106
+ total: findings.length
10107
+ };
10108
+ const depFindings = checkResults[0];
10109
+ const hasDeps = depFindings.length > 0;
10110
+ return {
10111
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
10112
+ projectRoot,
10113
+ filesScanned: filteredFiles.length,
10114
+ findings,
10115
+ summary,
10116
+ dependencyAudit: {
10117
+ ran: hasDeps,
10118
+ packageManager: hasDeps ? detectPackageManager(projectRoot) : null,
10119
+ rawOutput: ""
10120
+ }
10121
+ };
10122
+ }
10123
+ function detectPackageManager(projectRoot) {
10124
+ if (existsSync14(join23(projectRoot, "package.json"))) return "npm";
10125
+ if (existsSync14(join23(projectRoot, "requirements.txt"))) return "pip";
10126
+ if (existsSync14(join23(projectRoot, "pyproject.toml"))) return "pip";
10127
+ if (existsSync14(join23(projectRoot, "Cargo.toml"))) return "cargo";
10128
+ if (existsSync14(join23(projectRoot, "go.mod"))) return "go";
10129
+ return "unknown";
10130
+ }
10131
+
8665
10132
  export {
8666
10133
  findProjectRoot,
8667
10134
  parseTypeScriptFile,
@@ -8680,5 +10147,6 @@ export {
8680
10147
  analyzeDeadCode,
8681
10148
  loadMetadata,
8682
10149
  generateDocs,
8683
- SimulationEngine
10150
+ SimulationEngine,
10151
+ scanSecurity
8684
10152
  };