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/scan.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sertivibed Scanner
|
|
4
|
+
*
|
|
5
|
+
* Hovedlogikk for å kjøre regler og samle claims.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.RULES = void 0;
|
|
9
|
+
exports.scan = scan;
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
const index_1 = require("./rules/index");
|
|
12
|
+
const rg_1 = require("./utils/rg");
|
|
13
|
+
const fs_1 = require("./utils/fs");
|
|
14
|
+
// ============================================
|
|
15
|
+
// MAIN SCAN FUNCTION
|
|
16
|
+
// ============================================
|
|
17
|
+
async function scan(config) {
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
const { targetPath, verbose } = config;
|
|
20
|
+
if (verbose)
|
|
21
|
+
console.log(`🔍 Scanning ${targetPath}...`);
|
|
22
|
+
// Detect project
|
|
23
|
+
const project = (0, fs_1.detectProject)(targetPath);
|
|
24
|
+
if (verbose)
|
|
25
|
+
console.log(`📦 Project: ${project.name} (${project.stack.join(", ")})`);
|
|
26
|
+
// Discover files
|
|
27
|
+
const allFiles = (0, fs_1.discoverFiles)(targetPath);
|
|
28
|
+
if (verbose)
|
|
29
|
+
console.log(`📁 Found ${allFiles.length} files to analyze`);
|
|
30
|
+
// Run all rules
|
|
31
|
+
const claims = [];
|
|
32
|
+
const rulesChecked = [];
|
|
33
|
+
const rulesSkipped = [];
|
|
34
|
+
for (const rule of index_1.RULES) {
|
|
35
|
+
// Skip RLS rules for Prisma projects without Supabase
|
|
36
|
+
// Prisma handles authorization differently (API routes, middleware)
|
|
37
|
+
if (rule.category === "RLS" && project.hasPrisma && !project.hasSupabase) {
|
|
38
|
+
if (verbose)
|
|
39
|
+
console.log(` ⏭️ Skipping ${rule.id}: Prisma project (RLS not applicable)`);
|
|
40
|
+
rulesSkipped.push({
|
|
41
|
+
ruleId: rule.id,
|
|
42
|
+
reason: "Prisma detected - check API authorization instead"
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (verbose)
|
|
47
|
+
console.log(` ⏳ Checking ${rule.id}: ${rule.title}`);
|
|
48
|
+
const ruleClaims = runRule(rule, targetPath, verbose);
|
|
49
|
+
claims.push(...ruleClaims);
|
|
50
|
+
rulesChecked.push(rule.id);
|
|
51
|
+
if (verbose && ruleClaims.length > 0) {
|
|
52
|
+
console.log(` ⚠️ Found ${ruleClaims.length} issue(s)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Add info claim for Prisma projects
|
|
56
|
+
if (project.hasPrisma && !project.hasSupabase && rulesSkipped.length > 0) {
|
|
57
|
+
claims.push({
|
|
58
|
+
ruleId: "INFO001",
|
|
59
|
+
title: "Prisma-prosjekt oppdaget",
|
|
60
|
+
category: "RLS",
|
|
61
|
+
severity: "INFO",
|
|
62
|
+
impactType: "info_leak",
|
|
63
|
+
statement: `RLS-regler hoppet over. Prisma bruker ikke Row Level Security. Sjekk autorisasjon i API-ruter og middleware.`,
|
|
64
|
+
evidence: [],
|
|
65
|
+
verificationSteps: [
|
|
66
|
+
"Sjekk at alle API-ruter validerer brukerens tilgang",
|
|
67
|
+
"Verifiser at middleware beskytter sensitive ruter",
|
|
68
|
+
"Sjekk Prisma queries for proper filtering på user_id"
|
|
69
|
+
],
|
|
70
|
+
fixHint: "Implementer autorisasjonssjekker i API-ruter: if (session.user.id !== resource.userId) throw new Error('Unauthorized')",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Sort claims by severity
|
|
74
|
+
claims.sort((a, b) => types_1.SEVERITY_ORDER[b.severity] - types_1.SEVERITY_ORDER[a.severity]);
|
|
75
|
+
// Build result
|
|
76
|
+
const meta = {
|
|
77
|
+
projectName: project.name,
|
|
78
|
+
projectPath: targetPath,
|
|
79
|
+
stack: project.stack,
|
|
80
|
+
scannedAt: new Date().toISOString(),
|
|
81
|
+
certivibeVersion: "0.1.0",
|
|
82
|
+
rulesVersion: "0.1.0",
|
|
83
|
+
totalFilesScanned: allFiles.length,
|
|
84
|
+
scanDurationMs: Date.now() - startTime,
|
|
85
|
+
};
|
|
86
|
+
const summary = buildSummary(claims, rulesChecked.length);
|
|
87
|
+
return {
|
|
88
|
+
meta,
|
|
89
|
+
summary,
|
|
90
|
+
claims,
|
|
91
|
+
rulesChecked,
|
|
92
|
+
rulesSkipped,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ============================================
|
|
96
|
+
// RULE EXECUTION
|
|
97
|
+
// ============================================
|
|
98
|
+
function runRule(rule, cwd, verbose) {
|
|
99
|
+
switch (rule.kind) {
|
|
100
|
+
case "presence":
|
|
101
|
+
return runPresenceRule(rule, cwd);
|
|
102
|
+
case "absence":
|
|
103
|
+
return runAbsenceRule(rule, cwd);
|
|
104
|
+
case "correlation":
|
|
105
|
+
return runCorrelationRule(rule, cwd, verbose);
|
|
106
|
+
default:
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Presence rule: Problem hvis pattern FINNES
|
|
112
|
+
*/
|
|
113
|
+
function runPresenceRule(rule, cwd) {
|
|
114
|
+
if (!rule.pattern)
|
|
115
|
+
return [];
|
|
116
|
+
const result = (0, rg_1.search)({
|
|
117
|
+
pattern: rule.pattern,
|
|
118
|
+
paths: rule.paths,
|
|
119
|
+
cwd,
|
|
120
|
+
});
|
|
121
|
+
if (!result.found)
|
|
122
|
+
return [];
|
|
123
|
+
// Dedupliser på fil (ikke rapporter samme fil flere ganger)
|
|
124
|
+
const seenFiles = new Set();
|
|
125
|
+
const uniqueMatches = [];
|
|
126
|
+
for (const match of result.matches) {
|
|
127
|
+
if (!seenFiles.has(match.path)) {
|
|
128
|
+
seenFiles.add(match.path);
|
|
129
|
+
// Hent utvidet kontekst
|
|
130
|
+
match.snippet = (0, rg_1.getExpandedContext)(cwd, match.path, match.lineStart + 2);
|
|
131
|
+
uniqueMatches.push(match);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (uniqueMatches.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
return [{
|
|
137
|
+
ruleId: rule.id,
|
|
138
|
+
title: rule.title,
|
|
139
|
+
category: rule.category,
|
|
140
|
+
severity: rule.severity,
|
|
141
|
+
impactType: rule.impactType,
|
|
142
|
+
statement: `Fant ${uniqueMatches.length} forekomst(er) av potensielt problematisk mønster.`,
|
|
143
|
+
evidence: uniqueMatches,
|
|
144
|
+
verificationSteps: [
|
|
145
|
+
`Søk etter pattern: ${rule.pattern}`,
|
|
146
|
+
`Sjekk filene: ${uniqueMatches.map(m => m.path).join(", ")}`,
|
|
147
|
+
"Vurder om dette er intendert bruk eller et sikkerhetsproblem."
|
|
148
|
+
],
|
|
149
|
+
fixHint: rule.fixHint,
|
|
150
|
+
}];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Absence rule: Problem hvis pattern MANGLER
|
|
154
|
+
*/
|
|
155
|
+
function runAbsenceRule(rule, cwd) {
|
|
156
|
+
if (!rule.pattern)
|
|
157
|
+
return [];
|
|
158
|
+
// Sjekk først om relevante filer finnes
|
|
159
|
+
const sqlFiles = (0, fs_1.findSqlFiles)(cwd);
|
|
160
|
+
if (sqlFiles.length === 0 && rule.category === "RLS") {
|
|
161
|
+
// Ingen SQL-filer, kan ikke sjekke RLS
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const result = (0, rg_1.search)({
|
|
165
|
+
pattern: rule.pattern,
|
|
166
|
+
paths: rule.paths,
|
|
167
|
+
cwd,
|
|
168
|
+
});
|
|
169
|
+
// Hvis pattern finnes, ingen problem
|
|
170
|
+
if (result.found)
|
|
171
|
+
return [];
|
|
172
|
+
// Pattern mangler - dette er et funn
|
|
173
|
+
return [{
|
|
174
|
+
ruleId: rule.id,
|
|
175
|
+
title: rule.title,
|
|
176
|
+
category: rule.category,
|
|
177
|
+
severity: rule.severity,
|
|
178
|
+
impactType: rule.impactType,
|
|
179
|
+
statement: `Forventet mønster ble ikke funnet: ${rule.pattern}`,
|
|
180
|
+
evidence: [], // Ingen evidence for absence
|
|
181
|
+
verificationSteps: [
|
|
182
|
+
`Søk etter: ${rule.pattern}`,
|
|
183
|
+
`I filer som matcher: ${rule.paths.join(", ")}`,
|
|
184
|
+
"Hvis du ikke finner det, er funnet bekreftet."
|
|
185
|
+
],
|
|
186
|
+
fixHint: rule.fixHint,
|
|
187
|
+
}];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Correlation rule: Problem hvis kode finnes MEN SQL mangler
|
|
191
|
+
*/
|
|
192
|
+
function runCorrelationRule(rule, cwd, verbose) {
|
|
193
|
+
if (!rule.correlate)
|
|
194
|
+
return [];
|
|
195
|
+
const { codePattern, codePaths, sqlPattern, sqlPaths, condition } = rule.correlate;
|
|
196
|
+
// Steg 1: Finn kode som bruker funksjonaliteten
|
|
197
|
+
const codeResult = (0, rg_1.search)({
|
|
198
|
+
pattern: codePattern,
|
|
199
|
+
paths: codePaths,
|
|
200
|
+
cwd,
|
|
201
|
+
});
|
|
202
|
+
if (!codeResult.found) {
|
|
203
|
+
// Koden bruker ikke denne funksjonaliteten, ingen problem
|
|
204
|
+
if (verbose)
|
|
205
|
+
console.log(` ℹ️ No code using pattern: ${codePattern}`);
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
// Steg 2: Sjekk om SQL-pattern finnes
|
|
209
|
+
const sqlResult = (0, rg_1.search)({
|
|
210
|
+
pattern: sqlPattern,
|
|
211
|
+
paths: sqlPaths,
|
|
212
|
+
cwd,
|
|
213
|
+
});
|
|
214
|
+
if (condition === "code_without_sql" && !sqlResult.found) {
|
|
215
|
+
// Koden bruker funksjonaliteten, men SQL mangler
|
|
216
|
+
const codeEvidence = codeResult.matches.map(m => ({
|
|
217
|
+
...m,
|
|
218
|
+
snippet: (0, rg_1.getExpandedContext)(cwd, m.path, m.lineStart + 2),
|
|
219
|
+
}));
|
|
220
|
+
return [{
|
|
221
|
+
ruleId: rule.id,
|
|
222
|
+
title: rule.title,
|
|
223
|
+
category: rule.category,
|
|
224
|
+
severity: rule.severity,
|
|
225
|
+
impactType: rule.impactType,
|
|
226
|
+
statement: `Koden bruker ${codePattern} men tilsvarende ${sqlPattern} mangler i SQL.`,
|
|
227
|
+
evidence: codeEvidence,
|
|
228
|
+
verificationSteps: [
|
|
229
|
+
`1. Bekreft at koden bruker: ${codePattern}`,
|
|
230
|
+
`2. Søk etter: ${sqlPattern} i SQL-filer`,
|
|
231
|
+
"3. Hvis SQL-pattern mangler, er funnet bekreftet."
|
|
232
|
+
],
|
|
233
|
+
fixHint: rule.fixHint,
|
|
234
|
+
correlationDetails: {
|
|
235
|
+
codeEvidence,
|
|
236
|
+
sqlMissing: sqlPattern,
|
|
237
|
+
},
|
|
238
|
+
}];
|
|
239
|
+
}
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
// ============================================
|
|
243
|
+
// SUMMARY BUILDING
|
|
244
|
+
// ============================================
|
|
245
|
+
function buildSummary(claims, rulesChecked) {
|
|
246
|
+
const bySeverity = {
|
|
247
|
+
INFO: 0,
|
|
248
|
+
LOW: 0,
|
|
249
|
+
MEDIUM: 0,
|
|
250
|
+
HIGH: 0,
|
|
251
|
+
CRITICAL: 0,
|
|
252
|
+
};
|
|
253
|
+
const byCategory = {
|
|
254
|
+
RLS: 0,
|
|
255
|
+
AUTH: 0,
|
|
256
|
+
XSS: 0,
|
|
257
|
+
SECRETS: 0,
|
|
258
|
+
PRIVACY: 0,
|
|
259
|
+
STORAGE: 0,
|
|
260
|
+
CONFIG: 0,
|
|
261
|
+
};
|
|
262
|
+
for (const claim of claims) {
|
|
263
|
+
bySeverity[claim.severity]++;
|
|
264
|
+
byCategory[claim.category]++;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
total: claims.length,
|
|
268
|
+
bySeverity,
|
|
269
|
+
byCategory,
|
|
270
|
+
passed: rulesChecked - claims.length,
|
|
271
|
+
failed: claims.length,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ============================================
|
|
275
|
+
// EXPORTS
|
|
276
|
+
// ============================================
|
|
277
|
+
var index_2 = require("./rules/index");
|
|
278
|
+
Object.defineProperty(exports, "RULES", { enumerable: true, get: function () { return index_2.RULES; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sertivibed Types
|
|
3
|
+
*
|
|
4
|
+
* Datamodeller for scanning, regler, claims og rapporter.
|
|
5
|
+
*/
|
|
6
|
+
export type Severity = "INFO" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
|
7
|
+
export declare const SEVERITY_ORDER: Record<Severity, number>;
|
|
8
|
+
export type Category = "RLS" | "AUTH" | "XSS" | "SECRETS" | "PRIVACY" | "STORAGE" | "CONFIG";
|
|
9
|
+
export type RuleKind = "presence" | "absence" | "correlation";
|
|
10
|
+
export interface Rule {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
category: Category;
|
|
14
|
+
severity: Severity;
|
|
15
|
+
kind: RuleKind;
|
|
16
|
+
pattern?: string;
|
|
17
|
+
paths: string[];
|
|
18
|
+
correlate?: {
|
|
19
|
+
codePattern: string;
|
|
20
|
+
codePaths: string[];
|
|
21
|
+
sqlPattern: string;
|
|
22
|
+
sqlPaths: string[];
|
|
23
|
+
condition: "code_without_sql";
|
|
24
|
+
};
|
|
25
|
+
rationale: string;
|
|
26
|
+
fixHint: string;
|
|
27
|
+
impactType: ImpactType;
|
|
28
|
+
}
|
|
29
|
+
export type ImpactType = "data_exposure" | "privilege_escalation" | "functional_break" | "info_leak";
|
|
30
|
+
export interface Evidence {
|
|
31
|
+
path: string;
|
|
32
|
+
lineStart: number;
|
|
33
|
+
lineEnd: number;
|
|
34
|
+
snippet: string;
|
|
35
|
+
matchText?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface Claim {
|
|
38
|
+
ruleId: string;
|
|
39
|
+
title: string;
|
|
40
|
+
category: Category;
|
|
41
|
+
severity: Severity;
|
|
42
|
+
impactType: ImpactType;
|
|
43
|
+
statement: string;
|
|
44
|
+
evidence: Evidence[];
|
|
45
|
+
verificationSteps: string[];
|
|
46
|
+
fixHint: string;
|
|
47
|
+
correlationDetails?: {
|
|
48
|
+
codeEvidence: Evidence[];
|
|
49
|
+
sqlMissing: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface ScanMeta {
|
|
53
|
+
projectName: string;
|
|
54
|
+
projectPath: string;
|
|
55
|
+
stack: string[];
|
|
56
|
+
scannedAt: string;
|
|
57
|
+
certivibeVersion: string;
|
|
58
|
+
rulesVersion: string;
|
|
59
|
+
totalFilesScanned: number;
|
|
60
|
+
scanDurationMs: number;
|
|
61
|
+
}
|
|
62
|
+
export interface ScanSummary {
|
|
63
|
+
total: number;
|
|
64
|
+
bySeverity: Record<Severity, number>;
|
|
65
|
+
byCategory: Record<Category, number>;
|
|
66
|
+
passed: number;
|
|
67
|
+
failed: number;
|
|
68
|
+
}
|
|
69
|
+
export interface SkippedRule {
|
|
70
|
+
ruleId: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
}
|
|
73
|
+
export interface ScanResult {
|
|
74
|
+
meta: ScanMeta;
|
|
75
|
+
summary: ScanSummary;
|
|
76
|
+
claims: Claim[];
|
|
77
|
+
rulesChecked: string[];
|
|
78
|
+
rulesSkipped: SkippedRule[];
|
|
79
|
+
}
|
|
80
|
+
export interface RgMatch {
|
|
81
|
+
type: "match";
|
|
82
|
+
data: {
|
|
83
|
+
path: {
|
|
84
|
+
text: string;
|
|
85
|
+
};
|
|
86
|
+
lines: {
|
|
87
|
+
text: string;
|
|
88
|
+
};
|
|
89
|
+
line_number: number;
|
|
90
|
+
submatches: Array<{
|
|
91
|
+
match: {
|
|
92
|
+
text: string;
|
|
93
|
+
};
|
|
94
|
+
start: number;
|
|
95
|
+
end: number;
|
|
96
|
+
}>;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export interface RgSummary {
|
|
100
|
+
type: "summary";
|
|
101
|
+
data: {
|
|
102
|
+
stats: {
|
|
103
|
+
matches: number;
|
|
104
|
+
searches: number;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export type RgOutput = RgMatch | RgSummary | {
|
|
109
|
+
type: string;
|
|
110
|
+
};
|
|
111
|
+
export interface ScanConfig {
|
|
112
|
+
targetPath: string;
|
|
113
|
+
includePaths?: string[];
|
|
114
|
+
excludePaths?: string[];
|
|
115
|
+
failOn?: Severity;
|
|
116
|
+
outputJson?: string;
|
|
117
|
+
outputMd?: string;
|
|
118
|
+
verbose?: boolean;
|
|
119
|
+
quiet?: boolean;
|
|
120
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sertivibed Types
|
|
4
|
+
*
|
|
5
|
+
* Datamodeller for scanning, regler, claims og rapporter.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.SEVERITY_ORDER = void 0;
|
|
9
|
+
exports.SEVERITY_ORDER = {
|
|
10
|
+
INFO: 0,
|
|
11
|
+
LOW: 1,
|
|
12
|
+
MEDIUM: 2,
|
|
13
|
+
HIGH: 3,
|
|
14
|
+
CRITICAL: 4,
|
|
15
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem Utilities
|
|
3
|
+
*
|
|
4
|
+
* Hjelpefunksjoner for å jobbe med filer og prosjektstruktur.
|
|
5
|
+
*/
|
|
6
|
+
export interface ProjectInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
stack: string[];
|
|
10
|
+
hasSupabase: boolean;
|
|
11
|
+
hasNextJs: boolean;
|
|
12
|
+
hasReact: boolean;
|
|
13
|
+
hasVite: boolean;
|
|
14
|
+
hasPrisma: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function detectProject(projectPath: string): ProjectInfo;
|
|
17
|
+
export interface FileInfo {
|
|
18
|
+
path: string;
|
|
19
|
+
relativePath: string;
|
|
20
|
+
extension: string;
|
|
21
|
+
size: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function discoverFiles(projectPath: string, extensions?: Set<string>): FileInfo[];
|
|
24
|
+
export declare function readFile(filePath: string): string | null;
|
|
25
|
+
export declare function fileExists(filePath: string): boolean;
|
|
26
|
+
export declare function getLines(filePath: string, startLine: number, endLine: number): string[];
|
|
27
|
+
export declare function findSqlFiles(projectPath: string): string[];
|
|
28
|
+
export declare function findSourceFiles(projectPath: string): string[];
|
|
29
|
+
export interface TableInfo {
|
|
30
|
+
name: string;
|
|
31
|
+
file: string;
|
|
32
|
+
hasRls: boolean;
|
|
33
|
+
policies: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function extractTables(sqlContent: string, fileName: string): TableInfo[];
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Filesystem Utilities
|
|
4
|
+
*
|
|
5
|
+
* Hjelpefunksjoner for å jobbe med filer og prosjektstruktur.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.detectProject = detectProject;
|
|
9
|
+
exports.discoverFiles = discoverFiles;
|
|
10
|
+
exports.readFile = readFile;
|
|
11
|
+
exports.fileExists = fileExists;
|
|
12
|
+
exports.getLines = getLines;
|
|
13
|
+
exports.findSqlFiles = findSqlFiles;
|
|
14
|
+
exports.findSourceFiles = findSourceFiles;
|
|
15
|
+
exports.extractTables = extractTables;
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
function detectProject(projectPath) {
|
|
19
|
+
const info = {
|
|
20
|
+
name: "unknown",
|
|
21
|
+
path: projectPath,
|
|
22
|
+
stack: [],
|
|
23
|
+
hasSupabase: false,
|
|
24
|
+
hasNextJs: false,
|
|
25
|
+
hasReact: false,
|
|
26
|
+
hasVite: false,
|
|
27
|
+
hasPrisma: false,
|
|
28
|
+
};
|
|
29
|
+
// Les package.json
|
|
30
|
+
const pkgPath = (0, path_1.join)(projectPath, "package.json");
|
|
31
|
+
if ((0, fs_1.existsSync)(pkgPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, "utf-8"));
|
|
34
|
+
info.name = pkg.name || "unknown";
|
|
35
|
+
const allDeps = {
|
|
36
|
+
...pkg.dependencies,
|
|
37
|
+
...pkg.devDependencies,
|
|
38
|
+
};
|
|
39
|
+
// Detect stack
|
|
40
|
+
if (allDeps["@supabase/supabase-js"] || allDeps["@supabase/ssr"]) {
|
|
41
|
+
info.hasSupabase = true;
|
|
42
|
+
info.stack.push("Supabase");
|
|
43
|
+
}
|
|
44
|
+
if (allDeps["next"]) {
|
|
45
|
+
info.hasNextJs = true;
|
|
46
|
+
info.stack.push("Next.js");
|
|
47
|
+
}
|
|
48
|
+
if (allDeps["react"]) {
|
|
49
|
+
info.hasReact = true;
|
|
50
|
+
info.stack.push("React");
|
|
51
|
+
}
|
|
52
|
+
if (allDeps["vite"]) {
|
|
53
|
+
info.hasVite = true;
|
|
54
|
+
info.stack.push("Vite");
|
|
55
|
+
}
|
|
56
|
+
if (allDeps["typescript"]) {
|
|
57
|
+
info.stack.push("TypeScript");
|
|
58
|
+
}
|
|
59
|
+
if (allDeps["prisma"] || allDeps["@prisma/client"]) {
|
|
60
|
+
info.hasPrisma = true;
|
|
61
|
+
info.stack.push("Prisma");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Ignore parse errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Check for Supabase files
|
|
69
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(projectPath, "supabase")) ||
|
|
70
|
+
(0, fs_1.existsSync)((0, path_1.join)(projectPath, "supabase-setup.sql"))) {
|
|
71
|
+
if (!info.hasSupabase) {
|
|
72
|
+
info.hasSupabase = true;
|
|
73
|
+
info.stack.push("Supabase");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Check for Prisma folder
|
|
77
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(projectPath, "prisma"))) {
|
|
78
|
+
if (!info.hasPrisma) {
|
|
79
|
+
info.hasPrisma = true;
|
|
80
|
+
info.stack.push("Prisma");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return info;
|
|
84
|
+
}
|
|
85
|
+
const IGNORED_DIRS = new Set([
|
|
86
|
+
"node_modules",
|
|
87
|
+
".git",
|
|
88
|
+
"dist",
|
|
89
|
+
"build",
|
|
90
|
+
".next",
|
|
91
|
+
".vercel",
|
|
92
|
+
"coverage",
|
|
93
|
+
".nyc_output",
|
|
94
|
+
]);
|
|
95
|
+
const SCAN_EXTENSIONS = new Set([
|
|
96
|
+
".ts",
|
|
97
|
+
".tsx",
|
|
98
|
+
".js",
|
|
99
|
+
".jsx",
|
|
100
|
+
".sql",
|
|
101
|
+
".json",
|
|
102
|
+
".mjs",
|
|
103
|
+
".cjs",
|
|
104
|
+
]);
|
|
105
|
+
function discoverFiles(projectPath, extensions = SCAN_EXTENSIONS) {
|
|
106
|
+
const files = [];
|
|
107
|
+
function walk(dir) {
|
|
108
|
+
try {
|
|
109
|
+
const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const fullPath = (0, path_1.join)(dir, entry.name);
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
114
|
+
walk(fullPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (entry.isFile()) {
|
|
118
|
+
const ext = (0, path_1.extname)(entry.name);
|
|
119
|
+
if (extensions.has(ext)) {
|
|
120
|
+
const stat = (0, fs_1.statSync)(fullPath);
|
|
121
|
+
files.push({
|
|
122
|
+
path: fullPath,
|
|
123
|
+
relativePath: (0, path_1.relative)(projectPath, fullPath),
|
|
124
|
+
extension: ext,
|
|
125
|
+
size: stat.size,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Ignore permission errors etc
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
walk(projectPath);
|
|
136
|
+
return files;
|
|
137
|
+
}
|
|
138
|
+
// ============================================
|
|
139
|
+
// FILE READING
|
|
140
|
+
// ============================================
|
|
141
|
+
function readFile(filePath) {
|
|
142
|
+
try {
|
|
143
|
+
return (0, fs_1.readFileSync)(filePath, "utf-8");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function fileExists(filePath) {
|
|
150
|
+
return (0, fs_1.existsSync)(filePath);
|
|
151
|
+
}
|
|
152
|
+
function getLines(filePath, startLine, endLine) {
|
|
153
|
+
const content = readFile(filePath);
|
|
154
|
+
if (!content)
|
|
155
|
+
return [];
|
|
156
|
+
const lines = content.split("\n");
|
|
157
|
+
return lines.slice(Math.max(0, startLine - 1), endLine);
|
|
158
|
+
}
|
|
159
|
+
// ============================================
|
|
160
|
+
// SQL FILE HELPERS
|
|
161
|
+
// ============================================
|
|
162
|
+
function findSqlFiles(projectPath) {
|
|
163
|
+
const files = discoverFiles(projectPath, new Set([".sql"]));
|
|
164
|
+
return files.map((f) => f.relativePath);
|
|
165
|
+
}
|
|
166
|
+
function findSourceFiles(projectPath) {
|
|
167
|
+
const files = discoverFiles(projectPath, new Set([".ts", ".tsx", ".js", ".jsx"]));
|
|
168
|
+
return files.map((f) => f.relativePath);
|
|
169
|
+
}
|
|
170
|
+
function extractTables(sqlContent, fileName) {
|
|
171
|
+
const tables = [];
|
|
172
|
+
// Find CREATE TABLE statements
|
|
173
|
+
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?(\w+)/gi;
|
|
174
|
+
let match;
|
|
175
|
+
while ((match = tableRegex.exec(sqlContent)) !== null) {
|
|
176
|
+
const tableName = match[1];
|
|
177
|
+
// Check if RLS is enabled for this table
|
|
178
|
+
const rlsRegex = new RegExp(`ALTER\\s+TABLE\\s+(?:public\\.)?${tableName}\\s+ENABLE\\s+ROW\\s+LEVEL\\s+SECURITY`, "i");
|
|
179
|
+
const hasRls = rlsRegex.test(sqlContent);
|
|
180
|
+
// Find policies for this table
|
|
181
|
+
const policyRegex = new RegExp(`CREATE\\s+POLICY\\s+[\\w"']+\\s+ON\\s+(?:public\\.)?${tableName}`, "gi");
|
|
182
|
+
const policies = [];
|
|
183
|
+
let policyMatch;
|
|
184
|
+
while ((policyMatch = policyRegex.exec(sqlContent)) !== null) {
|
|
185
|
+
policies.push(policyMatch[0]);
|
|
186
|
+
}
|
|
187
|
+
tables.push({
|
|
188
|
+
name: tableName,
|
|
189
|
+
file: fileName,
|
|
190
|
+
hasRls,
|
|
191
|
+
policies,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return tables;
|
|
195
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ripgrep Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Kjører ripgrep (rg) med JSON output for stabil parsing.
|
|
5
|
+
* Fallback til grep hvis rg ikke er installert.
|
|
6
|
+
*/
|
|
7
|
+
import { Evidence } from "../types";
|
|
8
|
+
export declare function isRipgrepAvailable(): boolean;
|
|
9
|
+
export interface SearchOptions {
|
|
10
|
+
pattern: string;
|
|
11
|
+
paths: string[];
|
|
12
|
+
cwd: string;
|
|
13
|
+
ignoreCase?: boolean;
|
|
14
|
+
contextLines?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SearchResult {
|
|
17
|
+
found: boolean;
|
|
18
|
+
matches: Evidence[];
|
|
19
|
+
matchCount: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Søk etter pattern i filer med ripgrep
|
|
23
|
+
*/
|
|
24
|
+
export declare function search(options: SearchOptions): SearchResult;
|
|
25
|
+
/**
|
|
26
|
+
* Sjekk om pattern IKKE finnes (for absence-regler)
|
|
27
|
+
*/
|
|
28
|
+
export declare function searchAbsence(options: SearchOptions): SearchResult;
|
|
29
|
+
/**
|
|
30
|
+
* Hent flere linjer rundt en match for bedre kontekst
|
|
31
|
+
*/
|
|
32
|
+
export declare function getExpandedContext(cwd: string, filePath: string, lineNumber: number, contextBefore?: number, contextAfter?: number): string;
|