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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Markdown Reporter
|
|
4
|
+
*
|
|
5
|
+
* Genererer profesjonell rapport UTEN LLM.
|
|
6
|
+
* Alt er deterministisk og basert på claims + evidence.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.generateMarkdownReport = generateMarkdownReport;
|
|
10
|
+
const types_1 = require("../types");
|
|
11
|
+
// ============================================
|
|
12
|
+
// MAIN EXPORT
|
|
13
|
+
// ============================================
|
|
14
|
+
function generateMarkdownReport(result) {
|
|
15
|
+
const sections = [];
|
|
16
|
+
sections.push(generateHeader(result));
|
|
17
|
+
sections.push(generateScopeSection(result));
|
|
18
|
+
sections.push(generateCoverageSection(result));
|
|
19
|
+
sections.push(generateSummarySection(result));
|
|
20
|
+
if (result.claims.length > 0) {
|
|
21
|
+
sections.push(generateFindingsSection(result));
|
|
22
|
+
}
|
|
23
|
+
sections.push(generatePassedSection(result));
|
|
24
|
+
sections.push(generateActionPlanSection(result));
|
|
25
|
+
sections.push(generateFooter(result));
|
|
26
|
+
return sections.join("\n\n");
|
|
27
|
+
}
|
|
28
|
+
// ============================================
|
|
29
|
+
// HEADER
|
|
30
|
+
// ============================================
|
|
31
|
+
function generateHeader(result) {
|
|
32
|
+
const verdict = getVerdict(result);
|
|
33
|
+
const verdictEmoji = verdict === "BESTÅTT" ? "✅" : verdict === "BESTÅTT MED MERKNADER" ? "⚠️" : "❌";
|
|
34
|
+
return `
|
|
35
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
SERTIVIBED SIKKERHETSRAPPORT
|
|
37
|
+
${result.meta.projectName}
|
|
38
|
+
${new Date(result.meta.scannedAt).toLocaleDateString("no-NO")}
|
|
39
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
${verdictEmoji} **Totalvurdering: ${verdict}**
|
|
42
|
+
`.trim();
|
|
43
|
+
}
|
|
44
|
+
// ============================================
|
|
45
|
+
// SCOPE
|
|
46
|
+
// ============================================
|
|
47
|
+
function generateScopeSection(result) {
|
|
48
|
+
const confidence = result.meta.totalFilesScanned > 10 ? "HØY" :
|
|
49
|
+
result.meta.totalFilesScanned > 3 ? "MEDIUM" : "LAV";
|
|
50
|
+
const skippedCount = result.rulesSkipped?.length || 0;
|
|
51
|
+
const totalRules = result.rulesChecked.length + skippedCount;
|
|
52
|
+
return `
|
|
53
|
+
## Scope & Dekning
|
|
54
|
+
|
|
55
|
+
| Område | Detaljer |
|
|
56
|
+
|---------------------|----------------------------------------|
|
|
57
|
+
| Prosjekt | ${result.meta.projectName} |
|
|
58
|
+
| Stack | ${result.meta.stack.join(", ") || "Ukjent"} |
|
|
59
|
+
| Filer analysert | ${result.meta.totalFilesScanned} |
|
|
60
|
+
| Regler kjørt | ${result.rulesChecked.length}/${totalRules} |
|
|
61
|
+
| Varighet | ${result.meta.scanDurationMs}ms |
|
|
62
|
+
| Confidence | ${confidence} |
|
|
63
|
+
`.trim();
|
|
64
|
+
}
|
|
65
|
+
// ============================================
|
|
66
|
+
// COVERAGE
|
|
67
|
+
// ============================================
|
|
68
|
+
function generateCoverageSection(result) {
|
|
69
|
+
const rulesExecuted = result.rulesChecked.join(", ");
|
|
70
|
+
let skippedSection = "";
|
|
71
|
+
if (result.rulesSkipped && result.rulesSkipped.length > 0) {
|
|
72
|
+
const skippedList = result.rulesSkipped
|
|
73
|
+
.map(s => `- ⏭️ ${s.ruleId}: ${s.reason}`)
|
|
74
|
+
.join("\n");
|
|
75
|
+
skippedSection = `
|
|
76
|
+
**Regler hoppet over:**
|
|
77
|
+
${skippedList}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
return `
|
|
81
|
+
## Coverage
|
|
82
|
+
|
|
83
|
+
**Regler kjørt:**
|
|
84
|
+
✅ ${rulesExecuted}
|
|
85
|
+
${skippedSection}
|
|
86
|
+
**Filer analysert:**
|
|
87
|
+
- ${result.meta.totalFilesScanned} totalt
|
|
88
|
+
`.trim();
|
|
89
|
+
}
|
|
90
|
+
// ============================================
|
|
91
|
+
// SUMMARY
|
|
92
|
+
// ============================================
|
|
93
|
+
function generateSummarySection(result) {
|
|
94
|
+
const { summary } = result;
|
|
95
|
+
return `
|
|
96
|
+
## Sammendrag
|
|
97
|
+
|
|
98
|
+
| Severity | Antall | Betydning |
|
|
99
|
+
|-----------|--------|------------------------------------------|
|
|
100
|
+
| 🔴 CRITICAL | ${summary.bySeverity.CRITICAL.toString().padStart(2)} | Må fikses umiddelbart |
|
|
101
|
+
| 🟠 HIGH | ${summary.bySeverity.HIGH.toString().padStart(2)} | Må fikses før produksjon |
|
|
102
|
+
| 🟡 MEDIUM | ${summary.bySeverity.MEDIUM.toString().padStart(2)} | Bør fikses snart |
|
|
103
|
+
| 🟢 LOW | ${summary.bySeverity.LOW.toString().padStart(2)} | Vurder å fikse |
|
|
104
|
+
| ⚪ INFO | ${summary.bySeverity.INFO.toString().padStart(2)} | Informasjon |
|
|
105
|
+
|
|
106
|
+
**Totalt:** ${summary.total} funn | **Bestått:** ${summary.passed} regler | **Feilet:** ${summary.failed} regler
|
|
107
|
+
`.trim();
|
|
108
|
+
}
|
|
109
|
+
// ============================================
|
|
110
|
+
// FINDINGS
|
|
111
|
+
// ============================================
|
|
112
|
+
function generateFindingsSection(result) {
|
|
113
|
+
const findings = result.claims
|
|
114
|
+
.sort((a, b) => types_1.SEVERITY_ORDER[b.severity] - types_1.SEVERITY_ORDER[a.severity])
|
|
115
|
+
.map((claim, index) => generateFinding(claim, index + 1))
|
|
116
|
+
.join("\n\n---\n\n");
|
|
117
|
+
return `
|
|
118
|
+
## Funn
|
|
119
|
+
|
|
120
|
+
${findings}
|
|
121
|
+
`.trim();
|
|
122
|
+
}
|
|
123
|
+
function generateFinding(claim, index) {
|
|
124
|
+
const severityEmoji = getSeverityEmoji(claim.severity);
|
|
125
|
+
const impactLabel = getImpactLabel(claim.impactType);
|
|
126
|
+
let evidenceSection = "";
|
|
127
|
+
if (claim.evidence.length > 0) {
|
|
128
|
+
evidenceSection = `
|
|
129
|
+
**Bevis fra koden:**
|
|
130
|
+
|
|
131
|
+
${claim.evidence.slice(0, 3).map(e => `
|
|
132
|
+
📄 \`${e.path}\` (linje ${e.lineStart}-${e.lineEnd})
|
|
133
|
+
\`\`\`
|
|
134
|
+
${e.snippet}
|
|
135
|
+
\`\`\`
|
|
136
|
+
`).join("\n")}`;
|
|
137
|
+
}
|
|
138
|
+
let correlationSection = "";
|
|
139
|
+
if (claim.correlationDetails) {
|
|
140
|
+
correlationSection = `
|
|
141
|
+
**Kryss-korrelasjon:**
|
|
142
|
+
- Koden bruker funksjonaliteten ✓
|
|
143
|
+
- SQL-pattern som mangler: \`${claim.correlationDetails.sqlMissing}\`
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
return `
|
|
147
|
+
### ${severityEmoji} Funn #${index}: ${claim.title}
|
|
148
|
+
|
|
149
|
+
**Regel:** ${claim.ruleId}
|
|
150
|
+
**Kategori:** ${claim.category}
|
|
151
|
+
**Severity:** ${claim.severity}
|
|
152
|
+
**Impact:** ${impactLabel}
|
|
153
|
+
|
|
154
|
+
**Beskrivelse:**
|
|
155
|
+
${claim.statement}
|
|
156
|
+
${evidenceSection}
|
|
157
|
+
${correlationSection}
|
|
158
|
+
**Verifisering:**
|
|
159
|
+
${claim.verificationSteps.map((s, i) => `${i + 1}. ${s}`).join("\n")}
|
|
160
|
+
|
|
161
|
+
**Anbefalt fiks:**
|
|
162
|
+
\`\`\`
|
|
163
|
+
${claim.fixHint}
|
|
164
|
+
\`\`\`
|
|
165
|
+
`.trim();
|
|
166
|
+
}
|
|
167
|
+
// ============================================
|
|
168
|
+
// PASSED
|
|
169
|
+
// ============================================
|
|
170
|
+
function generatePassedSection(result) {
|
|
171
|
+
const failedRules = new Set(result.claims.map(c => c.ruleId));
|
|
172
|
+
const passedRules = result.rulesChecked.filter(id => !failedRules.has(id));
|
|
173
|
+
if (passedRules.length === 0) {
|
|
174
|
+
return `## Bestått\n\nIngen regler bestått uten funn.`;
|
|
175
|
+
}
|
|
176
|
+
return `
|
|
177
|
+
## Bestått (ingen funn)
|
|
178
|
+
|
|
179
|
+
${passedRules.map(id => `✅ ${id}`).join("\n")}
|
|
180
|
+
`.trim();
|
|
181
|
+
}
|
|
182
|
+
// ============================================
|
|
183
|
+
// ACTION PLAN
|
|
184
|
+
// ============================================
|
|
185
|
+
function generateActionPlanSection(result) {
|
|
186
|
+
if (result.claims.length === 0) {
|
|
187
|
+
return `
|
|
188
|
+
## Handlingsplan
|
|
189
|
+
|
|
190
|
+
🎉 Ingen sikkerhetsfunn! Prosjektet bestod alle ${result.rulesChecked.length} sjekker.
|
|
191
|
+
`.trim();
|
|
192
|
+
}
|
|
193
|
+
const prioritized = result.claims
|
|
194
|
+
.sort((a, b) => types_1.SEVERITY_ORDER[b.severity] - types_1.SEVERITY_ORDER[a.severity])
|
|
195
|
+
.slice(0, 5);
|
|
196
|
+
const rows = prioritized.map((claim, i) => {
|
|
197
|
+
const time = estimateFixTime(claim);
|
|
198
|
+
return `| ${i + 1} | ${claim.title} | ${claim.severity} | ${time} |`;
|
|
199
|
+
});
|
|
200
|
+
return `
|
|
201
|
+
## Prioritert Handlingsplan
|
|
202
|
+
|
|
203
|
+
| # | Funn | Severity | Estimert tid |
|
|
204
|
+
|---|------|----------|--------------|
|
|
205
|
+
${rows.join("\n")}
|
|
206
|
+
|
|
207
|
+
**Anbefaling:** Start med CRITICAL og HIGH severity funn. Disse bør fikses før produksjon.
|
|
208
|
+
`.trim();
|
|
209
|
+
}
|
|
210
|
+
// ============================================
|
|
211
|
+
// FOOTER
|
|
212
|
+
// ============================================
|
|
213
|
+
function generateFooter(result) {
|
|
214
|
+
return `
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Metadata
|
|
218
|
+
|
|
219
|
+
| | |
|
|
220
|
+
|---|---|
|
|
221
|
+
| Rapport generert | ${result.meta.scannedAt} |
|
|
222
|
+
| Sertivibed versjon | ${result.meta.certivibeVersion} |
|
|
223
|
+
| Regler versjon | ${result.meta.rulesVersion} |
|
|
224
|
+
|
|
225
|
+
**Ansvarsfraskrivelse:**
|
|
226
|
+
Denne rapporten er en automatisert screening av vanlige sikkerhetsfeil.
|
|
227
|
+
Den erstatter ikke en full sikkerhetsrevisjon utført av sertifiserte eksperter.
|
|
228
|
+
Sertivibed garanterer ikke at alle sårbarheter er identifisert.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
*Generert av Sertivibed — Evidence-first security screening*
|
|
232
|
+
`.trim();
|
|
233
|
+
}
|
|
234
|
+
// ============================================
|
|
235
|
+
// HELPERS
|
|
236
|
+
// ============================================
|
|
237
|
+
function getVerdict(result) {
|
|
238
|
+
const { summary } = result;
|
|
239
|
+
if (summary.bySeverity.CRITICAL > 0)
|
|
240
|
+
return "IKKE BESTÅTT";
|
|
241
|
+
if (summary.bySeverity.HIGH >= 2)
|
|
242
|
+
return "IKKE BESTÅTT";
|
|
243
|
+
if (summary.bySeverity.HIGH === 1)
|
|
244
|
+
return "BESTÅTT MED MERKNADER";
|
|
245
|
+
if (summary.bySeverity.MEDIUM > 2)
|
|
246
|
+
return "BESTÅTT MED MERKNADER";
|
|
247
|
+
return "BESTÅTT";
|
|
248
|
+
}
|
|
249
|
+
function getSeverityEmoji(severity) {
|
|
250
|
+
switch (severity) {
|
|
251
|
+
case "CRITICAL": return "🔴";
|
|
252
|
+
case "HIGH": return "🟠";
|
|
253
|
+
case "MEDIUM": return "🟡";
|
|
254
|
+
case "LOW": return "🟢";
|
|
255
|
+
case "INFO": return "⚪";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function getImpactLabel(impact) {
|
|
259
|
+
switch (impact) {
|
|
260
|
+
case "data_exposure": return "Dataeksponering";
|
|
261
|
+
case "privilege_escalation": return "Privilegie-eskalering";
|
|
262
|
+
case "functional_break": return "Funksjonell feil";
|
|
263
|
+
case "info_leak": return "Informasjonslekkasje";
|
|
264
|
+
default: return impact;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function estimateFixTime(claim) {
|
|
268
|
+
switch (claim.category) {
|
|
269
|
+
case "RLS": return "15-30 min";
|
|
270
|
+
case "AUTH": return "30-60 min";
|
|
271
|
+
case "XSS": return "10-20 min";
|
|
272
|
+
case "SECRETS": return "5-10 min";
|
|
273
|
+
case "PRIVACY": return "10-15 min";
|
|
274
|
+
case "STORAGE": return "20-30 min";
|
|
275
|
+
case "CONFIG": return "10-15 min";
|
|
276
|
+
default: return "15-30 min";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sertivibed Rules v0.1
|
|
3
|
+
*
|
|
4
|
+
* 12 regler som gir høy verdi med lav falsk positiv rate.
|
|
5
|
+
*
|
|
6
|
+
* Kategorier:
|
|
7
|
+
* - RLS (3 regler): Database row-level security
|
|
8
|
+
* - AUTH (1 regel): Service role leakage
|
|
9
|
+
* - PRIVACY (2 regler): Token storage, PII logging
|
|
10
|
+
* - SECRETS (1 regel): Hardcoded API keys
|
|
11
|
+
* - XSS (2 regler): Frontend injection risks
|
|
12
|
+
* - STORAGE (2 regler): File upload security
|
|
13
|
+
* - CONFIG (1 regel): Security headers
|
|
14
|
+
*/
|
|
15
|
+
import { Rule } from "../types";
|
|
16
|
+
export declare const RULES: Rule[];
|
|
17
|
+
export declare function getRuleById(id: string): Rule | undefined;
|
|
18
|
+
export declare function getRulesByCategory(category: Rule["category"]): Rule[];
|
|
19
|
+
export declare function getRulesBySeverity(severity: Rule["severity"]): Rule[];
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sertivibed Rules v0.1
|
|
4
|
+
*
|
|
5
|
+
* 12 regler som gir høy verdi med lav falsk positiv rate.
|
|
6
|
+
*
|
|
7
|
+
* Kategorier:
|
|
8
|
+
* - RLS (3 regler): Database row-level security
|
|
9
|
+
* - AUTH (1 regel): Service role leakage
|
|
10
|
+
* - PRIVACY (2 regler): Token storage, PII logging
|
|
11
|
+
* - SECRETS (1 regel): Hardcoded API keys
|
|
12
|
+
* - XSS (2 regler): Frontend injection risks
|
|
13
|
+
* - STORAGE (2 regler): File upload security
|
|
14
|
+
* - CONFIG (1 regel): Security headers
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.RULES = void 0;
|
|
18
|
+
exports.getRuleById = getRuleById;
|
|
19
|
+
exports.getRulesByCategory = getRulesByCategory;
|
|
20
|
+
exports.getRulesBySeverity = getRulesBySeverity;
|
|
21
|
+
exports.RULES = [
|
|
22
|
+
// ============================================
|
|
23
|
+
// RLS RULES (3)
|
|
24
|
+
// ============================================
|
|
25
|
+
{
|
|
26
|
+
id: "RLS001",
|
|
27
|
+
title: "RLS ikke aktivert på tabell",
|
|
28
|
+
category: "RLS",
|
|
29
|
+
severity: "CRITICAL",
|
|
30
|
+
kind: "absence",
|
|
31
|
+
pattern: "ENABLE ROW LEVEL SECURITY",
|
|
32
|
+
paths: [
|
|
33
|
+
"**/supabase-setup.sql",
|
|
34
|
+
"**/supabase/migrations/**/*.sql",
|
|
35
|
+
"**/*.sql"
|
|
36
|
+
],
|
|
37
|
+
rationale: "Uten RLS kan enhver autentisert bruker lese alle rader i tabellen. Dette er den vanligste og mest alvorlige feilen i Supabase-apper.",
|
|
38
|
+
fixHint: "ALTER TABLE <tabellnavn> ENABLE ROW LEVEL SECURITY;",
|
|
39
|
+
impactType: "data_exposure"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "RLS002",
|
|
43
|
+
title: "Manglende DELETE policy når .delete() brukes",
|
|
44
|
+
category: "RLS",
|
|
45
|
+
severity: "MEDIUM",
|
|
46
|
+
kind: "correlation",
|
|
47
|
+
paths: [], // Ikke brukt for correlation
|
|
48
|
+
correlate: {
|
|
49
|
+
codePattern: "\\.delete\\s*\\(",
|
|
50
|
+
codePaths: ["**/src/**/*.ts", "**/src/**/*.tsx", "**/src/**/*.js", "**/src/**/*.jsx"],
|
|
51
|
+
sqlPattern: "FOR DELETE",
|
|
52
|
+
sqlPaths: ["**/*.sql"],
|
|
53
|
+
condition: "code_without_sql"
|
|
54
|
+
},
|
|
55
|
+
rationale: "Koden prøver å slette data, men ingen DELETE-policy finnes. Dette vil enten feile eller (verre) tillate sletting uten autorisasjonssjekk.",
|
|
56
|
+
fixHint: "CREATE POLICY \"users_delete_own\" ON <tabell> FOR DELETE USING (auth.uid() = user_id);",
|
|
57
|
+
impactType: "functional_break"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "RLS003",
|
|
61
|
+
title: "Policy mangler eierskapssjekk",
|
|
62
|
+
category: "RLS",
|
|
63
|
+
severity: "HIGH",
|
|
64
|
+
kind: "presence",
|
|
65
|
+
pattern: "USING\\s*\\(\\s*true\\s*\\)|USING\\s*\\(\\s*auth\\.uid\\(\\)\\s+IS\\s+NOT\\s+NULL\\s*\\)",
|
|
66
|
+
paths: ["**/*.sql"],
|
|
67
|
+
rationale: "En policy som bare sjekker 'true' eller 'auth.uid() IS NOT NULL' bekrefter innlogging, men ikke eierskap. Bruker A kan lese bruker B sine data.",
|
|
68
|
+
fixHint: "Endre USING (true) til USING (user_id = auth.uid())",
|
|
69
|
+
impactType: "data_exposure"
|
|
70
|
+
},
|
|
71
|
+
// ============================================
|
|
72
|
+
// AUTH RULES (1)
|
|
73
|
+
// ============================================
|
|
74
|
+
{
|
|
75
|
+
id: "AUTH001",
|
|
76
|
+
title: "Service role key i klientkode",
|
|
77
|
+
category: "AUTH",
|
|
78
|
+
severity: "CRITICAL",
|
|
79
|
+
kind: "presence",
|
|
80
|
+
pattern: "service_role|SUPABASE_SERVICE_ROLE|SERVICE_ROLE_KEY",
|
|
81
|
+
paths: [
|
|
82
|
+
"**/src/**/*.ts",
|
|
83
|
+
"**/src/**/*.tsx",
|
|
84
|
+
"**/src/**/*.js",
|
|
85
|
+
"**/src/**/*.jsx",
|
|
86
|
+
"**/app/**/*.ts",
|
|
87
|
+
"**/app/**/*.tsx",
|
|
88
|
+
"**/pages/**/*.ts",
|
|
89
|
+
"**/pages/**/*.tsx",
|
|
90
|
+
"**/components/**/*.ts",
|
|
91
|
+
"**/components/**/*.tsx"
|
|
92
|
+
],
|
|
93
|
+
rationale: "Service role key omgår ALL Row Level Security. Hvis den er i klientkode, kan en angriper hente den fra browser DevTools og få full databasetilgang.",
|
|
94
|
+
fixHint: "Bruk kun anon key i frontend. Service role kun på server med proper auth.",
|
|
95
|
+
impactType: "privilege_escalation"
|
|
96
|
+
},
|
|
97
|
+
// ============================================
|
|
98
|
+
// PRIVACY RULES (2)
|
|
99
|
+
// ============================================
|
|
100
|
+
{
|
|
101
|
+
id: "PRIV001",
|
|
102
|
+
title: "Auth token i localStorage",
|
|
103
|
+
category: "PRIVACY",
|
|
104
|
+
severity: "MEDIUM",
|
|
105
|
+
kind: "presence",
|
|
106
|
+
pattern: "localStorage\\.(setItem|getItem)\\s*\\([^)]*(?:token|auth|session|jwt|access_token|refresh_token)",
|
|
107
|
+
paths: [
|
|
108
|
+
"**/src/**/*.ts",
|
|
109
|
+
"**/src/**/*.tsx",
|
|
110
|
+
"**/src/**/*.js",
|
|
111
|
+
"**/src/**/*.jsx"
|
|
112
|
+
],
|
|
113
|
+
rationale: "Tokens i localStorage er sårbare for XSS-angrep. Supabase håndterer dette automatisk — manuell lagring tyder på feil implementasjon.",
|
|
114
|
+
fixHint: "La Supabase håndtere token-lagring automatisk, eller bruk httpOnly cookies.",
|
|
115
|
+
impactType: "data_exposure"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "PRIV002",
|
|
119
|
+
title: "PII i console.log",
|
|
120
|
+
category: "PRIVACY",
|
|
121
|
+
severity: "MEDIUM",
|
|
122
|
+
kind: "presence",
|
|
123
|
+
pattern: "console\\.(log|info|debug|warn|error)\\s*\\([^)]*(?:email|password|token|user|session|phone|ssn|personnummer|fødselsnummer)",
|
|
124
|
+
paths: [
|
|
125
|
+
"**/src/**/*.ts",
|
|
126
|
+
"**/src/**/*.tsx",
|
|
127
|
+
"**/src/**/*.js",
|
|
128
|
+
"**/src/**/*.jsx"
|
|
129
|
+
],
|
|
130
|
+
rationale: "Logger i produksjon kan inneholde sensitiv info som ender i browser console, Sentry, eller log-filer. GDPR-brudd waiting to happen.",
|
|
131
|
+
fixHint: "Fjern console.log eller redakt sensitive felter før logging.",
|
|
132
|
+
impactType: "info_leak"
|
|
133
|
+
},
|
|
134
|
+
// ============================================
|
|
135
|
+
// SECRETS RULES (1)
|
|
136
|
+
// ============================================
|
|
137
|
+
{
|
|
138
|
+
id: "SEC001",
|
|
139
|
+
title: "Hardkodet API-nøkkel",
|
|
140
|
+
category: "SECRETS",
|
|
141
|
+
severity: "HIGH",
|
|
142
|
+
kind: "presence",
|
|
143
|
+
// Matcher vanlige API-nøkkel-mønstre, men ikke env-variabler
|
|
144
|
+
pattern: "(?:apiKey|api_key|apikey)\\s*[:=]\\s*['\"][A-Za-z0-9_-]{20,}['\"]|sk-[A-Za-z0-9]{32,}|AIza[A-Za-z0-9_-]{35}",
|
|
145
|
+
paths: [
|
|
146
|
+
"**/src/**/*.ts",
|
|
147
|
+
"**/src/**/*.tsx",
|
|
148
|
+
"**/src/**/*.js",
|
|
149
|
+
"**/src/**/*.jsx",
|
|
150
|
+
"**/*.json"
|
|
151
|
+
],
|
|
152
|
+
rationale: "API-nøkler hardkodet i koden havner i git-historikk og er vanskelig å rotere. Bruk miljøvariabler.",
|
|
153
|
+
fixHint: "Flytt til .env fil og bruk process.env.API_KEY",
|
|
154
|
+
impactType: "privilege_escalation"
|
|
155
|
+
},
|
|
156
|
+
// ============================================
|
|
157
|
+
// XSS RULES (2)
|
|
158
|
+
// ============================================
|
|
159
|
+
{
|
|
160
|
+
id: "XSS001",
|
|
161
|
+
title: "dangerouslySetInnerHTML brukt",
|
|
162
|
+
category: "XSS",
|
|
163
|
+
severity: "HIGH",
|
|
164
|
+
kind: "presence",
|
|
165
|
+
pattern: "dangerouslySetInnerHTML",
|
|
166
|
+
paths: [
|
|
167
|
+
"**/src/**/*.tsx",
|
|
168
|
+
"**/src/**/*.jsx",
|
|
169
|
+
"**/components/**/*.tsx",
|
|
170
|
+
"**/components/**/*.jsx"
|
|
171
|
+
],
|
|
172
|
+
rationale: "dangerouslySetInnerHTML injiserer rå HTML i DOM. Hvis innholdet kommer fra bruker-input eller database, er dette en XSS-vektor.",
|
|
173
|
+
fixHint: "Bruk en sanitizer som DOMPurify, eller unngå rå HTML helt.",
|
|
174
|
+
impactType: "data_exposure"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "XSS002",
|
|
178
|
+
title: "innerHTML assignment",
|
|
179
|
+
category: "XSS",
|
|
180
|
+
severity: "HIGH",
|
|
181
|
+
kind: "presence",
|
|
182
|
+
pattern: "\\.innerHTML\\s*=",
|
|
183
|
+
paths: [
|
|
184
|
+
"**/src/**/*.ts",
|
|
185
|
+
"**/src/**/*.tsx",
|
|
186
|
+
"**/src/**/*.js",
|
|
187
|
+
"**/src/**/*.jsx"
|
|
188
|
+
],
|
|
189
|
+
rationale: "Direkte innerHTML-tilordning er en klassisk XSS-vektor. React bruker dette internt med dangerouslySetInnerHTML, men manuell bruk er risikabelt.",
|
|
190
|
+
fixHint: "Bruk textContent for tekst, eller sanitize HTML med DOMPurify.",
|
|
191
|
+
impactType: "data_exposure"
|
|
192
|
+
},
|
|
193
|
+
// ============================================
|
|
194
|
+
// STORAGE RULES (2)
|
|
195
|
+
// ============================================
|
|
196
|
+
{
|
|
197
|
+
id: "STOR001",
|
|
198
|
+
title: "Fil-opplasting uten validering",
|
|
199
|
+
category: "STORAGE",
|
|
200
|
+
severity: "MEDIUM",
|
|
201
|
+
kind: "presence",
|
|
202
|
+
pattern: "\\.upload\\s*\\(|storage\\.from\\s*\\(",
|
|
203
|
+
paths: [
|
|
204
|
+
"**/src/**/*.ts",
|
|
205
|
+
"**/src/**/*.tsx",
|
|
206
|
+
"**/src/**/*.js",
|
|
207
|
+
"**/src/**/*.jsx"
|
|
208
|
+
],
|
|
209
|
+
rationale: "Filopplasting uten type/størrelse-validering kan føre til storage-misbruk eller ondsinnet innhold. Sjekk at det er validering FØR upload-kallet.",
|
|
210
|
+
fixHint: "Valider filtype (MIME + extension) og størrelse før upload.",
|
|
211
|
+
impactType: "info_leak"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "STOR002",
|
|
215
|
+
title: "Offentlig storage URL uten signering",
|
|
216
|
+
category: "STORAGE",
|
|
217
|
+
severity: "MEDIUM",
|
|
218
|
+
kind: "presence",
|
|
219
|
+
pattern: "getPublicUrl\\s*\\(",
|
|
220
|
+
paths: [
|
|
221
|
+
"**/src/**/*.ts",
|
|
222
|
+
"**/src/**/*.tsx",
|
|
223
|
+
"**/src/**/*.js",
|
|
224
|
+
"**/src/**/*.jsx"
|
|
225
|
+
],
|
|
226
|
+
rationale: "getPublicUrl gir en URL som er tilgjengelig for alle. Hvis filen inneholder sensitiv data, bruk createSignedUrl med kort TTL i stedet.",
|
|
227
|
+
fixHint: "Bruk createSignedUrl for sensitive filer, eller sett opp storage policies.",
|
|
228
|
+
impactType: "data_exposure"
|
|
229
|
+
},
|
|
230
|
+
// ============================================
|
|
231
|
+
// CONFIG RULES (1)
|
|
232
|
+
// ============================================
|
|
233
|
+
{
|
|
234
|
+
id: "CONF001",
|
|
235
|
+
title: "Manglende Content Security Policy",
|
|
236
|
+
category: "CONFIG",
|
|
237
|
+
severity: "LOW",
|
|
238
|
+
kind: "absence",
|
|
239
|
+
pattern: "Content-Security-Policy|contentSecurityPolicy",
|
|
240
|
+
paths: [
|
|
241
|
+
"**/next.config.js",
|
|
242
|
+
"**/next.config.ts",
|
|
243
|
+
"**/next.config.mjs",
|
|
244
|
+
"**/vercel.json",
|
|
245
|
+
"**/middleware.ts"
|
|
246
|
+
],
|
|
247
|
+
rationale: "Content Security Policy (CSP) begrenser hvilke scripts som kan kjøre. Uten CSP er XSS-angrep lettere å utføre.",
|
|
248
|
+
fixHint: "Legg til CSP headers i next.config.js eller middleware.",
|
|
249
|
+
impactType: "info_leak"
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
// ============================================
|
|
253
|
+
// HELPERS
|
|
254
|
+
// ============================================
|
|
255
|
+
function getRuleById(id) {
|
|
256
|
+
return exports.RULES.find(r => r.id === id);
|
|
257
|
+
}
|
|
258
|
+
function getRulesByCategory(category) {
|
|
259
|
+
return exports.RULES.filter(r => r.category === category);
|
|
260
|
+
}
|
|
261
|
+
function getRulesBySeverity(severity) {
|
|
262
|
+
return exports.RULES.filter(r => r.severity === severity);
|
|
263
|
+
}
|
package/dist/scan.d.ts
ADDED