@greenarmor/ges-mcp-server 0.5.5 → 0.6.1
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/bundle/server.js +4156 -111
- package/dist/server.js +2418 -51
- package/package.json +9 -6
package/dist/server.js
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as readline from "node:readline";
|
|
3
|
-
import
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { getAllPacks, getPacksForProjectType, getPack } from "@greenarmor/ges-policy-engine";
|
|
4
6
|
import { generateScoreFile, formatScoreOutput } from "@greenarmor/ges-scoring-engine";
|
|
7
|
+
import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
|
|
5
8
|
import { GESF_VERSION } from "@greenarmor/ges-core";
|
|
6
9
|
const TOOLS = [
|
|
7
10
|
{
|
|
8
11
|
name: "check_compliance",
|
|
9
|
-
description: "Check GDPR compliance status for a project",
|
|
12
|
+
description: "Check GDPR compliance status for a project. Returns compliance scores per framework (GDPR, OWASP, CIS, NIST) with grades and control breakdown.",
|
|
10
13
|
inputSchema: {
|
|
11
14
|
type: "object",
|
|
12
15
|
properties: {
|
|
13
|
-
project_type: { type: "string", description: "Project type" },
|
|
16
|
+
project_type: { type: "string", description: "Project type (saas, ai-application, mcp-server, blockchain, wallet, government-system, healthcare-system, event-platform, photo-storage-platform, vulnerability-scanner, generic-web-application, api-backend, mobile-application)" },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "check_project_status",
|
|
22
|
+
description: "Read the actual project's .ges/ directory to get real-time compliance status, scores, config, and audit results. Use this when the project has already been initialized with 'ges init'.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
project_path: { type: "string", description: "Absolute path to the project root. Defaults to current working directory." },
|
|
14
27
|
},
|
|
15
28
|
},
|
|
16
29
|
},
|
|
17
30
|
{
|
|
18
31
|
name: "list_missing_controls",
|
|
19
|
-
description: "Show missing compliance controls",
|
|
32
|
+
description: "Show missing or failed compliance controls for a given framework. Returns control ID, severity, name, and implementation guidance.",
|
|
20
33
|
inputSchema: {
|
|
21
34
|
type: "object",
|
|
22
35
|
properties: {
|
|
@@ -26,8 +39,69 @@ const TOOLS = [
|
|
|
26
39
|
},
|
|
27
40
|
framework: {
|
|
28
41
|
type: "string",
|
|
29
|
-
description: "Framework name (GDPR, OWASP,
|
|
42
|
+
description: "Framework name (GDPR, OWASP, CIS, NIST)",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "list_framework_controls",
|
|
49
|
+
description: "List all controls for a given framework with their status, severity, category, and implementation guidance. Useful for understanding the full control landscape.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
framework: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Framework name (GDPR, OWASP, CIS, NIST, AI, blockchain, government)",
|
|
30
56
|
},
|
|
57
|
+
status_filter: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Filter by status (pass, fail, warning, not-implemented, not-applicable). Omit to show all.",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "run_audit",
|
|
66
|
+
description: "Run a full source code security audit on the project. Scans for secrets, weak cryptography, injection vulnerabilities, auth issues, config problems, and database anti-patterns. Returns findings with severity, file location, evidence, and fix guidance.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
project_path: { type: "string", description: "Absolute path to the project root to audit." },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "generate_compliance_report",
|
|
76
|
+
description: "Generate a full compliance report with executive summary, findings, framework scores, risk assessment, security controls, and actionable recommendations. The primary report tool for compliance status.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
project_type: { type: "string", description: "Project type" },
|
|
81
|
+
project_name: { type: "string", description: "Project name" },
|
|
82
|
+
frameworks: { type: "string", description: "Comma-separated framework names (GDPR,OWASP,CIS,NIST)" },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "generate_audit_report",
|
|
88
|
+
description: "Generate a report from actual source code audit findings. Combines audit results with compliance scoring and detailed recommendations for each finding. Requires a project path.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
project_path: { type: "string", description: "Absolute path to the project root to audit and report on." },
|
|
93
|
+
project_name: { type: "string", description: "Project name for the report title." },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "fix_recommendation",
|
|
99
|
+
description: "Get detailed step-by-step remediation guidance for a specific control or finding. Provides implementation steps, code examples, and verification steps. Use this to fix issues one by one.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
control_id: { type: "string", description: "Control ID to get fix guidance for (e.g. GDPR-ART32-001, OWASP-AUTH-001)" },
|
|
104
|
+
finding_title: { type: "string", description: "Title of a specific audit finding to get fix guidance for." },
|
|
31
105
|
},
|
|
32
106
|
},
|
|
33
107
|
},
|
|
@@ -71,10 +145,1816 @@ const TOOLS = [
|
|
|
71
145
|
},
|
|
72
146
|
},
|
|
73
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: "generate_data_inventory",
|
|
150
|
+
description: "Generate a data inventory document listing data categories, classifications, retention periods, and legal basis. Required for GDPR Article 30 compliance.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
project_name: { type: "string", description: "Project name" },
|
|
155
|
+
project_type: { type: "string", description: "Project type" },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "generate_processing_records",
|
|
161
|
+
description: "Generate Article 30 Records of Processing Activities (ROPA). Documents all processing activities, purposes, data categories, recipients, and retention periods.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
project_name: { type: "string", description: "Project name" },
|
|
166
|
+
controller_name: { type: "string", description: "Data controller organization name" },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "auto_fix",
|
|
172
|
+
description: "Run an audit and automatically fix all fixable security/compliance issues in the project source code. Creates files, modifies source, generates security scaffolding. Returns a detailed report of what was fixed and what requires manual review.",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
project_path: { type: "string", description: "Absolute path to the project root." },
|
|
177
|
+
dry_run: { type: "boolean", description: "If true, show what would be fixed without making changes. Default: false." },
|
|
178
|
+
rule_ids: { type: "string", description: "Comma-separated rule IDs to fix (e.g. 'CONFIG-001,AUTH-002'). Omit to fix all auto-fixable issues." },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "apply_control_override",
|
|
184
|
+
description: "Mark a compliance control as not-applicable, pass, or another status in the project's .ges/control-overrides.json. Use this when a control doesn't apply to the project or has been verified manually.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
project_path: { type: "string", description: "Absolute path to the project root." },
|
|
189
|
+
control_id: { type: "string", description: "Control ID to override (e.g. GDPR-ART32-004)" },
|
|
190
|
+
status: { type: "string", description: "New status: 'not-applicable' or 'pass'" },
|
|
191
|
+
reason: { type: "string", description: "Reason for the override" },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "implement_control",
|
|
197
|
+
description: "Generate and write actual implementation files for a compliance control into the target project. Creates source files, configuration, and middleware. Returns what was created and next steps.",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
project_path: { type: "string", description: "Absolute path to the project root." },
|
|
202
|
+
control_id: { type: "string", description: "Control ID to implement (e.g. GDPR-ART32-002, GDPR-ART32-006, AUTH-002)" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
74
206
|
];
|
|
75
207
|
function send(message) {
|
|
76
208
|
process.stdout.write(JSON.stringify(message) + "\n");
|
|
77
209
|
}
|
|
210
|
+
function resolveProjectPath(projectPath) {
|
|
211
|
+
return projectPath || process.cwd();
|
|
212
|
+
}
|
|
213
|
+
function readJsonFileSafe(filePath) {
|
|
214
|
+
try {
|
|
215
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
216
|
+
return JSON.parse(content);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function loadProjectConfig(projectPath) {
|
|
223
|
+
const gesDir = path.join(projectPath, ".ges");
|
|
224
|
+
const config = readJsonFileSafe(path.join(gesDir, "config.json"));
|
|
225
|
+
const score = readJsonFileSafe(path.join(gesDir, "score.json"));
|
|
226
|
+
const overrides = readJsonFileSafe(path.join(gesDir, "control-overrides.json"));
|
|
227
|
+
return {
|
|
228
|
+
config,
|
|
229
|
+
score,
|
|
230
|
+
overrides: Array.isArray(overrides) ? overrides : [],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function applyControlOverrides(controls, overrides) {
|
|
234
|
+
if (overrides.length === 0)
|
|
235
|
+
return controls;
|
|
236
|
+
const overrideMap = new Map(overrides.map(o => [o.control_id, o]));
|
|
237
|
+
return controls.map(control => {
|
|
238
|
+
const override = overrideMap.get(control.id);
|
|
239
|
+
if (!override)
|
|
240
|
+
return control;
|
|
241
|
+
return {
|
|
242
|
+
...control,
|
|
243
|
+
status: override.status,
|
|
244
|
+
checks: control.checks.map(check => ({ ...check, status: override.status })),
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function updateControlsFromFindings(controls, findings) {
|
|
249
|
+
return controls.map(control => {
|
|
250
|
+
if (control.status === "pass" || control.status === "not-applicable")
|
|
251
|
+
return control;
|
|
252
|
+
const relevantFindings = findings.filter(f => f.controlIds && f.controlIds.includes(control.id));
|
|
253
|
+
if (relevantFindings.length === 0)
|
|
254
|
+
return control;
|
|
255
|
+
const hasCritical = relevantFindings.some(f => f.severity === "critical" || f.severity === "high");
|
|
256
|
+
return {
|
|
257
|
+
...control,
|
|
258
|
+
status: hasCritical ? "fail" : "warning",
|
|
259
|
+
checks: control.checks.map(check => ({
|
|
260
|
+
...check,
|
|
261
|
+
status: hasCritical ? "fail" : "warning",
|
|
262
|
+
})),
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function getControlsForProject(projectType, frameworks) {
|
|
267
|
+
const projectPacks = getPacksForProjectType(projectType);
|
|
268
|
+
const packIds = new Set(projectPacks.map(p => p.id));
|
|
269
|
+
const fwLower = new Set(frameworks.map(f => f.toLowerCase()));
|
|
270
|
+
const allPacks = getAllPacks();
|
|
271
|
+
for (const p of allPacks) {
|
|
272
|
+
if (fwLower.has(p.id))
|
|
273
|
+
packIds.add(p.id);
|
|
274
|
+
}
|
|
275
|
+
return allPacks.filter(p => packIds.has(p.id)).flatMap(p => p.controls);
|
|
276
|
+
}
|
|
277
|
+
function generateFullComplianceReport(projectName, projectType, frameworks, findings, overrides) {
|
|
278
|
+
const controls = getControlsForProject(projectType, frameworks);
|
|
279
|
+
const overriddenControls = applyControlOverrides(controls, overrides || []);
|
|
280
|
+
const auditedControls = findings ? updateControlsFromFindings(overriddenControls, findings) : overriddenControls;
|
|
281
|
+
const score = generateScoreFile(auditedControls, frameworks, findings);
|
|
282
|
+
const sections = [];
|
|
283
|
+
sections.push(`# Compliance Report - ${projectName}`);
|
|
284
|
+
sections.push(`\nGenerated: ${new Date().toISOString()}`);
|
|
285
|
+
sections.push(`Project Type: ${projectType}`);
|
|
286
|
+
sections.push(`Frameworks: ${frameworks.join(", ")}\n`);
|
|
287
|
+
sections.push("## Executive Summary\n");
|
|
288
|
+
sections.push(`**Overall Score: ${score.overall}% (Grade: ${score.overall_grade})**\n`);
|
|
289
|
+
sections.push("| Framework | Score | Grade | Passed | Failed | Warnings | Critical Failures |");
|
|
290
|
+
sections.push("|-----------|-------|-------|--------|--------|----------|-------------------|");
|
|
291
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
292
|
+
sections.push(`| ${fw} | ${data.score}% | ${data.grade} | ${data.passed_controls} | ${data.failed_controls} | ${data.warning_controls} | ${data.critical_failures} |`);
|
|
293
|
+
}
|
|
294
|
+
if (findings && findings.length > 0) {
|
|
295
|
+
sections.push(`\n**Security Findings**: ${findings.length} total`);
|
|
296
|
+
const crit = findings.filter(f => f.severity === "critical").length;
|
|
297
|
+
const high = findings.filter(f => f.severity === "high").length;
|
|
298
|
+
sections.push(`- Critical: ${crit}, High: ${high}`);
|
|
299
|
+
}
|
|
300
|
+
if (score.audit_impact) {
|
|
301
|
+
const ai = score.audit_impact;
|
|
302
|
+
sections.push(`\n**Audit Impact**: -${ai.total_deduction}% deduction`);
|
|
303
|
+
}
|
|
304
|
+
if (findings && findings.length > 0) {
|
|
305
|
+
sections.push("\n## Security Findings\n");
|
|
306
|
+
const grouped = {};
|
|
307
|
+
for (const f of findings) {
|
|
308
|
+
if (!grouped[f.category])
|
|
309
|
+
grouped[f.category] = [];
|
|
310
|
+
grouped[f.category].push(f);
|
|
311
|
+
}
|
|
312
|
+
for (const [category, categoryFindings] of Object.entries(grouped)) {
|
|
313
|
+
sections.push(`### ${category.charAt(0).toUpperCase() + category.slice(1)}\n`);
|
|
314
|
+
sections.push("| Severity | Title | File | Fix |");
|
|
315
|
+
sections.push("|----------|-------|------|-----|");
|
|
316
|
+
for (const f of categoryFindings) {
|
|
317
|
+
const loc = f.file !== "project" ? `${f.file}${f.line ? `:${f.line}` : ""}` : "project-wide";
|
|
318
|
+
sections.push(`| ${f.severity} | ${f.title} | ${loc} | ${f.fix.slice(0, 80)} |`);
|
|
319
|
+
}
|
|
320
|
+
sections.push("");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
sections.push("\n## Compliance Details\n");
|
|
324
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
325
|
+
sections.push(`### ${fw} - ${data.score}% (Grade: ${data.grade})\n`);
|
|
326
|
+
sections.push(`- Total Controls: ${data.total_controls}`);
|
|
327
|
+
sections.push(`- Passed: ${data.passed_controls}`);
|
|
328
|
+
sections.push(`- Failed: ${data.failed_controls}`);
|
|
329
|
+
sections.push(`- Warnings: ${data.warning_controls}`);
|
|
330
|
+
sections.push(`- Not Implemented: ${data.not_implemented}`);
|
|
331
|
+
sections.push(`- Critical Failures: ${data.critical_failures}`);
|
|
332
|
+
const sb = data.severity_breakdown;
|
|
333
|
+
sections.push("\n**Severity Breakdown:**");
|
|
334
|
+
sections.push("| Level | Total | Passed | Failed | Warning | Not Implemented |");
|
|
335
|
+
sections.push("|-------|-------|--------|--------|---------|-----------------|");
|
|
336
|
+
if (sb.critical.total > 0)
|
|
337
|
+
sections.push(`| Critical | ${sb.critical.total} | ${sb.critical.passed} | ${sb.critical.failed} | ${sb.critical.warning} | ${sb.critical.not_implemented} |`);
|
|
338
|
+
if (sb.high.total > 0)
|
|
339
|
+
sections.push(`| High | ${sb.high.total} | ${sb.high.passed} | ${sb.high.failed} | ${sb.high.warning} | ${sb.high.not_implemented} |`);
|
|
340
|
+
if (sb.medium.total > 0)
|
|
341
|
+
sections.push(`| Medium | ${sb.medium.total} | ${sb.medium.passed} | ${sb.medium.failed} | ${sb.medium.warning} | ${sb.medium.not_implemented} |`);
|
|
342
|
+
if (sb.low.total > 0)
|
|
343
|
+
sections.push(`| Low | ${sb.low.total} | ${sb.low.passed} | ${sb.low.failed} | ${sb.low.warning} | ${sb.low.not_implemented} |`);
|
|
344
|
+
sections.push("");
|
|
345
|
+
}
|
|
346
|
+
sections.push(generateRecommendations(auditedControls, findings));
|
|
347
|
+
return sections.join("\n");
|
|
348
|
+
}
|
|
349
|
+
function generateRecommendations(controls, findings) {
|
|
350
|
+
const lines = ["## Recommendations\n"];
|
|
351
|
+
const failedControls = controls.filter(c => c.status === "fail");
|
|
352
|
+
const criticalFails = failedControls.filter(c => c.severity === "critical");
|
|
353
|
+
const highFails = failedControls.filter(c => c.severity === "high");
|
|
354
|
+
const warningControls = controls.filter(c => c.status === "warning");
|
|
355
|
+
const notImplemented = controls.filter(c => c.status === "not-implemented");
|
|
356
|
+
if (criticalFails.length > 0) {
|
|
357
|
+
lines.push("### Critical Actions Required\n");
|
|
358
|
+
for (const c of criticalFails) {
|
|
359
|
+
lines.push(`**${c.id}** (${c.severity}): ${c.name}`);
|
|
360
|
+
lines.push(` Category: ${c.category}`);
|
|
361
|
+
lines.push(` Guidance: ${c.implementation_guidance}`);
|
|
362
|
+
lines.push(` Fix: Use \`fix_recommendation\` tool with control_id="${c.id}" for detailed steps.\n`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (highFails.length > 0) {
|
|
366
|
+
lines.push("### High Priority Actions\n");
|
|
367
|
+
for (const c of highFails) {
|
|
368
|
+
lines.push(`**${c.id}** (${c.severity}): ${c.name}`);
|
|
369
|
+
lines.push(` Category: ${c.category}`);
|
|
370
|
+
lines.push(` Guidance: ${c.implementation_guidance}\n`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (findings && findings.length > 0) {
|
|
374
|
+
const critFindings = findings.filter(f => f.severity === "critical");
|
|
375
|
+
const highFindings = findings.filter(f => f.severity === "high");
|
|
376
|
+
if (critFindings.length > 0) {
|
|
377
|
+
lines.push("### Immediate Security Fixes\n");
|
|
378
|
+
for (const f of critFindings) {
|
|
379
|
+
lines.push(`- **[${f.severity.toUpperCase()}] ${f.title}** (${f.file}${f.line ? `:${f.line}` : ""})`);
|
|
380
|
+
lines.push(` Evidence: ${f.evidence}`);
|
|
381
|
+
lines.push(` Fix: ${f.fix}`);
|
|
382
|
+
if (f.controlIds && f.controlIds.length > 0) {
|
|
383
|
+
lines.push(` Related controls: ${f.controlIds.join(", ")}`);
|
|
384
|
+
}
|
|
385
|
+
lines.push("");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (highFindings.length > 0 && critFindings.length === 0) {
|
|
389
|
+
lines.push("### Security Fixes Needed\n");
|
|
390
|
+
for (const f of highFindings) {
|
|
391
|
+
lines.push(`- **[${f.severity.toUpperCase()}] ${f.title}** (${f.file}${f.line ? `:${f.line}` : ""})`);
|
|
392
|
+
lines.push(` Fix: ${f.fix}\n`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (warningControls.length > 0) {
|
|
397
|
+
lines.push("### Warnings to Address\n");
|
|
398
|
+
for (const c of warningControls.slice(0, 10)) {
|
|
399
|
+
lines.push(`- **${c.id}** (${c.severity}): ${c.name} — ${c.implementation_guidance.split(".")[0]}`);
|
|
400
|
+
}
|
|
401
|
+
if (warningControls.length > 10) {
|
|
402
|
+
lines.push(`- ... and ${warningControls.length - 10} more warnings`);
|
|
403
|
+
}
|
|
404
|
+
lines.push("");
|
|
405
|
+
}
|
|
406
|
+
if (notImplemented.length > 0) {
|
|
407
|
+
lines.push("### Not Yet Implemented\n");
|
|
408
|
+
lines.push(`${notImplemented.length} controls have not been implemented yet. Priority order:\n`);
|
|
409
|
+
const sorted = [...notImplemented].sort((a, b) => {
|
|
410
|
+
const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
411
|
+
return (sevOrder[a.severity] ?? 4) - (sevOrder[b.severity] ?? 4);
|
|
412
|
+
});
|
|
413
|
+
for (const c of sorted.slice(0, 10)) {
|
|
414
|
+
lines.push(`- **${c.id}** (${c.severity}): ${c.name}`);
|
|
415
|
+
lines.push(` ${c.implementation_guidance.split(".")[0]}`);
|
|
416
|
+
}
|
|
417
|
+
if (notImplemented.length > 10) {
|
|
418
|
+
lines.push(`\n... and ${notImplemented.length - 10} more not-implemented controls.`);
|
|
419
|
+
}
|
|
420
|
+
lines.push("");
|
|
421
|
+
}
|
|
422
|
+
const totalIssues = failedControls.length + warningControls.length + notImplemented.length;
|
|
423
|
+
if (totalIssues === 0) {
|
|
424
|
+
lines.push("**All controls are passing.** No recommendations at this time. Continue monitoring with regular audits.");
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
lines.push(`**Summary**: ${totalIssues} total issues (${criticalFails.length} critical, ${highFails.length} high, ${warningControls.length} warnings, ${notImplemented.length} not-implemented).`);
|
|
428
|
+
lines.push("\nUse the `fix_recommendation` tool with a specific control_id to get step-by-step implementation guidance for any issue.");
|
|
429
|
+
}
|
|
430
|
+
return lines.join("\n");
|
|
431
|
+
}
|
|
432
|
+
function generateFixGuidance(controlId, findingTitle) {
|
|
433
|
+
const allControls = getAllPacks().flatMap(p => p.controls);
|
|
434
|
+
const control = allControls.find(c => c.id === controlId);
|
|
435
|
+
const lines = [];
|
|
436
|
+
lines.push(`# Fix Guidance: ${controlId}\n`);
|
|
437
|
+
if (control) {
|
|
438
|
+
lines.push(`## Control: ${control.name}`);
|
|
439
|
+
lines.push(`**Framework**: ${control.framework}`);
|
|
440
|
+
lines.push(`**Category**: ${control.category}`);
|
|
441
|
+
lines.push(`**Severity**: ${control.severity}`);
|
|
442
|
+
lines.push(`**Current Status**: ${control.status}`);
|
|
443
|
+
lines.push(`**Article**: ${control.article || "N/A"}\n`);
|
|
444
|
+
lines.push(`### Description\n${control.description}\n`);
|
|
445
|
+
lines.push(`### Implementation Guidance\n${control.implementation_guidance}\n`);
|
|
446
|
+
lines.push("### Implementation Steps\n");
|
|
447
|
+
const steps = generateImplementationSteps(control);
|
|
448
|
+
for (let i = 0; i < steps.length; i++) {
|
|
449
|
+
lines.push(`${i + 1}. ${steps[i]}`);
|
|
450
|
+
}
|
|
451
|
+
lines.push("\n### Verification\n");
|
|
452
|
+
lines.push("After implementing the fix:");
|
|
453
|
+
lines.push("1. Run `ges audit` to verify the finding no longer appears");
|
|
454
|
+
lines.push("2. Run `ges score` to see the updated compliance score");
|
|
455
|
+
lines.push("3. If the control is not applicable to your project, add it to `.ges/control-overrides.json`:");
|
|
456
|
+
lines.push("```json");
|
|
457
|
+
lines.push('[\n {\n "control_id": "' + controlId + '",\n "status": "not-applicable",\n "reason": "Explain why this control does not apply"\n }\n]');
|
|
458
|
+
lines.push("```");
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
lines.push(`Control **${controlId}** not found in any framework pack.`);
|
|
462
|
+
lines.push("\nAvailable control IDs:");
|
|
463
|
+
const grouped = {};
|
|
464
|
+
for (const c of allControls) {
|
|
465
|
+
if (!grouped[c.framework])
|
|
466
|
+
grouped[c.framework] = [];
|
|
467
|
+
grouped[c.framework].push(` ${c.id}: ${c.name} (${c.severity})`);
|
|
468
|
+
}
|
|
469
|
+
for (const [fw, ids] of Object.entries(grouped)) {
|
|
470
|
+
lines.push(`\n**${fw}:**`);
|
|
471
|
+
lines.push(ids.join("\n"));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (findingTitle) {
|
|
475
|
+
lines.push(`\n### Finding: ${findingTitle}\n`);
|
|
476
|
+
lines.push("To fix this specific finding:");
|
|
477
|
+
lines.push("1. Locate the file mentioned in the finding");
|
|
478
|
+
lines.push("2. Apply the fix suggested in the finding details");
|
|
479
|
+
lines.push("3. Run `ges audit` to verify the fix");
|
|
480
|
+
}
|
|
481
|
+
return lines.join("\n");
|
|
482
|
+
}
|
|
483
|
+
function generateImplementationSteps(control) {
|
|
484
|
+
const steps = [];
|
|
485
|
+
const category = control.category;
|
|
486
|
+
const id = control.id;
|
|
487
|
+
if (category === "encryption") {
|
|
488
|
+
steps.push("Install an encryption library: `npm install crypto-js` or use Node.js built-in `crypto` module");
|
|
489
|
+
steps.push("Implement AES-256-GCM encryption for data at rest");
|
|
490
|
+
steps.push("Ensure TLS 1.2+ is configured for all data in transit");
|
|
491
|
+
steps.push("Add encryption key management (use environment variables or a vault service)");
|
|
492
|
+
steps.push("Verify encryption is applied to all sensitive data fields in your database schema");
|
|
493
|
+
}
|
|
494
|
+
else if (category === "authentication") {
|
|
495
|
+
steps.push("Implement Argon2id password hashing: `npm install argon2`");
|
|
496
|
+
steps.push("Add multi-factor authentication (MFA) support");
|
|
497
|
+
steps.push("Implement session expiration (recommended: 15-30 minutes of inactivity)");
|
|
498
|
+
steps.push("Add rate limiting to authentication endpoints: `npm install express-rate-limit`");
|
|
499
|
+
steps.push("Configure CORS to restrict origins (never use `*` in production)");
|
|
500
|
+
}
|
|
501
|
+
else if (category === "authorization") {
|
|
502
|
+
steps.push("Implement Role-Based Access Control (RBAC) with defined roles and permissions");
|
|
503
|
+
steps.push("Apply the principle of least privilege to all user roles");
|
|
504
|
+
steps.push("Configure deny-by-default access control policies");
|
|
505
|
+
steps.push("Add authorization middleware to all protected routes");
|
|
506
|
+
steps.push("Document the access control matrix in your compliance documentation");
|
|
507
|
+
}
|
|
508
|
+
else if (category === "audit") {
|
|
509
|
+
steps.push("Implement audit logging middleware that captures: userId, action, resource, timestamp, ipAddress");
|
|
510
|
+
steps.push("Store audit logs in a separate, append-only data store");
|
|
511
|
+
steps.push("Ensure logs are immutable (no update or delete operations)");
|
|
512
|
+
steps.push("Add logging for: authentication, authorization, data exports, role changes, admin actions");
|
|
513
|
+
steps.push("Configure log retention policy (minimum 1 year for compliance)");
|
|
514
|
+
}
|
|
515
|
+
else if (category === "secrets") {
|
|
516
|
+
steps.push("Audit all source files for hardcoded secrets: `ges scan` or `npx gitleaks detect`");
|
|
517
|
+
steps.push("Move all secrets to environment variables or a secrets manager (Vault, AWS KMS, etc.)");
|
|
518
|
+
steps.push("Add secrets to `.gitignore` (.env files, key files, certificate files)");
|
|
519
|
+
steps.push("Implement secret rotation policy (rotate every 90 days minimum)");
|
|
520
|
+
steps.push("Add pre-commit hooks to prevent secrets from being committed: `npx gitleaks protect --staged`");
|
|
521
|
+
}
|
|
522
|
+
else if (category === "security-testing") {
|
|
523
|
+
steps.push("Set up automated security scanning in CI/CD (Trivy, Semgrep, npm audit)");
|
|
524
|
+
steps.push("Add dependency scanning to detect vulnerable packages");
|
|
525
|
+
steps.push("Implement static application security testing (SAST)");
|
|
526
|
+
steps.push("Schedule regular penetration testing (quarterly recommended)");
|
|
527
|
+
steps.push("Create a security testing checklist and integrate into your development workflow");
|
|
528
|
+
}
|
|
529
|
+
else if (category === "privacy") {
|
|
530
|
+
steps.push("Implement data minimization - only collect data that is necessary");
|
|
531
|
+
steps.push("Add privacy-by-design principles to your development process");
|
|
532
|
+
steps.push("Implement data subject rights endpoints (access, rectification, erasure, portability)");
|
|
533
|
+
steps.push("Create and publish a privacy policy");
|
|
534
|
+
steps.push("Conduct a Privacy Impact Assessment (PIA) for high-risk processing");
|
|
535
|
+
}
|
|
536
|
+
else if (category === "data-protection") {
|
|
537
|
+
steps.push("Classify all data into categories: public, internal, confidential, restricted");
|
|
538
|
+
steps.push("Apply appropriate protection controls based on classification");
|
|
539
|
+
steps.push("Implement data retention policies with automated deletion");
|
|
540
|
+
steps.push("Add data access logging for all restricted and confidential data");
|
|
541
|
+
steps.push("Create a data inventory documenting all personal data processing activities");
|
|
542
|
+
}
|
|
543
|
+
else if (category === "access-control") {
|
|
544
|
+
steps.push("Review and document all user roles and their permissions");
|
|
545
|
+
steps.push("Implement the principle of least privilege");
|
|
546
|
+
steps.push("Add separation of duties for critical operations");
|
|
547
|
+
steps.push("Implement regular access reviews (quarterly recommended)");
|
|
548
|
+
steps.push("Automate provisioning and deprovisioning of access");
|
|
549
|
+
}
|
|
550
|
+
else if (category === "incident-response") {
|
|
551
|
+
steps.push("Create an incident response plan with defined severity levels and escalation paths");
|
|
552
|
+
steps.push("Define communication templates for GDPR breach notification (72-hour requirement)");
|
|
553
|
+
steps.push("Set up incident detection and alerting (monitoring, SIEM)");
|
|
554
|
+
steps.push("Conduct regular incident response tabletop exercises");
|
|
555
|
+
steps.push("Document lessons learned after each incident");
|
|
556
|
+
}
|
|
557
|
+
else if (category === "vulnerability-management") {
|
|
558
|
+
steps.push("Implement automated vulnerability scanning in CI/CD pipeline");
|
|
559
|
+
steps.push("Set up dependency scanning (npm audit, Dependabot, Snyk)");
|
|
560
|
+
steps.push("Define SLA for fixing vulnerabilities based on severity (critical: 24h, high: 7d)");
|
|
561
|
+
steps.push("Maintain a vulnerability register with tracking");
|
|
562
|
+
steps.push("Regularly review and update dependencies");
|
|
563
|
+
}
|
|
564
|
+
else if (category === "configuration") {
|
|
565
|
+
steps.push("Review and harden all service configurations");
|
|
566
|
+
steps.push("Implement security headers (helmet for Node.js: `npm install helmet`)");
|
|
567
|
+
steps.push("Configure proper CORS policies");
|
|
568
|
+
steps.push("Ensure containers do not run as root");
|
|
569
|
+
steps.push("Remove all default credentials and configurations");
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
steps.push(`Review the control requirements: ${control.description}`);
|
|
573
|
+
steps.push(`Follow the implementation guidance: ${control.implementation_guidance}`);
|
|
574
|
+
steps.push("Implement the required controls based on your project's architecture");
|
|
575
|
+
steps.push("Test the implementation thoroughly");
|
|
576
|
+
steps.push("Document the implementation in your compliance documentation");
|
|
577
|
+
}
|
|
578
|
+
if (id.includes("AI") || id.includes("ai-")) {
|
|
579
|
+
steps.push("");
|
|
580
|
+
steps.push("**AI-Specific Considerations:**");
|
|
581
|
+
steps.push("- Implement prompt logging and monitoring");
|
|
582
|
+
steps.push("- Add PII detection for all inputs and outputs");
|
|
583
|
+
steps.push("- Rate limit AI API calls to prevent abuse");
|
|
584
|
+
steps.push("- Validate all AI outputs before presenting to users");
|
|
585
|
+
steps.push("- Classify data before sending to AI providers");
|
|
586
|
+
}
|
|
587
|
+
if (id.includes("BLOCK") || id.includes("blockchain")) {
|
|
588
|
+
steps.push("");
|
|
589
|
+
steps.push("**Blockchain-Specific Considerations:**");
|
|
590
|
+
steps.push("- Never store plaintext personal data on-chain");
|
|
591
|
+
steps.push("- Store only hashes, CIDs, or encrypted references on-chain");
|
|
592
|
+
steps.push("- Implement key rotation procedures");
|
|
593
|
+
steps.push("- Use cryptographic signatures for all on-chain transactions");
|
|
594
|
+
steps.push("- Maintain immutable audit trails off-chain");
|
|
595
|
+
}
|
|
596
|
+
return steps;
|
|
597
|
+
}
|
|
598
|
+
function createAutoFixPlan(root, findings, filterRuleIds) {
|
|
599
|
+
const actions = [];
|
|
600
|
+
const warnings = [];
|
|
601
|
+
const processedRules = new Set();
|
|
602
|
+
for (const f of findings) {
|
|
603
|
+
if (filterRuleIds && !filterRuleIds.has(f.ruleId))
|
|
604
|
+
continue;
|
|
605
|
+
const key = `${f.ruleId}:${f.file}`;
|
|
606
|
+
if (processedRules.has(key))
|
|
607
|
+
continue;
|
|
608
|
+
processedRules.add(key);
|
|
609
|
+
switch (f.ruleId) {
|
|
610
|
+
case "CONFIG-001":
|
|
611
|
+
actions.push(...buildHelmetFix(root));
|
|
612
|
+
break;
|
|
613
|
+
case "CONFIG-002":
|
|
614
|
+
actions.push(...buildCorsFix(root));
|
|
615
|
+
break;
|
|
616
|
+
case "CONFIG-004":
|
|
617
|
+
actions.push(...buildEnvGitignoreFix(root));
|
|
618
|
+
break;
|
|
619
|
+
case "CONFIG-005":
|
|
620
|
+
actions.push(...buildDockerNonRootFix(root));
|
|
621
|
+
break;
|
|
622
|
+
case "CONFIG-007":
|
|
623
|
+
actions.push(...buildTLSFix(root, f));
|
|
624
|
+
break;
|
|
625
|
+
case "CONFIG-008":
|
|
626
|
+
actions.push(...buildGitignoreCreateFix(root));
|
|
627
|
+
break;
|
|
628
|
+
case "CONFIG-009":
|
|
629
|
+
actions.push(...buildGitignoreEntryFix(root, f));
|
|
630
|
+
break;
|
|
631
|
+
case "CONFIG-010":
|
|
632
|
+
actions.push(...buildLoggingFix(root));
|
|
633
|
+
break;
|
|
634
|
+
case "SECRETS-001":
|
|
635
|
+
actions.push(...buildSecretsFix(root, f));
|
|
636
|
+
warnings.push(`[SECRETS-001] Secret in ${f.file}:${f.line}. Verify .env is in .gitignore and never committed.`);
|
|
637
|
+
break;
|
|
638
|
+
case "CRYPTO-001":
|
|
639
|
+
actions.push(...buildWeakHashFix(root, f));
|
|
640
|
+
warnings.push("[CRYPTO-001] For passwords, use Argon2id instead of SHA-256.");
|
|
641
|
+
break;
|
|
642
|
+
case "CRYPTO-003":
|
|
643
|
+
actions.push(...buildPasswordFix(root, f));
|
|
644
|
+
break;
|
|
645
|
+
case "AUTH-002":
|
|
646
|
+
actions.push(...buildRateLimitFix(root));
|
|
647
|
+
break;
|
|
648
|
+
case "AUTH-003":
|
|
649
|
+
actions.push(...buildSessionTimeoutFix(root));
|
|
650
|
+
break;
|
|
651
|
+
case "AUTH-004":
|
|
652
|
+
actions.push(...buildCORSWildcardFix(root));
|
|
653
|
+
break;
|
|
654
|
+
case "DB-001":
|
|
655
|
+
actions.push(...buildTimestampsFix(root, f));
|
|
656
|
+
break;
|
|
657
|
+
case "DB-002":
|
|
658
|
+
actions.push(...buildSoftDeleteFix(root, f));
|
|
659
|
+
break;
|
|
660
|
+
case "DB-003":
|
|
661
|
+
actions.push(...buildUserAuditFix(root, f));
|
|
662
|
+
break;
|
|
663
|
+
case "DB-004":
|
|
664
|
+
actions.push(...buildAuditModelFix(root));
|
|
665
|
+
break;
|
|
666
|
+
default:
|
|
667
|
+
warnings.push(`[${f.severity.toUpperCase()}] ${f.title} in ${f.file}${f.line ? `:${f.line}` : ""}: Manual fix required.`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return { actions, warnings };
|
|
671
|
+
}
|
|
672
|
+
function applyAutoFixAction(root, action) {
|
|
673
|
+
const fullPath = path.join(root, action.filePath);
|
|
674
|
+
try {
|
|
675
|
+
switch (action.type) {
|
|
676
|
+
case "create": {
|
|
677
|
+
if (fs.existsSync(fullPath)) {
|
|
678
|
+
return { applied: false, action, error: "File already exists" };
|
|
679
|
+
}
|
|
680
|
+
const dir = path.dirname(fullPath);
|
|
681
|
+
if (!fs.existsSync(dir))
|
|
682
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
683
|
+
fs.writeFileSync(fullPath, action.content || "", "utf-8");
|
|
684
|
+
return { applied: true, action };
|
|
685
|
+
}
|
|
686
|
+
case "modify": {
|
|
687
|
+
if (!fs.existsSync(fullPath)) {
|
|
688
|
+
return { applied: false, action, error: "File not found" };
|
|
689
|
+
}
|
|
690
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
691
|
+
if (action.search && !content.includes(action.search)) {
|
|
692
|
+
return { applied: false, action, error: "Search string not found" };
|
|
693
|
+
}
|
|
694
|
+
fs.writeFileSync(fullPath, content.replace(action.search || "", action.replace || ""), "utf-8");
|
|
695
|
+
return { applied: true, action };
|
|
696
|
+
}
|
|
697
|
+
case "append": {
|
|
698
|
+
const dir = path.dirname(fullPath);
|
|
699
|
+
if (!fs.existsSync(dir))
|
|
700
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
701
|
+
fs.appendFileSync(fullPath, action.content || "", "utf-8");
|
|
702
|
+
return { applied: true, action };
|
|
703
|
+
}
|
|
704
|
+
case "npm-install": {
|
|
705
|
+
return { applied: true, action };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
return { applied: false, action, error: err instanceof Error ? err.message : String(err) };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function findMainAppFile(root) {
|
|
714
|
+
const lang = detectProjectLanguage(root);
|
|
715
|
+
const candidates = {
|
|
716
|
+
typescript: ["src/index.ts", "src/app.ts", "src/server.ts", "src/main.ts", "index.ts", "app.ts", "server.ts"],
|
|
717
|
+
javascript: ["src/index.js", "src/app.js", "src/server.js", "src/main.js", "index.js", "app.js", "server.js"],
|
|
718
|
+
python: ["app.py", "main.py", "manage.py", "wsgi.py", "asgi.py", "src/app.py", "src/main.py"],
|
|
719
|
+
ruby: ["config.ru", "app.rb", "server.rb", "main.rb", "config/application.rb"],
|
|
720
|
+
go: ["main.go", "cmd/server/main.go", "cmd/app/main.go"],
|
|
721
|
+
java: ["src/main/java/com/example/Application.java", "src/main/java/Application.java"],
|
|
722
|
+
php: ["public/index.php", "index.php", "app.php", "app/Http/Kernel.php"],
|
|
723
|
+
rust: ["src/main.rs", "src/bin/main.rs", "src/app.rs"],
|
|
724
|
+
csharp: ["Program.cs", "Startup.cs"],
|
|
725
|
+
};
|
|
726
|
+
const exts = candidates[lang] || [];
|
|
727
|
+
for (const c of exts) {
|
|
728
|
+
if (fs.existsSync(path.join(root, c)))
|
|
729
|
+
return c;
|
|
730
|
+
}
|
|
731
|
+
if (lang === "java") {
|
|
732
|
+
const found = findFileRecursive(root, "Application.java", "src/main/java");
|
|
733
|
+
if (found)
|
|
734
|
+
return found;
|
|
735
|
+
}
|
|
736
|
+
if (lang === "go") {
|
|
737
|
+
for (const c of ["cmd/server/main.go", "cmd/app/main.go", "main.go"]) {
|
|
738
|
+
if (fs.existsSync(path.join(root, c)))
|
|
739
|
+
return c;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
function findFileRecursive(root, name, baseDir) {
|
|
745
|
+
const dir = path.join(root, baseDir);
|
|
746
|
+
if (!fs.existsSync(dir))
|
|
747
|
+
return null;
|
|
748
|
+
try {
|
|
749
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
750
|
+
for (const e of entries) {
|
|
751
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "venv" || e.name === "__pycache__" || e.name === ".git")
|
|
752
|
+
continue;
|
|
753
|
+
const childPath = path.join(baseDir, e.name);
|
|
754
|
+
if (e.isDirectory()) {
|
|
755
|
+
const found = findFileRecursive(root, name, childPath);
|
|
756
|
+
if (found)
|
|
757
|
+
return found;
|
|
758
|
+
}
|
|
759
|
+
else if (e.name === name) {
|
|
760
|
+
return childPath;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
catch { /* skip */ }
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
function detectProjectLanguage(root) {
|
|
768
|
+
if (fs.existsSync(path.join(root, "go.mod")))
|
|
769
|
+
return "go";
|
|
770
|
+
if (fs.existsSync(path.join(root, "Cargo.toml")))
|
|
771
|
+
return "rust";
|
|
772
|
+
if (fs.existsSync(path.join(root, "requirements.txt")) || fs.existsSync(path.join(root, "pyproject.toml")) || fs.existsSync(path.join(root, "Pipfile")) || fs.existsSync(path.join(root, "setup.py")))
|
|
773
|
+
return "python";
|
|
774
|
+
if (fs.existsSync(path.join(root, "go.mod")))
|
|
775
|
+
return "go";
|
|
776
|
+
if (fs.existsSync(path.join(root, "pom.xml")) || fs.existsSync(path.join(root, "build.gradle")) || fs.existsSync(path.join(root, "build.gradle.kts")))
|
|
777
|
+
return "java";
|
|
778
|
+
if (fs.existsSync(path.join(root, "Gemfile")))
|
|
779
|
+
return "ruby";
|
|
780
|
+
if (fs.existsSync(path.join(root, "composer.json")))
|
|
781
|
+
return "php";
|
|
782
|
+
const pkgContent = readFileSafe(path.join(root, "package.json"));
|
|
783
|
+
if (pkgContent) {
|
|
784
|
+
try {
|
|
785
|
+
const pkg = JSON.parse(pkgContent);
|
|
786
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
787
|
+
if (deps.typescript || deps["@types/node"] || fs.existsSync(path.join(root, "tsconfig.json")))
|
|
788
|
+
return "typescript";
|
|
789
|
+
return "javascript";
|
|
790
|
+
}
|
|
791
|
+
catch { /* fallthrough */ }
|
|
792
|
+
}
|
|
793
|
+
if (fs.existsSync(path.join(root, "tsconfig.json")))
|
|
794
|
+
return "typescript";
|
|
795
|
+
return "javascript";
|
|
796
|
+
}
|
|
797
|
+
function detectWebFramework(root, lang) {
|
|
798
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
799
|
+
if (hasDep(root, "express"))
|
|
800
|
+
return "express";
|
|
801
|
+
if (hasDep(root, "fastify"))
|
|
802
|
+
return "fastify";
|
|
803
|
+
if (hasDep(root, "koa"))
|
|
804
|
+
return "koa";
|
|
805
|
+
if (hasDep(root, "hono"))
|
|
806
|
+
return "hono";
|
|
807
|
+
if (hasDep(root, "next"))
|
|
808
|
+
return "next";
|
|
809
|
+
if (hasDep(root, "@nestjs/core"))
|
|
810
|
+
return "nestjs";
|
|
811
|
+
if (hasDep(root, "@sveltejs/kit"))
|
|
812
|
+
return "sveltekit";
|
|
813
|
+
}
|
|
814
|
+
if (lang === "python") {
|
|
815
|
+
const reqFiles = ["requirements.txt", "pyproject.toml", "Pipfile"];
|
|
816
|
+
for (const f of reqFiles) {
|
|
817
|
+
const c = readFileSafe(path.join(root, f));
|
|
818
|
+
if (c) {
|
|
819
|
+
if (/^\s*django\b/mi.test(c) || /django/i.test(c))
|
|
820
|
+
return "django";
|
|
821
|
+
if (/^\s*flask\b/mi.test(c) || /flask/i.test(c))
|
|
822
|
+
return "flask";
|
|
823
|
+
if (/^\s*fastapi\b/mi.test(c) || /fastapi/i.test(c))
|
|
824
|
+
return "fastapi";
|
|
825
|
+
if (/^\s*sanic\b/mi.test(c) || /sanic/i.test(c))
|
|
826
|
+
return "sanic";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const settingsPy = readFileSafe(path.join(root, "settings.py")) || readFileSafe(path.join(root, "app/settings.py")) || readFileSafe(path.join(root, "config/settings.py"));
|
|
830
|
+
if (settingsPy && /DJANGO_SETTINGS_MODULE|INSTALLED_APPS|django/.test(settingsPy))
|
|
831
|
+
return "django";
|
|
832
|
+
const appPy = readFileSafe(path.join(root, "app.py")) || readFileSafe(path.join(root, "main.py"));
|
|
833
|
+
if (appPy) {
|
|
834
|
+
if (/from\s+flask\s+import|import\s+flask/.test(appPy))
|
|
835
|
+
return "flask";
|
|
836
|
+
if (/from\s+fastapi\s+import|import\s+fastapi/.test(appPy))
|
|
837
|
+
return "fastapi";
|
|
838
|
+
if (/from\s+django/.test(appPy))
|
|
839
|
+
return "django";
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (lang === "ruby") {
|
|
843
|
+
const gemfile = readFileSafe(path.join(root, "Gemfile"));
|
|
844
|
+
if (gemfile) {
|
|
845
|
+
if (/rails/i.test(gemfile))
|
|
846
|
+
return "rails";
|
|
847
|
+
if (/sinatra/i.test(gemfile))
|
|
848
|
+
return "sinatra";
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (lang === "go") {
|
|
852
|
+
const goMod = readFileSafe(path.join(root, "go.mod")) || "";
|
|
853
|
+
const mainGo = readFileSafe(path.join(root, "main.go")) || "";
|
|
854
|
+
const allGo = goMod + mainGo;
|
|
855
|
+
if (/gin-gonic|gin\.Default|gin\.New/.test(allGo))
|
|
856
|
+
return "gin";
|
|
857
|
+
if (/fiber\.New/.test(allGo))
|
|
858
|
+
return "fiber";
|
|
859
|
+
if (/echo\.New/.test(allGo))
|
|
860
|
+
return "echo";
|
|
861
|
+
if (/chi\.NewRouter|chi\.Mux/.test(allGo))
|
|
862
|
+
return "chi";
|
|
863
|
+
if (/mux\.NewRouter/.test(allGo))
|
|
864
|
+
return "gorilla";
|
|
865
|
+
if (/http\.ListenAndServe|http\.HandleFunc/.test(allGo))
|
|
866
|
+
return "nethttp";
|
|
867
|
+
}
|
|
868
|
+
if (lang === "java") {
|
|
869
|
+
const pom = readFileSafe(path.join(root, "pom.xml")) || "";
|
|
870
|
+
const gradle = readFileSafe(path.join(root, "build.gradle")) || "";
|
|
871
|
+
const all = pom + gradle;
|
|
872
|
+
if (/spring-boot|springframework/.test(all))
|
|
873
|
+
return "spring";
|
|
874
|
+
if (/ktor/.test(all))
|
|
875
|
+
return "ktor";
|
|
876
|
+
if (/quarkus/.test(all))
|
|
877
|
+
return "quarkus";
|
|
878
|
+
if (/micronaut/.test(all))
|
|
879
|
+
return "micronaut";
|
|
880
|
+
}
|
|
881
|
+
if (lang === "rust") {
|
|
882
|
+
const cargo = readFileSafe(path.join(root, "Cargo.toml")) || "";
|
|
883
|
+
const mainRs = readFileSafe(path.join(root, "src/main.rs")) || "";
|
|
884
|
+
const libRs = readFileSafe(path.join(root, "src/lib.rs")) || "";
|
|
885
|
+
const all = cargo + mainRs + libRs;
|
|
886
|
+
if (/actix-web|actix_web/.test(all))
|
|
887
|
+
return "actix";
|
|
888
|
+
if (/axum/.test(all))
|
|
889
|
+
return "axum";
|
|
890
|
+
if (/rocket/.test(all))
|
|
891
|
+
return "rocket";
|
|
892
|
+
if (/warp/.test(all))
|
|
893
|
+
return "warp";
|
|
894
|
+
}
|
|
895
|
+
if (lang === "php") {
|
|
896
|
+
const composer = readFileSafe(path.join(root, "composer.json"));
|
|
897
|
+
if (composer) {
|
|
898
|
+
try {
|
|
899
|
+
const pkg = JSON.parse(composer);
|
|
900
|
+
const req = pkg.require || {};
|
|
901
|
+
if (req["laravel/framework"])
|
|
902
|
+
return "laravel";
|
|
903
|
+
if (req["symfony/symfony"] || req["symfony/framework-bundle"])
|
|
904
|
+
return "symfony";
|
|
905
|
+
if (req["slim/slim"])
|
|
906
|
+
return "slim";
|
|
907
|
+
if (req["laravel/lumen-framework"])
|
|
908
|
+
return "lumen";
|
|
909
|
+
}
|
|
910
|
+
catch { /* skip */ }
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return "generic";
|
|
914
|
+
}
|
|
915
|
+
function hasDep(root, dep) {
|
|
916
|
+
const pkg = readJsonFileSafe(path.join(root, "package.json"));
|
|
917
|
+
if (!pkg)
|
|
918
|
+
return false;
|
|
919
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
920
|
+
return dep in deps;
|
|
921
|
+
}
|
|
922
|
+
function hasPyDep(root, dep) {
|
|
923
|
+
for (const f of ["requirements.txt", "requirements-dev.txt"]) {
|
|
924
|
+
const c = readFileSafe(path.join(root, f));
|
|
925
|
+
if (c && new RegExp(`^\\s*${dep}\\b`, "mi").test(c))
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
const pyproject = readFileSafe(path.join(root, "pyproject.toml"));
|
|
929
|
+
if (pyproject && new RegExp(`^\\s*${dep}\\b`, "mi").test(pyproject))
|
|
930
|
+
return true;
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
function hasGoDep(root, dep) {
|
|
934
|
+
const goMod = readFileSafe(path.join(root, "go.mod"));
|
|
935
|
+
return goMod ? goMod.includes(dep) : false;
|
|
936
|
+
}
|
|
937
|
+
function hasRubyDep(root, dep) {
|
|
938
|
+
const gemfile = readFileSafe(path.join(root, "Gemfile"));
|
|
939
|
+
return gemfile ? new RegExp(`gem\\s+['"]${dep}`, "i").test(gemfile) : false;
|
|
940
|
+
}
|
|
941
|
+
function hasJavaDep(root, dep) {
|
|
942
|
+
const pom = readFileSafe(path.join(root, "pom.xml"));
|
|
943
|
+
if (pom && pom.includes(dep))
|
|
944
|
+
return true;
|
|
945
|
+
const gradle = readFileSafe(path.join(root, "build.gradle"));
|
|
946
|
+
return gradle ? gradle.includes(dep) : false;
|
|
947
|
+
}
|
|
948
|
+
function hasPhpDep(root, dep) {
|
|
949
|
+
const composer = readFileSafe(path.join(root, "composer.json"));
|
|
950
|
+
if (!composer)
|
|
951
|
+
return false;
|
|
952
|
+
try {
|
|
953
|
+
const pkg = JSON.parse(composer);
|
|
954
|
+
const req = { ...(pkg.require || {}), ...(pkg["require-dev"] || {}) };
|
|
955
|
+
return dep in req;
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function hasRustDep(root, dep) {
|
|
962
|
+
const cargo = readFileSafe(path.join(root, "Cargo.toml"));
|
|
963
|
+
return cargo ? new RegExp(`^${dep}\\b`, "m").test(cargo) || new RegExp(`${dep}\\s*=`).test(cargo) : false;
|
|
964
|
+
}
|
|
965
|
+
function readFileSafe(filePath) {
|
|
966
|
+
try {
|
|
967
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function buildHelmetFix(root) {
|
|
974
|
+
const lang = detectProjectLanguage(root);
|
|
975
|
+
const fw = detectWebFramework(root, lang);
|
|
976
|
+
const actions = [];
|
|
977
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
978
|
+
const appFile = findMainAppFile(root);
|
|
979
|
+
if (!appFile)
|
|
980
|
+
return [];
|
|
981
|
+
if (fw === "express") {
|
|
982
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install helmet", ruleId: "CONFIG-001" });
|
|
983
|
+
const content = readFileSafe(path.join(root, appFile));
|
|
984
|
+
if (content && content.includes("const app = express()")) {
|
|
985
|
+
actions.push({ type: "modify", filePath: appFile, search: "const app = express()", replace: "const app = express()\n\napp.use(helmet())", description: "Add helmet middleware", ruleId: "CONFIG-001" });
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport helmet from 'helmet';\napp.use(helmet());\n", description: "Add helmet import and middleware", ruleId: "CONFIG-001" });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
else if (fw === "fastify") {
|
|
992
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install @fastify/helmet", ruleId: "CONFIG-001" });
|
|
993
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport helmet from '@fastify/helmet';\napp.register(helmet);\n", description: "Add Fastify helmet plugin", ruleId: "CONFIG-001" });
|
|
994
|
+
}
|
|
995
|
+
else if (fw === "koa") {
|
|
996
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install koa-helmet", ruleId: "CONFIG-001" });
|
|
997
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport helmet from 'koa-helmet';\napp.use(helmet());\n", description: "Add koa-helmet middleware", ruleId: "CONFIG-001" });
|
|
998
|
+
}
|
|
999
|
+
else if (fw === "hono") {
|
|
1000
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport { secureHeaders } from 'hono/secure-headers';\napp.use(secureHeaders());\n", description: "Add Hono secure headers", ruleId: "CONFIG-001" });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else if (lang === "python") {
|
|
1004
|
+
if (fw === "django") {
|
|
1005
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Python uses django-csp/secure", ruleId: "CONFIG-001" });
|
|
1006
|
+
const settingsFile = findFileRecursive(root, "settings.py", ".") || "settings.py";
|
|
1007
|
+
actions.push({ type: "append", filePath: settingsFile, content: "\n# Security headers\nSECURE_BROWSER_XSS_FILTER = True\nSECURE_CONTENT_TYPE_NOSNIFF = True\nSECURE_HSTS_SECONDS = 31536000\nSECURE_HSTS_INCLUDE_SUBDOMAINS = True\nSECURE_HSTS_PRELOAD = True\nX_FRAME_OPTIONS = 'DENY'\nSECURE_SSL_REDIRECT = True\nSESSION_COOKIE_SECURE = True\nCSRF_COOKIE_SECURE = True\n", description: "Add Django security headers settings", ruleId: "CONFIG-001" });
|
|
1008
|
+
}
|
|
1009
|
+
else if (fw === "flask" || fw === "fastapi" || fw === "sanic") {
|
|
1010
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
1011
|
+
actions.push({ type: "append", filePath: appFile, content: fw === "fastapi"
|
|
1012
|
+
? "\nfrom fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware\napp.add_middleware(HTTPSRedirectMiddleware)\n"
|
|
1013
|
+
: "\nfrom flask_talisman import Talisman\nTalisman(app, force_https=True, strict_transport_security=True, session_cookie_secure=True)\n",
|
|
1014
|
+
description: `Add security headers for ${fw}`, ruleId: "CONFIG-001" });
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
else if (lang === "ruby") {
|
|
1018
|
+
if (fw === "rails") {
|
|
1019
|
+
const envFile = fs.existsSync(path.join(root, "config/environments/production.rb")) ? "config/environments/production.rb" : "config/application.rb";
|
|
1020
|
+
actions.push({ type: "append", filePath: envFile, content: "\nconfig.force_ssl = true\nconfig.ssl_options = { hsts: { subdomains: true, preload: true, expires: 1.year } }\nconfig.x_frame_options = 'SAMEORIGIN'\nconfig.x_content_type_options = 'nosniff'\nconfig.x_xss_protection = '1; mode=block'\nconfig.strict_transport_security = 'max-age=31536000; includeSubDomains'\n", description: "Add Rails security headers", ruleId: "CONFIG-001" });
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
else if (lang === "go") {
|
|
1024
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
1025
|
+
if (fw === "gin" || fw === "echo" || fw === "fiber" || fw === "chi" || fw === "nethttp") {
|
|
1026
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport \"net/http\"\n\n// Security headers middleware\nfunc securityHeaders(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\t\tw.Header().Set(\"X-Frame-Options\", \"DENY\")\n\t\tw.Header().Set(\"X-XSS-Protection\", \"1; mode=block\")\n\t\tw.Header().Set(\"Strict-Transport-Security\", \"max-age=31536000; includeSubDomains\")\n\t\tw.Header().Set(\"Referrer-Policy\", \"strict-origin-when-cross-origin\")\n\t\tw.Header().Set(\"Content-Security-Policy\", \"default-src 'self'\")\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n", description: "Add Go security headers middleware", ruleId: "CONFIG-001" });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
else if (lang === "java") {
|
|
1030
|
+
if (fw === "spring") {
|
|
1031
|
+
const hasSrc = fs.existsSync(path.join(root, "src/main/java"));
|
|
1032
|
+
const configPath = hasSrc ? "src/main/java/com/example/SecurityConfig.java" : "SecurityConfig.java";
|
|
1033
|
+
actions.push({ type: "create", filePath: configPath, content: `import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.web.SecurityFilterChain;\nimport org.springframework.security.web.header.writers.StaticHeadersWriter;\n\n@Configuration\npublic class SecurityConfig {\n @Bean\n public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\n http.headers()\n .contentSecurityPolicy("default-src 'self'")\n .and()\n .xssProtection()\n .and()\n .frameOptions().deny()\n .httpStrictTransportSecurity()\n .includeSubDomains(true)\n .preload(true)\n .maxAgeInSeconds(31536000);\n return http.build();\n }\n}\n`, description: "Create Spring Security config with headers", ruleId: "CONFIG-001" });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
else if (lang === "php") {
|
|
1037
|
+
if (fw === "laravel" || fw === "symfony") {
|
|
1038
|
+
const middleware = fw === "laravel" ? "app/Http/Middleware/SecurityHeaders.php" : "src/Middleware/SecurityHeadersMiddleware.php";
|
|
1039
|
+
const content = fw === "laravel"
|
|
1040
|
+
? `<?php\n\nnamespace App\\Http\\Middleware;\n\nuse Closure;\n\nclass SecurityHeaders\n{\n public function handle($request, Closure $next)\n {\n $response = $next($request);\n $response->headers->set('X-Content-Type-Options', 'nosniff');\n $response->headers->set('X-Frame-Options', 'DENY');\n $response->headers->set('X-XSS-Protection', '1; mode=block');\n $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');\n return $response;\n }\n}\n`
|
|
1041
|
+
: `<?php\n\nnamespace App\\Middleware;\n\nuse Symfony\\Component\\HttpFoundation\\Response;\n\nclass SecurityHeadersMiddleware\n{\n public function __invoke($request, $handler)\n {\n $response = $handler->handle($request);\n $response->headers->set('X-Content-Type-Options', 'nosniff');\n $response->headers->set('X-Frame-Options', 'DENY');\n $response->headers->set('X-XSS-Protection', '1; mode=block');\n $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n return $response;\n }\n}\n`;
|
|
1042
|
+
actions.push({ type: "create", filePath: middleware, content, description: `Create security headers middleware for ${fw}`, ruleId: "CONFIG-001" });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
else if (lang === "rust") {
|
|
1046
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
1047
|
+
if (fw === "actix") {
|
|
1048
|
+
actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: `use actix_web::{HttpResponse, dev::{ServiceRequest, Service, ServiceResponse}};
|
|
1049
|
+
|
|
1050
|
+
pub fn add_security_headers(res: &mut HttpResponse) {
|
|
1051
|
+
res.headers_mut().insert(("X-Content-Type-Options", "nosniff"));
|
|
1052
|
+
res.headers_mut().insert(("X-Frame-Options", "DENY"));
|
|
1053
|
+
res.headers_mut().insert(("X-XSS-Protection", "1; mode=block"));
|
|
1054
|
+
res.headers_mut().insert(("Strict-Transport-Security", "max-age=31536000; includeSubDomains"));
|
|
1055
|
+
res.headers_mut().insert(("Referrer-Policy", "strict-origin-when-cross-origin"));
|
|
1056
|
+
res.headers_mut().insert(("Content-Security-Policy", "default-src 'self'"));
|
|
1057
|
+
}
|
|
1058
|
+
`, description: "Create Actix-web security headers middleware", ruleId: "CONFIG-001" });
|
|
1059
|
+
}
|
|
1060
|
+
else if (fw === "axum") {
|
|
1061
|
+
actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: `use axum::{http::HeaderValue, response::Response};
|
|
1062
|
+
|
|
1063
|
+
pub async fn security_headers(mut res: Response) -> Response {
|
|
1064
|
+
let headers = res.headers_mut();
|
|
1065
|
+
headers.insert("X-Content-Type-Options", HeaderValue::from_static("nosniff"));
|
|
1066
|
+
headers.insert("X-Frame-Options", HeaderValue::from_static("DENY"));
|
|
1067
|
+
headers.insert("X-XSS-Protection", HeaderValue::from_static("1; mode=block"));
|
|
1068
|
+
headers.insert("Strict-Transport-Security", HeaderValue::from_static("max-age=31536000; includeSubDomains"));
|
|
1069
|
+
headers.insert("Referrer-Policy", HeaderValue::from_static("strict-origin-when-cross-origin"));
|
|
1070
|
+
headers.insert("Content-Security-Policy", HeaderValue::from_static("default-src 'self'"));
|
|
1071
|
+
res
|
|
1072
|
+
}
|
|
1073
|
+
`, description: "Create Axum security headers middleware", ruleId: "CONFIG-001" });
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
actions.push({ type: "append", filePath: appFile, content: "\n// GESF: Add security headers middleware\n// actix-web: use actix_web::middleware::DefaultHeaders\n// axum: use tower-http::set-header::SetResponseHeader\n// rocket: use rocket::fairing\n", description: "Add Rust security headers guidance", ruleId: "CONFIG-001" });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return actions;
|
|
1080
|
+
}
|
|
1081
|
+
function buildCorsFix(root) {
|
|
1082
|
+
const lang = detectProjectLanguage(root);
|
|
1083
|
+
const fw = detectWebFramework(root, lang);
|
|
1084
|
+
const actions = [];
|
|
1085
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1086
|
+
const appFile = findMainAppFile(root);
|
|
1087
|
+
if (!appFile)
|
|
1088
|
+
return [];
|
|
1089
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install cors", ruleId: "CONFIG-002" });
|
|
1090
|
+
if (fw === "fastify") {
|
|
1091
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install @fastify/cors", ruleId: "CONFIG-002" });
|
|
1092
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport cors from '@fastify/cors';\napp.register(cors, { origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'] });\n", description: "Add Fastify CORS", ruleId: "CONFIG-002" });
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport cors from 'cors';\napp.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'] }));\n", description: "Add CORS with configured origins", ruleId: "CONFIG-002" });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
else if (lang === "python") {
|
|
1099
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
1100
|
+
if (fw === "django") {
|
|
1101
|
+
const settingsFile = findFileRecursive(root, "settings.py", ".") || "settings.py";
|
|
1102
|
+
actions.push({ type: "append", filePath: settingsFile, content: "\nCORS_ALLOWED_ORIGINS = ['https://yourdomain.com']\nCORS_ALLOW_CREDENTIALS = True\n", description: "Add Django CORS settings", ruleId: "CONFIG-002" });
|
|
1103
|
+
}
|
|
1104
|
+
else if (fw === "fastapi") {
|
|
1105
|
+
actions.push({ type: "append", filePath: appFile, content: "\nfrom fastapi.middleware.cors import CORSMiddleware\napp.add_middleware(CORSMiddleware, allow_origins=['http://localhost:3000'], allow_credentials=True, allow_methods=['*'], allow_headers=['*'])\n", description: "Add FastAPI CORS middleware", ruleId: "CONFIG-002" });
|
|
1106
|
+
}
|
|
1107
|
+
else if (fw === "flask") {
|
|
1108
|
+
actions.push({ type: "append", filePath: appFile, content: "\nfrom flask_cors import CORS\nCORS(app, origins=['http://localhost:3000'])\n", description: "Add Flask CORS", ruleId: "CONFIG-002" });
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
actions.push({ type: "append", filePath: appFile, content: "\n# CORS: Configure allowed origins in production\n# pip install flask-cors or fastapi[all]\n", description: "Add CORS note", ruleId: "CONFIG-002" });
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
else if (lang === "ruby") {
|
|
1115
|
+
if (fw === "rails") {
|
|
1116
|
+
actions.push({ type: "append", filePath: "config/application.rb", content: "\nconfig.middleware.insert_before 0, Rack::Cors do\n allow do\n origins 'https://yourdomain.com'\n resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete]\n end\nend\n", description: "Add Rails CORS via Rack::Cors", ruleId: "CONFIG-002" });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
else if (lang === "go") {
|
|
1120
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
1121
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport \"net/http\"\n\nfunc corsMiddleware(allowedOrigins []string, next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\torigin := r.Header.Get(\"Origin\")\n\t\tfor _, o := range allowedOrigins {\n\t\t\tif origin == o {\n\t\t\t\tw.Header().Set(\"Access-Control-Allow-Origin\", origin)\n\t\t\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\")\n\t\t\t\tw.Header().Set(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif r.Method == \"OPTIONS\" { w.WriteHeader(http.StatusNoContent); return }\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n", description: "Add Go CORS middleware", ruleId: "CONFIG-002" });
|
|
1122
|
+
}
|
|
1123
|
+
else if (lang === "java") {
|
|
1124
|
+
if (fw === "spring") {
|
|
1125
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/CorsConfig.java", content: `import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.UrlBasedCorsConfigurationSource;\nimport org.springframework.web.filter.CorsFilter;\n\n@Configuration\npublic class CorsConfig {\n @Bean\n public CorsFilter corsFilter() {\n CorsConfiguration config = new CorsConfiguration();\n config.addAllowedOrigin(\"https://yourdomain.com\");\n config.addAllowedHeader(\"*\");\n config.addAllowedMethod(\"*\");\n config.setAllowCredentials(true);\n UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\n source.registerCorsConfiguration(\"/**\", config);\n return new CorsFilter(source);\n }\n}\n`, description: "Create Spring CORS configuration", ruleId: "CONFIG-002" });
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
else if (lang === "rust") {
|
|
1129
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
1130
|
+
if (fw === "actix") {
|
|
1131
|
+
actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: `use actix_cors::Cors;
|
|
1132
|
+
use actix_web::http::header;
|
|
1133
|
+
|
|
1134
|
+
pub fn cors_config() -> Cors {
|
|
1135
|
+
Cors::default()
|
|
1136
|
+
.allowed_origin("http://localhost:3000")
|
|
1137
|
+
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
|
|
1138
|
+
.allowed_headers(vec![header::CONTENT_TYPE, header::AUTHORIZATION])
|
|
1139
|
+
.max_age(3600)
|
|
1140
|
+
}
|
|
1141
|
+
`, description: "Create Actix-web CORS configuration", ruleId: "CONFIG-002" });
|
|
1142
|
+
}
|
|
1143
|
+
else if (fw === "axum") {
|
|
1144
|
+
actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: `use tower_http::cors::{CorsLayer, Any};
|
|
1145
|
+
use http::Method;
|
|
1146
|
+
|
|
1147
|
+
pub fn cors_layer() -> CorsLayer {
|
|
1148
|
+
CorsLayer::new()
|
|
1149
|
+
.allow_origin(["http://localhost:3000".parse().unwrap()])
|
|
1150
|
+
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
|
1151
|
+
.allow_headers(Any)
|
|
1152
|
+
}
|
|
1153
|
+
`, description: "Create Axum CORS layer", ruleId: "CONFIG-002" });
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
actions.push({ type: "append", filePath: appFile, content: "\n// GESF CORS: Configure allowed origins\n// actix-web: cargo add actix-cors\n// axum: cargo add tower-http --features cors\n// rocket: cargo add rocket_cors\n", description: "Add Rust CORS guidance", ruleId: "CONFIG-002" });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return actions;
|
|
1160
|
+
}
|
|
1161
|
+
function buildEnvGitignoreFix(root) {
|
|
1162
|
+
const gi = fs.existsSync(path.join(root, ".gitignore")) ? ".gitignore" : null;
|
|
1163
|
+
const envFiles = detectProjectLanguage(root) === "python" ? "\n.env\n.env.*\n!.env.example\n*.pyc\n__pycache__/\n"
|
|
1164
|
+
: detectProjectLanguage(root) === "go" ? "\n.env\n.env.*\n!.env.example\n*.exe\n"
|
|
1165
|
+
: detectProjectLanguage(root) === "ruby" ? "\n.env\n.env.*\n!.env.example\n*.gem\n"
|
|
1166
|
+
: detectProjectLanguage(root) === "java" ? "\n.env\n.env.*\n!.env.example\n*.class\ntarget/\n"
|
|
1167
|
+
: detectProjectLanguage(root) === "php" ? "\n.env\n.env.*\n!.env.example\nvendor/\n"
|
|
1168
|
+
: detectProjectLanguage(root) === "rust" ? "\n.env\n.env.*\n!.env.example\ntarget/\n*.key\n*.pem\n"
|
|
1169
|
+
: "\n.env\n.env.*\n!.env.example\n";
|
|
1170
|
+
if (!gi)
|
|
1171
|
+
return buildGitignoreCreateFix(root);
|
|
1172
|
+
const content = readFileSafe(path.join(root, gi)) || "";
|
|
1173
|
+
if (content.includes(".env"))
|
|
1174
|
+
return [];
|
|
1175
|
+
return [{ type: "append", filePath: ".gitignore", content: envFiles, description: "Add .env to .gitignore", ruleId: "CONFIG-004" }];
|
|
1176
|
+
}
|
|
1177
|
+
function buildDockerNonRootFix(root) {
|
|
1178
|
+
if (!fs.existsSync(path.join(root, "Dockerfile")))
|
|
1179
|
+
return [];
|
|
1180
|
+
return [{ type: "append", filePath: "Dockerfile", content: "\nUSER node\n", description: "Add non-root USER to Dockerfile", ruleId: "CONFIG-005" }];
|
|
1181
|
+
}
|
|
1182
|
+
function buildTLSFix(root, f) {
|
|
1183
|
+
return [{ type: "modify", filePath: f.file, search: "NODE_TLS_REJECT_UNAUTHORIZED=0", replace: "NODE_TLS_REJECT_UNAUTHORIZED=1", description: "Re-enable TLS verification", ruleId: "CONFIG-007" }];
|
|
1184
|
+
}
|
|
1185
|
+
function buildGitignoreCreateFix(root) {
|
|
1186
|
+
const lang = detectProjectLanguage(root);
|
|
1187
|
+
const templates = {
|
|
1188
|
+
typescript: "node_modules/\n.env\n.env.*\n!.env.example\ndist/\nbuild/\n*.key\n*.pem\ncoverage/\n.DS_Store\n",
|
|
1189
|
+
javascript: "node_modules/\n.env\n.env.*\n!.env.example\ndist/\nbuild/\n*.key\n*.pem\ncoverage/\n.DS_Store\n",
|
|
1190
|
+
python: "__pycache__/\n*.pyc\n*.pyo\n.env\n.env.*\n!.env.example\n*.key\n*.pem\n.pytest_cache/\n.venv/\nvenv/\n*.egg-info/\ndist/\nbuild/\n.DS_Store\n",
|
|
1191
|
+
ruby: ".env\n.env.*\n!.env.example\n*.key\n*.pem\nlog/\ntmp/\n*.gem\n.DS_Store\n",
|
|
1192
|
+
go: ".env\n.env.*\n!.env.example\n*.key\n*.pem\n*.exe\n/bin/\n.DS_Store\n",
|
|
1193
|
+
java: ".env\n.env.*\n!.env.example\n*.key\n*.pem\n*.class\ntarget/\n.idea/\n*.iml\n.DS_Store\n",
|
|
1194
|
+
php: ".env\n.env.*\n!.env.example\nvendor/\n*.key\n*.pem\n.DS_Store\n",
|
|
1195
|
+
rust: "target/\nCargo.lock\n.env\n.env.*\n!.env.example\n*.key\n*.pem\n.DS_Store\n",
|
|
1196
|
+
csharp: ".env\n.env.*\n!.env.example\nbin/\nobj/\n*.key\n*.pem\n.DS_Store\n",
|
|
1197
|
+
};
|
|
1198
|
+
return [{ type: "create", filePath: ".gitignore", content: templates[lang] || templates.javascript, description: `Create .gitignore for ${lang} project`, ruleId: "CONFIG-008" }];
|
|
1199
|
+
}
|
|
1200
|
+
function buildGitignoreEntryFix(root, f) {
|
|
1201
|
+
const entry = f.fix.replace("Add ", "").replace(" to .gitignore.", "");
|
|
1202
|
+
if (!fs.existsSync(path.join(root, ".gitignore")))
|
|
1203
|
+
return buildGitignoreCreateFix(root);
|
|
1204
|
+
return [{ type: "append", filePath: ".gitignore", content: `\n${entry}\n`, description: `Add ${entry} to .gitignore`, ruleId: "CONFIG-009" }];
|
|
1205
|
+
}
|
|
1206
|
+
function buildLoggingFix(root) {
|
|
1207
|
+
const lang = detectProjectLanguage(root);
|
|
1208
|
+
const actions = [];
|
|
1209
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1210
|
+
const hasSrc = fs.existsSync(path.join(root, "src"));
|
|
1211
|
+
const loggerPath = hasSrc ? "src/lib/logger.ts" : "lib/logger.ts";
|
|
1212
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install pino logger", ruleId: "CONFIG-010" });
|
|
1213
|
+
actions.push({ type: "create", filePath: loggerPath, content: `import pino from 'pino';\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n timestamp: pino.stdTimeFunctions.isoTime,\n});\n\ninterface AuditLogParams {\n userId: string;\n action: string;\n resource: string;\n ipAddress: string;\n metadata?: Record<string, unknown>;\n}\n\nexport function auditLog(params: AuditLogParams): void {\n logger.info({ ...params, timestamp: new Date().toISOString(), type: 'audit' });\n}\n\nexport default logger;\n`, description: "Create structured logger with audit logging", ruleId: "CONFIG-010" });
|
|
1214
|
+
}
|
|
1215
|
+
else if (lang === "python") {
|
|
1216
|
+
actions.push({ type: "create", filePath: "lib/logger.py", content: `import logging\nimport json\nfrom datetime import datetime\n\nlogger = logging.getLogger("audit")\nlogger.setLevel(logging.INFO)\n\nhandler = logging.StreamHandler()\nhandler.setFormatter(logging.Formatter('%(message)s'))\nlogger.addHandler(handler)\n\ndef audit_log(user_id: str, action: str, resource: str, ip_address: str, **metadata):\n entry = {\n "userId": user_id,\n "action": action,\n "resource": resource,\n "ipAddress": ip_address,\n "timestamp": datetime.utcnow().isoformat() + "Z",\n "type": "audit",\n **metadata,\n }\n logger.info(json.dumps(entry))\n`, description: "Create Python audit logger", ruleId: "CONFIG-010" });
|
|
1217
|
+
}
|
|
1218
|
+
else if (lang === "ruby") {
|
|
1219
|
+
actions.push({ type: "create", filePath: "lib/audit_logger.rb", content: `require 'logger'\nrequire 'json'\n\nclass AuditLogger\n def initialize(logdev = $stdout)\n @logger = Logger.new(logdev)\n @logger.formatter = proc { |_, _, _, msg| msg }\n end\n\n def audit_log(user_id:, action:, resource:, ip_address:, **metadata)\n entry = {\n userId: user_id,\n action: action,\n resource: resource,\n ipAddress: ip_address,\n timestamp: Time.now.utc.iso8601,\n type: 'audit',\n **metadata,\n }\n @logger.info(entry.to_json)\n end\nend\n\nAUDIT = AuditLogger.new\n`, description: "Create Ruby audit logger", ruleId: "CONFIG-010" });
|
|
1220
|
+
}
|
|
1221
|
+
else if (lang === "go") {
|
|
1222
|
+
actions.push({ type: "create", filePath: "lib/audit.go", content: `package lib\n\nimport (\n\t"encoding/json"\n\t"log"\n\t"os"\n\t"time"\n)\n\ntype AuditEntry struct {\n\tUserID string "json:\\"userId\\""\n\tAction string "json:\\"action\\""\n\tResource string "json:\\"resource\\""\n\tIPAddress string "json:\\"ipAddress\\""\n\tTimestamp string "json:\\"timestamp\\""\n\tType string "json:\\"type\\""\n\tMetadata map[string]interface{} "json:\\"metadata,omitempty\\""\n}\n\nvar auditLogger = log.New(os.Stdout, "", 0)\n\nfunc AuditLog(userID, action, resource, ipAddr string, metadata map[string]interface{}) {\n\tentry := AuditEntry{\n\t\tUserID: userID,\n\t\tAction: action,\n\t\tResource: resource,\n\t\tIPAddress: ipAddr,\n\t\tTimestamp: time.Now().UTC().Format(time.RFC3339),\n\t\tType: "audit",\n\t\tMetadata: metadata,\n\t}\n\tdata, _ := json.Marshal(entry)\n\tauditLogger.Println(string(data))\n}\n`, description: "Create Go audit logger", ruleId: "CONFIG-010" });
|
|
1223
|
+
}
|
|
1224
|
+
else if (lang === "java") {
|
|
1225
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/AuditLogger.java", content: `package com.example;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport java.time.Instant;\nimport java.util.Map;\n\npublic class AuditLogger {\n private static final Logger logger = LoggerFactory.getLogger("audit");\n private static final ObjectMapper mapper = new ObjectMapper();\n\n public static void auditLog(String userId, String action, String resource, String ipAddress, Map<String, Object> metadata) {\n try {\n Map<String, Object> entry = Map.of(\n "userId", userId,\n "action", action,\n "resource", resource,\n "ipAddress", ipAddress,\n "timestamp", Instant.now().toString(),\n "type", "audit"\n );\n if (metadata != null) entry.putAll(metadata);\n logger.info(mapper.writeValueAsString(entry));\n } catch (Exception e) {\n logger.error("Audit log failed", e);\n }\n }\n}\n`, description: "Create Java audit logger", ruleId: "CONFIG-010" });
|
|
1226
|
+
}
|
|
1227
|
+
else if (lang === "php") {
|
|
1228
|
+
actions.push({ type: "create", filePath: "lib/audit_logger.php", content: `<?php\n\nclass AuditLogger\n{\n public static function log(string $userId, string $action, string $resource, string $ipAddress, array $metadata = []): void\n {\n $entry = array_merge([\n 'userId' => $userId,\n 'action' => $action,\n 'resource' => $resource,\n 'ipAddress' => $ipAddress,\n 'timestamp' => gmdate('c'),\n 'type' => 'audit',\n ], $metadata);\n error_log(json_encode($entry));\n }\n}\n`, description: "Create PHP audit logger", ruleId: "CONFIG-010" });
|
|
1229
|
+
}
|
|
1230
|
+
else if (lang === "rust") {
|
|
1231
|
+
actions.push({ type: "create", filePath: "src/logger.rs", content: `use serde_json::json;
|
|
1232
|
+
use tracing::{info, instrument};
|
|
1233
|
+
use chrono::Utc;
|
|
1234
|
+
|
|
1235
|
+
#[derive(Debug, serde::Serialize)]
|
|
1236
|
+
pub struct AuditEntry {
|
|
1237
|
+
pub user_id: String,
|
|
1238
|
+
pub action: String,
|
|
1239
|
+
pub resource: String,
|
|
1240
|
+
pub ip_address: String,
|
|
1241
|
+
pub timestamp: String,
|
|
1242
|
+
#[serde(rename = "type")]
|
|
1243
|
+
pub entry_type: String,
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
pub fn audit_log(user_id: &str, action: &str, resource: &str, ip_address: &str) {
|
|
1247
|
+
let entry = AuditEntry {
|
|
1248
|
+
user_id: user_id.to_string(),
|
|
1249
|
+
action: action.to_string(),
|
|
1250
|
+
resource: resource.to_string(),
|
|
1251
|
+
ip_address: ip_address.to_string(),
|
|
1252
|
+
timestamp: Utc::now().to_rfc3339(),
|
|
1253
|
+
entry_type: "audit".to_string(),
|
|
1254
|
+
};
|
|
1255
|
+
info!("{}", serde_json::to_string(&entry).unwrap_or_default());
|
|
1256
|
+
}
|
|
1257
|
+
`, description: "Create Rust audit logger (tracing)", ruleId: "CONFIG-010" });
|
|
1258
|
+
}
|
|
1259
|
+
return actions;
|
|
1260
|
+
}
|
|
1261
|
+
function buildSecretsFix(root, f) {
|
|
1262
|
+
const actions = [];
|
|
1263
|
+
const content = readFileSafe(path.join(root, f.file));
|
|
1264
|
+
if (!content)
|
|
1265
|
+
return actions;
|
|
1266
|
+
const lines = content.split("\n");
|
|
1267
|
+
const idx = (f.line || 1) - 1;
|
|
1268
|
+
if (idx >= lines.length)
|
|
1269
|
+
return actions;
|
|
1270
|
+
const line = lines[idx];
|
|
1271
|
+
const lang = detectProjectLanguage(root);
|
|
1272
|
+
const match = line.match(/(\w+)\s*[:=]\s*['"]([^'"]+)['"]/);
|
|
1273
|
+
if (match) {
|
|
1274
|
+
const varName = match[1];
|
|
1275
|
+
const value = match[2];
|
|
1276
|
+
actions.push({ type: "append", filePath: ".env", content: `\n${varName}=${value}\n`, description: `Move ${varName} to .env`, ruleId: "SECRETS-001" });
|
|
1277
|
+
let replacement;
|
|
1278
|
+
if (lang === "python") {
|
|
1279
|
+
replacement = line.replace(match[0], `${varName} = os.environ.get('${varName}')`);
|
|
1280
|
+
}
|
|
1281
|
+
else if (lang === "ruby") {
|
|
1282
|
+
replacement = line.replace(match[0], `${varName} = ENV['${varName}']`);
|
|
1283
|
+
}
|
|
1284
|
+
else if (lang === "go") {
|
|
1285
|
+
replacement = line.replace(match[0], `${varName} := os.Getenv("${varName}")`);
|
|
1286
|
+
}
|
|
1287
|
+
else if (lang === "java") {
|
|
1288
|
+
replacement = line.replace(match[0], `String ${varName} = System.getenv("${varName}")`);
|
|
1289
|
+
}
|
|
1290
|
+
else if (lang === "php") {
|
|
1291
|
+
replacement = line.replace(match[0], `$${varName} = getenv('${varName}')`);
|
|
1292
|
+
}
|
|
1293
|
+
else if (lang === "rust") {
|
|
1294
|
+
replacement = line.replace(match[0], `let ${varName} = std::env::var("${varName}").unwrap_or_default()`);
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
replacement = `${varName}: process.env.${varName}`;
|
|
1298
|
+
}
|
|
1299
|
+
actions.push({ type: "modify", filePath: f.file, search: line, replace: replacement, description: `Replace hardcoded ${varName} with env variable`, ruleId: "SECRETS-001" });
|
|
1300
|
+
actions.push(...buildEnvGitignoreFix(root));
|
|
1301
|
+
}
|
|
1302
|
+
return actions;
|
|
1303
|
+
}
|
|
1304
|
+
function buildWeakHashFix(root, f) {
|
|
1305
|
+
const lang = detectProjectLanguage(root);
|
|
1306
|
+
const content = readFileSafe(path.join(root, f.file));
|
|
1307
|
+
if (!content)
|
|
1308
|
+
return [];
|
|
1309
|
+
const lines = content.split("\n");
|
|
1310
|
+
const idx = (f.line || 1) - 1;
|
|
1311
|
+
if (idx >= lines.length)
|
|
1312
|
+
return [];
|
|
1313
|
+
const line = lines[idx];
|
|
1314
|
+
let replacement = line;
|
|
1315
|
+
if (lang === "python") {
|
|
1316
|
+
replacement = line.replace(/hashlib\.md5\(/gi, "hashlib.sha256(").replace(/hashlib\.sha1\(/gi, "hashlib.sha256(");
|
|
1317
|
+
}
|
|
1318
|
+
else if (lang === "go") {
|
|
1319
|
+
replacement = line.replace(/md5\.New\(\)/gi, "sha256.New()").replace(/sha1\.New\(\)/gi, "sha256.New()");
|
|
1320
|
+
}
|
|
1321
|
+
else if (lang === "ruby") {
|
|
1322
|
+
replacement = line.replace(/Digest::MD5/gi, "Digest::SHA256").replace(/Digest::SHA1/gi, "Digest::SHA256");
|
|
1323
|
+
}
|
|
1324
|
+
else if (lang === "java") {
|
|
1325
|
+
replacement = line.replace(/MessageDigest\.getInstance\(["']MD5["']\)/gi, 'MessageDigest.getInstance("SHA-256")').replace(/MessageDigest\.getInstance\(["']SHA-1["']\)/gi, 'MessageDigest.getInstance("SHA-256")');
|
|
1326
|
+
}
|
|
1327
|
+
else if (lang === "php") {
|
|
1328
|
+
replacement = line.replace(/md5\(/gi, "hash('sha256', ").replace(/sha1\(/gi, "hash('sha256', ");
|
|
1329
|
+
}
|
|
1330
|
+
else if (lang === "rust") {
|
|
1331
|
+
replacement = line.replace(/md5::compute/gi, "sha2::Sha256::digest").replace(/use md5/gi, "use sha2::{Sha256, Digest}");
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
replacement = line.replace(/createHash\s*\(\s*['"](?:md5|sha1)['"]\s*\)/, "createHash('sha256')");
|
|
1335
|
+
}
|
|
1336
|
+
if (replacement === line)
|
|
1337
|
+
return [];
|
|
1338
|
+
return [{ type: "modify", filePath: f.file, search: line, replace: replacement, description: "Replace weak hash with SHA-256", ruleId: "CRYPTO-001" }];
|
|
1339
|
+
}
|
|
1340
|
+
function buildPasswordFix(root, _f) {
|
|
1341
|
+
const lang = detectProjectLanguage(root);
|
|
1342
|
+
const actions = [];
|
|
1343
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1344
|
+
const hasSrc = fs.existsSync(path.join(root, "src"));
|
|
1345
|
+
const authPath = hasSrc ? "src/lib/auth.ts" : "lib/auth.ts";
|
|
1346
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install argon2", ruleId: "CRYPTO-003" });
|
|
1347
|
+
if (!fs.existsSync(path.join(root, authPath))) {
|
|
1348
|
+
actions.push({ type: "create", filePath: authPath, content: `import argon2 from 'argon2';\n\nexport async function hashPassword(password: string): Promise<string> {\n return argon2.hash(password, { type: argon2.argon2id });\n}\n\nexport async function verifyPassword(hashedPassword: string, inputPassword: string): Promise<boolean> {\n return argon2.verify(hashedPassword, inputPassword);\n}\n`, description: "Create Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
else if (lang === "python") {
|
|
1352
|
+
actions.push({ type: "create", filePath: "lib/auth.py", content: `import hashlib\nimport os\n\ndef hash_password(password: str) -> str:\n salt = os.urandom(16)\n key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)\n return salt.hex() + ':' + key.hex()\n\ndef verify_password(stored: str, provided: str) -> bool:\n salt_hex, key_hex = stored.split(':')\n salt = bytes.fromhex(salt_hex)\n new_key = hashlib.pbkdf2_hmac('sha256', provided.encode(), salt, 100000)\n return new_key.hex() == key_hex\n`, description: "Create Python password utility (PBKDF2-SHA256)", ruleId: "CRYPTO-003" });
|
|
1353
|
+
}
|
|
1354
|
+
else if (lang === "go") {
|
|
1355
|
+
actions.push({ type: "create", filePath: "lib/auth.go", content: `package lib\n\nimport (\n\t"crypto/rand"\n\t"crypto/subtle"\n\t"encoding/hex"\n\t"golang.org/x/crypto/argon2"\n)\n\nfunc HashPassword(password string) (string, error) {\n\tsalt := make([]byte, 16)\n\tif _, err := rand.Read(salt); err != nil {\n\t\treturn "", err\n\t}\n\thash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)\n\treturn hex.EncodeToString(salt) + ":" + hex.EncodeToString(hash), nil\n}\n\nfunc VerifyPassword(stored, provided string) (bool, error) {\n\tparts := strings.SplitN(stored, ":", 2)\n\tif len(parts) != 2 { return false, nil }\n\tsalt, _ := hex.DecodeString(parts[0])\n\tstoredHash, _ := hex.DecodeString(parts[1])\n\tprovidedHash := argon2.IDKey([]byte(provided), salt, 1, 64*1024, 4, 32)\n\treturn subtle.ConstantTimeCompare(storedHash, providedHash) == 1, nil\n}\n`, description: "Create Go Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
1356
|
+
}
|
|
1357
|
+
else if (lang === "ruby") {
|
|
1358
|
+
actions.push({ type: "create", filePath: "lib/auth.rb", content: `require 'bcrypt'\n\ndef hash_password(password)\n BCrypt::Password.create(password)\nend\n\ndef verify_password(stored_hash, provided_password)\n BCrypt::Password.new(stored_hash) == provided_password\nend\n`, description: "Create Ruby BCrypt password utility", ruleId: "CRYPTO-003" });
|
|
1359
|
+
}
|
|
1360
|
+
else if (lang === "java") {
|
|
1361
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/PasswordUtil.java", content: `package com.example;\n\nimport javax.crypto.SecretKeyFactory;\nimport javax.crypto.spec.PBEKeySpec;\nimport java.security.SecureRandom;\nimport java.util.Base64;\n\npublic class PasswordUtil {\n private static final int ITERATIONS = 100000;\n private static final int KEY_LENGTH = 256;\n private static final SecureRandom RANDOM = new SecureRandom();\n\n public static String hashPassword(String password) throws Exception {\n byte[] salt = new byte[16];\n RANDOM.nextBytes(salt);\n PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);\n byte[] hash = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();\n return Base64.getEncoder().encodeToString(salt) + ":" + Base64.getEncoder().encodeToString(hash);\n }\n\n public static boolean verifyPassword(String stored, String provided) throws Exception {\n String[] parts = stored.split(":");\n byte[] salt = Base64.getDecoder().decode(parts[0]);\n byte[] storedHash = Base64.getDecoder().decode(parts[1]);\n PBEKeySpec spec = new PBEKeySpec(provided.toCharArray(), salt, ITERATIONS, KEY_LENGTH);\n byte[] testHash = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();\n return java.util.Arrays.equals(storedHash, testHash);\n }\n}\n`, description: "Create Java PBKDF2 password utility", ruleId: "CRYPTO-003" });
|
|
1362
|
+
}
|
|
1363
|
+
else if (lang === "php") {
|
|
1364
|
+
actions.push({ type: "create", filePath: "lib/auth.php", content: `<?php\n\nfunction hash_password(string $password): string {\n return password_hash($password, PASSWORD_ARGON2ID);\n}\n\nfunction verify_password(string $hash, string $password): bool {\n return password_verify($password, $hash);\n}\n`, description: "Create PHP Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
1365
|
+
}
|
|
1366
|
+
else if (lang === "rust") {
|
|
1367
|
+
actions.push({ type: "create", filePath: "src/auth.rs", content: `use argon2::{Argon2, Algorithm, Version, Params};
|
|
1368
|
+
use argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};
|
|
1369
|
+
use rand::rngs::OsRng;
|
|
1370
|
+
|
|
1371
|
+
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
|
1372
|
+
let salt = SaltString::generate(&mut OsRng);
|
|
1373
|
+
let params = Params::new(65536, 3, 4, Some(32))?;
|
|
1374
|
+
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
|
1375
|
+
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
|
|
1376
|
+
Ok(hash.to_string())
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
|
|
1380
|
+
let parsed = argon2::PasswordHash::new(hash)?;
|
|
1381
|
+
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())
|
|
1382
|
+
}
|
|
1383
|
+
`, description: "Create Rust Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
1384
|
+
}
|
|
1385
|
+
return actions;
|
|
1386
|
+
}
|
|
1387
|
+
function buildRateLimitFix(root) {
|
|
1388
|
+
const lang = detectProjectLanguage(root);
|
|
1389
|
+
const fw = detectWebFramework(root, lang);
|
|
1390
|
+
const actions = [];
|
|
1391
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1392
|
+
const appFile = findMainAppFile(root);
|
|
1393
|
+
if (!appFile)
|
|
1394
|
+
return [];
|
|
1395
|
+
if (fw === "express") {
|
|
1396
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install express-rate-limit", ruleId: "AUTH-002" });
|
|
1397
|
+
actions.push({ type: "append", filePath: appFile, content: `\nimport rateLimit from 'express-rate-limit';\n\nconst limiter = rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 100,\n standardHeaders: true,\n legacyHeaders: false,\n});\napp.use(limiter);\n`, description: "Add rate limiting (100 req/15min)", ruleId: "AUTH-002" });
|
|
1398
|
+
}
|
|
1399
|
+
else if (fw === "fastify") {
|
|
1400
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install @fastify/rate-limit", ruleId: "AUTH-002" });
|
|
1401
|
+
actions.push({ type: "append", filePath: appFile, content: `\nimport rateLimit from '@fastify/rate-limit';\napp.register(rateLimit, { max: 100, timeWindow: '15 minutes' });\n`, description: "Add rate limiting to Fastify", ruleId: "AUTH-002" });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
else if (lang === "python") {
|
|
1405
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
1406
|
+
if (fw === "django") {
|
|
1407
|
+
actions.push({ type: "append", filePath: appFile, content: "\n# Rate limiting: pip install django-ratelimit\n# Add to views: @ratelimit(key='ip', rate='100/h', block=True)\n", description: "Add Django rate limiting note", ruleId: "AUTH-002" });
|
|
1408
|
+
}
|
|
1409
|
+
else if (fw === "fastapi") {
|
|
1410
|
+
actions.push({ type: "append", filePath: appFile, content: "\nfrom slowapi import Limiter\nfrom slowapi.util import get_remote_address\n\nlimiter = Limiter(key_func=get_remote_address)\n# Add to routes: @limiter.limit('100/15minutes')\n", description: "Add FastAPI rate limiting (slowapi)", ruleId: "AUTH-002" });
|
|
1411
|
+
}
|
|
1412
|
+
else if (fw === "flask") {
|
|
1413
|
+
actions.push({ type: "append", filePath: appFile, content: "\nfrom flask_limiter import Limiter\nfrom flask_limiter.util import get_remote_address\n\nlimiter = Limiter(app=app, key_func=get_remote_address, default_limits=['100 per 15 minute'])\n", description: "Add Flask rate limiting", ruleId: "AUTH-002" });
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
else if (lang === "ruby") {
|
|
1417
|
+
if (fw === "rails") {
|
|
1418
|
+
actions.push({ type: "append", filePath: "Gemfile", content: "\ngem 'rack-attack'\n", description: "Add rack-attack for rate limiting", ruleId: "AUTH-002" });
|
|
1419
|
+
actions.push({ type: "append", filePath: "config/application.rb", content: "\nconfig.middleware.use Rack::Attack\nRack::Attack.throttle('req/ip', limit: 100, period: 15.minutes) { |req| req.ip }\n", description: "Add Rails rate limiting config", ruleId: "AUTH-002" });
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
else if (lang === "go") {
|
|
1423
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
1424
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport (\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype rateLimiter struct {\n\tmu sync.Mutex\n\tvisitors map[string][]time.Time\n\tlimit int\n\twindow time.Duration\n}\n\nfunc newRateLimiter(limit int, window time.Duration) *rateLimiter {\n\treturn &rateLimiter{visitors: make(map[string][]time.Time), limit: limit, window: window}\n}\n\nfunc (rl *rateLimiter) allow(ip string) bool {\n\trl.mu.Lock()\n\tdefer rl.mu.Unlock()\n\tnow := time.Now()\n\twindowStart := now.Add(-rl.window)\n\tvar recent []time.Time\n\tfor _, t := range rl.visitors[ip] {\n\t\tif t.After(windowStart) { recent = append(recent, t) }\n\t}\n\trl.visitors[ip] = recent\n\tif len(recent) >= rl.limit { return false }\n\trl.visitors[ip] = append(rl.visitors[ip], now)\n\treturn true\n}\n\nvar limiter = newRateLimiter(100, 15*time.Minute)\n\nfunc rateLimitMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif !limiter.allow(r.RemoteAddr) {\n\t\t\thttp.Error(w, \"Too many requests\", http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n", description: "Add Go rate limiter middleware", ruleId: "AUTH-002" });
|
|
1425
|
+
}
|
|
1426
|
+
else if (lang === "java") {
|
|
1427
|
+
if (fw === "spring") {
|
|
1428
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/RateLimitConfig.java", content: `package com.example;\n\nimport io.github.bucket4j.Bandwidth;\nimport io.github.bucket4j.Bucket;\nimport io.github.bucket4j.Refill;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.servlet.HandlerInterceptor;\n\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n@Component\npublic class RateLimitInterceptor implements HandlerInterceptor {\n private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();\n\n private Bucket newBucket() {\n Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(15)));\n return Bucket.builder().addLimit(limit).build();\n }\n\n @Override\n public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {\n Bucket bucket = buckets.computeIfAbsent(request.getRemoteAddr(), k -> newBucket());\n if (bucket.tryConsume(1)) return true;\n response.setStatus(429);\n return false;\n }\n}\n`, description: "Create Spring rate limiter (bucket4j)", ruleId: "AUTH-002" });
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
else if (lang === "php") {
|
|
1432
|
+
const appFile = findMainAppFile(root) || "public/index.php";
|
|
1433
|
+
actions.push({ type: "append", filePath: appFile, content: "\n// Rate limiting middleware\n$ip = $_SERVER['REMOTE_ADDR'];\n$limit = 100;\n$window = 900; // 15 minutes\n$cacheKey = 'rate_limit_' . $ip;\n// Implement with your cache layer (Redis, APCu, file-based)\n", description: "Add PHP rate limiting scaffolding", ruleId: "AUTH-002" });
|
|
1434
|
+
}
|
|
1435
|
+
else if (lang === "rust") {
|
|
1436
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
1437
|
+
if (fw === "actix") {
|
|
1438
|
+
actions.push({ type: "append", filePath: appFile, content: "\n// Rate limiting: cargo add actix-governor\n// use actix_governor::{GovernorConfigBuilder, Governor};\n// let governor_conf = GovernorConfigBuilder::default()\n// .per_second(1)\n// .burst_size(20)\n// .finish()\n// .unwrap();\n// app.wrap(Governor::new(&governor_conf));\n", description: "Add Actix-web rate limiting (actix-governor)", ruleId: "AUTH-002" });
|
|
1439
|
+
}
|
|
1440
|
+
else if (fw === "axum") {
|
|
1441
|
+
actions.push({ type: "append", filePath: appFile, content: "\n// Rate limiting: cargo add tower --features limit\n// use tower::ServiceBuilder;\n// use tower::limit::RateLimitLayer;\n// use std::time::Duration;\n// let app = axum::Router::new()\n// .layer(ServiceBuilder::new()\n// .layer(RateLimitLayer::new(100, Duration::from_secs(900))));\n", description: "Add Axum rate limiting (tower)", ruleId: "AUTH-002" });
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
actions.push({ type: "append", filePath: appFile, content: "\n// GESF Rate Limiting: 100 requests per 15 minutes\n// actix-web: cargo add actix-governor\n// axum: cargo add tower --features limit\n", description: "Add Rust rate limiting guidance", ruleId: "AUTH-002" });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return actions;
|
|
1448
|
+
}
|
|
1449
|
+
function buildSessionTimeoutFix(root) {
|
|
1450
|
+
const lang = detectProjectLanguage(root);
|
|
1451
|
+
const fw = detectWebFramework(root, lang);
|
|
1452
|
+
const actions = [];
|
|
1453
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
1454
|
+
const appFile = findMainAppFile(root);
|
|
1455
|
+
if (!appFile)
|
|
1456
|
+
return [];
|
|
1457
|
+
if (fw === "express") {
|
|
1458
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install express-session", ruleId: "AUTH-003" });
|
|
1459
|
+
actions.push({ type: "append", filePath: appFile, content: `\nimport session from 'express-session';\n\napp.use(session({\n secret: process.env.SESSION_SECRET || 'change-me-in-production',\n resave: false,\n saveUninitialized: false,\n cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 30 * 60 * 1000 },\n}));\n`, description: "Add session with 30-min timeout", ruleId: "AUTH-003" });
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
actions.push({ type: "append", filePath: appFile, content: "\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n", description: "Add session timeout constant", ruleId: "AUTH-003" });
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
else if (lang === "python") {
|
|
1466
|
+
if (fw === "django") {
|
|
1467
|
+
const settingsFile = findFileRecursive(root, "settings.py", ".") || "settings.py";
|
|
1468
|
+
actions.push({ type: "append", filePath: settingsFile, content: "\nSESSION_COOKIE_AGE = 1800 # 30 minutes\nSESSION_COOKIE_SECURE = True\nSESSION_COOKIE_HTTPONLY = True\nSESSION_EXPIRE_AT_BROWSER_CLOSE = True\n", description: "Add Django session timeout settings", ruleId: "AUTH-003" });
|
|
1469
|
+
}
|
|
1470
|
+
else {
|
|
1471
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
1472
|
+
actions.push({ type: "append", filePath: appFile, content: "\n# Session timeout: 30 minutes\nSESSION_TIMEOUT = 30 * 60\n", description: "Add session timeout constant", ruleId: "AUTH-003" });
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
else if (lang === "ruby") {
|
|
1476
|
+
if (fw === "rails") {
|
|
1477
|
+
actions.push({ type: "append", filePath: "config/initializers/session_store.rb", content: "\nRails.application.config.session_store :cookie_store, expire_after: 30.minutes, secure: Rails.env.production?, httponly: true\n", description: "Add Rails session timeout", ruleId: "AUTH-003" });
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
else if (lang === "go") {
|
|
1481
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
1482
|
+
actions.push({ type: "append", filePath: appFile, content: "\nconst sessionTimeout = 30 * time.Minute\n", description: "Add Go session timeout constant", ruleId: "AUTH-003" });
|
|
1483
|
+
}
|
|
1484
|
+
else if (lang === "java") {
|
|
1485
|
+
if (fw === "spring") {
|
|
1486
|
+
actions.push({ type: "append", filePath: "src/main/resources/application.properties", content: "\nserver.servlet.session.timeout=30m\nserver.servlet.session.cookie.http-only=true\nserver.servlet.session.cookie.secure=true\n", description: "Add Spring session timeout config", ruleId: "AUTH-003" });
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
else if (lang === "php") {
|
|
1490
|
+
if (fw === "laravel") {
|
|
1491
|
+
actions.push({ type: "append", filePath: "config/session.php", content: "\n'lifetime' => 30,\n'expire_on_close' => true,\n'secure' => env('APP_ENV') === 'production',\n'http_only' => true,\n", description: "Add Laravel session timeout", ruleId: "AUTH-003" });
|
|
1492
|
+
}
|
|
1493
|
+
else {
|
|
1494
|
+
const appFile = findMainAppFile(root) || "public/index.php";
|
|
1495
|
+
actions.push({ type: "append", filePath: appFile, content: "\nini_set('session.gc_maxlifetime', 1800); // 30 minutes\nsession_set_cookie_params(1800, '/', '', true, true);\n", description: "Add PHP session timeout config", ruleId: "AUTH-003" });
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
else if (lang === "rust") {
|
|
1499
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
1500
|
+
actions.push({ type: "append", filePath: appFile, content: "\nconst SESSION_TIMEOUT_SECS: u64 = 30 * 60; // 30 minutes\n", description: "Add Rust session timeout constant", ruleId: "AUTH-003" });
|
|
1501
|
+
}
|
|
1502
|
+
return actions;
|
|
1503
|
+
}
|
|
1504
|
+
function buildCORSWildcardFix(root) {
|
|
1505
|
+
const lang = detectProjectLanguage(root);
|
|
1506
|
+
const appFile = findMainAppFile(root);
|
|
1507
|
+
if (!appFile)
|
|
1508
|
+
return [];
|
|
1509
|
+
const content = readFileSafe(path.join(root, appFile)) || "";
|
|
1510
|
+
const actions = [];
|
|
1511
|
+
const wildcardPatterns = ["origin: '*'", "origin:'*'", 'origin:"*"', "Access-Control-Allow-Origin: *"];
|
|
1512
|
+
for (const pattern of wildcardPatterns) {
|
|
1513
|
+
if (!content.includes(pattern))
|
|
1514
|
+
continue;
|
|
1515
|
+
if (lang === "python") {
|
|
1516
|
+
const replacement = pattern.includes("*'") || pattern.includes('*"')
|
|
1517
|
+
? "origins=['http://localhost:3000']"
|
|
1518
|
+
: "origins=['http://localhost:3000']";
|
|
1519
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: replacement, description: "Replace CORS wildcard", ruleId: "AUTH-004" });
|
|
1520
|
+
}
|
|
1521
|
+
else if (lang === "go") {
|
|
1522
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: 'w.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN"))', description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
|
|
1523
|
+
}
|
|
1524
|
+
else if (lang === "ruby") {
|
|
1525
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "origins ENV.fetch('ALLOWED_ORIGINS', 'http://localhost:3000').split(',')", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
|
|
1526
|
+
}
|
|
1527
|
+
else if (lang === "java") {
|
|
1528
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: 'config.addAllowedOrigin(System.getenv("ALLOWED_ORIGIN"))', description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
|
|
1529
|
+
}
|
|
1530
|
+
else if (lang === "php") {
|
|
1531
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "$response->headers->set('Access-Control-Allow-Origin', getenv('ALLOWED_ORIGIN'))", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
|
|
1532
|
+
}
|
|
1533
|
+
else if (lang === "rust") {
|
|
1534
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "allowed_origin(std::env::var(\"ALLOWED_ORIGIN\").unwrap_or(\"http://localhost:3000\".to_string()))", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']", description: "Replace CORS wildcard", ruleId: "AUTH-004" });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return actions;
|
|
1541
|
+
}
|
|
1542
|
+
function buildTimestampsFix(root, f) {
|
|
1543
|
+
if (f.file.endsWith(".prisma")) {
|
|
1544
|
+
const content = readFileSafe(path.join(root, f.file));
|
|
1545
|
+
if (!content)
|
|
1546
|
+
return [];
|
|
1547
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
1548
|
+
if (!modelMatch || modelMatch.length === 0)
|
|
1549
|
+
return [];
|
|
1550
|
+
const block = modelMatch[0];
|
|
1551
|
+
const closingBrace = block.lastIndexOf("}");
|
|
1552
|
+
if (closingBrace === -1)
|
|
1553
|
+
return [];
|
|
1554
|
+
const insertion = "\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt";
|
|
1555
|
+
return [{ type: "modify", filePath: f.file, search: block, replace: block.slice(0, closingBrace) + insertion + block.slice(closingBrace), description: "Add createdAt/updatedAt to Prisma model", ruleId: "DB-001" }];
|
|
1556
|
+
}
|
|
1557
|
+
if (f.file.endsWith(".py")) {
|
|
1558
|
+
return [{ type: "append", filePath: f.file, content: "\n# GESF: Add audit timestamps\n# For Django models:\n# created_at = models.DateTimeField(auto_now_add=True)\n# updated_at = models.DateTimeField(auto_now=True)\n# For SQLAlchemy:\n# created_at = Column(DateTime, default=datetime.utcnow)\n# updated_at = Column(DateTime, onupdate=datetime.utcnow)\n", description: "Add Python timestamp guidance", ruleId: "DB-001" }];
|
|
1559
|
+
}
|
|
1560
|
+
if (f.file.endsWith(".rb")) {
|
|
1561
|
+
return [{ type: "append", filePath: f.file, content: "\n# GESF: Rails has built-in timestamps. Add to model:\n# create_table :your_table do |t|\n# t.timestamps\n# end\n", description: "Add Rails timestamp guidance", ruleId: "DB-001" }];
|
|
1562
|
+
}
|
|
1563
|
+
if (f.file.endsWith(".go")) {
|
|
1564
|
+
return [{ type: "append", filePath: f.file, content: "\n// GESF: Add audit timestamps to GORM models:\n// type YourModel struct {\n// ID uint `json:\"id\" gorm:\"primaryKey\"`\n// CreatedAt time.Time `json:\"created_at\"`\n// UpdatedAt time.Time `json:\"updated_at\"`\n// }\n", description: "Add Go timestamp guidance", ruleId: "DB-001" }];
|
|
1565
|
+
}
|
|
1566
|
+
if (f.file.endsWith(".java")) {
|
|
1567
|
+
return [{ type: "append", filePath: f.file, content: "\n// GESF: Add JPA audit timestamps:\n// @CreatedDate\n// @Column(name = \"created_at\", updatable = false)\n// private Instant createdAt;\n//\n// @LastModifiedDate\n// @Column(name = \"updated_at\")\n// private Instant updatedAt;\n", description: "Add Java JPA timestamp guidance", ruleId: "DB-001" }];
|
|
1568
|
+
}
|
|
1569
|
+
if (f.file.endsWith(".php")) {
|
|
1570
|
+
return [{ type: "append", filePath: f.file, content: "\n// GESF: Laravel uses timestamps() in migrations:\n// $table->timestamps(); // adds created_at, updated_at\n// $table->softDeletes(); // adds deleted_at\n", description: "Add Laravel timestamp guidance", ruleId: "DB-001" }];
|
|
1571
|
+
}
|
|
1572
|
+
if (f.file.endsWith(".rs")) {
|
|
1573
|
+
return [{ type: "append", filePath: f.file, content: "\n// GESF: Add audit timestamps to ORM models:\n// Diesel: created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n// updated_at TIMESTAMP NOT NULL DEFAULT NOW(),\n// SQLx: created_at: chrono::NaiveDateTime,\n// updated_at: chrono::NaiveDateTime,\n// SeaORM: created_at: DateTime,\n// updated_at: DateTime,\n", description: "Add Rust timestamp guidance", ruleId: "DB-001" }];
|
|
1574
|
+
}
|
|
1575
|
+
return [];
|
|
1576
|
+
}
|
|
1577
|
+
function buildSoftDeleteFix(root, f) {
|
|
1578
|
+
if (f.file.endsWith(".prisma")) {
|
|
1579
|
+
const content = readFileSafe(path.join(root, f.file));
|
|
1580
|
+
if (!content)
|
|
1581
|
+
return [];
|
|
1582
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
1583
|
+
if (!modelMatch || modelMatch.length === 0)
|
|
1584
|
+
return [];
|
|
1585
|
+
const block = modelMatch[0];
|
|
1586
|
+
const closingBrace = block.lastIndexOf("}");
|
|
1587
|
+
if (closingBrace === -1)
|
|
1588
|
+
return [];
|
|
1589
|
+
return [{ type: "modify", filePath: f.file, search: block, replace: block.slice(0, closingBrace) + "\n deletedAt DateTime?" + block.slice(closingBrace), description: "Add deletedAt to Prisma model", ruleId: "DB-002" }];
|
|
1590
|
+
}
|
|
1591
|
+
if (f.file.endsWith(".py")) {
|
|
1592
|
+
return [{ type: "append", filePath: f.file, content: "\n# GESF: Add soft delete to Django/SQLAlchemy:\n# Django: deleted_at = models.DateTimeField(null=True, blank=True)\n# SQLAlchemy: deleted_at = Column(DateTime, nullable=True)\n", description: "Add Python soft delete guidance", ruleId: "DB-002" }];
|
|
1593
|
+
}
|
|
1594
|
+
if (f.file.endsWith(".go")) {
|
|
1595
|
+
return [{ type: "append", filePath: f.file, content: "\n// GESF: Add soft delete to GORM:\n// DeletedAt gorm.DeletedAt `json:\"deleted_at\" gorm:\"index\"`\n", description: "Add Go soft delete guidance", ruleId: "DB-002" }];
|
|
1596
|
+
}
|
|
1597
|
+
if (f.file.endsWith(".rs")) {
|
|
1598
|
+
return [{ type: "append", filePath: f.file, content: "\n// GESF: Add soft delete:\n// Diesel: deleted_at TIMESTAMP NULL,\n// SQLx: deleted_at: Option<chrono::NaiveDateTime>,\n// SeaORM: deleted_at: Option<DateTime>,\n", description: "Add Rust soft delete guidance", ruleId: "DB-002" }];
|
|
1599
|
+
}
|
|
1600
|
+
return [];
|
|
1601
|
+
}
|
|
1602
|
+
function buildUserAuditFix(root, f) {
|
|
1603
|
+
if (f.file.endsWith(".prisma")) {
|
|
1604
|
+
const content = readFileSafe(path.join(root, f.file));
|
|
1605
|
+
if (!content)
|
|
1606
|
+
return [];
|
|
1607
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
1608
|
+
if (!modelMatch || modelMatch.length === 0)
|
|
1609
|
+
return [];
|
|
1610
|
+
const block = modelMatch[0];
|
|
1611
|
+
const closingBrace = block.lastIndexOf("}");
|
|
1612
|
+
if (closingBrace === -1)
|
|
1613
|
+
return [];
|
|
1614
|
+
return [{ type: "modify", filePath: f.file, search: block, replace: block.slice(0, closingBrace) + "\n createdBy String?\n updatedBy String?" + block.slice(closingBrace), description: "Add createdBy/updatedBy columns", ruleId: "DB-003" }];
|
|
1615
|
+
}
|
|
1616
|
+
if (f.file.endsWith(".py")) {
|
|
1617
|
+
return [{ type: "append", filePath: f.file, content: "\n# GESF: Add user audit columns:\n# Django: created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='+')\n# SQLAlchemy: created_by = Column(Integer, ForeignKey('users.id'))\n", description: "Add Python user audit guidance", ruleId: "DB-003" }];
|
|
1618
|
+
}
|
|
1619
|
+
if (f.file.endsWith(".rs")) {
|
|
1620
|
+
return [{ type: "append", filePath: f.file, content: "\n// GESF: Add user audit columns:\n// Diesel: created_by VARCHAR(255) NULL,\n// updated_by VARCHAR(255) NULL,\n// SQLx: created_by: Option<String>,\n// updated_by: Option<String>,\n", description: "Add Rust user audit guidance", ruleId: "DB-003" }];
|
|
1621
|
+
}
|
|
1622
|
+
return [];
|
|
1623
|
+
}
|
|
1624
|
+
function buildAuditModelFix(root) {
|
|
1625
|
+
if (fs.existsSync(path.join(root, "prisma/schema.prisma"))) {
|
|
1626
|
+
return [{ type: "append", filePath: "prisma/schema.prisma", content: "\\nmodel Audit {\\n id Int @id @default(autoincrement())\\n userId String\\n action String\\n resource String\\n timestamp DateTime @default(now())\\n ipAddress String\\n metadata Json?\\n}\\n", description: "Add Audit model to Prisma schema", ruleId: "DB-004" }];
|
|
1627
|
+
}
|
|
1628
|
+
const lang = detectProjectLanguage(root);
|
|
1629
|
+
if (lang === "python") {
|
|
1630
|
+
return [{ type: "create", filePath: "lib/models/audit.py", content: `from datetime import datetime\nfrom sqlalchemy import Column, Integer, String, DateTime, JSON\nfrom sqlalchemy.ext.declarative import declarative_base\n\nBase = declarative_base()\n\nclass Audit(Base):\n __tablename__ = 'audit'\n id = Column(Integer, primary_key=True, autoincrement=True)\n user_id = Column(String(255))\n action = Column(String(255))\n resource = Column(String(255))\n timestamp = Column(DateTime, default=datetime.utcnow)\n ip_address = Column(String(45))\n metadata = Column(JSON)\n`, description: "Create Python Audit model (SQLAlchemy)", ruleId: "DB-004" }];
|
|
1631
|
+
}
|
|
1632
|
+
if (lang === "go") {
|
|
1633
|
+
return [{ type: "create", filePath: "lib/models/audit.go", content: `package models\n\nimport "time"\n\ntype Audit struct {\n\tID uint \`json:"id" gorm:"primaryKey;autoIncrement"\`\n\tUserID string \`json:"userId"\`\n\tAction string \`json:"action"\`\n\tResource string \`json:"resource"\`\n\tTimestamp time.Time \`json:"timestamp" gorm:"default:now()"\`\n\tIPAddress string \`json:"ipAddress"\`\n}\n`, description: "Create Go Audit model (GORM)", ruleId: "DB-004" }];
|
|
1634
|
+
}
|
|
1635
|
+
if (lang === "java") {
|
|
1636
|
+
return [{ type: "create", filePath: "src/main/java/com/example/Audit.java", content: `package com.example;\n\nimport jakarta.persistence.*;\nimport java.time.Instant;\n\n@Entity\n@Table(name = "audit")\npublic class Audit {\n @Id @GeneratedValue(strategy = GenerationType.IDENTITY)\n private Long id;\n private String userId;\n private String action;\n private String resource;\n private String ipAddress;\n @Column(columnDefinition = "jsonb")\n private String metadata;\n private Instant timestamp = Instant.now();\n}\n`, description: "Create Java Audit entity (JPA)", ruleId: "DB-004" }];
|
|
1637
|
+
}
|
|
1638
|
+
if (lang === "rust") {
|
|
1639
|
+
return [{ type: "create", filePath: "src/models/audit.rs", content: `use chrono::NaiveDateTime;
|
|
1640
|
+
|
|
1641
|
+
#[derive(Debug, Queryable, Serialize)]
|
|
1642
|
+
pub struct Audit {
|
|
1643
|
+
pub id: i32,
|
|
1644
|
+
pub user_id: String,
|
|
1645
|
+
pub action: String,
|
|
1646
|
+
pub resource: String,
|
|
1647
|
+
pub ip_address: String,
|
|
1648
|
+
pub timestamp: NaiveDateTime,
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Diesel table definition:
|
|
1652
|
+
// table! {
|
|
1653
|
+
// audit (id) {
|
|
1654
|
+
// id -> Int4,
|
|
1655
|
+
// user_id -> Varchar,
|
|
1656
|
+
// action -> Varchar,
|
|
1657
|
+
// resource -> Varchar,
|
|
1658
|
+
// ip_address -> Varchar,
|
|
1659
|
+
// timestamp -> Timestamp,
|
|
1660
|
+
// }
|
|
1661
|
+
// }
|
|
1662
|
+
`, description: "Create Rust Audit model (Diesel)", ruleId: "DB-004" }];
|
|
1663
|
+
}
|
|
1664
|
+
return [];
|
|
1665
|
+
}
|
|
1666
|
+
function getNpmInstallsFromActions(actions) {
|
|
1667
|
+
const installs = new Set();
|
|
1668
|
+
for (const a of actions) {
|
|
1669
|
+
if (a.type !== "npm-install")
|
|
1670
|
+
continue;
|
|
1671
|
+
const map = {
|
|
1672
|
+
"CONFIG-001": "helmet",
|
|
1673
|
+
"CONFIG-002": "cors",
|
|
1674
|
+
"CONFIG-010": "pino",
|
|
1675
|
+
"CRYPTO-003": "argon2",
|
|
1676
|
+
"AUTH-002": "express-rate-limit",
|
|
1677
|
+
"AUTH-003": "express-session",
|
|
1678
|
+
};
|
|
1679
|
+
if (map[a.ruleId])
|
|
1680
|
+
installs.add(map[a.ruleId]);
|
|
1681
|
+
}
|
|
1682
|
+
return [...installs];
|
|
1683
|
+
}
|
|
1684
|
+
function buildEncryptionAtRestImpl(root, hasSrc) {
|
|
1685
|
+
const lang = detectProjectLanguage(root);
|
|
1686
|
+
if (lang === "rust") {
|
|
1687
|
+
return [
|
|
1688
|
+
{ type: "create", filePath: "src/encryption.rs", content: `use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
|
1689
|
+
use aes_gcm::aead::Aead;
|
|
1690
|
+
use rand::RngCore;
|
|
1691
|
+
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
|
1692
|
+
|
|
1693
|
+
pub fn encrypt(plaintext: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {
|
|
1694
|
+
let cipher = Aes256Gcm::new(key.into());
|
|
1695
|
+
let mut nonce_bytes = [0u8; 12];
|
|
1696
|
+
rand::thread_rng().fill_bytes(&mut nonce_bytes);
|
|
1697
|
+
let nonce = Nonce::from_slice(&nonce_bytes);
|
|
1698
|
+
let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())?;
|
|
1699
|
+
let mut combined = nonce_bytes.to_vec();
|
|
1700
|
+
combined.extend_from_slice(&ciphertext);
|
|
1701
|
+
Ok(BASE64.encode(&combined))
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
pub fn decrypt(encoded: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {
|
|
1705
|
+
let combined = BASE64.decode(encoded).map_err(|_| aes_gcm::Error)?;
|
|
1706
|
+
let (nonce_bytes, ciphertext) = combined.split_at(12);
|
|
1707
|
+
let cipher = Aes256Gcm::new(key.into());
|
|
1708
|
+
let nonce = Nonce::from_slice(nonce_bytes);
|
|
1709
|
+
let plaintext = cipher.decrypt(nonce, ciphertext)?;
|
|
1710
|
+
String::from_utf8(plaintext).map_err(|_| aes_gcm::Error)
|
|
1711
|
+
}
|
|
1712
|
+
`, description: "Create Rust AES-256-GCM encryption utility", ruleId: "GDPR-ART32-002" },
|
|
1713
|
+
];
|
|
1714
|
+
}
|
|
1715
|
+
const cryptoPath = hasSrc ? "src/lib/encryption.ts" : "lib/encryption.ts";
|
|
1716
|
+
return [
|
|
1717
|
+
{ type: "npm-install", filePath: "package.json", description: "Node.js crypto is built-in", ruleId: "GDPR-ART32-002" },
|
|
1718
|
+
{ type: "create", filePath: cryptoPath, content: `import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 16;\nconst TAG_LENGTH = 16;\n\nfunction deriveKey(secret: string, salt: Buffer): Buffer {\n return scryptSync(secret, salt, 32);\n}\n\nexport function encrypt(plaintext: string, secret: string): string {\n const salt = randomBytes(16);\n const key = deriveKey(secret, salt);\n const iv = randomBytes(IV_LENGTH);\n const cipher = createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n return Buffer.concat([salt, iv, tag, encrypted]).toString('base64');\n}\n\nexport function decrypt(ciphertext: string, secret: string): string {\n const data = Buffer.from(ciphertext, 'base64');\n const salt = data.subarray(0, 16);\n const iv = data.subarray(16, 32);\n const tag = data.subarray(32, 48);\n const encrypted = data.subarray(48);\n const key = deriveKey(secret, salt);\n const decipher = createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(tag);\n return decipher.update(encrypted) + decipher.final('utf8');\n}\n`, description: "Create AES-256-GCM encryption utility", ruleId: "GDPR-ART32-002" },
|
|
1719
|
+
];
|
|
1720
|
+
}
|
|
1721
|
+
function buildEncryptionInTransitImpl(root, _hasSrc) {
|
|
1722
|
+
const lang = detectProjectLanguage(root);
|
|
1723
|
+
const appFile = findMainAppFile(root);
|
|
1724
|
+
const actions = [];
|
|
1725
|
+
if (lang === "rust") {
|
|
1726
|
+
if (appFile) {
|
|
1727
|
+
actions.push({ type: "append", filePath: appFile, content: "\n// GESF: Enforce TLS in production\n// Use a reverse proxy (nginx, caddy) for TLS termination\n// or configure rustls with your certificate:\n// let config = rustls::ServerConfig::builder()\n// .with_safe_defaults()\n// .with_no_client_auth()\n// .with_single_cert(certs, key);\n", description: "Add Rust TLS guidance", ruleId: "GDPR-ART32-003" });
|
|
1728
|
+
}
|
|
1729
|
+
return actions;
|
|
1730
|
+
}
|
|
1731
|
+
if (appFile) {
|
|
1732
|
+
actions.push({ type: "append", filePath: appFile, content: "\nif (process.env.NODE_ENV === 'production') {\n app.use((req, res, next) => {\n if (req.headers['x-forwarded-proto'] === 'http') {\n return res.redirect(301, `https://${req.headers.host}${req.url}`);\n }\n next();\n });\n}\n", description: "Add HTTPS redirect middleware", ruleId: "GDPR-ART32-003" });
|
|
1733
|
+
}
|
|
1734
|
+
return actions;
|
|
1735
|
+
}
|
|
1736
|
+
function buildUserIdentificationImpl(root, hasSrc) {
|
|
1737
|
+
const lang = detectProjectLanguage(root);
|
|
1738
|
+
if (lang === "rust") {
|
|
1739
|
+
const authPath = "src/auth.rs";
|
|
1740
|
+
if (fs.existsSync(path.join(root, authPath)))
|
|
1741
|
+
return [];
|
|
1742
|
+
return [
|
|
1743
|
+
{ type: "create", filePath: authPath, content: `use argon2::{Argon2, Algorithm, Version, Params};
|
|
1744
|
+
use argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};
|
|
1745
|
+
use rand::rngs::OsRng;
|
|
1746
|
+
|
|
1747
|
+
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
|
1748
|
+
let salt = SaltString::generate(&mut OsRng);
|
|
1749
|
+
let params = Params::new(65536, 3, 4, Some(32))?;
|
|
1750
|
+
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
|
1751
|
+
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
|
|
1752
|
+
Ok(hash.to_string())
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
|
|
1756
|
+
let parsed = argon2::PasswordHash::new(hash)?;
|
|
1757
|
+
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())
|
|
1758
|
+
}
|
|
1759
|
+
`, description: "Create Rust auth utility with Argon2id", ruleId: "GDPR-ART32-004" },
|
|
1760
|
+
];
|
|
1761
|
+
}
|
|
1762
|
+
const authPath = hasSrc ? "src/lib/auth.ts" : "lib/auth.ts";
|
|
1763
|
+
if (fs.existsSync(path.join(root, authPath)))
|
|
1764
|
+
return [];
|
|
1765
|
+
return [
|
|
1766
|
+
{ type: "npm-install", filePath: "package.json", description: "Install argon2 for password hashing", ruleId: "GDPR-ART32-004" },
|
|
1767
|
+
{ type: "create", filePath: authPath, content: `import argon2 from 'argon2';\n\nexport async function hashPassword(password: string): Promise<string> {\n return argon2.hash(password, { type: argon2.argon2id });\n}\n\nexport async function verifyPassword(hashedPassword: string, inputPassword: string): Promise<boolean> {\n return argon2.verify(hashedPassword, inputPassword);\n}\n`, description: "Create auth utility with Argon2id", ruleId: "GDPR-ART32-004" },
|
|
1768
|
+
];
|
|
1769
|
+
}
|
|
1770
|
+
function buildIntegrityControlsImpl(root, hasSrc) {
|
|
1771
|
+
const lang = detectProjectLanguage(root);
|
|
1772
|
+
if (lang === "rust") {
|
|
1773
|
+
return [
|
|
1774
|
+
{ type: "create", filePath: "src/integrity.rs", content: `use sha2::{Sha256, Digest};
|
|
1775
|
+
|
|
1776
|
+
pub fn hash_data(data: &str) -> String {
|
|
1777
|
+
let mut hasher = Sha256::new();
|
|
1778
|
+
hasher.update(data.as_bytes());
|
|
1779
|
+
format!("{:x}", hasher.finalize())
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
pub fn verify_integrity(data: &str, expected_hash: &str) -> bool {
|
|
1783
|
+
hash_data(data) == expected_hash
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
pub fn generate_checksum(content: &[u8]) -> String {
|
|
1787
|
+
let mut hasher = Sha256::new();
|
|
1788
|
+
hasher.update(content);
|
|
1789
|
+
format!("{:x}", hasher.finalize())
|
|
1790
|
+
}
|
|
1791
|
+
`, description: "Create Rust integrity verification utility", ruleId: "GDPR-ART32-007" },
|
|
1792
|
+
];
|
|
1793
|
+
}
|
|
1794
|
+
const integrityPath = hasSrc ? "src/lib/integrity.ts" : "lib/integrity.ts";
|
|
1795
|
+
return [
|
|
1796
|
+
{ type: "create", filePath: integrityPath, content: `import { createHash } from 'node:crypto';\n\nexport function hashData(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\nexport function verifyIntegrity(data: string, expectedHash: string): boolean {\n return hashData(data) === expectedHash;\n}\n\nexport function generateChecksum(content: string): string {\n return createHash('sha256').update(content).digest('base64');\n}\n`, description: "Create integrity verification utility", ruleId: "GDPR-ART32-007" },
|
|
1797
|
+
];
|
|
1798
|
+
}
|
|
1799
|
+
function buildBackupPolicyImpl(root, _hasSrc) {
|
|
1800
|
+
return [
|
|
1801
|
+
{ type: "create", filePath: "scripts/backup.sh", content: `#!/bin/bash\nset -euo pipefail\n\nBACKUP_DIR="\${BACKUP_DIR:-./backups}"\nTIMESTAMP=$(date +%Y%m%d_%H%M%S)\nBACKUP_FILE="$BACKUP_DIR/backup_$TIMESTAMP.tar.gz.gpg"\nENCRYPTION_KEY="'''\${BACKUP_ENCRYPTION_KEY:-change-me}'''"\n\nmkdir -p "$BACKUP_DIR"\n\necho "[$(date)] Starting backup..."\n\nif command -v pg_dump &> /dev/null; then\n pg_dump "$DATABASE_URL" | gpg --symmetric --cipher-algo AES256 --batch --passphrase "$ENCRYPTION_KEY" -o "$BACKUP_FILE"\n echo "[$(date)] Database backup completed: $BACKUP_FILE"\nfi\n\ntar czf - ./data 2>/dev/null | gpg --symmetric --cipher-algo AES256 --batch --passphrase "$ENCRYPTION_KEY" -o "$BACKUP_DIR/data_$TIMESTAMP.tar.gz.gpg" 2>/dev/null || true\n\necho "[$(date)] Backup completed."\n\nfind "$BACKUP_DIR" -name "*.gpg" -mtime +30 -delete\necho "[$(date)] Cleaned up backups older than 30 days."\n`, description: "Create encrypted backup script", ruleId: "GDPR-ART32-008" },
|
|
1802
|
+
];
|
|
1803
|
+
}
|
|
1804
|
+
function buildSecurityTestingImpl(root) {
|
|
1805
|
+
const lang = detectProjectLanguage(root);
|
|
1806
|
+
const setupSteps = lang === "rust"
|
|
1807
|
+
? ` - uses: actions-rs/toolchain@v1\n with:\n toolchain: stable\n - run: cargo build\n - name: cargo audit\n run: cargo install cargo-audit && cargo audit`
|
|
1808
|
+
: ` - uses: actions/setup-node@v4\n with:\n node-version: '22'\n - run: npm ci\n - name: npm audit\n run: npm audit --audit-level=high\n continue-on-error: true`;
|
|
1809
|
+
return [
|
|
1810
|
+
{ type: "create", filePath: ".github/workflows/security-scan.yml", content: `name: Security Scan\non:\n push:\n branches: [main, master]\n pull_request:\n branches: [main, master]\n schedule:\n - cron: '0 6 * * 1'\n\njobs:\n security:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n${setupSteps}\n - name: Run GESF compliance check\n run: npx @greenarmor/ges audit --ci\n`, description: "Create security scanning GitHub Actions workflow", ruleId: "GDPR-ART32-009" },
|
|
1811
|
+
{ type: "create", filePath: ".github/workflows/sbom-scan.yml", content: `name: SBOM Generation & Scan\non:\n push:\n branches: [main, master]\n pull_request:\n branches: [main, master]\n schedule:\n - cron: '0 6 * * 1'\n\njobs:\n sbom:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Generate SBOM with Syft\n uses: anchore/sbom-action@v0\n with:\n image: \"\"\n path: .\n format: cyclonedx-json\n output-file: sbom.json\n fail-build: false\n\n - name: Scan SBOM for vulnerabilities with Grype\n uses: anchore/scan-action@v6\n with:\n sbom: sbom.json\n fail-build: true\n severity-cutoff: high\n\n - name: Upload SBOM artifacts\n if: always()\n uses: actions/upload-artifact@v4\n with:\n name: sbom-artifacts\n path: sbom.json\n retention-days: 90\n`, description: "Create SBOM generation and scanning GitHub Actions workflow", ruleId: "GDPR-ART32-009" },
|
|
1812
|
+
];
|
|
1813
|
+
}
|
|
1814
|
+
function generateDataInventory(projectName, projectType) {
|
|
1815
|
+
const webCategories = [
|
|
1816
|
+
{ category: "User Profiles", type: "Personal", classification: "Restricted", retention: "Account + 30 days", basis: "Contract (Art. 6(1)(b))" },
|
|
1817
|
+
{ category: "Email Addresses", type: "Personal", classification: "Confidential", retention: "Account + 30 days", basis: "Contract (Art. 6(1)(b))" },
|
|
1818
|
+
{ category: "Authentication Credentials", type: "Personal", classification: "Restricted", retention: "Session duration", basis: "Contract (Art. 6(1)(b))" },
|
|
1819
|
+
{ category: "IP Addresses", type: "Personal", classification: "Internal", retention: "30 days", basis: "Legitimate interest (Art. 6(1)(f))" },
|
|
1820
|
+
{ category: "Session Data", type: "Operational", classification: "Internal", retention: "Session duration", basis: "Contract (Art. 6(1)(b))" },
|
|
1821
|
+
{ category: "Audit Logs", type: "Operational", classification: "Internal", retention: "1 year", basis: "Legal obligation (Art. 6(1)(c))" },
|
|
1822
|
+
];
|
|
1823
|
+
const aiCategories = [
|
|
1824
|
+
{ category: "AI Prompts", type: "Personal", classification: "Confidential", retention: "90 days", basis: "Legitimate interest (Art. 6(1)(f))" },
|
|
1825
|
+
{ category: "AI Outputs", type: "Personal", classification: "Internal", retention: "30 days", basis: "Legitimate interest (Art. 6(1)(f))" },
|
|
1826
|
+
{ category: "Training Data References", type: "Personal", classification: "Restricted", retention: "Duration of use", basis: "Consent (Art. 6(1)(a))" },
|
|
1827
|
+
];
|
|
1828
|
+
const blockchainCategories = [
|
|
1829
|
+
{ category: "Wallet Addresses", type: "Pseudonymous", classification: "Public", retention: "Indefinite (on-chain)", basis: "Contract (Art. 6(1)(b))" },
|
|
1830
|
+
{ category: "Transaction History", type: "Pseudonymous", classification: "Public", retention: "Indefinite (on-chain)", basis: "Contract (Art. 6(1)(b))" },
|
|
1831
|
+
{ category: "KYC Data", type: "Personal", classification: "Restricted", retention: "5 years", basis: "Legal obligation (Art. 6(1)(c))" },
|
|
1832
|
+
];
|
|
1833
|
+
let categories = webCategories;
|
|
1834
|
+
if (projectType.includes("ai"))
|
|
1835
|
+
categories = [...webCategories, ...aiCategories];
|
|
1836
|
+
if (projectType.includes("blockchain") || projectType.includes("wallet"))
|
|
1837
|
+
categories = [...webCategories, ...blockchainCategories];
|
|
1838
|
+
if (projectType.includes("healthcare")) {
|
|
1839
|
+
categories = [...webCategories, { category: "Health Records", type: "Special Category", classification: "Restricted", retention: "10 years", basis: "Legal obligation (Art. 6(1)(c) + Art. 9)" }];
|
|
1840
|
+
}
|
|
1841
|
+
if (projectType.includes("photo")) {
|
|
1842
|
+
categories = [...webCategories, { category: "Photos/Images", type: "Personal", classification: "Restricted", retention: "Account + 30 days", basis: "Contract (Art. 6(1)(b))" }];
|
|
1843
|
+
}
|
|
1844
|
+
const lines = [
|
|
1845
|
+
`# Data Inventory - ${projectName}\n`,
|
|
1846
|
+
`Generated: ${new Date().toISOString()}\n`,
|
|
1847
|
+
`## Data Categories\n`,
|
|
1848
|
+
`| Category | Type | Classification | Retention | Legal Basis |`,
|
|
1849
|
+
`|----------|------|---------------|-----------|-------------|`,
|
|
1850
|
+
];
|
|
1851
|
+
for (const cat of categories) {
|
|
1852
|
+
lines.push(`| ${cat.category} | ${cat.type} | ${cat.classification} | ${cat.retention} | ${cat.basis} |`);
|
|
1853
|
+
}
|
|
1854
|
+
lines.push("");
|
|
1855
|
+
lines.push("## Data Classification Rules\n");
|
|
1856
|
+
lines.push("| Classification | Encryption | Access Controls | Audit Logging |");
|
|
1857
|
+
lines.push("|---------------|-----------|-----------------|---------------|");
|
|
1858
|
+
lines.push("| Public | Not required | Not required | Not required |");
|
|
1859
|
+
lines.push("| Internal | Not required | Required | Recommended |");
|
|
1860
|
+
lines.push("| Confidential | Required | Required | Required |");
|
|
1861
|
+
lines.push("| Restricted | Required | Required + MFA | Required + Immutable |");
|
|
1862
|
+
lines.push("");
|
|
1863
|
+
lines.push("## Data Subject Rights Implementation\n");
|
|
1864
|
+
lines.push("- [ ] Right of access (Article 15) - API endpoint or process implemented");
|
|
1865
|
+
lines.push("- [ ] Right to rectification (Article 16) - Update process documented");
|
|
1866
|
+
lines.push("- [ ] Right to erasure (Article 17) - Deletion process with verification");
|
|
1867
|
+
lines.push("- [ ] Right to restriction (Article 18) - Mark-and-hold process");
|
|
1868
|
+
lines.push("- [ ] Right to data portability (Article 20) - Export in machine-readable format");
|
|
1869
|
+
lines.push("- [ ] Right to object (Article 21) - Opt-out mechanism");
|
|
1870
|
+
lines.push("");
|
|
1871
|
+
lines.push("## Third-Party Processors\n");
|
|
1872
|
+
lines.push("| Processor | Data Shared | Purpose | DPA Signed | Location |");
|
|
1873
|
+
lines.push("|-----------|------------|---------|------------|----------|");
|
|
1874
|
+
lines.push("| [Cloud Provider] | [Data categories] | [Purpose] | [Yes/No] | [Country] |");
|
|
1875
|
+
lines.push("");
|
|
1876
|
+
lines.push("## Cross-Border Transfers\n");
|
|
1877
|
+
lines.push("| Transfer From | Transfer To | Safeguard |");
|
|
1878
|
+
lines.push("|--------------|------------|-----------|");
|
|
1879
|
+
lines.push("| [EU] | [Country] | [SCCs / Adequacy Decision / BCRs] |");
|
|
1880
|
+
return lines.join("\n");
|
|
1881
|
+
}
|
|
1882
|
+
function generateProcessingRecords(projectName, controllerName) {
|
|
1883
|
+
const lines = [
|
|
1884
|
+
`# Records of Processing Activities (ROPA) - ${projectName}\n`,
|
|
1885
|
+
`**Controller**: ${controllerName}`,
|
|
1886
|
+
`**Date**: ${new Date().toISOString().split("T")[0]}`,
|
|
1887
|
+
`**Document Reference**: ROPA-${projectName.replace(/\s+/g, "-").toUpperCase()}-001\n`,
|
|
1888
|
+
`## Article 30(1) — Controller Records\n`,
|
|
1889
|
+
];
|
|
1890
|
+
const activities = [
|
|
1891
|
+
{
|
|
1892
|
+
name: "User Account Management",
|
|
1893
|
+
purpose: "Provision and manage user accounts",
|
|
1894
|
+
categories: "Identity data, contact data, authentication data",
|
|
1895
|
+
recipients: "Internal systems, identity provider",
|
|
1896
|
+
transfers: "None (or specify if applicable)",
|
|
1897
|
+
retention: "Account lifetime + 30 days post-deletion",
|
|
1898
|
+
security: "Encryption at rest (AES-256-GCM), TLS 1.2+ in transit, MFA, RBAC",
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
name: "Service Delivery",
|
|
1902
|
+
purpose: "Deliver core product/service functionality",
|
|
1903
|
+
categories: "Usage data, preferences, content data",
|
|
1904
|
+
recipients: "Internal systems, CDN provider",
|
|
1905
|
+
transfers: "None (or specify)",
|
|
1906
|
+
retention: "Account lifetime",
|
|
1907
|
+
security: "Encryption, access controls, audit logging",
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
name: "Communication",
|
|
1911
|
+
purpose: "Service notifications, support, marketing (with consent)",
|
|
1912
|
+
categories: "Email addresses, communication preferences",
|
|
1913
|
+
recipients: "Email service provider",
|
|
1914
|
+
transfers: "None (or specify)",
|
|
1915
|
+
retention: "Until consent withdrawal or account closure",
|
|
1916
|
+
security: "Encryption, access controls",
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
name: "Analytics and Monitoring",
|
|
1920
|
+
purpose: "Service improvement and security monitoring",
|
|
1921
|
+
categories: "Usage data, IP addresses, device information",
|
|
1922
|
+
recipients: "Analytics provider, monitoring systems",
|
|
1923
|
+
transfers: "None (or specify)",
|
|
1924
|
+
retention: "12 months for analytics, 1 year for security logs",
|
|
1925
|
+
security: "Pseudonymization, access controls, aggregated reporting",
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
name: "Legal Compliance",
|
|
1929
|
+
purpose: "Meet regulatory and legal obligations",
|
|
1930
|
+
categories: "Identity data, transaction records, audit logs",
|
|
1931
|
+
recipients: "Legal authorities (upon request), auditors",
|
|
1932
|
+
transfers: "As required by law",
|
|
1933
|
+
retention: "Per legal requirements (typically 5-7 years)",
|
|
1934
|
+
security: "Encryption, access controls, immutable audit trail",
|
|
1935
|
+
},
|
|
1936
|
+
];
|
|
1937
|
+
for (const activity of activities) {
|
|
1938
|
+
lines.push(`### ${activity.name}\n`);
|
|
1939
|
+
lines.push(`- **Purpose**: ${activity.purpose}`);
|
|
1940
|
+
lines.push(`- **Categories of Data Subjects**: Users, customers, employees`);
|
|
1941
|
+
lines.push(`- **Categories of Personal Data**: ${activity.categories}`);
|
|
1942
|
+
lines.push(`- **Categories of Recipients**: ${activity.recipients}`);
|
|
1943
|
+
lines.push(`- **International Transfers**: ${activity.transfers}`);
|
|
1944
|
+
lines.push(`- **Retention Period**: ${activity.retention}`);
|
|
1945
|
+
lines.push(`- **Technical and Organizational Measures**: ${activity.security}`);
|
|
1946
|
+
lines.push(`- **Legal Basis**: Contract (Art. 6(1)(b)), Legitimate Interest (Art. 6(1)(f))\n`);
|
|
1947
|
+
}
|
|
1948
|
+
lines.push("## Data Protection Officer\n");
|
|
1949
|
+
lines.push("- **Name**: [DPO Name or N/A if not required]");
|
|
1950
|
+
lines.push("- **Contact**: [DPO Contact Details]");
|
|
1951
|
+
lines.push("");
|
|
1952
|
+
lines.push("## Review History\n");
|
|
1953
|
+
lines.push("| Date | Reviewer | Changes |");
|
|
1954
|
+
lines.push("|------|----------|---------|");
|
|
1955
|
+
lines.push(`| ${new Date().toISOString().split("T")[0]} | Initial | Created ROPA |`);
|
|
1956
|
+
return lines.join("\n");
|
|
1957
|
+
}
|
|
78
1958
|
export function handleRequest(request) {
|
|
79
1959
|
const isNotification = request.id === undefined || request.id === null;
|
|
80
1960
|
if (request.method === "initialize") {
|
|
@@ -117,53 +1997,540 @@ export function handleRequest(request) {
|
|
|
117
1997
|
const toolName = request.params?.name || "";
|
|
118
1998
|
const args = request.params?.arguments || {};
|
|
119
1999
|
let resultText;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
2000
|
+
try {
|
|
2001
|
+
switch (toolName) {
|
|
2002
|
+
case "check_compliance": {
|
|
2003
|
+
const projectType = (args.project_type || "saas");
|
|
2004
|
+
const packs = getPacksForProjectType(projectType);
|
|
2005
|
+
const controls = packs.flatMap((p) => p.controls);
|
|
2006
|
+
const frameworks = [...new Set(controls.map(c => c.framework))];
|
|
2007
|
+
const score = generateScoreFile(controls, frameworks);
|
|
2008
|
+
resultText = formatScoreOutput(score);
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
case "check_project_status": {
|
|
2012
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
2013
|
+
const { config, score, overrides } = loadProjectConfig(projectPath);
|
|
2014
|
+
if (!config) {
|
|
2015
|
+
resultText = `No GESF project found at ${projectPath}. Run 'ges init' first to initialize the project.`;
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
const lines = [];
|
|
2019
|
+
lines.push(`# Project Status: ${config.project_name || "Unknown"}\n`);
|
|
2020
|
+
lines.push(`**Path**: ${projectPath}`);
|
|
2021
|
+
lines.push(`**Type**: ${config.project_type || "Unknown"}`);
|
|
2022
|
+
lines.push(`**Initialized**: ${config.created_at || "Unknown"}`);
|
|
2023
|
+
lines.push(`**Frameworks**: ${Array.isArray(config.frameworks) ? config.frameworks.join(", ") : "Unknown"}`);
|
|
2024
|
+
if (overrides.length > 0) {
|
|
2025
|
+
const naCount = overrides.filter(o => o.status === "not-applicable").length;
|
|
2026
|
+
const passCount = overrides.filter(o => o.status === "pass").length;
|
|
2027
|
+
lines.push(`**Control Overrides**: ${overrides.length} (${naCount} not-applicable, ${passCount} pre-verified)`);
|
|
2028
|
+
}
|
|
2029
|
+
if (score) {
|
|
2030
|
+
lines.push(`\n## Compliance Score\n`);
|
|
2031
|
+
lines.push(`**Overall: ${score.overall}% (Grade: ${score.overall_grade})**\n`);
|
|
2032
|
+
lines.push("| Framework | Score | Grade | Passed | Failed | Warnings | Critical |");
|
|
2033
|
+
lines.push("|-----------|-------|-------|--------|--------|----------|----------|");
|
|
2034
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
2035
|
+
lines.push(`| ${fw} | ${data.score}% | ${data.grade} | ${data.passed_controls} | ${data.failed_controls} | ${data.warning_controls} | ${data.critical_failures} |`);
|
|
2036
|
+
}
|
|
2037
|
+
if (score.audit_impact) {
|
|
2038
|
+
const ai = score.audit_impact;
|
|
2039
|
+
lines.push(`\n**Audit Impact**: -${ai.total_deduction}% (${ai.critical_findings} critical, ${ai.high_findings} high, ${ai.medium_findings} medium, ${ai.low_findings} low findings)`);
|
|
2040
|
+
}
|
|
2041
|
+
lines.push(`\nLast evaluated: ${score.evaluated_at}`);
|
|
2042
|
+
}
|
|
2043
|
+
else {
|
|
2044
|
+
lines.push("\nNo compliance score found. Run 'ges audit' then 'ges score'.");
|
|
2045
|
+
}
|
|
2046
|
+
const controlsDir = path.join(projectPath, "controls");
|
|
2047
|
+
if (fs.existsSync(controlsDir)) {
|
|
2048
|
+
const controlFiles = fs.readdirSync(controlsDir).filter(f => f.endsWith(".json"));
|
|
2049
|
+
if (controlFiles.length > 0) {
|
|
2050
|
+
lines.push(`\n**Control Files**: ${controlFiles.join(", ")}`);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
resultText = lines.join("\n");
|
|
2054
|
+
break;
|
|
2055
|
+
}
|
|
2056
|
+
case "list_missing_controls": {
|
|
2057
|
+
const framework = args.framework || "GDPR";
|
|
2058
|
+
const projectType = args.project_type;
|
|
2059
|
+
let controls;
|
|
2060
|
+
if (projectType) {
|
|
2061
|
+
const packs = getPacksForProjectType(projectType);
|
|
2062
|
+
controls = packs.flatMap(p => p.controls);
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
controls = getAllPacks().flatMap(p => p.controls);
|
|
2066
|
+
}
|
|
2067
|
+
const missing = controls.filter((c) => c.framework.toLowerCase() === framework.toLowerCase() && c.status !== "pass");
|
|
2068
|
+
if (missing.length === 0) {
|
|
2069
|
+
resultText = `All ${framework} controls are passing. No missing controls found.`;
|
|
2070
|
+
}
|
|
2071
|
+
else {
|
|
2072
|
+
const lines = [`# Missing Controls - ${framework}\n`];
|
|
2073
|
+
const critical = missing.filter(c => c.severity === "critical");
|
|
2074
|
+
const high = missing.filter(c => c.severity === "high");
|
|
2075
|
+
const medium = missing.filter(c => c.severity === "medium");
|
|
2076
|
+
const low = missing.filter(c => c.severity === "low");
|
|
2077
|
+
lines.push(`**Total**: ${missing.length} (${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low)\n`);
|
|
2078
|
+
for (const group of [
|
|
2079
|
+
{ label: "Critical", items: critical },
|
|
2080
|
+
{ label: "High", items: high },
|
|
2081
|
+
{ label: "Medium", items: medium },
|
|
2082
|
+
{ label: "Low", items: low },
|
|
2083
|
+
]) {
|
|
2084
|
+
if (group.items.length > 0) {
|
|
2085
|
+
lines.push(`## ${group.label} Severity\n`);
|
|
2086
|
+
for (const c of group.items) {
|
|
2087
|
+
lines.push(`**${c.id}**: ${c.name}`);
|
|
2088
|
+
lines.push(` Status: ${c.status} | Category: ${c.category}`);
|
|
2089
|
+
lines.push(` ${c.implementation_guidance.split(".")[0]}\n`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
lines.push(`\nUse \`fix_recommendation\` with a control_id to get detailed implementation guidance.`);
|
|
2094
|
+
resultText = lines.join("\n");
|
|
2095
|
+
}
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
case "list_framework_controls": {
|
|
2099
|
+
const framework = args.framework || "GDPR";
|
|
2100
|
+
const statusFilter = args.status_filter;
|
|
2101
|
+
let allControls;
|
|
2102
|
+
const pack = getPack(framework.toLowerCase());
|
|
2103
|
+
if (pack) {
|
|
2104
|
+
allControls = pack.controls;
|
|
2105
|
+
}
|
|
2106
|
+
else {
|
|
2107
|
+
allControls = getAllPacks().flatMap(p => p.controls);
|
|
2108
|
+
}
|
|
2109
|
+
const filtered = framework.toLowerCase() !== "all"
|
|
2110
|
+
? allControls.filter(c => c.framework.toLowerCase() === framework.toLowerCase())
|
|
2111
|
+
: allControls;
|
|
2112
|
+
const controls = statusFilter
|
|
2113
|
+
? filtered.filter(c => c.status === statusFilter)
|
|
2114
|
+
: filtered;
|
|
2115
|
+
if (controls.length === 0) {
|
|
2116
|
+
resultText = statusFilter
|
|
2117
|
+
? `No ${framework} controls with status '${statusFilter}' found.`
|
|
2118
|
+
: `No controls found for framework '${framework}'. Available: GDPR, OWASP, CIS, NIST, AI, blockchain, government.`;
|
|
2119
|
+
}
|
|
2120
|
+
else {
|
|
2121
|
+
const lines = [`# ${framework} Controls (${controls.length} total${statusFilter ? `, filtered by: ${statusFilter}` : ""})\n`];
|
|
2122
|
+
lines.push("| ID | Name | Severity | Category | Status |");
|
|
2123
|
+
lines.push("|----|------|----------|----------|--------|");
|
|
2124
|
+
for (const c of controls) {
|
|
2125
|
+
lines.push(`| ${c.id} | ${c.name} | ${c.severity} | ${c.category} | ${c.status} |`);
|
|
2126
|
+
}
|
|
2127
|
+
lines.push(`\n### Summary`);
|
|
2128
|
+
const byStatus = {};
|
|
2129
|
+
for (const c of controls) {
|
|
2130
|
+
byStatus[c.status] = (byStatus[c.status] || 0) + 1;
|
|
2131
|
+
}
|
|
2132
|
+
for (const [status, count] of Object.entries(byStatus)) {
|
|
2133
|
+
lines.push(`- ${status}: ${count}`);
|
|
2134
|
+
}
|
|
2135
|
+
resultText = lines.join("\n");
|
|
2136
|
+
}
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
case "run_audit": {
|
|
2140
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
2141
|
+
if (!fs.existsSync(projectPath)) {
|
|
2142
|
+
resultText = `Project path does not exist: ${projectPath}`;
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2145
|
+
if (!fs.existsSync(path.join(projectPath, ".ges"))) {
|
|
2146
|
+
resultText = `GESF not initialized at ${projectPath}. Run 'ges init' first.`;
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
const { findings: rawFindings, scannedFiles } = runAudit(projectPath);
|
|
2150
|
+
const findings = deduplicateFindings(rawFindings);
|
|
2151
|
+
const projectConfig = loadProjectConfig(projectPath);
|
|
2152
|
+
const config = projectConfig.config;
|
|
2153
|
+
const frameworks = (config?.frameworks || ["GDPR", "OWASP"]);
|
|
2154
|
+
const projectType = (config?.project_type || "generic-web-application");
|
|
2155
|
+
const controls = getControlsForProject(projectType, frameworks);
|
|
2156
|
+
const overriddenControls = applyControlOverrides(controls, projectConfig.overrides);
|
|
2157
|
+
const auditedControls = updateControlsFromFindings(overriddenControls, findings);
|
|
2158
|
+
const score = generateScoreFile(auditedControls, frameworks, findings);
|
|
2159
|
+
const critical = findings.filter(f => f.severity === "critical");
|
|
2160
|
+
const high = findings.filter(f => f.severity === "high");
|
|
2161
|
+
const medium = findings.filter(f => f.severity === "medium");
|
|
2162
|
+
const low = findings.filter(f => f.severity === "low");
|
|
2163
|
+
const lines = [];
|
|
2164
|
+
lines.push(`# Security Audit Report\n`);
|
|
2165
|
+
lines.push(`**Scanned**: ${scannedFiles} files`);
|
|
2166
|
+
lines.push(`**Findings**: ${findings.length} total (${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low)\n`);
|
|
2167
|
+
if (findings.length > 0) {
|
|
2168
|
+
const grouped = {};
|
|
2169
|
+
for (const f of findings) {
|
|
2170
|
+
if (!grouped[f.category])
|
|
2171
|
+
grouped[f.category] = [];
|
|
2172
|
+
grouped[f.category].push(f);
|
|
2173
|
+
}
|
|
2174
|
+
const categoryOrder = ["secrets", "encryption", "authentication", "injection", "xss", "security", "database", "config", "infrastructure", "dependencies"];
|
|
2175
|
+
for (const cat of categoryOrder) {
|
|
2176
|
+
if (!grouped[cat])
|
|
2177
|
+
continue;
|
|
2178
|
+
lines.push(`## ${cat.charAt(0).toUpperCase() + cat.slice(1)}\n`);
|
|
2179
|
+
for (const f of grouped[cat]) {
|
|
2180
|
+
const loc = f.file !== "project" ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : " (project-wide)";
|
|
2181
|
+
lines.push(`- **[${f.severity.toUpperCase()}] ${f.title}**${loc}`);
|
|
2182
|
+
lines.push(` Evidence: ${f.evidence.slice(0, 150)}`);
|
|
2183
|
+
lines.push(` Fix: ${f.fix}`);
|
|
2184
|
+
if (f.controlIds && f.controlIds.length > 0) {
|
|
2185
|
+
lines.push(` Controls: ${f.controlIds.join(", ")}`);
|
|
2186
|
+
}
|
|
2187
|
+
lines.push("");
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
else {
|
|
2192
|
+
lines.push("**No security findings detected.** All scanned files are clean.\n");
|
|
2193
|
+
}
|
|
2194
|
+
lines.push("## Compliance Score\n");
|
|
2195
|
+
lines.push(`**Overall: ${score.overall}% (Grade: ${score.overall_grade})**\n`);
|
|
2196
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
2197
|
+
lines.push(`- ${fw}: ${data.score}% (${data.grade}) — ${data.passed_controls}/${data.total_controls} controls passed, ${data.critical_failures} critical failures`);
|
|
2198
|
+
}
|
|
2199
|
+
if (projectConfig.overrides.length > 0) {
|
|
2200
|
+
lines.push(`\n*Note: ${projectConfig.overrides.length} control overrides applied.*`);
|
|
2201
|
+
}
|
|
2202
|
+
resultText = lines.join("\n");
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
case "generate_compliance_report": {
|
|
2206
|
+
const projectName = args.project_name || "Project";
|
|
2207
|
+
const projectType = (args.project_type || "saas");
|
|
2208
|
+
const frameworksStr = args.frameworks || "GDPR,OWASP";
|
|
2209
|
+
const frameworks = frameworksStr.split(",").map(f => f.trim());
|
|
2210
|
+
resultText = generateFullComplianceReport(projectName, projectType, frameworks);
|
|
2211
|
+
break;
|
|
2212
|
+
}
|
|
2213
|
+
case "generate_audit_report": {
|
|
2214
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
2215
|
+
if (!args.project_path && !fs.existsSync(path.join(projectPath, ".ges"))) {
|
|
2216
|
+
const projectName = args.project_name || "Project";
|
|
2217
|
+
const projectType = "generic-web-application";
|
|
2218
|
+
resultText = generateFullComplianceReport(projectName, projectType, ["GDPR", "OWASP"]);
|
|
2219
|
+
resultText += "\n\n**Note: No project path specified and no .ges/ directory found. Showing default compliance report. Provide project_path for actual audit results.**";
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
if (!fs.existsSync(projectPath)) {
|
|
2223
|
+
resultText = `Project path does not exist: ${projectPath}`;
|
|
2224
|
+
break;
|
|
2225
|
+
}
|
|
2226
|
+
const projectName = args.project_name || path.basename(projectPath);
|
|
2227
|
+
const projectConfig = loadProjectConfig(projectPath);
|
|
2228
|
+
const config = projectConfig.config;
|
|
2229
|
+
const projectType = (config?.project_type || "generic-web-application");
|
|
2230
|
+
const frameworks = (config?.frameworks || ["GDPR", "OWASP"]);
|
|
2231
|
+
const { findings: rawFindings, scannedFiles } = runAudit(projectPath);
|
|
2232
|
+
const findings = deduplicateFindings(rawFindings);
|
|
2233
|
+
resultText = generateFullComplianceReport(projectName, projectType, frameworks, findings, projectConfig.overrides);
|
|
2234
|
+
resultText += `\n\n**Audit Details**: Scanned ${scannedFiles} files. ${findings.length} findings detected.`;
|
|
2235
|
+
break;
|
|
2236
|
+
}
|
|
2237
|
+
case "fix_recommendation": {
|
|
2238
|
+
const controlId = args.control_id || "";
|
|
2239
|
+
const findingTitle = args.finding_title;
|
|
2240
|
+
if (!controlId && !findingTitle) {
|
|
2241
|
+
resultText = "Please provide either a control_id (e.g. 'GDPR-ART32-001') or a finding_title to get fix guidance.";
|
|
2242
|
+
break;
|
|
2243
|
+
}
|
|
2244
|
+
resultText = generateFixGuidance(controlId, findingTitle);
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
case "generate_retention_policy": {
|
|
2248
|
+
const name = args.project_name || "Project";
|
|
2249
|
+
resultText = `# Data Retention Policy - ${name}\n\n## 1. Purpose\n\nThis policy defines the retention periods for all data categories processed by ${name}.\n\n## 2. Retention Periods\n\n| Category | Period | Justification | Legal Basis |\n|----------|--------|---------------|-------------|\n| User account data | Account lifetime + 30 days | Contract fulfillment | Art. 6(1)(b) |\n| Email addresses | Account lifetime + 30 days | Communication | Art. 6(1)(b) |\n| Authentication data | Session duration | Security | Art. 6(1)(f) |\n| IP addresses | 30 days | Security monitoring | Art. 6(1)(f) |\n| Audit logs | 1 year | Legal obligation | Art. 6(1)(c) |\n| Session data | Session duration | Operational | Art. 6(1)(b) |\n| Marketing consent | Until withdrawal | Consent | Art. 6(1)(a) |\n| Support tickets | 2 years | Quality assurance | Art. 6(1)(f) |\n\n## 3. Deletion Process\n\n1. Automated deletion: Data past retention period is flagged for deletion\n2. Deletion verification: Monthly audit of deletion jobs\n3. Backup purge: Backups containing expired data are purged within 90 days\n4. Deletion log: All deletions are logged with timestamp and scope\n\n## 4. Exceptions\n\n- Data subject to legal hold: Retained until hold is lifted\n- Data required for ongoing legal proceedings: Retained until proceedings conclude\n- Anonymized data may be retained indefinitely for statistical purposes\n\n## 5. Data Subject Rights\n\n- Users can request early deletion via the data subject rights process\n- Right to erasure (Article 17) requests are processed within 30 days\n- Verification of identity is required before any deletion\n\n## 6. Review Schedule\n\nThis policy is reviewed quarterly and updated as needed.\n\nLast reviewed: ${new Date().toISOString().split("T")[0]}`;
|
|
2250
|
+
break;
|
|
2251
|
+
}
|
|
2252
|
+
case "generate_incident_response": {
|
|
2253
|
+
const name = args.project_name || "Project";
|
|
2254
|
+
resultText = `# Incident Response Plan - ${name}\n\n## 1. Severity Levels\n\n| Level | Response Time | Examples |\n|-------|--------------|----------|\n| P1 (Critical) | 15 minutes | Data breach, system compromise, ransomware |\n| P2 (High) | 1 hour | Unauthorized access, vulnerability exploitation |\n| P3 (Medium) | 4 hours | Suspicious activity, policy violation |\n| P4 (Low) | 24 hours | Minor misconfiguration, informational findings |\n\n## 2. Response Team\n\n| Role | Responsibility |\n|------|---------------|\n| Incident Commander | Overall coordination and decision making |\n| Security Lead | Technical investigation and containment |\n| Communications Lead | Internal and external notifications |\n| Legal Advisor | Regulatory and legal compliance |\n| DPO (if applicable) | GDPR compliance and data subject notification |\n\n## 3. Response Process\n\n### Phase 1: Detection & Identification\n- Alert triggered by monitoring, user report, or external notification\n- Initial assessment of scope and severity\n- Assign severity level (P1-P4)\n\n### Phase 2: Containment\n- Isolate affected systems\n- Preserve evidence for forensic analysis\n- Implement temporary controls\n\n### Phase 3: Eradication\n- Identify root cause\n- Remove threat from all systems\n- Patch vulnerabilities\n\n### Phase 4: Recovery\n- Restore systems from verified backups\n- Verify system integrity\n- Resume normal operations with enhanced monitoring\n\n### Phase 5: Post-Incident Review\n- Document timeline and actions taken\n- Identify lessons learned\n- Update security controls and processes\n- Update this plan if needed\n\n## 4. GDPR Breach Notification\n\n**72-hour rule**: If a breach is likely to result in a risk to data subjects:\n1. Notify supervisory authority within 72 hours (Article 33)\n2. If high risk: Notify affected data subjects without undue delay (Article 34)\n3. Document all actions in the breach register\n\n### Notification Template\n- Nature of the breach\n- Categories and approximate number of data subjects\n- Likely consequences\n- Measures taken or proposed\n\n## 5. Communication Templates\n\n### Internal Notification\nSubject: [P-level] Security Incident - [Brief Description]\n- What: [Description]\n- When: [Detection time]\n- Impact: [Known impact]\n- Actions: [Current containment measures]\n- Next update: [Time]\n\n### Regulatory Notification\nAddressed to: [Supervisory Authority]\n- DPO contact: [Name, email, phone]\n- Breach description: [Details]\n- Affected individuals: [Number and categories]\n- Measures taken: [Containment and remediation]\n\n## 6. Testing\n\n- Tabletop exercises: Quarterly\n- Full simulation: Annually\n- Plan review: After each incident and at least semi-annually\n\nLast reviewed: ${new Date().toISOString().split("T")[0]}`;
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
case "generate_risk_assessment": {
|
|
2258
|
+
const name = args.project_name || "Project";
|
|
2259
|
+
resultText = `# Risk Assessment - ${name}\n\n## 1. Methodology\n\nRisk assessment follows the GESF methodology based on ISO 27005 and NIST SP 800-30.\n\nRisk Score = Likelihood × Impact\n\n| Rating | Score |\n|--------|-------|\n| Critical | 5 |\n| High | 4 |\n| Medium | 3 |\n| Low | 2 |\n| Negligible | 1 |\n\n## 2. Risk Register\n\n| ID | Risk | Likelihood | Impact | Score | Mitigation | Residual |\n|----|------|-----------|--------|-------|------------|----------|\n| R001 | Data breach (external) | Medium (3) | Critical (5) | 15 | Encryption, MFA, WAF, pen testing | Medium |\n| R002 | Insider threat | Low (2) | High (4) | 8 | RBAC, audit logging, DLP | Low |\n| R003 | Data loss | Low (2) | Critical (5) | 10 | Backups, DR plan, replication | Low |\n| R004 | Ransomware | Low (2) | Critical (5) | 10 | Backups, EDR, email filtering | Low |\n| R005 | Supply chain attack | Medium (3) | High (4) | 12 | Dependency scanning, SBOM, vendor assessment | Medium |\n| R006 | Misconfiguration | Medium (3) | High (4) | 12 | IaC scanning, security review, hardening | Medium |\n| R007 | Credential compromise | Medium (3) | High (4) | 12 | MFA, password policy, monitoring | Low |\n| R008 | DDoS attack | Low (2) | Medium (3) | 6 | CDN, rate limiting, WAF | Low |\n| R009 | Non-compliance (GDPR) | Medium (3) | High (4) | 12 | Regular audits, compliance scanning | Low |\n| R010 | Third-party data breach | Medium (3) | High (4) | 12 | DPA requirements, vendor assessment | Medium |\n\n## 3. Risk Treatment Plan\n\n| ID | Treatment | Owner | Deadline | Status |\n|----|-----------|-------|----------|--------|\n| R001 | Implement WAF + annual pen testing | Security Lead | Quarterly | In progress |\n| R002 | Deploy DLP solution | Security Lead | Q2 | Planned |\n| R003 | Test DR plan monthly | Platform Lead | Monthly | In progress |\n| R005 | Automate dependency scanning | DevOps | Q1 | In progress |\n| R006 | Implement IaC security scanning | DevOps | Q2 | Planned |\n| R007 | Enforce MFA for all users | Security Lead | Q1 | Done |\n| R009 | Monthly compliance audits | Compliance Lead | Monthly | In progress |\n\n## 4. Acceptance Criteria\n\nRisks with residual score > 12 require executive sign-off.\nAll critical risks must have active mitigation plans.\n\n## 5. Review Schedule\n\n- Full assessment: Annually\n- Risk register review: Quarterly\n- After any significant change or incident\n\nLast reviewed: ${new Date().toISOString().split("T")[0]}`;
|
|
2260
|
+
break;
|
|
2261
|
+
}
|
|
2262
|
+
case "generate_dpa": {
|
|
2263
|
+
const name = args.project_name || "Project";
|
|
2264
|
+
resultText = `# Data Processing Agreement - ${name}\n\n## 1. Parties\n\n**Controller**: [Company Name]\nAddress: [Address]\nDPO: [Name, Email]\n\n**Processor**: [Service Provider Name]\nAddress: [Address]\nDPO: [Name, Email]\n\n## 2. Subject Matter and Duration\n\nThis Agreement governs the processing of personal data by the Processor on behalf of the Controller in connection with the provision of services for **${name}**.\n\n**Duration**: Effective from the date of signature until termination of the underlying service agreement.\n\n## 3. Details of Processing\n\n| Category | Type | Purpose |\n|----------|------|--------|\n| User data | Personal | Service delivery |\n| Authentication data | Personal | Access control |\n| Usage data | Operational | Analytics |\n| Communication data | Personal | Support |\n\n## 4. Obligations of the Processor\n\nThe Processor shall:\n\n1. Process data only on documented instructions from the Controller\n2. Ensure confidentiality of all persons authorized to process personal data\n3. Implement appropriate technical and organizational measures (Article 32)\n4. Not engage sub-processors without prior authorization\n5. Assist the Controller in responding to data subject rights requests\n6. Assist the Controller in ensuring compliance with Articles 32-36\n7. Delete or return all personal data upon termination\n8. Make available all information necessary to demonstrate compliance\n9. Allow and contribute to audits conducted by the Controller or mandated auditor\n\n## 5. Security Measures (Article 32)\n\n- Encryption of personal data at rest (AES-256-GCM)\n- Encryption of personal data in transit (TLS 1.2+)\n- Access controls with principle of least privilege\n- Regular security testing and vulnerability assessments\n- Incident response plan with 72-hour notification\n- Audit logging with immutable records\n- Regular backup and disaster recovery testing\n\n## 6. Sub-Processors\n\n| Sub-Processor | Purpose | Location |\n|-------------|---------|----------|\n| [Cloud Provider] | Hosting | [Country] |\n| [Email Provider] | Communications | [Country] |\n\nThe Controller authorizes the use of the above sub-processors. Any changes will be notified 30 days in advance.\n\n## 7. Data Breach Notification\n\nThe Processor shall notify the Controller within 24 hours of becoming aware of a personal data breach, providing:\n- Nature of the breach including categories and approximate numbers\n- Name and contact details of the DPO\n- Likely consequences of the breach\n- Measures taken or proposed to address the breach\n\n## 8. Data Subject Rights\n\nThe Processor shall assist the Controller in fulfilling its obligations to respond to data subject requests for:\n- Access (Article 15)\n- Rectification (Article 16)\n- Erasure (Article 17)\n- Restriction (Article 18)\n- Data portability (Article 20)\n- Objection (Article 21)\n\n## 9. International Transfers\n\nAny transfer of personal data outside the EEA shall be subject to:\n- Adequacy decision by the European Commission, OR\n- Standard Contractual Clauses (SCCs), OR\n- Binding Corporate Rules (BCRs)\n\n## 10. Termination\n\nUpon termination:\n1. Processor shall return all personal data to the Controller within 30 days\n2. If return is not possible, Processor shall delete all personal data\n3. Processor shall certify deletion in writing\n\n## 11. Liability and Indemnification\n\nEach party's liability shall be governed by the underlying service agreement and applicable GDPR provisions.\n\n## 12. Governing Law\n\nThis Agreement shall be governed by [Applicable Jurisdiction].\n\n---\n\n**Signed:**\n\nController: _________________________ Date: ____________\n\nProcessor: _________________________ Date: ____________`;
|
|
2265
|
+
break;
|
|
2266
|
+
}
|
|
2267
|
+
case "generate_data_inventory": {
|
|
2268
|
+
const projectName = args.project_name || "Project";
|
|
2269
|
+
const projectType = args.project_type || "saas";
|
|
2270
|
+
resultText = generateDataInventory(projectName, projectType);
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
case "generate_processing_records": {
|
|
2274
|
+
const projectName = args.project_name || "Project";
|
|
2275
|
+
const controllerName = args.controller_name || "[Organization Name]";
|
|
2276
|
+
resultText = generateProcessingRecords(projectName, controllerName);
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
case "auto_fix": {
|
|
2280
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
2281
|
+
const dryRun = args.dry_run === "true";
|
|
2282
|
+
const ruleFilter = args.rule_ids ? new Set(args.rule_ids.split(",").map(r => r.trim())) : undefined;
|
|
2283
|
+
if (!fs.existsSync(projectPath)) {
|
|
2284
|
+
resultText = `Project path does not exist: ${projectPath}`;
|
|
2285
|
+
break;
|
|
2286
|
+
}
|
|
2287
|
+
const { findings: rawFindings, scannedFiles } = runAudit(projectPath);
|
|
2288
|
+
const findings = deduplicateFindings(rawFindings);
|
|
2289
|
+
if (findings.length === 0) {
|
|
2290
|
+
resultText = `# Auto-Fix Report\n\n**Project**: ${projectPath}\n**Scanned**: ${scannedFiles} files\n\nNo issues found. Project is clean!`;
|
|
2291
|
+
break;
|
|
2292
|
+
}
|
|
2293
|
+
const { actions, warnings } = createAutoFixPlan(projectPath, findings, ruleFilter);
|
|
2294
|
+
if (actions.length === 0) {
|
|
2295
|
+
const lines = [
|
|
2296
|
+
`# Auto-Fix Report\n`,
|
|
2297
|
+
`**Project**: ${projectPath}`,
|
|
2298
|
+
`**Scanned**: ${scannedFiles} files`,
|
|
2299
|
+
`**Findings**: ${findings.length}\n`,
|
|
2300
|
+
`## No Auto-Fixable Issues\n`,
|
|
2301
|
+
`All ${findings.length} findings require manual review:\n`,
|
|
2302
|
+
];
|
|
2303
|
+
for (const w of warnings)
|
|
2304
|
+
lines.push(`- ${w}`);
|
|
2305
|
+
for (const f of findings.slice(0, 10)) {
|
|
2306
|
+
lines.push(`- [${f.severity.toUpperCase()}] ${f.title} (${f.file}${f.line ? `:${f.line}` : ""})`);
|
|
2307
|
+
}
|
|
2308
|
+
resultText = lines.join("\n");
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
const npmInstalls = getNpmInstallsFromActions(actions);
|
|
2312
|
+
const lines = [
|
|
2313
|
+
`# Auto-Fix Report\n`,
|
|
2314
|
+
`**Project**: ${projectPath}`,
|
|
2315
|
+
`**Scanned**: ${scannedFiles} files`,
|
|
2316
|
+
`**Findings**: ${findings.length} total`,
|
|
2317
|
+
`**Auto-fixable**: ${actions.length} actions`,
|
|
2318
|
+
`**Manual review**: ${warnings.length} items`,
|
|
2319
|
+
dryRun ? `**Mode**: DRY RUN (no changes applied)\n` : "",
|
|
2320
|
+
];
|
|
2321
|
+
if (dryRun) {
|
|
2322
|
+
lines.push("## Planned Actions (dry run)\n");
|
|
2323
|
+
}
|
|
2324
|
+
else {
|
|
2325
|
+
lines.push("## Applied Fixes\n");
|
|
2326
|
+
}
|
|
2327
|
+
let applied = 0;
|
|
2328
|
+
let failed = 0;
|
|
2329
|
+
for (const action of actions) {
|
|
2330
|
+
if (dryRun) {
|
|
2331
|
+
lines.push(`- [${action.type}] ${action.filePath}: ${action.description}`);
|
|
2332
|
+
applied++;
|
|
2333
|
+
}
|
|
2334
|
+
else {
|
|
2335
|
+
const result = applyAutoFixAction(projectPath, action);
|
|
2336
|
+
if (result.applied) {
|
|
2337
|
+
applied++;
|
|
2338
|
+
lines.push(`- ✓ [${action.type}] ${action.filePath}: ${action.description}`);
|
|
2339
|
+
}
|
|
2340
|
+
else {
|
|
2341
|
+
failed++;
|
|
2342
|
+
lines.push(`- ✗ [${action.type}] ${action.filePath}: ${action.description} — ${result.error}`);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
lines.push(`\n## Summary\n`);
|
|
2347
|
+
lines.push(`- Actions applied: ${applied}${failed > 0 ? ` (${failed} failed)` : ""}`);
|
|
2348
|
+
if (npmInstalls.length > 0) {
|
|
2349
|
+
lines.push(`\n## npm Packages to Install\n`);
|
|
2350
|
+
lines.push("```bash");
|
|
2351
|
+
lines.push(`npm install ${npmInstalls.join(" ")}`);
|
|
2352
|
+
lines.push("```\n");
|
|
2353
|
+
lines.push("Or if using pnpm:");
|
|
2354
|
+
lines.push("```bash");
|
|
2355
|
+
lines.push(`pnpm add ${npmInstalls.join(" ")}`);
|
|
2356
|
+
lines.push("```");
|
|
2357
|
+
}
|
|
2358
|
+
if (warnings.length > 0) {
|
|
2359
|
+
lines.push(`\n## Manual Review Required\n`);
|
|
2360
|
+
for (const w of warnings)
|
|
2361
|
+
lines.push(`- ${w}`);
|
|
2362
|
+
}
|
|
2363
|
+
lines.push(`\n## Next Steps`);
|
|
2364
|
+
lines.push("1. Install the npm packages listed above");
|
|
2365
|
+
lines.push("2. Review all changes with `git diff`");
|
|
2366
|
+
lines.push("3. Run `ges audit` to verify fixes");
|
|
2367
|
+
lines.push("4. Address remaining manual review items");
|
|
2368
|
+
lines.push("5. Use `fix_recommendation` tool for detailed guidance on manual items");
|
|
2369
|
+
resultText = lines.join("\n");
|
|
2370
|
+
break;
|
|
2371
|
+
}
|
|
2372
|
+
case "apply_control_override": {
|
|
2373
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
2374
|
+
const controlId = args.control_id || "";
|
|
2375
|
+
const status = (args.status || "not-applicable");
|
|
2376
|
+
const reason = args.reason || "";
|
|
2377
|
+
if (!controlId) {
|
|
2378
|
+
resultText = "Error: control_id is required.";
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
if (!["not-applicable", "pass"].includes(status)) {
|
|
2382
|
+
resultText = `Error: status must be 'not-applicable' or 'pass'. Got: ${status}`;
|
|
2383
|
+
break;
|
|
2384
|
+
}
|
|
2385
|
+
if (!fs.existsSync(path.join(projectPath, ".ges"))) {
|
|
2386
|
+
resultText = `Error: No .ges/ directory at ${projectPath}. Run 'ges init' first.`;
|
|
2387
|
+
break;
|
|
2388
|
+
}
|
|
2389
|
+
const overridePath = path.join(projectPath, ".ges", "control-overrides.json");
|
|
2390
|
+
let overrides = [];
|
|
2391
|
+
if (fs.existsSync(overridePath)) {
|
|
2392
|
+
const parsed = readJsonFileSafe(overridePath);
|
|
2393
|
+
if (Array.isArray(parsed))
|
|
2394
|
+
overrides = parsed;
|
|
2395
|
+
}
|
|
2396
|
+
const existing = overrides.findIndex(o => o.control_id === controlId);
|
|
2397
|
+
if (existing >= 0) {
|
|
2398
|
+
overrides[existing] = { control_id: controlId, status, reason };
|
|
2399
|
+
}
|
|
2400
|
+
else {
|
|
2401
|
+
overrides.push({ control_id: controlId, status, reason });
|
|
2402
|
+
}
|
|
2403
|
+
const dir = path.dirname(overridePath);
|
|
2404
|
+
if (!fs.existsSync(dir))
|
|
2405
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2406
|
+
fs.writeFileSync(overridePath, JSON.stringify(overrides, null, 2), "utf-8");
|
|
2407
|
+
const lines = [
|
|
2408
|
+
`# Control Override Applied\n`,
|
|
2409
|
+
`**Control**: ${controlId}`,
|
|
2410
|
+
`**Status**: ${status}`,
|
|
2411
|
+
`**Reason**: ${reason || "(none provided)"}`,
|
|
2412
|
+
`**File**: ${overridePath}`,
|
|
2413
|
+
`**Total overrides**: ${overrides.length}\n`,
|
|
2414
|
+
`The override will take effect on the next \`ges audit\` or \`ges score\` run.`,
|
|
2415
|
+
`\nRun \`ges audit\` then \`ges score\` to see the updated compliance score.`,
|
|
2416
|
+
];
|
|
2417
|
+
resultText = lines.join("\n");
|
|
2418
|
+
break;
|
|
2419
|
+
}
|
|
2420
|
+
case "implement_control": {
|
|
2421
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
2422
|
+
const controlId = args.control_id || "";
|
|
2423
|
+
if (!controlId) {
|
|
2424
|
+
resultText = "Error: control_id is required. Example: GDPR-ART32-002, GDPR-ART32-006, AUTH-002";
|
|
2425
|
+
break;
|
|
2426
|
+
}
|
|
2427
|
+
if (!fs.existsSync(projectPath)) {
|
|
2428
|
+
resultText = `Error: Project path does not exist: ${projectPath}`;
|
|
2429
|
+
break;
|
|
2430
|
+
}
|
|
2431
|
+
const hasSrc = fs.existsSync(path.join(projectPath, "src"));
|
|
2432
|
+
const appFile = findMainAppFile(projectPath);
|
|
2433
|
+
const lines = [`# Implement Control: ${controlId}\n`];
|
|
2434
|
+
const actions = [];
|
|
2435
|
+
const controlMap = {
|
|
2436
|
+
"GDPR-ART32-002": {
|
|
2437
|
+
name: "Encryption at Rest",
|
|
2438
|
+
actions: buildEncryptionAtRestImpl(projectPath, hasSrc),
|
|
2439
|
+
warnings: ["Configure encryption keys via environment variables or a vault service."],
|
|
2440
|
+
},
|
|
2441
|
+
"GDPR-ART32-003": {
|
|
2442
|
+
name: "Encryption in Transit",
|
|
2443
|
+
actions: buildEncryptionInTransitImpl(projectPath, hasSrc),
|
|
2444
|
+
warnings: ["Ensure your server/infrastructure is configured with TLS certificates."],
|
|
2445
|
+
},
|
|
2446
|
+
"GDPR-ART32-004": {
|
|
2447
|
+
name: "Unique User Identification",
|
|
2448
|
+
actions: buildUserIdentificationImpl(projectPath, hasSrc),
|
|
2449
|
+
warnings: ["Integrate the auth middleware into your routes."],
|
|
2450
|
+
},
|
|
2451
|
+
"GDPR-ART32-005": {
|
|
2452
|
+
name: "Automatic Session Timeout",
|
|
2453
|
+
actions: buildSessionTimeoutFix(projectPath),
|
|
2454
|
+
warnings: [],
|
|
2455
|
+
},
|
|
2456
|
+
"GDPR-ART32-006": {
|
|
2457
|
+
name: "Audit Logging",
|
|
2458
|
+
actions: buildLoggingFix(projectPath),
|
|
2459
|
+
warnings: ["Use auditLog() for all security-relevant actions."],
|
|
2460
|
+
},
|
|
2461
|
+
"GDPR-ART32-007": {
|
|
2462
|
+
name: "Integrity Controls",
|
|
2463
|
+
actions: buildIntegrityControlsImpl(projectPath, hasSrc),
|
|
2464
|
+
warnings: ["Apply integrity hashing to all critical data flows."],
|
|
2465
|
+
},
|
|
2466
|
+
"GDPR-ART32-008": {
|
|
2467
|
+
name: "Backup and Recovery",
|
|
2468
|
+
actions: buildBackupPolicyImpl(projectPath, hasSrc),
|
|
2469
|
+
warnings: ["Test your backup recovery process monthly."],
|
|
2470
|
+
},
|
|
2471
|
+
"GDPR-ART32-009": {
|
|
2472
|
+
name: "Regular Security Testing",
|
|
2473
|
+
actions: buildSecurityTestingImpl(projectPath),
|
|
2474
|
+
warnings: ["Schedule regular security scans in CI/CD."],
|
|
2475
|
+
},
|
|
2476
|
+
};
|
|
2477
|
+
const plan = controlMap[controlId];
|
|
2478
|
+
if (!plan) {
|
|
2479
|
+
resultText = `Control ${controlId} does not have an auto-implementation. Use \`fix_recommendation\` for manual guidance.\n\nAvailable auto-implementations: ${Object.keys(controlMap).join(", ")}`;
|
|
2480
|
+
break;
|
|
2481
|
+
}
|
|
2482
|
+
lines.push(`**Control**: ${plan.name}\n`);
|
|
2483
|
+
for (const action of plan.actions) {
|
|
2484
|
+
const result = applyAutoFixAction(projectPath, action);
|
|
2485
|
+
if (result.applied) {
|
|
2486
|
+
lines.push(`- ✓ [${action.type}] ${action.filePath}: ${action.description}`);
|
|
2487
|
+
}
|
|
2488
|
+
else if (result.error === "File already exists") {
|
|
2489
|
+
lines.push(`- → [${action.type}] ${action.filePath}: Already exists (skipped)`);
|
|
2490
|
+
}
|
|
2491
|
+
else {
|
|
2492
|
+
lines.push(`- ✗ [${action.type}] ${action.filePath}: ${result.error}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
const npmInstalls = getNpmInstallsFromActions(plan.actions);
|
|
2496
|
+
if (npmInstalls.length > 0) {
|
|
2497
|
+
lines.push(`\n## Install Dependencies\n`);
|
|
2498
|
+
lines.push("```bash");
|
|
2499
|
+
lines.push(`npm install ${npmInstalls.join(" ")}`);
|
|
2500
|
+
lines.push("```");
|
|
2501
|
+
}
|
|
2502
|
+
if (plan.warnings.length > 0) {
|
|
2503
|
+
lines.push(`\n## Notes`);
|
|
2504
|
+
for (const w of plan.warnings)
|
|
2505
|
+
lines.push(`- ${w}`);
|
|
2506
|
+
}
|
|
2507
|
+
lines.push(`\n## Next Steps`);
|
|
2508
|
+
lines.push("1. Install any npm packages listed above");
|
|
2509
|
+
lines.push("2. Import and integrate the generated files into your app");
|
|
2510
|
+
lines.push("3. Run `ges audit` to verify the control is now passing");
|
|
2511
|
+
lines.push(`4. Or use \`apply_control_override\` with control_id="${controlId}" if verified manually`);
|
|
2512
|
+
resultText = lines.join("\n");
|
|
2513
|
+
break;
|
|
2514
|
+
}
|
|
2515
|
+
default:
|
|
2516
|
+
return {
|
|
2517
|
+
jsonrpc: "2.0",
|
|
2518
|
+
id: request.id,
|
|
2519
|
+
error: { code: -32601, message: `Unknown tool: ${toolName}` },
|
|
2520
|
+
};
|
|
128
2521
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
case "generate_retention_policy": {
|
|
142
|
-
const name = args.project_name || "Project";
|
|
143
|
-
resultText = `# Data Retention Policy - ${name}\n\n## Retention Periods\n\n| Category | Period | Justification |\n|----------|--------|---------------|\n| User data | Account + 30 days | Contract |\n| Audit logs | 1 year | Legal obligation |\n| Session data | Session duration | Operational |\n\nReview quarterly and update as needed.`;
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
case "generate_incident_response": {
|
|
147
|
-
const name = args.project_name || "Project";
|
|
148
|
-
resultText = `# Incident Response Plan - ${name}\n\n## Severity Levels\n- P1 (Critical): 15 min response\n- P2 (High): 1 hour response\n- P3 (Medium): 4 hour response\n\n## Process\n1. Detection → 2. Assessment → 3. Containment → 4. Eradication → 5. Recovery → 6. Post-Incident\n\n## GDPR: Notify supervisory authority within 72 hours.`;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
case "generate_risk_assessment": {
|
|
152
|
-
const name = args.project_name || "Project";
|
|
153
|
-
resultText = `# Risk Assessment - ${name}\n\n| Risk | Likelihood | Impact | Mitigation |\n|------|-----------|--------|------------|\n| Data breach | Medium | Critical | Encryption, MFA, access controls |\n| Insider threat | Low | High | RBAC, audit logging |\n| Data loss | Low | Critical | Backups, DR plan |\n| Non-compliance | Medium | High | Regular audits |`;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
case "generate_dpa": {
|
|
157
|
-
const name = args.project_name || "Project";
|
|
158
|
-
resultText = `# Data Processing Agreement - ${name}\n\n## Parties\n- Controller: [Company Name]\n- Processor: [Service Provider]\n\n## Subject Matter\nProcessing of personal data as described in the attached schedule.\n\n## Duration\nEffective until termination of services.\n\n## Obligations\n- Process data only on documented instructions\n- Ensure confidentiality\n- Implement appropriate security (Article 32)\n- Assist with data subject rights\n- Assist with breach notification\n- Delete/return data on termination`;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
default:
|
|
162
|
-
return {
|
|
163
|
-
jsonrpc: "2.0",
|
|
164
|
-
id: request.id,
|
|
165
|
-
error: { code: -32601, message: `Unknown tool: ${toolName}` },
|
|
166
|
-
};
|
|
2522
|
+
}
|
|
2523
|
+
catch (err) {
|
|
2524
|
+
return {
|
|
2525
|
+
jsonrpc: "2.0",
|
|
2526
|
+
id: request.id,
|
|
2527
|
+
result: {
|
|
2528
|
+
content: [{
|
|
2529
|
+
type: "text",
|
|
2530
|
+
text: `Error executing tool '${toolName}': ${err instanceof Error ? err.message : String(err)}. Check your parameters and try again.`,
|
|
2531
|
+
}],
|
|
2532
|
+
},
|
|
2533
|
+
};
|
|
167
2534
|
}
|
|
168
2535
|
return {
|
|
169
2536
|
jsonrpc: "2.0",
|