sertivibed 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/README.md +209 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +167 -0
- package/dist/reporters/json.d.ts +12 -0
- package/dist/reporters/json.js +39 -0
- package/dist/reporters/md.d.ts +8 -0
- package/dist/reporters/md.js +278 -0
- package/dist/rules/index.d.ts +19 -0
- package/dist/rules/index.js +263 -0
- package/dist/scan.d.ts +8 -0
- package/dist/scan.js +278 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +15 -0
- package/dist/utils/fs.d.ts +35 -0
- package/dist/utils/fs.js +195 -0
- package/dist/utils/rg.d.ts +32 -0
- package/dist/utils/rg.js +222 -0
- package/package.json +47 -0
package/dist/utils/rg.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Ripgrep Wrapper
|
|
4
|
+
*
|
|
5
|
+
* Kjører ripgrep (rg) med JSON output for stabil parsing.
|
|
6
|
+
* Fallback til grep hvis rg ikke er installert.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.isRipgrepAvailable = isRipgrepAvailable;
|
|
10
|
+
exports.search = search;
|
|
11
|
+
exports.searchAbsence = searchAbsence;
|
|
12
|
+
exports.getExpandedContext = getExpandedContext;
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
// ============================================
|
|
15
|
+
// CHECK AVAILABILITY
|
|
16
|
+
// ============================================
|
|
17
|
+
function isRipgrepAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
// På Windows må vi bruke cmd.exe som shell for å finne rg i PATH
|
|
20
|
+
const result = (0, child_process_1.execSync)("rg --version", {
|
|
21
|
+
stdio: "pipe",
|
|
22
|
+
shell: process.platform === "win32" ? "cmd.exe" : "/bin/sh"
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// rg not available, will fall back to grep
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Søk etter pattern i filer med ripgrep
|
|
33
|
+
*/
|
|
34
|
+
function search(options) {
|
|
35
|
+
const { pattern, paths, cwd, ignoreCase = true, contextLines = 3 } = options;
|
|
36
|
+
if (!isRipgrepAvailable()) {
|
|
37
|
+
return searchWithGrep(options);
|
|
38
|
+
}
|
|
39
|
+
const args = [
|
|
40
|
+
"--json",
|
|
41
|
+
"--no-heading",
|
|
42
|
+
"--line-number",
|
|
43
|
+
`-C${contextLines}`,
|
|
44
|
+
ignoreCase ? "--ignore-case" : "",
|
|
45
|
+
"--glob", "!node_modules/**",
|
|
46
|
+
"--glob", "!dist/**",
|
|
47
|
+
"--glob", "!.next/**",
|
|
48
|
+
"--glob", "!.git/**",
|
|
49
|
+
"--glob", "!*.min.js",
|
|
50
|
+
"-e", pattern,
|
|
51
|
+
...paths.flatMap(p => ["--glob", p])
|
|
52
|
+
].filter(Boolean);
|
|
53
|
+
try {
|
|
54
|
+
const result = (0, child_process_1.spawnSync)("rg", args, {
|
|
55
|
+
cwd,
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
58
|
+
});
|
|
59
|
+
// rg returnerer exit code 1 hvis ingen matches (ikke en feil)
|
|
60
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
61
|
+
return { found: false, matches: [], matchCount: 0 };
|
|
62
|
+
}
|
|
63
|
+
const matches = parseRipgrepOutput(result.stdout || "", cwd);
|
|
64
|
+
return {
|
|
65
|
+
found: matches.length > 0,
|
|
66
|
+
matches,
|
|
67
|
+
matchCount: matches.length
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { found: false, matches: [], matchCount: 0 };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Sjekk om pattern IKKE finnes (for absence-regler)
|
|
76
|
+
*/
|
|
77
|
+
function searchAbsence(options) {
|
|
78
|
+
const result = search(options);
|
|
79
|
+
return {
|
|
80
|
+
found: !result.found, // Invertert: "funnet" betyr at pattern MANGLER
|
|
81
|
+
matches: [], // Ingen matches for absence
|
|
82
|
+
matchCount: 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ============================================
|
|
86
|
+
// PARSING
|
|
87
|
+
// ============================================
|
|
88
|
+
function parseRipgrepOutput(output, cwd) {
|
|
89
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
90
|
+
const matches = [];
|
|
91
|
+
const seenLocations = new Set();
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(line);
|
|
95
|
+
if (parsed.type === "match") {
|
|
96
|
+
const match = parsed;
|
|
97
|
+
const path = match.data.path.text;
|
|
98
|
+
const lineNum = match.data.line_number;
|
|
99
|
+
const locationKey = `${path}:${lineNum}`;
|
|
100
|
+
// Dedupliser
|
|
101
|
+
if (seenLocations.has(locationKey))
|
|
102
|
+
continue;
|
|
103
|
+
seenLocations.add(locationKey);
|
|
104
|
+
const matchText = match.data.submatches?.[0]?.match?.text || "";
|
|
105
|
+
matches.push({
|
|
106
|
+
path: path.replace(cwd + "/", ""), // Relativ path
|
|
107
|
+
lineStart: Math.max(1, lineNum - 2),
|
|
108
|
+
lineEnd: lineNum + 2,
|
|
109
|
+
snippet: match.data.lines.text.trim(),
|
|
110
|
+
matchText
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Ignorer linjer som ikke er valid JSON
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return matches;
|
|
119
|
+
}
|
|
120
|
+
// ============================================
|
|
121
|
+
// FALLBACK: GREP
|
|
122
|
+
// ============================================
|
|
123
|
+
function searchWithGrep(options) {
|
|
124
|
+
const { pattern, paths, cwd, ignoreCase = true } = options;
|
|
125
|
+
// Build include patterns from paths
|
|
126
|
+
const includeArgs = [];
|
|
127
|
+
for (const p of paths) {
|
|
128
|
+
// Convert glob to grep include pattern
|
|
129
|
+
// e.g., "**/*.sql" -> "--include=*.sql"
|
|
130
|
+
const match = p.match(/\*\*\/\*\.(\w+)$/) || p.match(/\*\.(\w+)$/);
|
|
131
|
+
if (match) {
|
|
132
|
+
includeArgs.push("--include", `*.${match[1]}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Fallback: include common file types if no specific includes
|
|
136
|
+
if (includeArgs.length === 0) {
|
|
137
|
+
includeArgs.push("--include", "*.ts", "--include", "*.tsx", "--include", "*.js", "--include", "*.jsx", "--include", "*.sql");
|
|
138
|
+
}
|
|
139
|
+
const args = [
|
|
140
|
+
"-r",
|
|
141
|
+
"-n",
|
|
142
|
+
ignoreCase ? "-i" : "",
|
|
143
|
+
"-E", // Extended regex
|
|
144
|
+
...includeArgs,
|
|
145
|
+
"--exclude-dir=node_modules",
|
|
146
|
+
"--exclude-dir=dist",
|
|
147
|
+
"--exclude-dir=.next",
|
|
148
|
+
"--exclude-dir=.git",
|
|
149
|
+
pattern,
|
|
150
|
+
"."
|
|
151
|
+
].filter(Boolean);
|
|
152
|
+
try {
|
|
153
|
+
// Don't use shell on Windows - it breaks argument handling
|
|
154
|
+
const result = (0, child_process_1.spawnSync)("grep", args, {
|
|
155
|
+
cwd,
|
|
156
|
+
encoding: "utf-8",
|
|
157
|
+
});
|
|
158
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
159
|
+
return { found: false, matches: [], matchCount: 0 };
|
|
160
|
+
}
|
|
161
|
+
const matches = parseGrepOutput(result.stdout || "");
|
|
162
|
+
return {
|
|
163
|
+
found: matches.length > 0,
|
|
164
|
+
matches,
|
|
165
|
+
matchCount: matches.length
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { found: false, matches: [], matchCount: 0 };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function parseGrepOutput(output) {
|
|
173
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
174
|
+
const matches = [];
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
// Format: ./path/to/file.ts:123:matched line content
|
|
177
|
+
const match = line.match(/^\.?\/?([^:]+):(\d+):(.*)$/);
|
|
178
|
+
if (match) {
|
|
179
|
+
const [, path, lineStr, content] = match;
|
|
180
|
+
const lineNum = parseInt(lineStr, 10);
|
|
181
|
+
matches.push({
|
|
182
|
+
path,
|
|
183
|
+
lineStart: lineNum,
|
|
184
|
+
lineEnd: lineNum,
|
|
185
|
+
snippet: content.trim(),
|
|
186
|
+
matchText: content.trim()
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return matches;
|
|
191
|
+
}
|
|
192
|
+
// ============================================
|
|
193
|
+
// CONTEXT FETCHING
|
|
194
|
+
// ============================================
|
|
195
|
+
const fs_1 = require("fs");
|
|
196
|
+
const path_1 = require("path");
|
|
197
|
+
/**
|
|
198
|
+
* Hent flere linjer rundt en match for bedre kontekst
|
|
199
|
+
*/
|
|
200
|
+
function getExpandedContext(cwd, filePath, lineNumber, contextBefore = 3, contextAfter = 3) {
|
|
201
|
+
const fullPath = (0, path_1.join)(cwd, filePath);
|
|
202
|
+
if (!(0, fs_1.existsSync)(fullPath)) {
|
|
203
|
+
return "[File not found]";
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const content = (0, fs_1.readFileSync)(fullPath, "utf-8");
|
|
207
|
+
const lines = content.split("\n");
|
|
208
|
+
const start = Math.max(0, lineNumber - 1 - contextBefore);
|
|
209
|
+
const end = Math.min(lines.length, lineNumber + contextAfter);
|
|
210
|
+
return lines
|
|
211
|
+
.slice(start, end)
|
|
212
|
+
.map((line, i) => {
|
|
213
|
+
const num = start + i + 1;
|
|
214
|
+
const marker = num === lineNumber ? "→" : " ";
|
|
215
|
+
return `${marker} ${num.toString().padStart(4)}: ${line}`;
|
|
216
|
+
})
|
|
217
|
+
.join("\n");
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return "[Could not read file]";
|
|
221
|
+
}
|
|
222
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sertivibed",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Evidence-first security scanner for Supabase/React apps",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sertivibed": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "ts-node src/cli.ts",
|
|
11
|
+
"scan": "ts-node src/cli.ts scan",
|
|
12
|
+
"test": "node --test"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/sertivibed/sertivibed-cli"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"security",
|
|
20
|
+
"supabase",
|
|
21
|
+
"rls",
|
|
22
|
+
"scanner",
|
|
23
|
+
"audit",
|
|
24
|
+
"react",
|
|
25
|
+
"nextjs",
|
|
26
|
+
"prisma"
|
|
27
|
+
],
|
|
28
|
+
"author": "Håkon Haugen",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.0.0",
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"handlebars": "^4.7.8"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"typescript": "^5.0.0",
|
|
38
|
+
"ts-node": "^10.9.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"templates"
|
|
46
|
+
]
|
|
47
|
+
}
|