@reliabilityworks/vibesec 0.1.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/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +221 -0
- package/dist/cli.js.map +1 -0
- package/package.json +22 -0
- package/src/cli.ts +275 -0
- package/test/smoke.test.js +7 -0
- package/test/workspaceScan.test.js +34 -0
- package/tsconfig.json +8 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAiMA,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyE1D"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
exports.runCli = runCli;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const commander_1 = require("commander");
|
|
12
|
+
const analyzer_javascript_1 = require("@reliabilityworks/analyzer-javascript");
|
|
13
|
+
const core_1 = require("@reliabilityworks/core");
|
|
14
|
+
function formatCliOutput(result) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
if (result.frameworks.length > 0) {
|
|
17
|
+
const frameworkList = result.frameworks.map((f) => f.id).join(', ');
|
|
18
|
+
lines.push(`Frameworks: ${frameworkList}`);
|
|
19
|
+
lines.push('');
|
|
20
|
+
}
|
|
21
|
+
for (const finding of result.findings) {
|
|
22
|
+
lines.push(`${finding.severity.toUpperCase()} ${finding.ruleId} ${finding.location.path}:${finding.location.startLine}`);
|
|
23
|
+
lines.push(` ${finding.message}`);
|
|
24
|
+
lines.push('');
|
|
25
|
+
}
|
|
26
|
+
lines.push(`Scan complete: ${result.findings.length} issue(s) found`);
|
|
27
|
+
return lines.join('\n') + '\n';
|
|
28
|
+
}
|
|
29
|
+
function parseFrameworkIds(input) {
|
|
30
|
+
if (!input || input === 'auto')
|
|
31
|
+
return [];
|
|
32
|
+
return input
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((s) => s.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((s) => s);
|
|
37
|
+
}
|
|
38
|
+
function selectFrameworks(detected, requested) {
|
|
39
|
+
if (requested === 'auto')
|
|
40
|
+
return detected;
|
|
41
|
+
const requestedIds = parseFrameworkIds(requested);
|
|
42
|
+
const byId = new Map(detected.map((d) => [d.id, d]));
|
|
43
|
+
const selected = [];
|
|
44
|
+
for (const id of requestedIds) {
|
|
45
|
+
const hit = byId.get(id);
|
|
46
|
+
if (hit) {
|
|
47
|
+
selected.push(hit);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
selected.push({ id, confidence: 'low', evidence: ['user: --framework'] });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return selected;
|
|
54
|
+
}
|
|
55
|
+
function toPosixPath(p) {
|
|
56
|
+
return p.split(node_path_1.default.sep).join('/');
|
|
57
|
+
}
|
|
58
|
+
function joinPrefixedGlob(prefix, glob) {
|
|
59
|
+
const normalizedPrefix = prefix.replace(/\/+$/, '');
|
|
60
|
+
if (!normalizedPrefix || normalizedPrefix === '.')
|
|
61
|
+
return glob;
|
|
62
|
+
const normalizedGlob = glob.replace(/^\/+/, '');
|
|
63
|
+
return `${normalizedPrefix}/${normalizedGlob}`;
|
|
64
|
+
}
|
|
65
|
+
function scopeRulesToPrefix(rules, prefix) {
|
|
66
|
+
const normalizedPrefix = toPosixPath(prefix);
|
|
67
|
+
if (!normalizedPrefix || normalizedPrefix === '.')
|
|
68
|
+
return rules;
|
|
69
|
+
return rules.map((rule) => {
|
|
70
|
+
if (rule.matcher.type === 'regex') {
|
|
71
|
+
return {
|
|
72
|
+
...rule,
|
|
73
|
+
matcher: {
|
|
74
|
+
...rule.matcher,
|
|
75
|
+
fileGlobs: rule.matcher.fileGlobs.map((glob) => joinPrefixedGlob(normalizedPrefix, glob)),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...rule,
|
|
81
|
+
matcher: {
|
|
82
|
+
...rule.matcher,
|
|
83
|
+
paths: rule.matcher.paths.map((p) => joinPrefixedGlob(normalizedPrefix, p)),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function loadWorkspaceScopedRules(workspaceRoot) {
|
|
89
|
+
const projectRoots = await (0, core_1.listWorkspaceProjectRoots)(workspaceRoot);
|
|
90
|
+
const rules = [];
|
|
91
|
+
for (const projectRoot of projectRoots) {
|
|
92
|
+
const frameworks = await (0, core_1.detectFrameworks)(projectRoot);
|
|
93
|
+
if (frameworks.length === 0)
|
|
94
|
+
continue;
|
|
95
|
+
const projectRules = await loadRulesForFrameworks(frameworks);
|
|
96
|
+
const projectPrefix = node_path_1.default.relative(workspaceRoot, projectRoot);
|
|
97
|
+
rules.push(...scopeRulesToPrefix(projectRules, projectPrefix));
|
|
98
|
+
}
|
|
99
|
+
return rules;
|
|
100
|
+
}
|
|
101
|
+
async function loadRulesetRules(packageName) {
|
|
102
|
+
const pkgJsonPath = require.resolve(`${packageName}/package.json`);
|
|
103
|
+
const rulesPath = node_path_1.default.join(node_path_1.default.dirname(pkgJsonPath), 'rules.json');
|
|
104
|
+
const raw = await promises_1.default.readFile(rulesPath, 'utf8');
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
if (!Array.isArray(parsed)) {
|
|
107
|
+
throw new Error(`${packageName} rules.json must be an array`);
|
|
108
|
+
}
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
async function loadRulesForFrameworks(frameworks) {
|
|
112
|
+
const ids = new Set(frameworks.map((f) => f.id));
|
|
113
|
+
const rules = [];
|
|
114
|
+
if (ids.has('nextjs')) {
|
|
115
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-nextjs')));
|
|
116
|
+
}
|
|
117
|
+
if (ids.has('react-native') || ids.has('expo')) {
|
|
118
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-react-native')));
|
|
119
|
+
}
|
|
120
|
+
if (ids.has('express')) {
|
|
121
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-express')));
|
|
122
|
+
}
|
|
123
|
+
if (ids.has('sveltekit')) {
|
|
124
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-sveltekit')));
|
|
125
|
+
}
|
|
126
|
+
return rules;
|
|
127
|
+
}
|
|
128
|
+
function readCliVersion() {
|
|
129
|
+
const packageJsonPath = node_path_1.default.join(__dirname, '..', 'package.json');
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf8');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return '0.0.0';
|
|
136
|
+
}
|
|
137
|
+
const parsed = JSON.parse(raw);
|
|
138
|
+
if (parsed && typeof parsed === 'object' && 'version' in parsed) {
|
|
139
|
+
const version = parsed.version;
|
|
140
|
+
if (typeof version === 'string' && version.length > 0)
|
|
141
|
+
return version;
|
|
142
|
+
}
|
|
143
|
+
return '0.0.0';
|
|
144
|
+
}
|
|
145
|
+
async function runCli(argv) {
|
|
146
|
+
const program = new commander_1.Command();
|
|
147
|
+
program
|
|
148
|
+
.name('vibesec')
|
|
149
|
+
.description('Local security scanner for modern frameworks')
|
|
150
|
+
.version(readCliVersion());
|
|
151
|
+
program
|
|
152
|
+
.command('scan')
|
|
153
|
+
.argument('[path]', 'Path to scan', '.')
|
|
154
|
+
.option('-o, --output <format>', 'Output format: cli|json|sarif|html', 'cli')
|
|
155
|
+
.option('--out-file <path>', 'Write output to file')
|
|
156
|
+
.option('--fail-on <severity>', 'Fail on or above: low|medium|high|critical', 'high')
|
|
157
|
+
.option('--framework <name>', 'Framework: auto|nextjs|react-native|expo|express|sveltekit (comma-separated)', 'auto')
|
|
158
|
+
.option('--config <path>', 'Config file path (.vibesec.yaml by default)')
|
|
159
|
+
.option('--rules-dir <path>', 'Custom rules dir (.vibesec/rules by default)')
|
|
160
|
+
.action(async (scanPath, options) => {
|
|
161
|
+
const failOn = (0, core_1.severityFromString)(options.failOn);
|
|
162
|
+
const absoluteRoot = node_path_1.default.resolve(scanPath);
|
|
163
|
+
const detected = await (0, core_1.detectFrameworksInWorkspace)(absoluteRoot);
|
|
164
|
+
const frameworks = selectFrameworks(detected, options.framework);
|
|
165
|
+
const analyzerRules = (0, analyzer_javascript_1.getJavaScriptRules)();
|
|
166
|
+
const additionalRules = [
|
|
167
|
+
...analyzerRules,
|
|
168
|
+
...(options.framework === 'auto'
|
|
169
|
+
? await loadWorkspaceScopedRules(absoluteRoot)
|
|
170
|
+
: await loadRulesForFrameworks(frameworks)),
|
|
171
|
+
];
|
|
172
|
+
const result = await (0, core_1.scanProject)({
|
|
173
|
+
rootDir: absoluteRoot,
|
|
174
|
+
frameworks,
|
|
175
|
+
additionalRules,
|
|
176
|
+
configPath: options.config,
|
|
177
|
+
customRulesDir: options.rulesDir,
|
|
178
|
+
});
|
|
179
|
+
if (options.output === 'json') {
|
|
180
|
+
const json = JSON.stringify(result, null, 2);
|
|
181
|
+
if (options.outFile) {
|
|
182
|
+
await promises_1.default.writeFile(options.outFile, json, 'utf8');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
process.stdout.write(json + '\n');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (options.output === 'sarif') {
|
|
189
|
+
const sarif = JSON.stringify((0, core_1.toSarif)(result), null, 2);
|
|
190
|
+
if (options.outFile) {
|
|
191
|
+
await promises_1.default.writeFile(options.outFile, sarif, 'utf8');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
process.stdout.write(sarif + '\n');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (options.output === 'html') {
|
|
198
|
+
const html = (0, core_1.toHtml)(result);
|
|
199
|
+
if (options.outFile) {
|
|
200
|
+
await promises_1.default.writeFile(options.outFile, html, 'utf8');
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
process.stdout.write(html + '\n');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
process.stdout.write(formatCliOutput(result));
|
|
208
|
+
}
|
|
209
|
+
const shouldFail = result.findings.some((finding) => finding.severityRank <= failOn.rank);
|
|
210
|
+
process.exitCode = shouldFail ? 1 : 0;
|
|
211
|
+
});
|
|
212
|
+
await program.parseAsync(argv);
|
|
213
|
+
}
|
|
214
|
+
if (require.main === module) {
|
|
215
|
+
runCli(process.argv).catch((error) => {
|
|
216
|
+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
217
|
+
process.stderr.write(message + '\n');
|
|
218
|
+
process.exitCode = 2;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reliabilityworks/vibesec",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"vibesec": "dist/cli.js"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"test": "pnpm build && node --test test",
|
|
11
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@reliabilityworks/analyzer-javascript": "0.1.0",
|
|
15
|
+
"@reliabilityworks/core": "0.1.0",
|
|
16
|
+
"@reliabilityworks/ruleset-express": "0.1.0",
|
|
17
|
+
"@reliabilityworks/ruleset-nextjs": "0.1.0",
|
|
18
|
+
"@reliabilityworks/ruleset-react-native": "0.1.0",
|
|
19
|
+
"@reliabilityworks/ruleset-sveltekit": "0.1.0",
|
|
20
|
+
"commander": "^12.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import fs from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander'
|
|
8
|
+
|
|
9
|
+
import { getJavaScriptRules } from '@reliabilityworks/analyzer-javascript'
|
|
10
|
+
import {
|
|
11
|
+
detectFrameworks,
|
|
12
|
+
detectFrameworksInWorkspace,
|
|
13
|
+
listWorkspaceProjectRoots,
|
|
14
|
+
scanProject,
|
|
15
|
+
severityFromString,
|
|
16
|
+
toHtml,
|
|
17
|
+
toSarif,
|
|
18
|
+
type FrameworkDetection,
|
|
19
|
+
type FrameworkId,
|
|
20
|
+
type Rule,
|
|
21
|
+
type SeverityName,
|
|
22
|
+
type ScanResult,
|
|
23
|
+
} from '@reliabilityworks/core'
|
|
24
|
+
|
|
25
|
+
type ScanCommandOptions = {
|
|
26
|
+
output: string
|
|
27
|
+
outFile?: string
|
|
28
|
+
failOn: SeverityName
|
|
29
|
+
framework: string
|
|
30
|
+
config?: string
|
|
31
|
+
rulesDir?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatCliOutput(result: ScanResult): string {
|
|
35
|
+
const lines: string[] = []
|
|
36
|
+
|
|
37
|
+
if (result.frameworks.length > 0) {
|
|
38
|
+
const frameworkList = result.frameworks.map((f) => f.id).join(', ')
|
|
39
|
+
lines.push(`Frameworks: ${frameworkList}`)
|
|
40
|
+
lines.push('')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const finding of result.findings) {
|
|
44
|
+
lines.push(
|
|
45
|
+
`${finding.severity.toUpperCase()} ${finding.ruleId} ${finding.location.path}:${finding.location.startLine}`,
|
|
46
|
+
)
|
|
47
|
+
lines.push(` ${finding.message}`)
|
|
48
|
+
lines.push('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines.push(`Scan complete: ${result.findings.length} issue(s) found`)
|
|
52
|
+
|
|
53
|
+
return lines.join('\n') + '\n'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseFrameworkIds(input: string): FrameworkId[] {
|
|
57
|
+
if (!input || input === 'auto') return []
|
|
58
|
+
|
|
59
|
+
return input
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((s) => s.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((s) => s as FrameworkId)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function selectFrameworks(detected: FrameworkDetection[], requested: string): FrameworkDetection[] {
|
|
67
|
+
if (requested === 'auto') return detected
|
|
68
|
+
|
|
69
|
+
const requestedIds = parseFrameworkIds(requested)
|
|
70
|
+
const byId = new Map(detected.map((d) => [d.id, d] as const))
|
|
71
|
+
|
|
72
|
+
const selected: FrameworkDetection[] = []
|
|
73
|
+
for (const id of requestedIds) {
|
|
74
|
+
const hit = byId.get(id)
|
|
75
|
+
if (hit) {
|
|
76
|
+
selected.push(hit)
|
|
77
|
+
} else {
|
|
78
|
+
selected.push({ id, confidence: 'low', evidence: ['user: --framework'] })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return selected
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toPosixPath(p: string): string {
|
|
86
|
+
return p.split(path.sep).join('/')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function joinPrefixedGlob(prefix: string, glob: string): string {
|
|
90
|
+
const normalizedPrefix = prefix.replace(/\/+$/, '')
|
|
91
|
+
if (!normalizedPrefix || normalizedPrefix === '.') return glob
|
|
92
|
+
|
|
93
|
+
const normalizedGlob = glob.replace(/^\/+/, '')
|
|
94
|
+
return `${normalizedPrefix}/${normalizedGlob}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function scopeRulesToPrefix(rules: Rule[], prefix: string): Rule[] {
|
|
98
|
+
const normalizedPrefix = toPosixPath(prefix)
|
|
99
|
+
if (!normalizedPrefix || normalizedPrefix === '.') return rules
|
|
100
|
+
|
|
101
|
+
return rules.map((rule) => {
|
|
102
|
+
if (rule.matcher.type === 'regex') {
|
|
103
|
+
return {
|
|
104
|
+
...rule,
|
|
105
|
+
matcher: {
|
|
106
|
+
...rule.matcher,
|
|
107
|
+
fileGlobs: rule.matcher.fileGlobs.map((glob) => joinPrefixedGlob(normalizedPrefix, glob)),
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...rule,
|
|
114
|
+
matcher: {
|
|
115
|
+
...rule.matcher,
|
|
116
|
+
paths: rule.matcher.paths.map((p) => joinPrefixedGlob(normalizedPrefix, p)),
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loadWorkspaceScopedRules(workspaceRoot: string): Promise<Rule[]> {
|
|
123
|
+
const projectRoots = await listWorkspaceProjectRoots(workspaceRoot)
|
|
124
|
+
const rules: Rule[] = []
|
|
125
|
+
|
|
126
|
+
for (const projectRoot of projectRoots) {
|
|
127
|
+
const frameworks = await detectFrameworks(projectRoot)
|
|
128
|
+
if (frameworks.length === 0) continue
|
|
129
|
+
|
|
130
|
+
const projectRules = await loadRulesForFrameworks(frameworks)
|
|
131
|
+
const projectPrefix = path.relative(workspaceRoot, projectRoot)
|
|
132
|
+
rules.push(...scopeRulesToPrefix(projectRules, projectPrefix))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return rules
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loadRulesetRules(packageName: string): Promise<Rule[]> {
|
|
139
|
+
const pkgJsonPath = require.resolve(`${packageName}/package.json`)
|
|
140
|
+
const rulesPath = path.join(path.dirname(pkgJsonPath), 'rules.json')
|
|
141
|
+
const raw = await fs.readFile(rulesPath, 'utf8')
|
|
142
|
+
const parsed = JSON.parse(raw) as unknown
|
|
143
|
+
|
|
144
|
+
if (!Array.isArray(parsed)) {
|
|
145
|
+
throw new Error(`${packageName} rules.json must be an array`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parsed as Rule[]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function loadRulesForFrameworks(frameworks: FrameworkDetection[]): Promise<Rule[]> {
|
|
152
|
+
const ids = new Set(frameworks.map((f) => f.id))
|
|
153
|
+
const rules: Rule[] = []
|
|
154
|
+
|
|
155
|
+
if (ids.has('nextjs')) {
|
|
156
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-nextjs')))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (ids.has('react-native') || ids.has('expo')) {
|
|
160
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-react-native')))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (ids.has('express')) {
|
|
164
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-express')))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (ids.has('sveltekit')) {
|
|
168
|
+
rules.push(...(await loadRulesetRules('@reliabilityworks/ruleset-sveltekit')))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return rules
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readCliVersion(): string {
|
|
175
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json')
|
|
176
|
+
|
|
177
|
+
let raw: string
|
|
178
|
+
try {
|
|
179
|
+
raw = readFileSync(packageJsonPath, 'utf8')
|
|
180
|
+
} catch {
|
|
181
|
+
return '0.0.0'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const parsed = JSON.parse(raw) as unknown
|
|
185
|
+
|
|
186
|
+
if (parsed && typeof parsed === 'object' && 'version' in parsed) {
|
|
187
|
+
const version = (parsed as { version?: unknown }).version
|
|
188
|
+
if (typeof version === 'string' && version.length > 0) return version
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return '0.0.0'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function runCli(argv: string[]): Promise<void> {
|
|
195
|
+
const program = new Command()
|
|
196
|
+
|
|
197
|
+
program
|
|
198
|
+
.name('vibesec')
|
|
199
|
+
.description('Local security scanner for modern frameworks')
|
|
200
|
+
.version(readCliVersion())
|
|
201
|
+
|
|
202
|
+
program
|
|
203
|
+
.command('scan')
|
|
204
|
+
.argument('[path]', 'Path to scan', '.')
|
|
205
|
+
.option('-o, --output <format>', 'Output format: cli|json|sarif|html', 'cli')
|
|
206
|
+
.option('--out-file <path>', 'Write output to file')
|
|
207
|
+
.option('--fail-on <severity>', 'Fail on or above: low|medium|high|critical', 'high')
|
|
208
|
+
.option(
|
|
209
|
+
'--framework <name>',
|
|
210
|
+
'Framework: auto|nextjs|react-native|expo|express|sveltekit (comma-separated)',
|
|
211
|
+
'auto',
|
|
212
|
+
)
|
|
213
|
+
.option('--config <path>', 'Config file path (.vibesec.yaml by default)')
|
|
214
|
+
.option('--rules-dir <path>', 'Custom rules dir (.vibesec/rules by default)')
|
|
215
|
+
.action(async (scanPath: string, options: ScanCommandOptions) => {
|
|
216
|
+
const failOn = severityFromString(options.failOn)
|
|
217
|
+
|
|
218
|
+
const absoluteRoot = path.resolve(scanPath)
|
|
219
|
+
const detected = await detectFrameworksInWorkspace(absoluteRoot)
|
|
220
|
+
const frameworks = selectFrameworks(detected, options.framework)
|
|
221
|
+
const analyzerRules = getJavaScriptRules()
|
|
222
|
+
const additionalRules = [
|
|
223
|
+
...analyzerRules,
|
|
224
|
+
...(options.framework === 'auto'
|
|
225
|
+
? await loadWorkspaceScopedRules(absoluteRoot)
|
|
226
|
+
: await loadRulesForFrameworks(frameworks)),
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
const result = await scanProject({
|
|
230
|
+
rootDir: absoluteRoot,
|
|
231
|
+
frameworks,
|
|
232
|
+
additionalRules,
|
|
233
|
+
configPath: options.config,
|
|
234
|
+
customRulesDir: options.rulesDir,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
if (options.output === 'json') {
|
|
238
|
+
const json = JSON.stringify(result, null, 2)
|
|
239
|
+
if (options.outFile) {
|
|
240
|
+
await fs.writeFile(options.outFile, json, 'utf8')
|
|
241
|
+
} else {
|
|
242
|
+
process.stdout.write(json + '\n')
|
|
243
|
+
}
|
|
244
|
+
} else if (options.output === 'sarif') {
|
|
245
|
+
const sarif = JSON.stringify(toSarif(result), null, 2)
|
|
246
|
+
if (options.outFile) {
|
|
247
|
+
await fs.writeFile(options.outFile, sarif, 'utf8')
|
|
248
|
+
} else {
|
|
249
|
+
process.stdout.write(sarif + '\n')
|
|
250
|
+
}
|
|
251
|
+
} else if (options.output === 'html') {
|
|
252
|
+
const html = toHtml(result)
|
|
253
|
+
if (options.outFile) {
|
|
254
|
+
await fs.writeFile(options.outFile, html, 'utf8')
|
|
255
|
+
} else {
|
|
256
|
+
process.stdout.write(html + '\n')
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
process.stdout.write(formatCliOutput(result))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const shouldFail = result.findings.some((finding) => finding.severityRank <= failOn.rank)
|
|
263
|
+
process.exitCode = shouldFail ? 1 : 0
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await program.parseAsync(argv)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (require.main === module) {
|
|
270
|
+
runCli(process.argv).catch((error: unknown) => {
|
|
271
|
+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error)
|
|
272
|
+
process.stderr.write(message + '\n')
|
|
273
|
+
process.exitCode = 2
|
|
274
|
+
})
|
|
275
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const { execFileSync } = require('node:child_process')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const test = require('node:test')
|
|
5
|
+
|
|
6
|
+
test('workspace scan scopes framework rules to project roots', async () => {
|
|
7
|
+
const fixtureRoot = path.join(__dirname, '..', '..', 'core', 'test', 'fixtures', 'monorepo')
|
|
8
|
+
const cliPath = path.join(__dirname, '..', 'dist', 'cli.js')
|
|
9
|
+
|
|
10
|
+
const output = execFileSync(
|
|
11
|
+
process.execPath,
|
|
12
|
+
[
|
|
13
|
+
'--enable-source-maps',
|
|
14
|
+
cliPath,
|
|
15
|
+
'scan',
|
|
16
|
+
fixtureRoot,
|
|
17
|
+
'--output',
|
|
18
|
+
'json',
|
|
19
|
+
'--framework',
|
|
20
|
+
'auto',
|
|
21
|
+
'--fail-on',
|
|
22
|
+
'critical',
|
|
23
|
+
],
|
|
24
|
+
{ encoding: 'utf8' },
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const result = JSON.parse(output)
|
|
28
|
+
const nextPaths = result.findings
|
|
29
|
+
.filter((finding) => finding.ruleId === 'nextjs/production-browser-sourcemaps')
|
|
30
|
+
.map((finding) => finding.location.path)
|
|
31
|
+
|
|
32
|
+
assert.ok(nextPaths.includes('apps/web/next.config.js'))
|
|
33
|
+
assert.equal(nextPaths.includes('apps/api/next.config.js'), false)
|
|
34
|
+
})
|