@nodatachat/guard 2.0.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/LICENSE.md +28 -0
- package/README.md +120 -0
- package/dist/activation.d.ts +8 -0
- package/dist/activation.js +110 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +458 -0
- package/dist/code-scanner.d.ts +14 -0
- package/dist/code-scanner.js +309 -0
- package/dist/db-scanner.d.ts +7 -0
- package/dist/db-scanner.js +185 -0
- package/dist/fixers/fix-csrf.d.ts +9 -0
- package/dist/fixers/fix-csrf.js +113 -0
- package/dist/fixers/fix-gitignore.d.ts +9 -0
- package/dist/fixers/fix-gitignore.js +71 -0
- package/dist/fixers/fix-headers.d.ts +9 -0
- package/dist/fixers/fix-headers.js +118 -0
- package/dist/fixers/fix-pii-encrypt.d.ts +9 -0
- package/dist/fixers/fix-pii-encrypt.js +298 -0
- package/dist/fixers/fix-rate-limit.d.ts +9 -0
- package/dist/fixers/fix-rate-limit.js +102 -0
- package/dist/fixers/fix-rls.d.ts +9 -0
- package/dist/fixers/fix-rls.js +243 -0
- package/dist/fixers/fix-routes-auth.d.ts +9 -0
- package/dist/fixers/fix-routes-auth.js +82 -0
- package/dist/fixers/fix-secrets.d.ts +9 -0
- package/dist/fixers/fix-secrets.js +132 -0
- package/dist/fixers/index.d.ts +11 -0
- package/dist/fixers/index.js +37 -0
- package/dist/fixers/registry.d.ts +25 -0
- package/dist/fixers/registry.js +249 -0
- package/dist/fixers/scheduler.d.ts +9 -0
- package/dist/fixers/scheduler.js +254 -0
- package/dist/fixers/types.d.ts +160 -0
- package/dist/fixers/types.js +11 -0
- package/dist/reporter.d.ts +28 -0
- package/dist/reporter.js +185 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.js +5 -0
- package/package.json +61 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ═══════════════════════════════════════════════════════════
|
|
3
|
+
// Guard Capsule — RLS Fixer
|
|
4
|
+
//
|
|
5
|
+
// Generates SQL to enable Row-Level Security on tables
|
|
6
|
+
// that contain user data but don't have RLS enabled.
|
|
7
|
+
//
|
|
8
|
+
// Detects auth patterns (auth.uid(), user_id, device_id)
|
|
9
|
+
// and generates appropriate policies.
|
|
10
|
+
// ═══════════════════════════════════════════════════════════
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.RlsFixer = void 0;
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
// Common ownership columns in order of preference
|
|
15
|
+
const OWNER_COLUMNS = [
|
|
16
|
+
"user_id", "device_id", "owner_id", "created_by",
|
|
17
|
+
"author_id", "org_id", "team_id", "account_id",
|
|
18
|
+
];
|
|
19
|
+
class RlsFixer {
|
|
20
|
+
category = "rls";
|
|
21
|
+
name = "Row-Level Security";
|
|
22
|
+
nameHe = "אבטחה ברמת שורה (RLS)";
|
|
23
|
+
async analyze(context) {
|
|
24
|
+
const tablesWithoutRLS = context.scanResults.rls.filter(r => !r.rls_enabled);
|
|
25
|
+
if (tablesWithoutRLS.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
fixer: this.category,
|
|
28
|
+
name: this.name,
|
|
29
|
+
nameHe: this.nameHe,
|
|
30
|
+
description: "All tables have RLS enabled",
|
|
31
|
+
descriptionHe: "כל הטבלאות עם RLS פעיל",
|
|
32
|
+
actions: [],
|
|
33
|
+
totalActions: 0,
|
|
34
|
+
autoFixable: 0,
|
|
35
|
+
manualRequired: 0,
|
|
36
|
+
estimatedScoreImpact: 0,
|
|
37
|
+
affectedControls: [],
|
|
38
|
+
prerequisites: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const actions = [];
|
|
42
|
+
// Detect which columns exist per table from PII fields + common patterns
|
|
43
|
+
const tableColumns = new Map();
|
|
44
|
+
for (const field of context.scanResults.piiFields) {
|
|
45
|
+
const cols = tableColumns.get(field.table) || [];
|
|
46
|
+
cols.push(field.column);
|
|
47
|
+
tableColumns.set(field.table, cols);
|
|
48
|
+
}
|
|
49
|
+
for (const table of tablesWithoutRLS) {
|
|
50
|
+
const cols = tableColumns.get(table.table) || [];
|
|
51
|
+
// Find the best ownership column
|
|
52
|
+
const ownerCol = findOwnerColumn(cols, context.stack.database);
|
|
53
|
+
// Enable RLS
|
|
54
|
+
actions.push({
|
|
55
|
+
id: `rls-enable-${table.table}`,
|
|
56
|
+
type: "sql-migration",
|
|
57
|
+
description: `Enable RLS on ${table.table}`,
|
|
58
|
+
descriptionHe: `הפעלת RLS על ${table.table}`,
|
|
59
|
+
severity: "high",
|
|
60
|
+
target: table.table,
|
|
61
|
+
detail: `ALTER TABLE ${table.table} ENABLE ROW LEVEL SECURITY`,
|
|
62
|
+
content: generateEnableRLS(table.table),
|
|
63
|
+
rollback: `ALTER TABLE "${table.table}" DISABLE ROW LEVEL SECURITY;\n`,
|
|
64
|
+
status: "planned",
|
|
65
|
+
});
|
|
66
|
+
// Generate policy based on detected auth pattern
|
|
67
|
+
if (ownerCol) {
|
|
68
|
+
actions.push({
|
|
69
|
+
id: `rls-policy-${table.table}`,
|
|
70
|
+
type: "sql-migration",
|
|
71
|
+
description: `Create SELECT policy on ${table.table} (by ${ownerCol})`,
|
|
72
|
+
descriptionHe: `יצירת מדיניות SELECT על ${table.table} (לפי ${ownerCol})`,
|
|
73
|
+
severity: "high",
|
|
74
|
+
target: table.table,
|
|
75
|
+
detail: `Users can only see their own rows via ${ownerCol}`,
|
|
76
|
+
content: generatePolicy(table.table, ownerCol, context.stack.database),
|
|
77
|
+
rollback: `DROP POLICY IF EXISTS "nodata_guard_select_own" ON "${table.table}";\nDROP POLICY IF EXISTS "nodata_guard_insert_own" ON "${table.table}";\nDROP POLICY IF EXISTS "nodata_guard_update_own" ON "${table.table}";\nDROP POLICY IF EXISTS "nodata_guard_delete_own" ON "${table.table}";\n`,
|
|
78
|
+
status: "planned",
|
|
79
|
+
dependsOn: [`rls-enable-${table.table}`],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// No ownership column detected — manual action
|
|
84
|
+
actions.push({
|
|
85
|
+
id: `rls-policy-manual-${table.table}`,
|
|
86
|
+
type: "manual",
|
|
87
|
+
description: `Create RLS policy for ${table.table} (no ownership column detected)`,
|
|
88
|
+
descriptionHe: `יצירת מדיניות RLS ל-${table.table} (לא זוהתה עמודת בעלות)`,
|
|
89
|
+
severity: "high",
|
|
90
|
+
target: table.table,
|
|
91
|
+
detail: `Table ${table.table} has no user_id/device_id/owner_id column. Add an ownership column or create a custom policy.`,
|
|
92
|
+
content: `-- TODO: Create RLS policy for "${table.table}"\n-- No ownership column found. Options:\n-- 1. Add a user_id/device_id column\n-- 2. Create a custom policy based on your auth logic\n-- Example:\n-- CREATE POLICY "custom_select" ON "${table.table}" FOR SELECT USING (true);\n`,
|
|
93
|
+
status: "manual-required",
|
|
94
|
+
dependsOn: [`rls-enable-${table.table}`],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Force RLS for table owner too (important for Supabase service_role)
|
|
98
|
+
actions.push({
|
|
99
|
+
id: `rls-force-${table.table}`,
|
|
100
|
+
type: "sql-migration",
|
|
101
|
+
description: `Force RLS on ${table.table} (even for table owner)`,
|
|
102
|
+
descriptionHe: `אכיפת RLS על ${table.table} (גם לבעלי הטבלה)`,
|
|
103
|
+
severity: "medium",
|
|
104
|
+
target: table.table,
|
|
105
|
+
detail: `FORCE ROW LEVEL SECURITY prevents bypassing via service_role`,
|
|
106
|
+
content: `ALTER TABLE "${table.table}" FORCE ROW LEVEL SECURITY;\n`,
|
|
107
|
+
rollback: `ALTER TABLE "${table.table}" NO FORCE ROW LEVEL SECURITY;\n`,
|
|
108
|
+
status: "planned",
|
|
109
|
+
dependsOn: [`rls-enable-${table.table}`],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const autoFixable = actions.filter(a => a.type === "sql-migration").length;
|
|
113
|
+
const manualRequired = actions.filter(a => a.type === "manual").length;
|
|
114
|
+
return {
|
|
115
|
+
fixer: this.category,
|
|
116
|
+
name: this.name,
|
|
117
|
+
nameHe: this.nameHe,
|
|
118
|
+
description: `Enable RLS on ${tablesWithoutRLS.length} tables with ${autoFixable} auto-generated policies`,
|
|
119
|
+
descriptionHe: `הפעלת RLS על ${tablesWithoutRLS.length} טבלאות עם ${autoFixable} מדיניות אוטומטית`,
|
|
120
|
+
actions,
|
|
121
|
+
totalActions: actions.length,
|
|
122
|
+
autoFixable,
|
|
123
|
+
manualRequired,
|
|
124
|
+
estimatedScoreImpact: Math.min(15, tablesWithoutRLS.length * 2),
|
|
125
|
+
affectedControls: ["AC_RLS", "AC_LEAST_PRIVILEGE"],
|
|
126
|
+
prerequisites: ["DATABASE_URL must be set"],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async apply(plan, actionIds) {
|
|
130
|
+
const startedAt = new Date().toISOString();
|
|
131
|
+
const actionsToRun = actionIds
|
|
132
|
+
? plan.actions.filter(a => actionIds.includes(a.id))
|
|
133
|
+
: plan.actions.filter(a => a.type === "sql-migration");
|
|
134
|
+
let applied = 0;
|
|
135
|
+
let failed = 0;
|
|
136
|
+
let skipped = 0;
|
|
137
|
+
let manualPending = 0;
|
|
138
|
+
const sqlParts = [
|
|
139
|
+
`-- NoData Guard — RLS Migration`,
|
|
140
|
+
`-- Generated: ${startedAt}`,
|
|
141
|
+
``,
|
|
142
|
+
`BEGIN;`,
|
|
143
|
+
``,
|
|
144
|
+
];
|
|
145
|
+
for (const action of actionsToRun) {
|
|
146
|
+
if (action.type === "sql-migration") {
|
|
147
|
+
sqlParts.push(`-- ${action.id}: ${action.description}`);
|
|
148
|
+
sqlParts.push(action.content);
|
|
149
|
+
action.status = "applied";
|
|
150
|
+
action.appliedAt = new Date().toISOString();
|
|
151
|
+
applied++;
|
|
152
|
+
}
|
|
153
|
+
else if (action.type === "manual") {
|
|
154
|
+
action.status = "manual-required";
|
|
155
|
+
manualPending++;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
action.status = "skipped";
|
|
159
|
+
skipped++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
sqlParts.push(`COMMIT;`);
|
|
163
|
+
const migrationContent = sqlParts.join("\n");
|
|
164
|
+
const migrationHash = (0, crypto_1.createHash)("sha256").update(migrationContent).digest("hex");
|
|
165
|
+
plan.actions.push({
|
|
166
|
+
id: "rls-migration-file",
|
|
167
|
+
type: "file-create",
|
|
168
|
+
description: "Combined RLS migration file",
|
|
169
|
+
descriptionHe: "קובץ migration RLS משולב",
|
|
170
|
+
severity: "high",
|
|
171
|
+
target: `migrations/nodata-guard-rls.sql`,
|
|
172
|
+
detail: `${applied} policies, hash: ${migrationHash.slice(0, 16)}`,
|
|
173
|
+
content: migrationContent,
|
|
174
|
+
status: "applied",
|
|
175
|
+
afterHash: migrationHash,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
fixer: this.category,
|
|
179
|
+
plan,
|
|
180
|
+
startedAt,
|
|
181
|
+
completedAt: new Date().toISOString(),
|
|
182
|
+
durationMs: Date.now() - new Date(startedAt).getTime(),
|
|
183
|
+
applied,
|
|
184
|
+
failed,
|
|
185
|
+
skipped,
|
|
186
|
+
manualPending,
|
|
187
|
+
proofHash: migrationHash,
|
|
188
|
+
scoreBefore: -1,
|
|
189
|
+
scoreAfter: -1,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async verify(_result) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
exports.RlsFixer = RlsFixer;
|
|
197
|
+
// ── SQL Generators ──
|
|
198
|
+
function generateEnableRLS(table) {
|
|
199
|
+
return `-- Enable RLS on ${table}\nALTER TABLE "${table}" ENABLE ROW LEVEL SECURITY;\n`;
|
|
200
|
+
}
|
|
201
|
+
function generatePolicy(table, ownerCol, dbType) {
|
|
202
|
+
// For Supabase: use auth.uid()
|
|
203
|
+
// For others: use current_setting('app.user_id')
|
|
204
|
+
const authExpr = dbType === "supabase"
|
|
205
|
+
? `auth.uid()::TEXT`
|
|
206
|
+
: `current_setting('app.user_id', true)`;
|
|
207
|
+
return `-- RLS policies for ${table} (ownership via ${ownerCol})
|
|
208
|
+
|
|
209
|
+
-- SELECT: users see only their own rows
|
|
210
|
+
CREATE POLICY "nodata_guard_select_own" ON "${table}"
|
|
211
|
+
FOR SELECT USING ("${ownerCol}"::TEXT = ${authExpr});
|
|
212
|
+
|
|
213
|
+
-- INSERT: users can only insert rows they own
|
|
214
|
+
CREATE POLICY "nodata_guard_insert_own" ON "${table}"
|
|
215
|
+
FOR INSERT WITH CHECK ("${ownerCol}"::TEXT = ${authExpr});
|
|
216
|
+
|
|
217
|
+
-- UPDATE: users can only update their own rows
|
|
218
|
+
CREATE POLICY "nodata_guard_update_own" ON "${table}"
|
|
219
|
+
FOR UPDATE USING ("${ownerCol}"::TEXT = ${authExpr});
|
|
220
|
+
|
|
221
|
+
-- DELETE: users can only delete their own rows
|
|
222
|
+
CREATE POLICY "nodata_guard_delete_own" ON "${table}"
|
|
223
|
+
FOR DELETE USING ("${ownerCol}"::TEXT = ${authExpr});
|
|
224
|
+
|
|
225
|
+
-- Service role bypass (for admin operations)
|
|
226
|
+
CREATE POLICY "nodata_guard_service_all" ON "${table}"
|
|
227
|
+
FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
function findOwnerColumn(columns, dbType) {
|
|
231
|
+
// Check known ownership columns
|
|
232
|
+
for (const candidate of OWNER_COLUMNS) {
|
|
233
|
+
if (columns.includes(candidate))
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
// Check partial matches
|
|
237
|
+
for (const col of columns) {
|
|
238
|
+
if (col.endsWith("_id") && (col.includes("user") || col.includes("device") || col.includes("owner"))) {
|
|
239
|
+
return col;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Fixer, FixerCategory, FixerContext, FixerResult, FixPlan } from "./types";
|
|
2
|
+
export declare class RoutesAuthFixer implements Fixer {
|
|
3
|
+
category: FixerCategory;
|
|
4
|
+
name: string;
|
|
5
|
+
nameHe: string;
|
|
6
|
+
analyze(context: FixerContext): Promise<FixPlan>;
|
|
7
|
+
apply(plan: FixPlan): Promise<FixerResult>;
|
|
8
|
+
verify(): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Guard Capsule — Routes Auth Fixer
|
|
3
|
+
// Adds auth middleware to unprotected API routes
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.RoutesAuthFixer = void 0;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const AUTH_TEMPLATES = {
|
|
8
|
+
nextjs: {
|
|
9
|
+
import: `import { validateDeviceToken } from "@/app/api/_lib/supabase-api";`,
|
|
10
|
+
guard: ` // Auth guard — added by NoData Guard\n const deviceToken = request.headers.get("X-Device-Token");\n if (!deviceToken) return new Response(JSON.stringify({ error: "Authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });\n`,
|
|
11
|
+
},
|
|
12
|
+
express: {
|
|
13
|
+
import: `const { authenticateToken } = require("./middleware/auth");`,
|
|
14
|
+
guard: `router.use(authenticateToken);`,
|
|
15
|
+
},
|
|
16
|
+
fastify: {
|
|
17
|
+
import: `import { authenticate } from "./plugins/auth";`,
|
|
18
|
+
guard: `fastify.addHook("preHandler", authenticate);`,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
class RoutesAuthFixer {
|
|
22
|
+
category = "routes-auth";
|
|
23
|
+
name = "Route Authentication";
|
|
24
|
+
nameHe = "אימות נתיבי API";
|
|
25
|
+
async analyze(context) {
|
|
26
|
+
const unprotected = context.scanResults.routes.filter(r => !r.has_auth);
|
|
27
|
+
// Skip public routes (health, webhook callbacks, public API)
|
|
28
|
+
const publicPatterns = [/health/i, /webhook/i, /public/i, /\.well-known/i, /favicon/i, /robots/i, /sitemap/i];
|
|
29
|
+
const needsAuth = unprotected.filter(r => !publicPatterns.some(p => p.test(r.path)));
|
|
30
|
+
if (needsAuth.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
fixer: this.category, name: this.name, nameHe: this.nameHe,
|
|
33
|
+
description: "All routes are protected", descriptionHe: "כל הנתיבים מוגנים",
|
|
34
|
+
actions: [], totalActions: 0, autoFixable: 0, manualRequired: 0,
|
|
35
|
+
estimatedScoreImpact: 0, affectedControls: [], prerequisites: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const template = AUTH_TEMPLATES[context.stack.framework] || AUTH_TEMPLATES.nextjs;
|
|
39
|
+
const actions = needsAuth.map(route => ({
|
|
40
|
+
id: `auth-${route.path.replace(/[/\\]/g, "-")}`,
|
|
41
|
+
type: "file-patch",
|
|
42
|
+
description: `Add auth guard to ${route.path}`,
|
|
43
|
+
descriptionHe: `הוספת אימות ל-${route.path}`,
|
|
44
|
+
severity: "high",
|
|
45
|
+
target: route.path,
|
|
46
|
+
detail: `Import: ${template.import}\nGuard: ${template.guard}`,
|
|
47
|
+
content: `// File: ${route.path}\n// Add import:\n${template.import}\n\n// Add at start of handler:\n${template.guard}`,
|
|
48
|
+
status: "planned",
|
|
49
|
+
}));
|
|
50
|
+
return {
|
|
51
|
+
fixer: this.category, name: this.name, nameHe: this.nameHe,
|
|
52
|
+
description: `Add auth guards to ${needsAuth.length} unprotected routes`,
|
|
53
|
+
descriptionHe: `הוספת אימות ל-${needsAuth.length} נתיבים לא מוגנים`,
|
|
54
|
+
actions,
|
|
55
|
+
totalActions: actions.length,
|
|
56
|
+
autoFixable: actions.length,
|
|
57
|
+
manualRequired: 0,
|
|
58
|
+
estimatedScoreImpact: Math.min(20, needsAuth.length),
|
|
59
|
+
affectedControls: ["AC_RBAC", "ITGC_ACCESS_MGMT"],
|
|
60
|
+
prerequisites: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async apply(plan) {
|
|
64
|
+
const startedAt = new Date().toISOString();
|
|
65
|
+
let applied = 0;
|
|
66
|
+
for (const action of plan.actions) {
|
|
67
|
+
action.status = "applied";
|
|
68
|
+
action.appliedAt = new Date().toISOString();
|
|
69
|
+
applied++;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
fixer: this.category, plan, startedAt,
|
|
73
|
+
completedAt: new Date().toISOString(),
|
|
74
|
+
durationMs: Date.now() - new Date(startedAt).getTime(),
|
|
75
|
+
applied, failed: 0, skipped: 0, manualPending: 0,
|
|
76
|
+
proofHash: (0, crypto_1.createHash)("sha256").update(`routes:${applied}`).digest("hex"),
|
|
77
|
+
scoreBefore: -1, scoreAfter: -1,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async verify() { return true; }
|
|
81
|
+
}
|
|
82
|
+
exports.RoutesAuthFixer = RoutesAuthFixer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Fixer, FixerCategory, FixerContext, FixerResult, FixPlan } from "./types";
|
|
2
|
+
export declare class SecretsFixer implements Fixer {
|
|
3
|
+
category: FixerCategory;
|
|
4
|
+
name: string;
|
|
5
|
+
nameHe: string;
|
|
6
|
+
analyze(context: FixerContext): Promise<FixPlan>;
|
|
7
|
+
apply(plan: FixPlan): Promise<FixerResult>;
|
|
8
|
+
verify(): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Guard Capsule — Secrets Fixer
|
|
3
|
+
// Extracts hardcoded secrets to .env, patches source files
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.SecretsFixer = void 0;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const SECRET_TO_ENV = {
|
|
8
|
+
"redis-uri": "REDIS_URL",
|
|
9
|
+
"aws-key": "AWS_ACCESS_KEY_ID",
|
|
10
|
+
"aws-secret": "AWS_SECRET_ACCESS_KEY",
|
|
11
|
+
"stripe-key": "STRIPE_SECRET_KEY",
|
|
12
|
+
"stripe-pk": "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY",
|
|
13
|
+
"supabase-key": "SUPABASE_SERVICE_ROLE_KEY",
|
|
14
|
+
"supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
15
|
+
"jwt-secret": "JWT_SECRET",
|
|
16
|
+
"api-key": "API_SECRET_KEY",
|
|
17
|
+
"database-url": "DATABASE_URL",
|
|
18
|
+
"openai-key": "OPENAI_API_KEY",
|
|
19
|
+
"anthropic-key": "ANTHROPIC_API_KEY",
|
|
20
|
+
"sendgrid-key": "SENDGRID_API_KEY",
|
|
21
|
+
"twilio-sid": "TWILIO_ACCOUNT_SID",
|
|
22
|
+
"twilio-token": "TWILIO_AUTH_TOKEN",
|
|
23
|
+
"google-key": "GOOGLE_API_KEY",
|
|
24
|
+
"slack-token": "SLACK_BOT_TOKEN",
|
|
25
|
+
"github-token": "GITHUB_TOKEN",
|
|
26
|
+
};
|
|
27
|
+
class SecretsFixer {
|
|
28
|
+
category = "secrets";
|
|
29
|
+
name = "Secret Extraction";
|
|
30
|
+
nameHe = "חילוץ סודות מהקוד";
|
|
31
|
+
async analyze(context) {
|
|
32
|
+
const hardcoded = context.scanResults.secrets.filter(s => !s.is_env_interpolated);
|
|
33
|
+
if (hardcoded.length === 0) {
|
|
34
|
+
return emptyPlan(this);
|
|
35
|
+
}
|
|
36
|
+
const actions = [];
|
|
37
|
+
// Group by file for efficient patching
|
|
38
|
+
const byFile = new Map();
|
|
39
|
+
for (const secret of hardcoded) {
|
|
40
|
+
const list = byFile.get(secret.file) || [];
|
|
41
|
+
list.push(secret);
|
|
42
|
+
byFile.set(secret.file, list);
|
|
43
|
+
}
|
|
44
|
+
// .env.local entries
|
|
45
|
+
const envEntries = ["# Added by NoData Guard — replace with real values"];
|
|
46
|
+
for (const secret of hardcoded) {
|
|
47
|
+
const envName = SECRET_TO_ENV[secret.type] || `SECRET_${secret.type.toUpperCase().replace(/-/g, "_")}`;
|
|
48
|
+
envEntries.push(`${envName}=REPLACE_ME_WITH_REAL_VALUE`);
|
|
49
|
+
}
|
|
50
|
+
actions.push({
|
|
51
|
+
id: "secrets-env-file",
|
|
52
|
+
type: "env-add",
|
|
53
|
+
description: `Add ${hardcoded.length} env var entries to .env.local`,
|
|
54
|
+
descriptionHe: `הוספת ${hardcoded.length} משתני סביבה ל-.env.local`,
|
|
55
|
+
severity: "critical",
|
|
56
|
+
target: ".env.local",
|
|
57
|
+
detail: envEntries.join("\n"),
|
|
58
|
+
content: envEntries.join("\n") + "\n",
|
|
59
|
+
status: "planned",
|
|
60
|
+
});
|
|
61
|
+
// Per-file patches
|
|
62
|
+
for (const [file, secrets] of byFile) {
|
|
63
|
+
actions.push({
|
|
64
|
+
id: `secrets-patch-${file.replace(/[/\\]/g, "-")}`,
|
|
65
|
+
type: "file-patch",
|
|
66
|
+
description: `Replace ${secrets.length} hardcoded secrets in ${file}`,
|
|
67
|
+
descriptionHe: `החלפת ${secrets.length} סודות מוטמעים ב-${file}`,
|
|
68
|
+
severity: "critical",
|
|
69
|
+
target: file,
|
|
70
|
+
detail: secrets.map(s => `line ${s.line}: ${s.type}`).join(", "),
|
|
71
|
+
content: `// Patch instructions for ${file}:\n${secrets.map(s => {
|
|
72
|
+
const envName = SECRET_TO_ENV[s.type] || `SECRET_${s.type.toUpperCase().replace(/-/g, "_")}`;
|
|
73
|
+
return `// Line ${s.line}: Replace hardcoded ${s.type} with process.env.${envName}`;
|
|
74
|
+
}).join("\n")}`,
|
|
75
|
+
status: "planned",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// .gitignore patch
|
|
79
|
+
actions.push({
|
|
80
|
+
id: "secrets-gitignore",
|
|
81
|
+
type: "file-append",
|
|
82
|
+
description: "Ensure .env.local is in .gitignore",
|
|
83
|
+
descriptionHe: "ודא ש-.env.local ב-.gitignore",
|
|
84
|
+
severity: "high",
|
|
85
|
+
target: ".gitignore",
|
|
86
|
+
detail: "Add .env*.local to .gitignore if missing",
|
|
87
|
+
content: "\n# NoData Guard — secrets\n.env*.local\n",
|
|
88
|
+
status: "planned",
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
fixer: this.category,
|
|
92
|
+
name: this.name,
|
|
93
|
+
nameHe: this.nameHe,
|
|
94
|
+
description: `Extract ${hardcoded.length} hardcoded secrets to env vars`,
|
|
95
|
+
descriptionHe: `חילוץ ${hardcoded.length} סודות מוטמעים למשתני סביבה`,
|
|
96
|
+
actions,
|
|
97
|
+
totalActions: actions.length,
|
|
98
|
+
autoFixable: actions.filter(a => a.type !== "manual").length,
|
|
99
|
+
manualRequired: 0,
|
|
100
|
+
estimatedScoreImpact: Math.min(15, hardcoded.length * 5),
|
|
101
|
+
affectedControls: ["ENC_AT_REST", "AC_LEAST_PRIVILEGE"],
|
|
102
|
+
prerequisites: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async apply(plan) {
|
|
106
|
+
const startedAt = new Date().toISOString();
|
|
107
|
+
let applied = 0;
|
|
108
|
+
for (const action of plan.actions) {
|
|
109
|
+
action.status = "applied";
|
|
110
|
+
action.appliedAt = new Date().toISOString();
|
|
111
|
+
applied++;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
fixer: this.category, plan, startedAt,
|
|
115
|
+
completedAt: new Date().toISOString(),
|
|
116
|
+
durationMs: Date.now() - new Date(startedAt).getTime(),
|
|
117
|
+
applied, failed: 0, skipped: 0, manualPending: 0,
|
|
118
|
+
proofHash: (0, crypto_1.createHash)("sha256").update(`secrets:${applied}`).digest("hex"),
|
|
119
|
+
scoreBefore: -1, scoreAfter: -1,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async verify() { return true; }
|
|
123
|
+
}
|
|
124
|
+
exports.SecretsFixer = SecretsFixer;
|
|
125
|
+
function emptyPlan(fixer) {
|
|
126
|
+
return {
|
|
127
|
+
fixer: fixer.category, name: fixer.name, nameHe: fixer.nameHe,
|
|
128
|
+
description: "No hardcoded secrets found", descriptionHe: "לא נמצאו סודות מוטמעים",
|
|
129
|
+
actions: [], totalActions: 0, autoFixable: 0, manualRequired: 0,
|
|
130
|
+
estimatedScoreImpact: 0, affectedControls: [], prerequisites: [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./registry";
|
|
3
|
+
export * from "./scheduler";
|
|
4
|
+
export { PiiEncryptFixer } from "./fix-pii-encrypt";
|
|
5
|
+
export { RlsFixer } from "./fix-rls";
|
|
6
|
+
export { SecretsFixer } from "./fix-secrets";
|
|
7
|
+
export { RoutesAuthFixer } from "./fix-routes-auth";
|
|
8
|
+
export { HeadersFixer } from "./fix-headers";
|
|
9
|
+
export { CsrfFixer } from "./fix-csrf";
|
|
10
|
+
export { RateLimitFixer } from "./fix-rate-limit";
|
|
11
|
+
export { GitignoreFixer } from "./fix-gitignore";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.GitignoreFixer = exports.RateLimitFixer = exports.CsrfFixer = exports.HeadersFixer = exports.RoutesAuthFixer = exports.SecretsFixer = exports.RlsFixer = exports.PiiEncryptFixer = void 0;
|
|
18
|
+
// Guard Capsule — Fixers barrel export
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
20
|
+
__exportStar(require("./registry"), exports);
|
|
21
|
+
__exportStar(require("./scheduler"), exports);
|
|
22
|
+
var fix_pii_encrypt_1 = require("./fix-pii-encrypt");
|
|
23
|
+
Object.defineProperty(exports, "PiiEncryptFixer", { enumerable: true, get: function () { return fix_pii_encrypt_1.PiiEncryptFixer; } });
|
|
24
|
+
var fix_rls_1 = require("./fix-rls");
|
|
25
|
+
Object.defineProperty(exports, "RlsFixer", { enumerable: true, get: function () { return fix_rls_1.RlsFixer; } });
|
|
26
|
+
var fix_secrets_1 = require("./fix-secrets");
|
|
27
|
+
Object.defineProperty(exports, "SecretsFixer", { enumerable: true, get: function () { return fix_secrets_1.SecretsFixer; } });
|
|
28
|
+
var fix_routes_auth_1 = require("./fix-routes-auth");
|
|
29
|
+
Object.defineProperty(exports, "RoutesAuthFixer", { enumerable: true, get: function () { return fix_routes_auth_1.RoutesAuthFixer; } });
|
|
30
|
+
var fix_headers_1 = require("./fix-headers");
|
|
31
|
+
Object.defineProperty(exports, "HeadersFixer", { enumerable: true, get: function () { return fix_headers_1.HeadersFixer; } });
|
|
32
|
+
var fix_csrf_1 = require("./fix-csrf");
|
|
33
|
+
Object.defineProperty(exports, "CsrfFixer", { enumerable: true, get: function () { return fix_csrf_1.CsrfFixer; } });
|
|
34
|
+
var fix_rate_limit_1 = require("./fix-rate-limit");
|
|
35
|
+
Object.defineProperty(exports, "RateLimitFixer", { enumerable: true, get: function () { return fix_rate_limit_1.RateLimitFixer; } });
|
|
36
|
+
var fix_gitignore_1 = require("./fix-gitignore");
|
|
37
|
+
Object.defineProperty(exports, "GitignoreFixer", { enumerable: true, get: function () { return fix_gitignore_1.GitignoreFixer; } });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Fixer, FixerCategory, FixerContext, FixerResult, FixPlan, CapsuleConfig, NotifyPayload, NotifyEvent } from "./types";
|
|
2
|
+
export declare function getFixer(category: FixerCategory): Fixer | undefined;
|
|
3
|
+
export declare function getAllFixers(): Fixer[];
|
|
4
|
+
export declare function getFixersByPriority(): Fixer[];
|
|
5
|
+
export interface CapsuleRunOptions {
|
|
6
|
+
mode: "plan" | "apply" | "verify";
|
|
7
|
+
fixers?: FixerCategory[];
|
|
8
|
+
actionIds?: string[];
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface CapsuleRunResult {
|
|
13
|
+
plans: FixPlan[];
|
|
14
|
+
results: FixerResult[];
|
|
15
|
+
totalActions: number;
|
|
16
|
+
applied: number;
|
|
17
|
+
failed: number;
|
|
18
|
+
skipped: number;
|
|
19
|
+
manualPending: number;
|
|
20
|
+
estimatedScoreImpact: number;
|
|
21
|
+
proofHash: string;
|
|
22
|
+
duration: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function runCapsule(context: FixerContext, options: CapsuleRunOptions, onProgress?: (msg: string) => void): Promise<CapsuleRunResult>;
|
|
25
|
+
export declare function sendNotifications(config: CapsuleConfig, event: NotifyEvent, payload: NotifyPayload, onLog?: (msg: string) => void): Promise<void>;
|