@vibecodeqa/cli 0.12.1 → 0.14.0

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.
@@ -17,20 +17,65 @@ import { basename, extname, join } from "node:path";
17
17
  import { gradeFromScore } from "../types.js";
18
18
  // ── Pattern dictionaries ──
19
19
  const SYNONYM_PAIRS = [
20
- ["utils", "helpers"], ["util", "helper"],
21
- ["types", "interfaces"], ["types", "models"], ["interfaces", "models"],
22
- ["constants", "config"], ["constants", "settings"], ["config", "settings"],
23
- ["service", "controller"], ["service", "handler"], ["controller", "handler"],
24
- ["store", "state"], ["context", "provider"],
20
+ ["utils", "helpers"],
21
+ ["util", "helper"],
22
+ ["types", "interfaces"],
23
+ ["types", "models"],
24
+ ["interfaces", "models"],
25
+ ["constants", "config"],
26
+ ["constants", "settings"],
27
+ ["config", "settings"],
28
+ ["service", "controller"],
29
+ ["service", "handler"],
30
+ ["controller", "handler"],
31
+ ["store", "state"],
32
+ ["context", "provider"],
25
33
  ];
26
34
  const GENERIC_NAMES = new Set([
27
- "process", "handle", "run", "execute", "do", "perform", "make",
28
- "get", "set", "update", "create", "delete", "remove", "add",
29
- "data", "result", "item", "value", "info", "temp", "obj",
30
- "stuff", "thing", "ret", "val", "res", "output", "input",
31
- "response", "request", "payload", "body", "args", "params",
32
- "list", "arr", "map", "dict", "collection",
33
- "callback", "cb", "fn", "func", "handler",
35
+ "process",
36
+ "handle",
37
+ "run",
38
+ "execute",
39
+ "do",
40
+ "perform",
41
+ "make",
42
+ "get",
43
+ "set",
44
+ "update",
45
+ "create",
46
+ "delete",
47
+ "remove",
48
+ "add",
49
+ "data",
50
+ "result",
51
+ "item",
52
+ "value",
53
+ "info",
54
+ "temp",
55
+ "obj",
56
+ "stuff",
57
+ "thing",
58
+ "ret",
59
+ "val",
60
+ "res",
61
+ "output",
62
+ "input",
63
+ "response",
64
+ "request",
65
+ "payload",
66
+ "body",
67
+ "args",
68
+ "params",
69
+ "list",
70
+ "arr",
71
+ "map",
72
+ "dict",
73
+ "collection",
74
+ "callback",
75
+ "cb",
76
+ "fn",
77
+ "func",
78
+ "handler",
34
79
  ]);
