@vibecodeqa/cli 0.13.0 → 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.
- package/dist/check-meta.js +2 -2
- package/dist/cli.js +19 -13
- package/dist/detect.js +5 -27
- package/dist/fs-utils.js +4 -2
- package/dist/report/html.js +62 -32
- package/dist/runners/architecture.js +29 -7
- package/dist/runners/complexity.js +2 -4
- package/dist/runners/confusion.js +107 -21
- package/dist/runners/context.js +31 -7
- package/dist/runners/dependencies.js +4 -12
- package/dist/runners/docs.js +19 -5
- package/dist/runners/duplication.js +13 -4
- package/dist/runners/lint.js +3 -7
- package/dist/runners/secrets.js +3 -20
- package/dist/runners/security.js +121 -19
- package/dist/runners/standards.js +57 -13
- package/dist/runners/structure.js +14 -6
- package/dist/runners/testing.js +97 -28
- package/dist/runners/type-safety.js +17 -4
- package/dist/runners/types-check.js +2 -3
- package/package.json +1 -1
|
@@ -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"],
|
|
21
|
-
["
|
|
22
|
-
["
|
|
23
|
-
["
|
|
24
|
-
["
|
|
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",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
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 {
|
|
106
|
+
catch {
|
|
107
|
+
/* dir doesn't exist */
|
|
108
|
+
}
|
|
62
109
|
}
|
|
63
110
|
if (files.length === 0) {
|
|
64
|
-
return {
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
}
|
package/dist/runners/context.js
CHANGED
|
@@ -27,10 +27,19 @@ export function runContext(cwd) {
|
|
|
27
27
|
try {
|
|
28
28
|
collectFiles(join(cwd, dir), cwd, files);
|
|
29
29
|
}
|
|
30
|
-
catch {
|
|
30
|
+
catch {
|
|
31
|
+
/* dir doesn't exist */
|
|
32
|
+
}
|
|
31
33
|
}
|
|
32
34
|
if (files.length === 0) {
|
|
33
|
-
return {
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
package/dist/runners/docs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Documentation check — README, JSDoc, code comments. */
|
|
2
|
-
import { existsSync,
|
|
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 {
|
|
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 ") ||
|
|
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({
|
|
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: {
|
|
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 {
|
|
16
|
+
catch {
|
|
17
|
+
/* dir doesn't exist */
|
|
18
|
+
}
|
|
17
19
|
}
|
|
18
20
|
if (files.length < 2) {
|
|
19
|
-
return {
|
|
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
|
};
|
package/dist/runners/lint.js
CHANGED
|
@@ -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
|
}
|
package/dist/runners/secrets.js
CHANGED
|
@@ -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
|
}
|
package/dist/runners/security.js
CHANGED
|
@@ -1,33 +1,123 @@
|
|
|
1
1
|
/** Security analysis — beyond secrets, checks for vulnerable code patterns. */
|
|
2
|
-
import { existsSync,
|
|
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
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
{
|
|
19
|
-
|
|
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
|
-
{
|
|
22
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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 {
|
|
131
|
+
catch {
|
|
132
|
+
/* dir doesn't exist */
|
|
133
|
+
}
|
|
42
134
|
}
|
|
43
135
|
if (files.length === 0) {
|
|
44
|
-
return {
|
|
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({
|