@flamki/vibeguard 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +30 -0
- package/dist/cli.js +34 -0
- package/dist/reporter/consoleReporter.js +30 -0
- package/dist/rules/hallucinatedApi.js +69 -0
- package/dist/rules/index.js +11 -0
- package/dist/rules/localStorageToken.js +19 -0
- package/dist/rules/missingTryCatch.js +20 -0
- package/dist/scanner/fileWalker.js +23 -0
- package/dist/scanner/index.js +14 -0
- package/dist/scanner/scanFile.js +17 -0
- package/dist/types.js +2 -0
- package/package.json +33 -0
- package/src/cli.ts +36 -0
- package/src/reporter/consoleReporter.ts +37 -0
- package/src/rules/hallucinatedApi.ts +89 -0
- package/src/rules/index.ts +9 -0
- package/src/rules/localStorageToken.ts +26 -0
- package/src/rules/missingTryCatch.ts +25 -0
- package/src/scanner/fileWalker.ts +21 -0
- package/src/scanner/index.ts +15 -0
- package/src/scanner/scanFile.ts +15 -0
- package/src/types.ts +7 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout repo
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Setup Node
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 18
|
|
19
|
+
registry-url: https://registry.npmjs.org/
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: npm ci
|
|
23
|
+
|
|
24
|
+
- name: Build
|
|
25
|
+
run: npm run build
|
|
26
|
+
|
|
27
|
+
- name: Publish
|
|
28
|
+
run: npm publish --access public
|
|
29
|
+
env:
|
|
30
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const scanner_1 = require("./scanner");
|
|
10
|
+
const consoleReporter_1 = require("./reporter/consoleReporter");
|
|
11
|
+
const program = new commander_1.Command();
|
|
12
|
+
console.log(chalk_1.default.cyan.bold(`
|
|
13
|
+
🛡️ VibeGuard
|
|
14
|
+
-----------------------
|
|
15
|
+
AI code safety net
|
|
16
|
+
`));
|
|
17
|
+
program
|
|
18
|
+
.name("vibeguard")
|
|
19
|
+
.description("Scan codebases for common AI-generated mistakes")
|
|
20
|
+
.version("0.1.0");
|
|
21
|
+
program
|
|
22
|
+
.command("scan")
|
|
23
|
+
.argument("<path>", "Path to project")
|
|
24
|
+
.action(async (path) => {
|
|
25
|
+
try {
|
|
26
|
+
const results = await (0, scanner_1.runScan)(path);
|
|
27
|
+
(0, consoleReporter_1.printReport)(results);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.error(chalk_1.default.red("❌ Scan failed"), err);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.printReport = printReport;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function printReport(results) {
|
|
10
|
+
if (results.length === 0) {
|
|
11
|
+
console.log(chalk_1.default.green("✅ No issues found. Ship it."));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const grouped = {};
|
|
15
|
+
for (const r of results) {
|
|
16
|
+
const file = path_1.default.basename(r.file || "unknown");
|
|
17
|
+
grouped[file] ?? (grouped[file] = []);
|
|
18
|
+
grouped[file].push(r);
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk_1.default.bold("\n🛡️ VibeGuard Report\n"));
|
|
21
|
+
for (const file in grouped) {
|
|
22
|
+
console.log(chalk_1.default.cyan(file));
|
|
23
|
+
for (const r of grouped[file]) {
|
|
24
|
+
const lineInfo = r.line ? `Line ${r.line}` : "";
|
|
25
|
+
console.log(` ${chalk_1.default.yellow("⚠️")} ${lineInfo} ${r.message}`);
|
|
26
|
+
}
|
|
27
|
+
console.log("");
|
|
28
|
+
}
|
|
29
|
+
console.log(chalk_1.default.bold(`Summary: ${results.length} warning(s)\n`));
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hallucinatedApiRule = hallucinatedApiRule;
|
|
4
|
+
/**
|
|
5
|
+
* Globals and well-known functions we NEVER want to flag
|
|
6
|
+
*/
|
|
7
|
+
const ALLOWED_GLOBALS = new Set([
|
|
8
|
+
"fetch",
|
|
9
|
+
"console",
|
|
10
|
+
"setTimeout",
|
|
11
|
+
"setInterval",
|
|
12
|
+
"clearTimeout",
|
|
13
|
+
"clearInterval",
|
|
14
|
+
"require",
|
|
15
|
+
"import",
|
|
16
|
+
]);
|
|
17
|
+
function hallucinatedApiRule(code, file) {
|
|
18
|
+
const warnings = [];
|
|
19
|
+
/**
|
|
20
|
+
* Matches function calls NOT preceded by a dot
|
|
21
|
+
* Example matched: getUserByIdSafe(
|
|
22
|
+
* Example ignored: obj.method(
|
|
23
|
+
*/
|
|
24
|
+
const callRegex = /(?<!\.)\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
25
|
+
/**
|
|
26
|
+
* Matches function declarations, variables, and imports
|
|
27
|
+
*/
|
|
28
|
+
const declaredRegex = /(function\s+([a-zA-Z_][a-zA-Z0-9_]*))|(const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=)|(let\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=)|(var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=)|import\s+.*\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
|
29
|
+
const calls = new Set();
|
|
30
|
+
const declared = new Set();
|
|
31
|
+
let match;
|
|
32
|
+
// Collect all function calls
|
|
33
|
+
while ((match = callRegex.exec(code))) {
|
|
34
|
+
const fn = match[1];
|
|
35
|
+
// Skip globals, constructors, and obvious safe cases
|
|
36
|
+
if (ALLOWED_GLOBALS.has(fn) ||
|
|
37
|
+
fn[0] === fn[0].toUpperCase()) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
calls.add(fn);
|
|
41
|
+
}
|
|
42
|
+
// Collect declared functions / variables / imports
|
|
43
|
+
while ((match = declaredRegex.exec(code))) {
|
|
44
|
+
const name = match[2] ||
|
|
45
|
+
match[4] ||
|
|
46
|
+
match[6] ||
|
|
47
|
+
match[8] ||
|
|
48
|
+
match[9];
|
|
49
|
+
if (name) {
|
|
50
|
+
declared.add(name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Diff: calls that were never declared
|
|
54
|
+
for (const fn of calls) {
|
|
55
|
+
if (!declared.has(fn)) {
|
|
56
|
+
const line = code
|
|
57
|
+
.split("\n")
|
|
58
|
+
.findIndex((l) => l.includes(`${fn}(`)) + 1;
|
|
59
|
+
warnings.push({
|
|
60
|
+
ruleId: "hallucinated-api",
|
|
61
|
+
severity: "warning",
|
|
62
|
+
message: `Possible hallucinated API: ${fn}() is used but never defined or imported.`,
|
|
63
|
+
file,
|
|
64
|
+
line: line > 0 ? line : undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return warnings;
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rules = void 0;
|
|
4
|
+
const localStorageToken_1 = require("./localStorageToken");
|
|
5
|
+
const missingTryCatch_1 = require("./missingTryCatch");
|
|
6
|
+
const hallucinatedApi_1 = require("./hallucinatedApi");
|
|
7
|
+
exports.rules = [
|
|
8
|
+
localStorageToken_1.localStorageTokenRule,
|
|
9
|
+
missingTryCatch_1.missingTryCatchRule,
|
|
10
|
+
hallucinatedApi_1.hallucinatedApiRule,
|
|
11
|
+
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.localStorageTokenRule = localStorageTokenRule;
|
|
4
|
+
function localStorageTokenRule(code, file) {
|
|
5
|
+
const warnings = [];
|
|
6
|
+
const regex = /localStorage\.setItem\(['"`].*token.*['"`]/gi;
|
|
7
|
+
let match;
|
|
8
|
+
while ((match = regex.exec(code))) {
|
|
9
|
+
const line = code.slice(0, match.index).split("\n").length;
|
|
10
|
+
warnings.push({
|
|
11
|
+
ruleId: "local-storage-token",
|
|
12
|
+
severity: "warning",
|
|
13
|
+
message: "Token stored in localStorage. This is vulnerable to XSS. Use httpOnly cookies.",
|
|
14
|
+
file,
|
|
15
|
+
line,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return warnings;
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.missingTryCatchRule = missingTryCatchRule;
|
|
4
|
+
function missingTryCatchRule(code, file) {
|
|
5
|
+
if (code.includes("await") && !code.includes("try {")) {
|
|
6
|
+
const line = code
|
|
7
|
+
.split("\n")
|
|
8
|
+
.findIndex((l) => l.includes("await")) + 1;
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
ruleId: "missing-try-catch",
|
|
12
|
+
severity: "warning",
|
|
13
|
+
message: "Async operation without try/catch. This may crash in production.",
|
|
14
|
+
file,
|
|
15
|
+
line,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.walkFiles = walkFiles;
|
|
7
|
+
const glob_1 = require("glob");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
async function walkFiles(rootPath) {
|
|
10
|
+
const absoluteRoot = path_1.default.resolve(rootPath);
|
|
11
|
+
const pattern = "**/*.{js,ts,jsx,tsx}";
|
|
12
|
+
const files = await (0, glob_1.glob)(pattern, {
|
|
13
|
+
cwd: absoluteRoot,
|
|
14
|
+
absolute: true,
|
|
15
|
+
ignore: [
|
|
16
|
+
"**/node_modules/**",
|
|
17
|
+
"**/dist/**",
|
|
18
|
+
"**/.git/**",
|
|
19
|
+
"**/src/**"
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runScan = runScan;
|
|
4
|
+
const fileWalker_1 = require("./fileWalker");
|
|
5
|
+
const scanFile_1 = require("./scanFile");
|
|
6
|
+
async function runScan(rootPath) {
|
|
7
|
+
const files = await (0, fileWalker_1.walkFiles)(rootPath);
|
|
8
|
+
const results = [];
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
const warnings = await (0, scanFile_1.scanFile)(file);
|
|
11
|
+
results.push(...warnings);
|
|
12
|
+
}
|
|
13
|
+
return results;
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scanFile = scanFile;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const rules_1 = require("../rules");
|
|
9
|
+
async function scanFile(filePath) {
|
|
10
|
+
const code = fs_1.default.readFileSync(filePath, "utf-8");
|
|
11
|
+
const warnings = [];
|
|
12
|
+
for (const rule of rules_1.rules) {
|
|
13
|
+
const result = rule(code, filePath);
|
|
14
|
+
warnings.push(...result);
|
|
15
|
+
}
|
|
16
|
+
return warnings;
|
|
17
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flamki/vibeguard",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A CLI safety net for AI-generated code",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vibeguard": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "npm run build && node dist/cli.js scan ."
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"cli",
|
|
17
|
+
"static-analysis",
|
|
18
|
+
"security",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"code-quality"
|
|
21
|
+
],
|
|
22
|
+
"author": "Ayush Singh",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"chalk": "^5.3.0",
|
|
26
|
+
"commander": "^11.0.0",
|
|
27
|
+
"glob": "^10.3.10"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"typescript": "^5.3.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { runScan } from "./scanner";
|
|
6
|
+
import { printReport } from "./reporter/consoleReporter";
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
console.log(
|
|
11
|
+
chalk.cyan.bold(`
|
|
12
|
+
🛡️ VibeGuard
|
|
13
|
+
-----------------------
|
|
14
|
+
AI code safety net
|
|
15
|
+
`)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name("vibeguard")
|
|
20
|
+
.description("Scan codebases for common AI-generated mistakes")
|
|
21
|
+
.version("0.1.0");
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("scan")
|
|
25
|
+
.argument("<path>", "Path to project")
|
|
26
|
+
.action(async (path: string) => {
|
|
27
|
+
try {
|
|
28
|
+
const results = await runScan(path);
|
|
29
|
+
printReport(results);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(chalk.red("❌ Scan failed"), err);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ScanWarning } from "../types";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export function printReport(results: ScanWarning[]) {
|
|
6
|
+
if (results.length === 0) {
|
|
7
|
+
console.log(chalk.green("✅ No issues found. Ship it."));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const grouped: Record<string, ScanWarning[]> = {};
|
|
12
|
+
|
|
13
|
+
for (const r of results) {
|
|
14
|
+
const file = path.basename(r.file || "unknown");
|
|
15
|
+
grouped[file] ??= [];
|
|
16
|
+
grouped[file].push(r);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(chalk.bold("\n🛡️ VibeGuard Report\n"));
|
|
20
|
+
|
|
21
|
+
for (const file in grouped) {
|
|
22
|
+
console.log(chalk.cyan(file));
|
|
23
|
+
|
|
24
|
+
for (const r of grouped[file]) {
|
|
25
|
+
const lineInfo = r.line ? `Line ${r.line}` : "";
|
|
26
|
+
console.log(
|
|
27
|
+
` ${chalk.yellow("⚠️")} ${lineInfo} ${r.message}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log("");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(
|
|
35
|
+
chalk.bold(`Summary: ${results.length} warning(s)\n`)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ScanWarning } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Globals and well-known functions we NEVER want to flag
|
|
5
|
+
*/
|
|
6
|
+
const ALLOWED_GLOBALS = new Set([
|
|
7
|
+
"fetch",
|
|
8
|
+
"console",
|
|
9
|
+
"setTimeout",
|
|
10
|
+
"setInterval",
|
|
11
|
+
"clearTimeout",
|
|
12
|
+
"clearInterval",
|
|
13
|
+
"require",
|
|
14
|
+
"import",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function hallucinatedApiRule(
|
|
18
|
+
code: string,
|
|
19
|
+
file: string
|
|
20
|
+
): ScanWarning[] {
|
|
21
|
+
const warnings: ScanWarning[] = [];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Matches function calls NOT preceded by a dot
|
|
25
|
+
* Example matched: getUserByIdSafe(
|
|
26
|
+
* Example ignored: obj.method(
|
|
27
|
+
*/
|
|
28
|
+
const callRegex = /(?<!\.)\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Matches function declarations, variables, and imports
|
|
32
|
+
*/
|
|
33
|
+
const declaredRegex =
|
|
34
|
+
/(function\s+([a-zA-Z_][a-zA-Z0-9_]*))|(const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=)|(let\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=)|(var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=)|import\s+.*\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
|
35
|
+
|
|
36
|
+
const calls = new Set<string>();
|
|
37
|
+
const declared = new Set<string>();
|
|
38
|
+
|
|
39
|
+
let match;
|
|
40
|
+
|
|
41
|
+
// Collect all function calls
|
|
42
|
+
while ((match = callRegex.exec(code))) {
|
|
43
|
+
const fn = match[1];
|
|
44
|
+
|
|
45
|
+
// Skip globals, constructors, and obvious safe cases
|
|
46
|
+
if (
|
|
47
|
+
ALLOWED_GLOBALS.has(fn) ||
|
|
48
|
+
fn[0] === fn[0].toUpperCase()
|
|
49
|
+
) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
calls.add(fn);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Collect declared functions / variables / imports
|
|
57
|
+
while ((match = declaredRegex.exec(code))) {
|
|
58
|
+
const name =
|
|
59
|
+
match[2] ||
|
|
60
|
+
match[4] ||
|
|
61
|
+
match[6] ||
|
|
62
|
+
match[8] ||
|
|
63
|
+
match[9];
|
|
64
|
+
|
|
65
|
+
if (name) {
|
|
66
|
+
declared.add(name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Diff: calls that were never declared
|
|
71
|
+
for (const fn of calls) {
|
|
72
|
+
if (!declared.has(fn)) {
|
|
73
|
+
const line =
|
|
74
|
+
code
|
|
75
|
+
.split("\n")
|
|
76
|
+
.findIndex((l) => l.includes(`${fn}(`)) + 1;
|
|
77
|
+
|
|
78
|
+
warnings.push({
|
|
79
|
+
ruleId: "hallucinated-api",
|
|
80
|
+
severity: "warning",
|
|
81
|
+
message: `Possible hallucinated API: ${fn}() is used but never defined or imported.`,
|
|
82
|
+
file,
|
|
83
|
+
line: line > 0 ? line : undefined,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return warnings;
|
|
89
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { localStorageTokenRule } from "./localStorageToken";
|
|
2
|
+
import { missingTryCatchRule } from "./missingTryCatch";
|
|
3
|
+
import { hallucinatedApiRule } from "./hallucinatedApi";
|
|
4
|
+
|
|
5
|
+
export const rules = [
|
|
6
|
+
localStorageTokenRule,
|
|
7
|
+
missingTryCatchRule,
|
|
8
|
+
hallucinatedApiRule,
|
|
9
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ScanWarning } from "../types";
|
|
2
|
+
|
|
3
|
+
export function localStorageTokenRule(
|
|
4
|
+
code: string,
|
|
5
|
+
file: string
|
|
6
|
+
): ScanWarning[] {
|
|
7
|
+
const warnings: ScanWarning[] = [];
|
|
8
|
+
const regex = /localStorage\.setItem\(['"`].*token.*['"`]/gi;
|
|
9
|
+
|
|
10
|
+
let match;
|
|
11
|
+
while ((match = regex.exec(code))) {
|
|
12
|
+
const line =
|
|
13
|
+
code.slice(0, match.index).split("\n").length;
|
|
14
|
+
|
|
15
|
+
warnings.push({
|
|
16
|
+
ruleId: "local-storage-token",
|
|
17
|
+
severity: "warning",
|
|
18
|
+
message:
|
|
19
|
+
"Token stored in localStorage. This is vulnerable to XSS. Use httpOnly cookies.",
|
|
20
|
+
file,
|
|
21
|
+
line,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return warnings;
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ScanWarning } from "../types";
|
|
2
|
+
|
|
3
|
+
export function missingTryCatchRule(
|
|
4
|
+
code: string,
|
|
5
|
+
file: string
|
|
6
|
+
): ScanWarning[] {
|
|
7
|
+
if (code.includes("await") && !code.includes("try {")) {
|
|
8
|
+
const line = code
|
|
9
|
+
.split("\n")
|
|
10
|
+
.findIndex((l) => l.includes("await")) + 1;
|
|
11
|
+
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
ruleId: "missing-try-catch",
|
|
15
|
+
severity: "warning",
|
|
16
|
+
message:
|
|
17
|
+
"Async operation without try/catch. This may crash in production.",
|
|
18
|
+
file,
|
|
19
|
+
line,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export async function walkFiles(rootPath: string): Promise<string[]> {
|
|
5
|
+
const absoluteRoot = path.resolve(rootPath);
|
|
6
|
+
|
|
7
|
+
const pattern = "**/*.{js,ts,jsx,tsx}";
|
|
8
|
+
|
|
9
|
+
const files = await glob(pattern, {
|
|
10
|
+
cwd: absoluteRoot,
|
|
11
|
+
absolute: true,
|
|
12
|
+
ignore: [
|
|
13
|
+
"**/node_modules/**",
|
|
14
|
+
"**/dist/**",
|
|
15
|
+
"**/.git/**",
|
|
16
|
+
"**/src/**"
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { walkFiles } from "./fileWalker";
|
|
2
|
+
import { scanFile } from "./scanFile";
|
|
3
|
+
import { ScanWarning } from "../types";
|
|
4
|
+
|
|
5
|
+
export async function runScan(rootPath: string): Promise<ScanWarning[]> {
|
|
6
|
+
const files = await walkFiles(rootPath);
|
|
7
|
+
const results: ScanWarning[] = [];
|
|
8
|
+
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
const warnings = await scanFile(file);
|
|
11
|
+
results.push(...warnings);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return results;
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { ScanWarning } from "../types";
|
|
3
|
+
import { rules } from "../rules";
|
|
4
|
+
|
|
5
|
+
export async function scanFile(filePath: string): Promise<ScanWarning[]> {
|
|
6
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
7
|
+
const warnings: ScanWarning[] = [];
|
|
8
|
+
|
|
9
|
+
for (const rule of rules) {
|
|
10
|
+
const result = rule(code, filePath);
|
|
11
|
+
warnings.push(...result);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return warnings;
|
|
15
|
+
}
|
package/src/types.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|