35
80
  const AMBIGUOUS_ABBREVS = {
36
81
  auth: "authentication OR authorization",
@@ -58,10 +103,19 @@ export function runConfusion(cwd) {
58
103
  try {
59
104
  collectFiles(join(cwd, dir), cwd, files);
60
105
  }
61
- catch { /* dir doesn't exist */ }
106
+ catch {
107
+ /* dir doesn't exist */
108
+ }
62
109
  }
63
110
  if (files.length === 0) {
64
- return { name: "confusion", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
111
+ return {
112
+ name: "confusion",
113
+ score: 100,
114
+ grade: "A",
115
+ details: { skipped: true, reason: "no source files" },
116
+ issues: [],
117
+ duration: Date.now() - start,
118
+ };
65
119
  }
66
120
  let fileConfusability = 0;
67
121
  let genericNames = 0;
@@ -75,13 +129,23 @@ export function runConfusion(cwd) {
75
129
  // Near-identical (Levenshtein ≤ 2)
76
130
  if (a !== b && levenshtein(a, b) <= 2) {
77
131
  fileConfusability++;
78
- issues.push({ severity: "warning", message: `Similar filenames: ${a} ↔ ${b} (edit distance ${levenshtein(a, b)})`, file: files[i].path, rule: "similar-filename" });
132
+ issues.push({
133
+ severity: "warning",
134
+ message: `Similar filenames: ${a} ↔ ${b} (edit distance ${levenshtein(a, b)})`,
135
+ file: files[i].path,
136
+ rule: "similar-filename",
137
+ });
79
138
  }
80
139
  // Synonym pairs
81
140
  for (const [s1, s2] of SYNONYM_PAIRS) {
82
141
  if ((a.includes(s1) && b.includes(s2)) || (a.includes(s2) && b.includes(s1))) {
83
142
  fileConfusability++;
84
- issues.push({ severity: "warning", message: `Synonym filenames: ${a} ↔ ${b} (${s1}/${s2} are interchangeable — pick one convention)`, file: files[i].path, rule: "synonym-filename" });
143
+ issues.push({
144
+ severity: "warning",
145
+ message: `Synonym filenames: ${a} ↔ ${b} (${s1}/${s2} are interchangeable — pick one convention)`,
146
+ file: files[i].path,
147
+ rule: "synonym-filename",
148
+ });
85
149
  break;
86
150
  }
87
151
  }
@@ -98,13 +162,25 @@ export function runConfusion(cwd) {
98
162
  const funcMatch = line.match(/^export\s+(?:async\s+)?function\s+(\w+)/);
99
163
  if (funcMatch && GENERIC_NAMES.has(funcMatch[1].toLowerCase())) {
100
164
  genericNames++;
101
- issues.push({ severity: "warning", message: `Generic export name: ${funcMatch[1]}() — not descriptive enough for LLM comprehension`, file: f.path, line: i + 1, rule: "generic-name" });
165
+ issues.push({
166
+ severity: "warning",
167
+ message: `Generic export name: ${funcMatch[1]}() — not descriptive enough for LLM comprehension`,
168
+ file: f.path,
169
+ line: i + 1,
170
+ rule: "generic-name",
171
+ });
102
172
  }
103
173
  // Match standalone variable assignments with generic names
104
174
  const varMatch = line.match(/^(?:export\s+)?(?:const|let)\s+(\w+)\s*=/);
105
175
  if (varMatch && GENERIC_NAMES.has(varMatch[1].toLowerCase()) && varMatch[1].length <= 6) {
106
176
  genericNames++;
107
- issues.push({ severity: "warning", message: `Generic variable: ${varMatch[1]} — use a descriptive name`, file: f.path, line: i + 1, rule: "generic-name" });
177
+ issues.push({
178
+ severity: "warning",
179
+ message: `Generic variable: ${varMatch[1]} — use a descriptive name`,
180
+ file: f.path,
181
+ line: i + 1,
182
+ rule: "generic-name",
183
+ });
108
184
  }
109
185
  }
110
186
  }
@@ -120,7 +196,12 @@ export function runConfusion(cwd) {
120
196
  for (const [name, paths] of exportMap) {
121
197
  if (paths.length > 1) {
122
198
  exportCollisions++;
123
- issues.push({ severity: "error", message: `Export collision: "${name}" exported from ${paths.length} files — LLMs may reference the wrong one`, file: paths.join(", "), rule: "export-collision" });
199
+ issues.push({
200
+ severity: "error",
201
+ message: `Export collision: "${name}" exported from ${paths.length} files — LLMs may reference the wrong one`,
202
+ file: paths.join(", "),
203
+ rule: "export-collision",
204
+ });
124
205
  }
125
206
  }
126
207
  // ── 4. Ambiguous abbreviations in filenames and exports ──
@@ -129,7 +210,12 @@ export function runConfusion(cwd) {
129
210
  for (const part of nameParts) {
130
211
  if (AMBIGUOUS_ABBREVS[part]) {
131
212
  ambiguousAbbrevs++;
132
- issues.push({ severity: "warning", message: `Ambiguous abbreviation "${part}" in filename — could mean: ${AMBIGUOUS_ABBREVS[part]}`, file: f.path, rule: "ambiguous-abbreviation" });
213
+ issues.push({
214
+ severity: "warning",
215
+ message: `Ambiguous abbreviation "${part}" in filename — could mean: ${AMBIGUOUS_ABBREVS[part]}`,
216
+ file: f.path,
217
+ rule: "ambiguous-abbreviation",
218
+ });
133
219
  }
134
220
  }
135
221
  }
@@ -189,7 +275,7 @@ function collectFiles(dir, cwd, out) {
189
275
  const ext = extname(entry);
190
276
  if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
191
277
  const content = readFileSync(full, "utf-8");
192
- const relPath = full.replace(cwd + "/", "");
278
+ const relPath = full.replace(`${cwd}/`, "");
193
279
  const base = basename(entry, ext);
194
280
  out.push({ path: relPath, base, content, exports: extractExports(content) });
195
281
  }
@@ -27,10 +27,19 @@ export function runContext(cwd) {
27
27
  try {
28
28
  collectFiles(join(cwd, dir), cwd, files);
29
29
  }
30
- catch { /* dir doesn't exist */ }
30
+ catch {
31
+ /* dir doesn't exist */
32
+ }
31
33
  }
32
34
  if (files.length === 0) {
33
- return { name: "context", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
35
+ return {
36
+ name: "context",
37
+ score: 100,
38
+ grade: "A",
39
+ details: { skipped: true, reason: "no source files" },
40
+ issues: [],
41
+ duration: Date.now() - start,
42
+ };
34
43
  }
35
44
  let highTokenFiles = 0;
36
45
  let heavyImportFiles = 0;
@@ -42,7 +51,12 @@ export function runContext(cwd) {
42
51
  totalTokens += f.tokens;
43
52
  if (f.tokens > MAX_FILE_TOKENS) {
44
53
  highTokenFiles++;
45
- issues.push({ severity: "warning", message: `~${f.tokens} tokens (>${MAX_FILE_TOKENS}) — large context cost for LLMs`, file: f.path, rule: "high-token-count" });
54
+ issues.push({
55
+ severity: "warning",
56
+ message: `~${f.tokens} tokens (>${MAX_FILE_TOKENS}) — large context cost for LLMs`,
57
+ file: f.path,
58
+ rule: "high-token-count",
59
+ });
46
60
  }
47
61
  }
48
62
  // ── 2. Import count per file ──
@@ -50,7 +64,12 @@ export function runContext(cwd) {
50
64
  totalImports += f.imports.length;
51
65
  if (f.imports.length > MAX_IMPORTS) {
52
66
  heavyImportFiles++;
53
- issues.push({ severity: "warning", message: `${f.imports.length} imports (>${MAX_IMPORTS}) — consider splitting or co-locating`, file: f.path, rule: "heavy-imports" });
67
+ issues.push({
68
+ severity: "warning",
69
+ message: `${f.imports.length} imports (>${MAX_IMPORTS}) — consider splitting or co-locating`,
70
+ file: f.path,
71
+ rule: "heavy-imports",
72
+ });
54
73
  }
55
74
  }
56
75
  // ── 3. Circular dependency detection ──
@@ -81,7 +100,12 @@ export function runContext(cwd) {
81
100
  const exportCount = (f.content.match(/\bexport\s+/g) || []).length;
82
101
  if (f.imports.length > 8 && exportCount <= 1) {
83
102
  contextSinks++;
84
- issues.push({ severity: "warning", message: `${f.imports.length} imports but only ${exportCount} export — hard to understand in isolation`, file: f.path, rule: "context-sink" });
103
+ issues.push({
104
+ severity: "warning",
105
+ message: `${f.imports.length} imports but only ${exportCount} export — hard to understand in isolation`,
106
+ file: f.path,
107
+ rule: "context-sink",
108
+ });
85
109
  }
86
110
  }
87
111
  // ── Score ──
@@ -141,7 +165,7 @@ function resolveImport(fromPath, importPath) {
141
165
  return resolved;
142
166
  }
143
167
  // Return with .ts as default assumption
144
- return resolved + ".ts";
168
+ return `${resolved}.ts`;
145
169
  }
146
170
  // ── Cycle detection (DFS) ──
147
171
  function findCycles(graph) {
@@ -190,7 +214,7 @@ function collectFiles(dir, cwd, out) {
190
214
  const ext = extname(entry);
191
215
  if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
192
216
  const content = readFileSync(full, "utf-8");
193
- const relPath = full.replace(cwd + "/", "");
217
+ const relPath = full.replace(`${cwd}/`, "");
194
218
  const imports = parseImports(content);
195
219
  const tokens = Math.round(content.length / CHARS_PER_TOKEN);
196
220
  out.push({ path: relPath, content, imports, tokens });
@@ -6,12 +6,8 @@ export function runDependencies(cwd, stack) {
6
6
  const issues = [];
7
7
  const pm = stack.packageManager;
8
8
  // Vulnerability audit
9
- const auditCmd = pm === "pnpm"
10
- ? "pnpm audit --json"
11
- : pm === "yarn"
12
- ? "yarn audit --json"
13
- : "npm audit --json";
14
- const auditResult = run(auditCmd + " 2>/dev/null || true", cwd);
9
+ const auditCmd = pm === "pnpm" ? "pnpm audit --json" : pm === "yarn" ? "yarn audit --json" : "npm audit --json";
10
+ const auditResult = run(`${auditCmd} 2>/dev/null || true`, cwd);
15
11
  let vulnCritical = 0, vulnHigh = 0, vulnModerate = 0, vulnLow = 0;
16
12
  try {
17
13
  const audit = JSON.parse(auditResult.stdout);
@@ -57,7 +53,7 @@ export function runDependencies(cwd, stack) {
57
53
  });
58
54
  // Outdated check
59
55
  const outdatedCmd = pm === "pnpm" ? "pnpm outdated --json" : "npm outdated --json";
60
- const outdatedResult = run(outdatedCmd + " 2>/dev/null || true", cwd);
56
+ const outdatedResult = run(`${outdatedCmd} 2>/dev/null || true`, cwd);
61
57
  let outdatedCount = 0;
62
58
  let majorOutdated = 0;
63
59
  try {
@@ -81,11 +77,7 @@ export function runDependencies(cwd, stack) {
81
77
  message: `${majorOutdated} packages behind by a major version`,
82
78
  });
83
79
  // Score: -25 per critical, -15 per high, -5 per moderate, -1 per major outdated
84
- const score = Math.max(0, Math.min(100, 100 -
85
- vulnCritical * 25 -
86
- vulnHigh * 15 -
87
- vulnModerate * 5 -
88
- majorOutdated * 1));
80
+ const score = Math.max(0, Math.min(100, 100 - vulnCritical * 25 - vulnHigh * 15 - vulnModerate * 5 - majorOutdated * 1));
89
81
  return {
90
82
  name: "dependencies",
91
83
  score,
@@ -1,5 +1,5 @@
1
1
  /** Documentation check — README, JSDoc, code comments. */
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { extname, join } from "node:path";
4
4
  import { gradeFromScore } from "../types.js";
5
5
  export function runDocs(cwd) {
@@ -40,7 +40,9 @@ export function runDocs(cwd) {
40
40
  try {
41
41
  collectFiles(join(cwd, dir), files);
42
42
  }
43
- catch { /* dir doesn't exist */ }
43
+ catch {
44
+ /* dir doesn't exist */
45
+ }
44
46
  }
45
47
  let totalExports = 0;
46
48
  let documentedExports = 0;
@@ -49,7 +51,10 @@ export function runDocs(cwd) {
49
51
  const lines = content.split("\n");
50
52
  for (let i = 0; i < lines.length; i++) {
51
53
  const line = lines[i].trim();
52
- if (line.startsWith("export function ") || line.startsWith("export async function ") || line.startsWith("export class ") || line.startsWith("export interface ")) {
54
+ if (line.startsWith("export function ") ||
55
+ line.startsWith("export async function ") ||
56
+ line.startsWith("export class ") ||
57
+ line.startsWith("export interface ")) {
53
58
  totalExports++;
54
59
  // Check if preceded by a JSDoc or // comment
55
60
  const prevLine = i > 0 ? lines[i - 1].trim() : "";
@@ -63,7 +68,11 @@ export function runDocs(cwd) {
63
68
  const pct = Math.round((documentedExports / totalExports) * 100);
64
69
  exportDocScore = pct;
65
70
  if (pct < 30) {
66
- issues.push({ severity: "warning", message: `Only ${pct}% of exports have documentation (${documentedExports}/${totalExports})`, rule: "undocumented-exports" });
71
+ issues.push({
72
+ severity: "warning",
73
+ message: `Only ${pct}% of exports have documentation (${documentedExports}/${totalExports})`,
74
+ rule: "undocumented-exports",
75
+ });
67
76
  }
68
77
  }
69
78
  else {
@@ -74,7 +83,12 @@ export function runDocs(cwd) {
74
83
  name: "docs",
75
84
  score,
76
85
  grade: gradeFromScore(score),
77
- details: { readmeLines: existsSync(readmePath) ? readFileSync(readmePath, "utf-8").split("\n").length : 0, totalExports, documentedExports, documentedPct: totalExports > 0 ? Math.round((documentedExports / totalExports) * 100) + "%" : "n/a" },
86
+ details: {
87
+ readmeLines: existsSync(readmePath) ? readFileSync(readmePath, "utf-8").split("\n").length : 0,
88
+ totalExports,
89
+ documentedExports,
90
+ documentedPct: totalExports > 0 ? `${Math.round((documentedExports / totalExports) * 100)}%` : "n/a",
91
+ },
78
92
  issues,
79
93
  duration: Date.now() - start,
80
94
  };
@@ -13,10 +13,19 @@ export function runDuplication(cwd) {
13
13
  try {
14
14
  collectFiles(join(cwd, dir), files);
15
15
  }
16
- catch { /* dir doesn't exist */ }
16
+ catch {
17
+ /* dir doesn't exist */
18
+ }
17
19
  }
18
20
  if (files.length < 2) {
19
- return { name: "duplication", score: 100, grade: "A", details: { filesScanned: files.length, duplicates: 0 }, issues: [], duration: Date.now() - start };
21
+ return {
22
+ name: "duplication",
23
+ score: 100,
24
+ grade: "A",
25
+ details: { filesScanned: files.length, duplicates: 0 },
26
+ issues: [],
27
+ duration: Date.now() - start,
28
+ };
20
29
  }
21
30
  // Simple line-based duplicate detection
22
31
  // Build a map of normalized line hashes → locations
@@ -24,7 +33,7 @@ export function runDuplication(cwd) {
24
33
  let totalSourceLines = 0;
25
34
  for (const file of files) {
26
35
  const content = readFileSync(file, "utf-8");
27
- const relPath = file.replace(cwd + "/", "");
36
+ const relPath = file.replace(`${cwd}/`, "");
28
37
  const lines = content.split("\n");
29
38
  totalSourceLines += lines.length;
30
39
  for (let i = 0; i <= lines.length - MIN_LINES; i++) {
@@ -77,7 +86,7 @@ export function runDuplication(cwd) {
77
86
  name: "duplication",
78
87
  score,
79
88
  grade: gradeFromScore(score),
80
- details: { filesScanned: files.length, totalSourceLines, duplicateBlocks: duplicates.length, duplicationPct: dupPct + "%" },
89
+ details: { filesScanned: files.length, totalSourceLines, duplicateBlocks: duplicates.length, duplicationPct: `${dupPct}%` },
81
90
  issues,
82
91
  duration: Date.now() - start,
83
92
  };
@@ -11,11 +11,7 @@ export function runLint(cwd, stack) {
11
11
  const diagnostics = data.diagnostics || [];
12
12
  for (const d of diagnostics) {
13
13
  issues.push({
14
- severity: d.severity === "error"
15
- ? "error"
16
- : d.severity === "warning"
17
- ? "warning"
18
- : "info",
14
+ severity: d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "info",
19
15
  message: d.description || d.message || "lint issue",
20
16
  file: d.location?.path,
21
17
  line: d.location?.span?.start?.line,
@@ -27,9 +23,9 @@ export function runLint(cwd, stack) {
27
23
  // biome may not output valid JSON on some errors — count from summary
28
24
  const errors = stdout.match(/Found (\d+) error/)?.[1] || "0";
29
25
  const warnings = stdout.match(/Found (\d+) warning/)?.[1] || "0";
30
- for (let i = 0; i < parseInt(errors); i++)
26
+ for (let i = 0; i < parseInt(errors, 10); i++)
31
27
  issues.push({ severity: "error", message: "lint error" });
32
- for (let i = 0; i < parseInt(warnings); i++)
28
+ for (let i = 0; i < parseInt(warnings, 10); i++)
33
29
  issues.push({ severity: "warning", message: "lint warning" });
34
30
  }
35
31
  }
@@ -39,7 +39,7 @@ export function runSecrets(cwd) {
39
39
  collectFiles(cwd, files);
40
40
  for (const file of files) {
41
41
  const content = readFileSync(file, "utf-8");
42
- const relPath = file.replace(cwd + "/", "");
42
+ const relPath = file.replace(`${cwd}/`, "");
43
43
  const lines = content.split("\n");
44
44
  for (let i = 0; i < lines.length; i++) {
45
45
  const line = lines[i];
@@ -74,14 +74,7 @@ export function runSecrets(cwd) {
74
74
  }
75
75
  function collectFiles(dir, out) {
76
76
  for (const entry of readdirSync(dir)) {
77
- if ([
78
- "node_modules",
79
- "dist",
80
- ".git",
81
- ".vibe-check",
82
- "coverage",
83
- "test-results",
84
- ].includes(entry))
77
+ if (["node_modules", "dist", ".git", ".vibe-check", "coverage", "test-results"].includes(entry))
85
78
  continue;
86
79
  const full = join(dir, entry);
87
80
  const stat = statSync(full);
@@ -90,17 +83,7 @@ function collectFiles(dir, out) {
90
83
  }
91
84
  else {
92
85
  const ext = extname(entry);
93
- if ([
94
- ".ts",
95
- ".tsx",
96
- ".js",
97
- ".jsx",
98
- ".json",
99
- ".env",
100
- ".yaml",
101
- ".yml",
102
- ".toml",
103
- ].includes(ext)) {
86
+ if ([".ts", ".tsx", ".js", ".jsx", ".json", ".env", ".yaml", ".yml", ".toml"].includes(ext)) {
104
87
  out.push(full);
105
88
  }
106
89
  }
@@ -1,33 +1,123 @@
1
1
  /** Security analysis — beyond secrets, checks for vulnerable code patterns. */
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { extname, join } from "node:path";
4
4
  import { gradeFromScore } from "../types.js";
5
5
  const PATTERNS = [
6
6
  // XSS
7
- { name: "innerHTML", pattern: /\.innerHTML\s*=/, severity: "warning", message: "XSS: innerHTML assignment — use textContent or DOM APIs", cwe: "CWE-79" },
8
- { name: "dangerouslySetInnerHTML", pattern: /dangerouslySetInnerHTML/, severity: "error", message: "XSS: dangerouslySetInnerHTML bypasses React protection", cwe: "CWE-79" },
9
- { name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "XSS: document.write is dangerous", cwe: "CWE-79" },
7
+ {
8
+ name: "innerHTML",
9
+ pattern: /\.innerHTML\s*=/,
10
+ severity: "warning",
11
+ message: "XSS: innerHTML assignment — use textContent or DOM APIs",
12
+ cwe: "CWE-79",
13
+ },
14
+ {
15
+ name: "dangerouslySetInnerHTML",
16
+ pattern: /dangerouslySetInnerHTML/,
17
+ severity: "error",
18
+ message: "XSS: dangerouslySetInnerHTML bypasses React protection",
19
+ cwe: "CWE-79",
20
+ },
21
+ {
22
+ name: "document.write",
23
+ pattern: /document\.write\s*\(/,
24
+ severity: "error",
25
+ message: "XSS: document.write is dangerous",
26
+ cwe: "CWE-79",
27
+ },
10
28
  { name: "outerHTML", pattern: /\.outerHTML\s*=/, severity: "warning", message: "XSS: outerHTML assignment", cwe: "CWE-79" },
11
- { name: "insertAdjacentHTML", pattern: /\.insertAdjacentHTML\s*\(/, severity: "warning", message: "XSS: insertAdjacentHTML with user data", cwe: "CWE-79" },
29
+ {
30
+ name: "insertAdjacentHTML",
31
+ pattern: /\.insertAdjacentHTML\s*\(/,
32
+ severity: "warning",
33
+ message: "XSS: insertAdjacentHTML with user data",
34
+ cwe: "CWE-79",
35
+ },
12
36
  // Injection
13
37
  { name: "eval", pattern: /\beval\s*\(/, severity: "error", message: "Injection: eval() executes arbitrary code", cwe: "CWE-94" },
14
- { name: "new Function", pattern: /new\s+Function\s*\(/, severity: "error", message: "Injection: new Function() is equivalent to eval()", cwe: "CWE-94" },
15
- { name: "child_process.exec", pattern: /\bexec(?:Sync)?\s*\((?!.*\{[^}]*encoding)/, severity: "warning", message: "Command injection risk: prefer execFile with argument array", cwe: "CWE-78" },
16
- { name: "template literal in SQL", pattern: /(?:query|prepare|execute)\s*\(\s*`[^`]*\$\{/, severity: "error", message: "SQL injection: use parameterized queries instead of template literals", cwe: "CWE-89" },
38
+ {
39
+ name: "new Function",
40
+ pattern: /new\s+Function\s*\(/,
41
+ severity: "error",
42
+ message: "Injection: new Function() is equivalent to eval()",
43
+ cwe: "CWE-94",
44
+ },
45
+ {
46
+ name: "child_process.exec",
47
+ pattern: /\bexec(?:Sync)?\s*\((?!.*\{[^}]*encoding)/,
48
+ severity: "warning",
49
+ message: "Command injection risk: prefer execFile with argument array",
50
+ cwe: "CWE-78",
51
+ },
52
+ {
53
+ name: "template literal in SQL",
54
+ pattern: /(?:query|prepare|execute)\s*\(\s*`[^`]*\$\{/,
55
+ severity: "error",
56
+ message: "SQL injection: use parameterized queries instead of template literals",
57
+ cwe: "CWE-89",
58
+ },
17
59
  // Crypto
18
- { name: "Math.random for security", pattern: /Math\.random\s*\(\).*(?:token|secret|key|password|nonce|salt)/i, severity: "error", message: "Weak randomness: use crypto.randomUUID() or crypto.getRandomValues()", cwe: "CWE-330" },
19
- { name: "MD5/SHA1", pattern: /\b(?:md5|sha1|SHA1|MD5)\b/, severity: "warning", message: "Weak hash: MD5/SHA1 are broken — use SHA-256+", cwe: "CWE-328" },
60
+ {
61
+ name: "Math.random for security",
62
+ pattern: /Math\.random\s*\(\).*(?:token|secret|key|password|nonce|salt)/i,
63
+ severity: "error",
64
+ message: "Weak randomness: use crypto.randomUUID() or crypto.getRandomValues()",
65
+ cwe: "CWE-330",
66
+ },
67
+ {
68
+ name: "MD5/SHA1",
69
+ pattern: /\b(?:md5|sha1|SHA1|MD5)\b/,
70
+ severity: "warning",
71
+ message: "Weak hash: MD5/SHA1 are broken — use SHA-256+",
72
+ cwe: "CWE-328",
73
+ },
20
74
  // Prototype pollution
21
- { name: "Object.assign from user input", pattern: /Object\.assign\s*\(\s*\{\s*\}\s*,\s*(?:req|request|body|params|query)/, severity: "warning", message: "Prototype pollution risk: validate/sanitize before Object.assign", cwe: "CWE-1321" },
22
- { name: "spread from user input", pattern: /\{\s*\.\.\.(?:req|request|body|params|query)\./, severity: "warning", message: "Prototype pollution: spreading unvalidated user input", cwe: "CWE-1321" },
75
+ {
76
+ name: "Object.assign from user input",
77
+ pattern: /Object\.assign\s*\(\s*\{\s*\}\s*,\s*(?:req|request|body|params|query)/,
78
+ severity: "warning",
79
+ message: "Prototype pollution risk: validate/sanitize before Object.assign",
80
+ cwe: "CWE-1321",
81
+ },
82
+ {
83
+ name: "spread from user input",
84
+ pattern: /\{\s*\.\.\.(?:req|request|body|params|query)\./,
85
+ severity: "warning",
86
+ message: "Prototype pollution: spreading unvalidated user input",
87
+ cwe: "CWE-1321",
88
+ },
23
89
  // Path traversal
24
- { name: "path traversal", pattern: /(?:readFile|writeFile|access|stat)(?:Sync)?\s*\([^)]*(?:req|request|body|params|query)/, severity: "warning", message: "Path traversal: validate file paths from user input", cwe: "CWE-22" },
90
+ {
91
+ name: "path traversal",
92
+ pattern: /(?:readFile|writeFile|access|stat)(?:Sync)?\s*\([^)]*(?:req|request|body|params|query)/,
93
+ severity: "warning",
94
+ message: "Path traversal: validate file paths from user input",
95
+ cwe: "CWE-22",
96
+ },
25
97
  // SSRF
26
- { name: "fetch with user URL", pattern: /fetch\s*\(\s*(?:req|request|body|params|query)\.(?:url|href|target)/, severity: "warning", message: "SSRF: validate URLs before fetching user-supplied targets", cwe: "CWE-918" },
98
+ {
99
+ name: "fetch with user URL",
100
+ pattern: /fetch\s*\(\s*(?:req|request|body|params|query)\.(?:url|href|target)/,
101
+ severity: "warning",
102
+ message: "SSRF: validate URLs before fetching user-supplied targets",
103
+ cwe: "CWE-918",
104
+ },
27
105
  // Sensitive data
28
- { name: "password in URL", pattern: /(?:password|secret|token|key)=[^&\s'"]+/i, severity: "warning", message: "Sensitive data in URL query string", cwe: "CWE-598" },
106
+ {
107
+ name: "password in URL",
108
+ pattern: /(?:password|secret|token|key)=[^&\s'"]+/i,
109
+ severity: "warning",
110
+ message: "Sensitive data in URL query string",
111
+ cwe: "CWE-598",
112
+ },
29
113
  // Missing security headers (in response construction)
30
- { name: "no-cache header missing", pattern: /new Response\([^)]*\{[^}]*["']Set-Cookie["']/, severity: "warning", message: "Set-Cookie without Cache-Control: no-store", cwe: "CWE-525" },
114
+ {
115
+ name: "no-cache header missing",
116
+ pattern: /new Response\([^)]*\{[^}]*["']Set-Cookie["']/,
117
+ severity: "warning",
118
+ message: "Set-Cookie without Cache-Control: no-store",
119
+ cwe: "CWE-525",
120
+ },
31
121
  ];
32
122
  export function runSecurity(cwd) {
33
123
  const start = Date.now();
@@ -38,21 +128,33 @@ export function runSecurity(cwd) {
38
128
  try {
39
129
  collectFiles(join(cwd, dir), files);
40
130
  }
41
- catch { /* dir doesn't exist */ }
131
+ catch {
132
+ /* dir doesn't exist */
133
+ }
42
134
  }
43
135
  if (files.length === 0) {
44
- return { name: "security", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
136
+ return {
137
+ name: "security",
138
+ score: 100,
139
+ grade: "A",
140
+ details: { skipped: true, reason: "no source files" },
141
+ issues: [],
142
+ duration: Date.now() - start,
143
+ };
45
144
  }
46
145
  const cwePrefixes = new Set();
47
146
  for (const file of files) {
48
147
  const content = readFileSync(file, "utf-8");
49
- const relPath = file.replace(cwd + "/", "");
148
+ const relPath = file.replace(`${cwd}/`, "");
50
149
  const lines = content.split("\n");
51
150
  for (let i = 0; i < lines.length; i++) {
52
151
  const line = lines[i];
53
152
  const trimmed = line.trim();
54
153
  if (trimmed.startsWith("//") || trimmed.startsWith("*"))
55
154
  continue;
155
+ // Skip pattern/config definition lines (prevents false positives on own code)
156
+ if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
157
+ continue;
56
158
  for (const p of PATTERNS) {
57
159
  if (p.pattern.test(line)) {
58
160
  issues.push({