@greenarmor/ges-mcp-server 0.6.0 → 0.6.2
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 +1200 -126
- package/dist/server.js +926 -127
- package/package.json +9 -9
package/bundle/server.js
CHANGED
|
@@ -326,12 +326,13 @@ function createArticle32Controls() {
|
|
|
326
326
|
article: "Article 32(1)(d)",
|
|
327
327
|
status: "not-implemented",
|
|
328
328
|
severity: "critical",
|
|
329
|
-
implementation_guidance: "Run dependency scans (Trivy, Dependabot). Perform secret scanning (Gitleaks). Use SAST (Semgrep). Schedule penetration tests.",
|
|
329
|
+
implementation_guidance: "Run dependency scans (Trivy, Dependabot). Perform secret scanning (Gitleaks). Use SAST (Semgrep). Schedule penetration tests. Generate SBOM for supply-chain visibility.",
|
|
330
330
|
checks: [
|
|
331
331
|
{ id: "GDPR-ART32-009-C1", description: "Dependency scanning in CI/CD", status: "not-implemented" },
|
|
332
332
|
{ id: "GDPR-ART32-009-C2", description: "Secret scanning in CI/CD", status: "not-implemented" },
|
|
333
333
|
{ id: "GDPR-ART32-009-C3", description: "SAST analysis integrated", status: "not-implemented" },
|
|
334
|
-
{ id: "GDPR-ART32-009-C4", description: "Penetration test schedule defined", status: "not-implemented" }
|
|
334
|
+
{ id: "GDPR-ART32-009-C4", description: "Penetration test schedule defined", status: "not-implemented" },
|
|
335
|
+
{ id: "GDPR-ART32-009-C5", description: "SBOM generated and scanned for vulnerabilities", status: "not-implemented" }
|
|
335
336
|
]
|
|
336
337
|
}
|
|
337
338
|
];
|
|
@@ -855,15 +856,18 @@ function createCISPolicyPack() {
|
|
|
855
856
|
{
|
|
856
857
|
id: "CIS-002",
|
|
857
858
|
name: "Inventory of Authorized and Unauthorized Software",
|
|
858
|
-
description: "Maintain a software inventory.",
|
|
859
|
+
description: "Maintain a software inventory via SBOM generation and scanning.",
|
|
859
860
|
category: "asset-management",
|
|
860
861
|
framework: "CIS",
|
|
861
862
|
status: "not-implemented",
|
|
862
863
|
severity: "high",
|
|
863
|
-
implementation_guidance: "
|
|
864
|
+
implementation_guidance: "Generate SBOM in CycloneDX or SPDX format using Syft or Trivy. Scan SBOM for vulnerabilities using Grype. Automate SBOM generation in CI/CD pipeline. Store SBOM artifacts alongside release artifacts.",
|
|
864
865
|
checks: [
|
|
865
866
|
{ id: "CIS-002-C1", description: "Software inventory (SBOM) maintained", status: "not-implemented" },
|
|
866
|
-
{ id: "CIS-002-C2", description: "Dependency scanning implemented", status: "not-implemented" }
|
|
867
|
+
{ id: "CIS-002-C2", description: "Dependency scanning implemented", status: "not-implemented" },
|
|
868
|
+
{ id: "CIS-002-C3", description: "SBOM generated in CycloneDX or SPDX format", status: "not-implemented" },
|
|
869
|
+
{ id: "CIS-002-C4", description: "SBOM vulnerability scanning configured", status: "not-implemented" },
|
|
870
|
+
{ id: "CIS-002-C5", description: "SBOM generation automated in CI/CD", status: "not-implemented" }
|
|
867
871
|
]
|
|
868
872
|
},
|
|
869
873
|
{
|
|
@@ -1015,6 +1019,21 @@ function createNISTPolicyPack() {
|
|
|
1015
1019
|
{ id: "NIST-RC-001-C2", description: "RTO and RPO defined", status: "not-implemented" },
|
|
1016
1020
|
{ id: "NIST-RC-001-C3", description: "Regular recovery tests", status: "not-implemented" }
|
|
1017
1021
|
]
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
id: "NIST-ID-002",
|
|
1025
|
+
name: "Supply Chain Risk Management",
|
|
1026
|
+
description: "Identify and manage supply chain risks through software Bill of Materials (SBOM).",
|
|
1027
|
+
category: "identify",
|
|
1028
|
+
framework: "NIST",
|
|
1029
|
+
status: "not-implemented",
|
|
1030
|
+
severity: "high",
|
|
1031
|
+
implementation_guidance: "Generate SBOM for all software components using Syft or Trivy. Scan SBOM for known vulnerabilities using Grype. Automate SBOM generation in CI/CD. Enforce SBOM-based policies for third-party dependencies.",
|
|
1032
|
+
checks: [
|
|
1033
|
+
{ id: "NIST-ID-002-C1", description: "SBOM generated for all dependencies", status: "not-implemented" },
|
|
1034
|
+
{ id: "NIST-ID-002-C2", description: "SBOM vulnerability scanning automated", status: "not-implemented" },
|
|
1035
|
+
{ id: "NIST-ID-002-C3", description: "Third-party dependency risk assessed", status: "not-implemented" }
|
|
1036
|
+
]
|
|
1018
1037
|
}
|
|
1019
1038
|
];
|
|
1020
1039
|
return {
|
|
@@ -6416,7 +6435,7 @@ import * as path2 from "node:path";
|
|
|
6416
6435
|
var __filename = url.fileURLToPath(import.meta.url);
|
|
6417
6436
|
var __dirname = path2.dirname(__filename);
|
|
6418
6437
|
var require2 = createRequire(import.meta.url);
|
|
6419
|
-
var pkg =
|
|
6438
|
+
var pkg = {"version":"0.6.2"};
|
|
6420
6439
|
var GESF_VERSION = pkg.version;
|
|
6421
6440
|
|
|
6422
6441
|
// src/server.ts
|
|
@@ -7115,12 +7134,156 @@ function applyAutoFixAction(root, action) {
|
|
|
7115
7134
|
}
|
|
7116
7135
|
}
|
|
7117
7136
|
function findMainAppFile(root) {
|
|
7118
|
-
const
|
|
7119
|
-
|
|
7137
|
+
const lang = detectProjectLanguage(root);
|
|
7138
|
+
const candidates = {
|
|
7139
|
+
typescript: ["src/index.ts", "src/app.ts", "src/server.ts", "src/main.ts", "index.ts", "app.ts", "server.ts"],
|
|
7140
|
+
javascript: ["src/index.js", "src/app.js", "src/server.js", "src/main.js", "index.js", "app.js", "server.js"],
|
|
7141
|
+
python: ["app.py", "main.py", "manage.py", "wsgi.py", "asgi.py", "src/app.py", "src/main.py"],
|
|
7142
|
+
ruby: ["config.ru", "app.rb", "server.rb", "main.rb", "config/application.rb"],
|
|
7143
|
+
go: ["main.go", "cmd/server/main.go", "cmd/app/main.go"],
|
|
7144
|
+
java: ["src/main/java/com/example/Application.java", "src/main/java/Application.java"],
|
|
7145
|
+
php: ["public/index.php", "index.php", "app.php", "app/Http/Kernel.php"],
|
|
7146
|
+
rust: ["src/main.rs", "src/bin/main.rs", "src/app.rs"],
|
|
7147
|
+
csharp: ["Program.cs", "Startup.cs"]
|
|
7148
|
+
};
|
|
7149
|
+
const exts = candidates[lang] || [];
|
|
7150
|
+
for (const c of exts) {
|
|
7120
7151
|
if (fs2.existsSync(path3.join(root, c))) return c;
|
|
7121
7152
|
}
|
|
7153
|
+
if (lang === "java") {
|
|
7154
|
+
const found = findFileRecursive(root, "Application.java", "src/main/java");
|
|
7155
|
+
if (found) return found;
|
|
7156
|
+
}
|
|
7157
|
+
if (lang === "go") {
|
|
7158
|
+
for (const c of ["cmd/server/main.go", "cmd/app/main.go", "main.go"]) {
|
|
7159
|
+
if (fs2.existsSync(path3.join(root, c))) return c;
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7122
7162
|
return null;
|
|
7123
7163
|
}
|
|
7164
|
+
function findFileRecursive(root, name, baseDir) {
|
|
7165
|
+
const dir = path3.join(root, baseDir);
|
|
7166
|
+
if (!fs2.existsSync(dir)) return null;
|
|
7167
|
+
try {
|
|
7168
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
7169
|
+
for (const e of entries) {
|
|
7170
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "venv" || e.name === "__pycache__" || e.name === ".git") continue;
|
|
7171
|
+
const childPath = path3.join(baseDir, e.name);
|
|
7172
|
+
if (e.isDirectory()) {
|
|
7173
|
+
const found = findFileRecursive(root, name, childPath);
|
|
7174
|
+
if (found) return found;
|
|
7175
|
+
} else if (e.name === name) {
|
|
7176
|
+
return childPath;
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
7179
|
+
} catch {
|
|
7180
|
+
}
|
|
7181
|
+
return null;
|
|
7182
|
+
}
|
|
7183
|
+
function detectProjectLanguage(root) {
|
|
7184
|
+
if (fs2.existsSync(path3.join(root, "go.mod"))) return "go";
|
|
7185
|
+
if (fs2.existsSync(path3.join(root, "Cargo.toml"))) return "rust";
|
|
7186
|
+
if (fs2.existsSync(path3.join(root, "requirements.txt")) || fs2.existsSync(path3.join(root, "pyproject.toml")) || fs2.existsSync(path3.join(root, "Pipfile")) || fs2.existsSync(path3.join(root, "setup.py"))) return "python";
|
|
7187
|
+
if (fs2.existsSync(path3.join(root, "go.mod"))) return "go";
|
|
7188
|
+
if (fs2.existsSync(path3.join(root, "pom.xml")) || fs2.existsSync(path3.join(root, "build.gradle")) || fs2.existsSync(path3.join(root, "build.gradle.kts"))) return "java";
|
|
7189
|
+
if (fs2.existsSync(path3.join(root, "Gemfile"))) return "ruby";
|
|
7190
|
+
if (fs2.existsSync(path3.join(root, "composer.json"))) return "php";
|
|
7191
|
+
const pkgContent = readFileSafe(path3.join(root, "package.json"));
|
|
7192
|
+
if (pkgContent) {
|
|
7193
|
+
try {
|
|
7194
|
+
const pkg2 = JSON.parse(pkgContent);
|
|
7195
|
+
const deps = { ...pkg2.dependencies, ...pkg2.devDependencies };
|
|
7196
|
+
if (deps.typescript || deps["@types/node"] || fs2.existsSync(path3.join(root, "tsconfig.json"))) return "typescript";
|
|
7197
|
+
return "javascript";
|
|
7198
|
+
} catch {
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
if (fs2.existsSync(path3.join(root, "tsconfig.json"))) return "typescript";
|
|
7202
|
+
return "javascript";
|
|
7203
|
+
}
|
|
7204
|
+
function detectWebFramework(root, lang) {
|
|
7205
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
7206
|
+
if (hasDep(root, "express")) return "express";
|
|
7207
|
+
if (hasDep(root, "fastify")) return "fastify";
|
|
7208
|
+
if (hasDep(root, "koa")) return "koa";
|
|
7209
|
+
if (hasDep(root, "hono")) return "hono";
|
|
7210
|
+
if (hasDep(root, "next")) return "next";
|
|
7211
|
+
if (hasDep(root, "@nestjs/core")) return "nestjs";
|
|
7212
|
+
if (hasDep(root, "@sveltejs/kit")) return "sveltekit";
|
|
7213
|
+
}
|
|
7214
|
+
if (lang === "python") {
|
|
7215
|
+
const reqFiles = ["requirements.txt", "pyproject.toml", "Pipfile"];
|
|
7216
|
+
for (const f of reqFiles) {
|
|
7217
|
+
const c = readFileSafe(path3.join(root, f));
|
|
7218
|
+
if (c) {
|
|
7219
|
+
if (/^\s*django\b/mi.test(c) || /django/i.test(c)) return "django";
|
|
7220
|
+
if (/^\s*flask\b/mi.test(c) || /flask/i.test(c)) return "flask";
|
|
7221
|
+
if (/^\s*fastapi\b/mi.test(c) || /fastapi/i.test(c)) return "fastapi";
|
|
7222
|
+
if (/^\s*sanic\b/mi.test(c) || /sanic/i.test(c)) return "sanic";
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
7225
|
+
const settingsPy = readFileSafe(path3.join(root, "settings.py")) || readFileSafe(path3.join(root, "app/settings.py")) || readFileSafe(path3.join(root, "config/settings.py"));
|
|
7226
|
+
if (settingsPy && /DJANGO_SETTINGS_MODULE|INSTALLED_APPS|django/.test(settingsPy)) return "django";
|
|
7227
|
+
const appPy = readFileSafe(path3.join(root, "app.py")) || readFileSafe(path3.join(root, "main.py"));
|
|
7228
|
+
if (appPy) {
|
|
7229
|
+
if (/from\s+flask\s+import|import\s+flask/.test(appPy)) return "flask";
|
|
7230
|
+
if (/from\s+fastapi\s+import|import\s+fastapi/.test(appPy)) return "fastapi";
|
|
7231
|
+
if (/from\s+django/.test(appPy)) return "django";
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
if (lang === "ruby") {
|
|
7235
|
+
const gemfile = readFileSafe(path3.join(root, "Gemfile"));
|
|
7236
|
+
if (gemfile) {
|
|
7237
|
+
if (/rails/i.test(gemfile)) return "rails";
|
|
7238
|
+
if (/sinatra/i.test(gemfile)) return "sinatra";
|
|
7239
|
+
}
|
|
7240
|
+
}
|
|
7241
|
+
if (lang === "go") {
|
|
7242
|
+
const goMod = readFileSafe(path3.join(root, "go.mod")) || "";
|
|
7243
|
+
const mainGo = readFileSafe(path3.join(root, "main.go")) || "";
|
|
7244
|
+
const allGo = goMod + mainGo;
|
|
7245
|
+
if (/gin-gonic|gin\.Default|gin\.New/.test(allGo)) return "gin";
|
|
7246
|
+
if (/fiber\.New/.test(allGo)) return "fiber";
|
|
7247
|
+
if (/echo\.New/.test(allGo)) return "echo";
|
|
7248
|
+
if (/chi\.NewRouter|chi\.Mux/.test(allGo)) return "chi";
|
|
7249
|
+
if (/mux\.NewRouter/.test(allGo)) return "gorilla";
|
|
7250
|
+
if (/http\.ListenAndServe|http\.HandleFunc/.test(allGo)) return "nethttp";
|
|
7251
|
+
}
|
|
7252
|
+
if (lang === "java") {
|
|
7253
|
+
const pom = readFileSafe(path3.join(root, "pom.xml")) || "";
|
|
7254
|
+
const gradle = readFileSafe(path3.join(root, "build.gradle")) || "";
|
|
7255
|
+
const all = pom + gradle;
|
|
7256
|
+
if (/spring-boot|springframework/.test(all)) return "spring";
|
|
7257
|
+
if (/ktor/.test(all)) return "ktor";
|
|
7258
|
+
if (/quarkus/.test(all)) return "quarkus";
|
|
7259
|
+
if (/micronaut/.test(all)) return "micronaut";
|
|
7260
|
+
}
|
|
7261
|
+
if (lang === "rust") {
|
|
7262
|
+
const cargo = readFileSafe(path3.join(root, "Cargo.toml")) || "";
|
|
7263
|
+
const mainRs = readFileSafe(path3.join(root, "src/main.rs")) || "";
|
|
7264
|
+
const libRs = readFileSafe(path3.join(root, "src/lib.rs")) || "";
|
|
7265
|
+
const all = cargo + mainRs + libRs;
|
|
7266
|
+
if (/actix-web|actix_web/.test(all)) return "actix";
|
|
7267
|
+
if (/axum/.test(all)) return "axum";
|
|
7268
|
+
if (/rocket/.test(all)) return "rocket";
|
|
7269
|
+
if (/warp/.test(all)) return "warp";
|
|
7270
|
+
}
|
|
7271
|
+
if (lang === "php") {
|
|
7272
|
+
const composer = readFileSafe(path3.join(root, "composer.json"));
|
|
7273
|
+
if (composer) {
|
|
7274
|
+
try {
|
|
7275
|
+
const pkg2 = JSON.parse(composer);
|
|
7276
|
+
const req = pkg2.require || {};
|
|
7277
|
+
if (req["laravel/framework"]) return "laravel";
|
|
7278
|
+
if (req["symfony/symfony"] || req["symfony/framework-bundle"]) return "symfony";
|
|
7279
|
+
if (req["slim/slim"]) return "slim";
|
|
7280
|
+
if (req["laravel/lumen-framework"]) return "lumen";
|
|
7281
|
+
} catch {
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
return "generic";
|
|
7286
|
+
}
|
|
7124
7287
|
function hasDep(root, dep) {
|
|
7125
7288
|
const pkg2 = readJsonFileSafe(path3.join(root, "package.json"));
|
|
7126
7289
|
if (!pkg2) return false;
|
|
@@ -7135,33 +7298,270 @@ function readFileSafe(filePath) {
|
|
|
7135
7298
|
}
|
|
7136
7299
|
}
|
|
7137
7300
|
function buildHelmetFix(root) {
|
|
7138
|
-
const
|
|
7139
|
-
|
|
7140
|
-
const actions = [
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7301
|
+
const lang = detectProjectLanguage(root);
|
|
7302
|
+
const fw = detectWebFramework(root, lang);
|
|
7303
|
+
const actions = [];
|
|
7304
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
7305
|
+
const appFile = findMainAppFile(root);
|
|
7306
|
+
if (!appFile) return [];
|
|
7307
|
+
if (fw === "express") {
|
|
7308
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install helmet", ruleId: "CONFIG-001" });
|
|
7309
|
+
const content = readFileSafe(path3.join(root, appFile));
|
|
7310
|
+
if (content && content.includes("const app = express()")) {
|
|
7311
|
+
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" });
|
|
7312
|
+
} else {
|
|
7313
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport helmet from 'helmet';\napp.use(helmet());\n", description: "Add helmet import and middleware", ruleId: "CONFIG-001" });
|
|
7314
|
+
}
|
|
7315
|
+
} else if (fw === "fastify") {
|
|
7316
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install @fastify/helmet", ruleId: "CONFIG-001" });
|
|
7317
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport helmet from '@fastify/helmet';\napp.register(helmet);\n", description: "Add Fastify helmet plugin", ruleId: "CONFIG-001" });
|
|
7318
|
+
} else if (fw === "koa") {
|
|
7319
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install koa-helmet", ruleId: "CONFIG-001" });
|
|
7320
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport helmet from 'koa-helmet';\napp.use(helmet());\n", description: "Add koa-helmet middleware", ruleId: "CONFIG-001" });
|
|
7321
|
+
} else if (fw === "hono") {
|
|
7322
|
+
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" });
|
|
7323
|
+
}
|
|
7324
|
+
} else if (lang === "python") {
|
|
7325
|
+
if (fw === "django") {
|
|
7326
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Python uses django-csp/secure", ruleId: "CONFIG-001" });
|
|
7327
|
+
const settingsFile = findFileRecursive(root, "settings.py", ".") || "settings.py";
|
|
7328
|
+
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" });
|
|
7329
|
+
} else if (fw === "flask" || fw === "fastapi" || fw === "sanic") {
|
|
7330
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
7331
|
+
actions.push({
|
|
7332
|
+
type: "append",
|
|
7333
|
+
filePath: appFile,
|
|
7334
|
+
content: fw === "fastapi" ? "\nfrom fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware\napp.add_middleware(HTTPSRedirectMiddleware)\n" : "\nfrom flask_talisman import Talisman\nTalisman(app, force_https=True, strict_transport_security=True, session_cookie_secure=True)\n",
|
|
7335
|
+
description: `Add security headers for ${fw}`,
|
|
7336
|
+
ruleId: "CONFIG-001"
|
|
7337
|
+
});
|
|
7338
|
+
}
|
|
7339
|
+
} else if (lang === "ruby") {
|
|
7340
|
+
if (fw === "rails") {
|
|
7341
|
+
const envFile = fs2.existsSync(path3.join(root, "config/environments/production.rb")) ? "config/environments/production.rb" : "config/application.rb";
|
|
7342
|
+
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" });
|
|
7343
|
+
}
|
|
7344
|
+
} else if (lang === "go") {
|
|
7345
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
7346
|
+
if (fw === "gin" || fw === "echo" || fw === "fiber" || fw === "chi" || fw === "nethttp") {
|
|
7347
|
+
actions.push({ type: "append", filePath: appFile, content: `
|
|
7348
|
+
import "net/http"
|
|
7349
|
+
|
|
7350
|
+
// Security headers middleware
|
|
7351
|
+
func securityHeaders(next http.Handler) http.Handler {
|
|
7352
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
7353
|
+
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
7354
|
+
w.Header().Set("X-Frame-Options", "DENY")
|
|
7355
|
+
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
|
7356
|
+
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
7357
|
+
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
7358
|
+
w.Header().Set("Content-Security-Policy", "default-src 'self'")
|
|
7359
|
+
next.ServeHTTP(w, r)
|
|
7360
|
+
})
|
|
7361
|
+
}
|
|
7362
|
+
`, description: "Add Go security headers middleware", ruleId: "CONFIG-001" });
|
|
7363
|
+
}
|
|
7364
|
+
} else if (lang === "java") {
|
|
7365
|
+
if (fw === "spring") {
|
|
7366
|
+
const hasSrc = fs2.existsSync(path3.join(root, "src/main/java"));
|
|
7367
|
+
const configPath = hasSrc ? "src/main/java/com/example/SecurityConfig.java" : "SecurityConfig.java";
|
|
7368
|
+
actions.push({ type: "create", filePath: configPath, content: `import org.springframework.context.annotation.Bean;
|
|
7369
|
+
import org.springframework.context.annotation.Configuration;
|
|
7370
|
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
7371
|
+
import org.springframework.security.web.SecurityFilterChain;
|
|
7372
|
+
import org.springframework.security.web.header.writers.StaticHeadersWriter;
|
|
7373
|
+
|
|
7374
|
+
@Configuration
|
|
7375
|
+
public class SecurityConfig {
|
|
7376
|
+
@Bean
|
|
7377
|
+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
7378
|
+
http.headers()
|
|
7379
|
+
.contentSecurityPolicy("default-src 'self'")
|
|
7380
|
+
.and()
|
|
7381
|
+
.xssProtection()
|
|
7382
|
+
.and()
|
|
7383
|
+
.frameOptions().deny()
|
|
7384
|
+
.httpStrictTransportSecurity()
|
|
7385
|
+
.includeSubDomains(true)
|
|
7386
|
+
.preload(true)
|
|
7387
|
+
.maxAgeInSeconds(31536000);
|
|
7388
|
+
return http.build();
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
`, description: "Create Spring Security config with headers", ruleId: "CONFIG-001" });
|
|
7392
|
+
}
|
|
7393
|
+
} else if (lang === "php") {
|
|
7394
|
+
if (fw === "laravel" || fw === "symfony") {
|
|
7395
|
+
const middleware = fw === "laravel" ? "app/Http/Middleware/SecurityHeaders.php" : "src/Middleware/SecurityHeadersMiddleware.php";
|
|
7396
|
+
const content = fw === "laravel" ? `<?php
|
|
7397
|
+
|
|
7398
|
+
namespace App\\Http\\Middleware;
|
|
7399
|
+
|
|
7400
|
+
use Closure;
|
|
7401
|
+
|
|
7402
|
+
class SecurityHeaders
|
|
7403
|
+
{
|
|
7404
|
+
public function handle($request, Closure $next)
|
|
7405
|
+
{
|
|
7406
|
+
$response = $next($request);
|
|
7407
|
+
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
|
7408
|
+
$response->headers->set('X-Frame-Options', 'DENY');
|
|
7409
|
+
$response->headers->set('X-XSS-Protection', '1; mode=block');
|
|
7410
|
+
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
7411
|
+
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
7412
|
+
return $response;
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
` : `<?php
|
|
7416
|
+
|
|
7417
|
+
namespace App\\Middleware;
|
|
7418
|
+
|
|
7419
|
+
use Symfony\\Component\\HttpFoundation\\Response;
|
|
7420
|
+
|
|
7421
|
+
class SecurityHeadersMiddleware
|
|
7422
|
+
{
|
|
7423
|
+
public function __invoke($request, $handler)
|
|
7424
|
+
{
|
|
7425
|
+
$response = $handler->handle($request);
|
|
7426
|
+
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
|
7427
|
+
$response->headers->set('X-Frame-Options', 'DENY');
|
|
7428
|
+
$response->headers->set('X-XSS-Protection', '1; mode=block');
|
|
7429
|
+
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
7430
|
+
return $response;
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
`;
|
|
7434
|
+
actions.push({ type: "create", filePath: middleware, content, description: `Create security headers middleware for ${fw}`, ruleId: "CONFIG-001" });
|
|
7435
|
+
}
|
|
7436
|
+
} else if (lang === "rust") {
|
|
7437
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
7438
|
+
if (fw === "actix") {
|
|
7439
|
+
actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: `use actix_web::{HttpResponse, dev::{ServiceRequest, Service, ServiceResponse}};
|
|
7440
|
+
|
|
7441
|
+
pub fn add_security_headers(res: &mut HttpResponse) {
|
|
7442
|
+
res.headers_mut().insert(("X-Content-Type-Options", "nosniff"));
|
|
7443
|
+
res.headers_mut().insert(("X-Frame-Options", "DENY"));
|
|
7444
|
+
res.headers_mut().insert(("X-XSS-Protection", "1; mode=block"));
|
|
7445
|
+
res.headers_mut().insert(("Strict-Transport-Security", "max-age=31536000; includeSubDomains"));
|
|
7446
|
+
res.headers_mut().insert(("Referrer-Policy", "strict-origin-when-cross-origin"));
|
|
7447
|
+
res.headers_mut().insert(("Content-Security-Policy", "default-src 'self'"));
|
|
7448
|
+
}
|
|
7449
|
+
`, description: "Create Actix-web security headers middleware", ruleId: "CONFIG-001" });
|
|
7450
|
+
} else if (fw === "axum") {
|
|
7451
|
+
actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: `use axum::{http::HeaderValue, response::Response};
|
|
7452
|
+
|
|
7453
|
+
pub async fn security_headers(mut res: Response) -> Response {
|
|
7454
|
+
let headers = res.headers_mut();
|
|
7455
|
+
headers.insert("X-Content-Type-Options", HeaderValue::from_static("nosniff"));
|
|
7456
|
+
headers.insert("X-Frame-Options", HeaderValue::from_static("DENY"));
|
|
7457
|
+
headers.insert("X-XSS-Protection", HeaderValue::from_static("1; mode=block"));
|
|
7458
|
+
headers.insert("Strict-Transport-Security", HeaderValue::from_static("max-age=31536000; includeSubDomains"));
|
|
7459
|
+
headers.insert("Referrer-Policy", HeaderValue::from_static("strict-origin-when-cross-origin"));
|
|
7460
|
+
headers.insert("Content-Security-Policy", HeaderValue::from_static("default-src 'self'"));
|
|
7461
|
+
res
|
|
7462
|
+
}
|
|
7463
|
+
`, description: "Create Axum security headers middleware", ruleId: "CONFIG-001" });
|
|
7464
|
+
} else {
|
|
7465
|
+
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" });
|
|
7466
|
+
}
|
|
7148
7467
|
}
|
|
7149
7468
|
return actions;
|
|
7150
7469
|
}
|
|
7151
7470
|
function buildCorsFix(root) {
|
|
7152
|
-
const
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7471
|
+
const lang = detectProjectLanguage(root);
|
|
7472
|
+
const fw = detectWebFramework(root, lang);
|
|
7473
|
+
const actions = [];
|
|
7474
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
7475
|
+
const appFile = findMainAppFile(root);
|
|
7476
|
+
if (!appFile) return [];
|
|
7477
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install cors", ruleId: "CONFIG-002" });
|
|
7478
|
+
if (fw === "fastify") {
|
|
7479
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install @fastify/cors", ruleId: "CONFIG-002" });
|
|
7480
|
+
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" });
|
|
7481
|
+
} else {
|
|
7482
|
+
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" });
|
|
7483
|
+
}
|
|
7484
|
+
} else if (lang === "python") {
|
|
7485
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
7486
|
+
if (fw === "django") {
|
|
7487
|
+
const settingsFile = findFileRecursive(root, "settings.py", ".") || "settings.py";
|
|
7488
|
+
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" });
|
|
7489
|
+
} else if (fw === "fastapi") {
|
|
7490
|
+
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" });
|
|
7491
|
+
} else if (fw === "flask") {
|
|
7492
|
+
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" });
|
|
7493
|
+
} else {
|
|
7494
|
+
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" });
|
|
7495
|
+
}
|
|
7496
|
+
} else if (lang === "ruby") {
|
|
7497
|
+
if (fw === "rails") {
|
|
7498
|
+
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" });
|
|
7499
|
+
}
|
|
7500
|
+
} else if (lang === "go") {
|
|
7501
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
7502
|
+
actions.push({ type: "append", filePath: appFile, content: '\nimport "net/http"\n\nfunc corsMiddleware(allowedOrigins []string, next http.Handler) http.Handler {\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n origin := r.Header.Get("Origin")\n for _, o := range allowedOrigins {\n if origin == o {\n w.Header().Set("Access-Control-Allow-Origin", origin)\n w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")\n w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")\n break\n }\n }\n if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent); return }\n next.ServeHTTP(w, r)\n })\n}\n', description: "Add Go CORS middleware", ruleId: "CONFIG-002" });
|
|
7503
|
+
} else if (lang === "java") {
|
|
7504
|
+
if (fw === "spring") {
|
|
7505
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/CorsConfig.java", content: `import org.springframework.context.annotation.Bean;
|
|
7506
|
+
import org.springframework.context.annotation.Configuration;
|
|
7507
|
+
import org.springframework.web.cors.CorsConfiguration;
|
|
7508
|
+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
7509
|
+
import org.springframework.web.filter.CorsFilter;
|
|
7510
|
+
|
|
7511
|
+
@Configuration
|
|
7512
|
+
public class CorsConfig {
|
|
7513
|
+
@Bean
|
|
7514
|
+
public CorsFilter corsFilter() {
|
|
7515
|
+
CorsConfiguration config = new CorsConfiguration();
|
|
7516
|
+
config.addAllowedOrigin("https://yourdomain.com");
|
|
7517
|
+
config.addAllowedHeader("*");
|
|
7518
|
+
config.addAllowedMethod("*");
|
|
7519
|
+
config.setAllowCredentials(true);
|
|
7520
|
+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
7521
|
+
source.registerCorsConfiguration("/**", config);
|
|
7522
|
+
return new CorsFilter(source);
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
`, description: "Create Spring CORS configuration", ruleId: "CONFIG-002" });
|
|
7526
|
+
}
|
|
7527
|
+
} else if (lang === "rust") {
|
|
7528
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
7529
|
+
if (fw === "actix") {
|
|
7530
|
+
actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: `use actix_cors::Cors;
|
|
7531
|
+
use actix_web::http::header;
|
|
7532
|
+
|
|
7533
|
+
pub fn cors_config() -> Cors {
|
|
7534
|
+
Cors::default()
|
|
7535
|
+
.allowed_origin("http://localhost:3000")
|
|
7536
|
+
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
|
|
7537
|
+
.allowed_headers(vec![header::CONTENT_TYPE, header::AUTHORIZATION])
|
|
7538
|
+
.max_age(3600)
|
|
7539
|
+
}
|
|
7540
|
+
`, description: "Create Actix-web CORS configuration", ruleId: "CONFIG-002" });
|
|
7541
|
+
} else if (fw === "axum") {
|
|
7542
|
+
actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: `use tower_http::cors::{CorsLayer, Any};
|
|
7543
|
+
use http::Method;
|
|
7544
|
+
|
|
7545
|
+
pub fn cors_layer() -> CorsLayer {
|
|
7546
|
+
CorsLayer::new()
|
|
7547
|
+
.allow_origin(["http://localhost:3000".parse().unwrap()])
|
|
7548
|
+
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
|
7549
|
+
.allow_headers(Any)
|
|
7550
|
+
}
|
|
7551
|
+
`, description: "Create Axum CORS layer", ruleId: "CONFIG-002" });
|
|
7552
|
+
} else {
|
|
7553
|
+
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" });
|
|
7554
|
+
}
|
|
7555
|
+
}
|
|
7556
|
+
return actions;
|
|
7158
7557
|
}
|
|
7159
7558
|
function buildEnvGitignoreFix(root) {
|
|
7160
7559
|
const gi = fs2.existsSync(path3.join(root, ".gitignore")) ? ".gitignore" : null;
|
|
7560
|
+
const envFiles = detectProjectLanguage(root) === "python" ? "\n.env\n.env.*\n!.env.example\n*.pyc\n__pycache__/\n" : detectProjectLanguage(root) === "go" ? "\n.env\n.env.*\n!.env.example\n*.exe\n" : detectProjectLanguage(root) === "ruby" ? "\n.env\n.env.*\n!.env.example\n*.gem\n" : detectProjectLanguage(root) === "java" ? "\n.env\n.env.*\n!.env.example\n*.class\ntarget/\n" : detectProjectLanguage(root) === "php" ? "\n.env\n.env.*\n!.env.example\nvendor/\n" : detectProjectLanguage(root) === "rust" ? "\n.env\n.env.*\n!.env.example\ntarget/\n*.key\n*.pem\n" : "\n.env\n.env.*\n!.env.example\n";
|
|
7161
7561
|
if (!gi) return buildGitignoreCreateFix(root);
|
|
7162
7562
|
const content = readFileSafe(path3.join(root, gi)) || "";
|
|
7163
7563
|
if (content.includes(".env")) return [];
|
|
7164
|
-
return [{ type: "append", filePath: ".gitignore", content:
|
|
7564
|
+
return [{ type: "append", filePath: ".gitignore", content: envFiles, description: "Add .env to .gitignore", ruleId: "CONFIG-004" }];
|
|
7165
7565
|
}
|
|
7166
7566
|
function buildDockerNonRootFix(root) {
|
|
7167
7567
|
if (!fs2.existsSync(path3.join(root, "Dockerfile"))) return [];
|
|
@@ -7171,7 +7571,19 @@ function buildTLSFix(root, f) {
|
|
|
7171
7571
|
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" }];
|
|
7172
7572
|
}
|
|
7173
7573
|
function buildGitignoreCreateFix(root) {
|
|
7174
|
-
|
|
7574
|
+
const lang = detectProjectLanguage(root);
|
|
7575
|
+
const templates = {
|
|
7576
|
+
typescript: "node_modules/\n.env\n.env.*\n!.env.example\ndist/\nbuild/\n*.key\n*.pem\ncoverage/\n.DS_Store\n",
|
|
7577
|
+
javascript: "node_modules/\n.env\n.env.*\n!.env.example\ndist/\nbuild/\n*.key\n*.pem\ncoverage/\n.DS_Store\n",
|
|
7578
|
+
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",
|
|
7579
|
+
ruby: ".env\n.env.*\n!.env.example\n*.key\n*.pem\nlog/\ntmp/\n*.gem\n.DS_Store\n",
|
|
7580
|
+
go: ".env\n.env.*\n!.env.example\n*.key\n*.pem\n*.exe\n/bin/\n.DS_Store\n",
|
|
7581
|
+
java: ".env\n.env.*\n!.env.example\n*.key\n*.pem\n*.class\ntarget/\n.idea/\n*.iml\n.DS_Store\n",
|
|
7582
|
+
php: ".env\n.env.*\n!.env.example\nvendor/\n*.key\n*.pem\n.DS_Store\n",
|
|
7583
|
+
rust: "target/\nCargo.lock\n.env\n.env.*\n!.env.example\n*.key\n*.pem\n.DS_Store\n",
|
|
7584
|
+
csharp: ".env\n.env.*\n!.env.example\nbin/\nobj/\n*.key\n*.pem\n.DS_Store\n"
|
|
7585
|
+
};
|
|
7586
|
+
return [{ type: "create", filePath: ".gitignore", content: templates[lang] || templates.javascript, description: `Create .gitignore for ${lang} project`, ruleId: "CONFIG-008" }];
|
|
7175
7587
|
}
|
|
7176
7588
|
function buildGitignoreEntryFix(root, f) {
|
|
7177
7589
|
const entry = f.fix.replace("Add ", "").replace(" to .gitignore.", "");
|
|
@@ -7181,12 +7593,13 @@ ${entry}
|
|
|
7181
7593
|
`, description: `Add ${entry} to .gitignore`, ruleId: "CONFIG-009" }];
|
|
7182
7594
|
}
|
|
7183
7595
|
function buildLoggingFix(root) {
|
|
7184
|
-
const
|
|
7185
|
-
const
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
{ type: "
|
|
7596
|
+
const lang = detectProjectLanguage(root);
|
|
7597
|
+
const actions = [];
|
|
7598
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
7599
|
+
const hasSrc = fs2.existsSync(path3.join(root, "src"));
|
|
7600
|
+
const loggerPath = hasSrc ? "src/lib/logger.ts" : "lib/logger.ts";
|
|
7601
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install pino logger", ruleId: "CONFIG-010" });
|
|
7602
|
+
actions.push({ type: "create", filePath: loggerPath, content: `import pino from 'pino';
|
|
7190
7603
|
|
|
7191
7604
|
const logger = pino({
|
|
7192
7605
|
level: process.env.LOG_LEVEL || 'info',
|
|
@@ -7206,12 +7619,171 @@ export function auditLog(params: AuditLogParams): void {
|
|
|
7206
7619
|
}
|
|
7207
7620
|
|
|
7208
7621
|
export default logger;
|
|
7209
|
-
`, description: "Create structured logger with audit logging", ruleId: "CONFIG-010" }
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7622
|
+
`, description: "Create structured logger with audit logging", ruleId: "CONFIG-010" });
|
|
7623
|
+
} else if (lang === "python") {
|
|
7624
|
+
actions.push({ type: "create", filePath: "lib/logger.py", content: `import logging
|
|
7625
|
+
import json
|
|
7626
|
+
from datetime import datetime
|
|
7627
|
+
|
|
7628
|
+
logger = logging.getLogger("audit")
|
|
7629
|
+
logger.setLevel(logging.INFO)
|
|
7630
|
+
|
|
7631
|
+
handler = logging.StreamHandler()
|
|
7632
|
+
handler.setFormatter(logging.Formatter('%(message)s'))
|
|
7633
|
+
logger.addHandler(handler)
|
|
7634
|
+
|
|
7635
|
+
def audit_log(user_id: str, action: str, resource: str, ip_address: str, **metadata):
|
|
7636
|
+
entry = {
|
|
7637
|
+
"userId": user_id,
|
|
7638
|
+
"action": action,
|
|
7639
|
+
"resource": resource,
|
|
7640
|
+
"ipAddress": ip_address,
|
|
7641
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
7642
|
+
"type": "audit",
|
|
7643
|
+
**metadata,
|
|
7644
|
+
}
|
|
7645
|
+
logger.info(json.dumps(entry))
|
|
7646
|
+
`, description: "Create Python audit logger", ruleId: "CONFIG-010" });
|
|
7647
|
+
} else if (lang === "ruby") {
|
|
7648
|
+
actions.push({ type: "create", filePath: "lib/audit_logger.rb", content: `require 'logger'
|
|
7649
|
+
require 'json'
|
|
7650
|
+
|
|
7651
|
+
class AuditLogger
|
|
7652
|
+
def initialize(logdev = $stdout)
|
|
7653
|
+
@logger = Logger.new(logdev)
|
|
7654
|
+
@logger.formatter = proc { |_, _, _, msg| msg }
|
|
7655
|
+
end
|
|
7656
|
+
|
|
7657
|
+
def audit_log(user_id:, action:, resource:, ip_address:, **metadata)
|
|
7658
|
+
entry = {
|
|
7659
|
+
userId: user_id,
|
|
7660
|
+
action: action,
|
|
7661
|
+
resource: resource,
|
|
7662
|
+
ipAddress: ip_address,
|
|
7663
|
+
timestamp: Time.now.utc.iso8601,
|
|
7664
|
+
type: 'audit',
|
|
7665
|
+
**metadata,
|
|
7666
|
+
}
|
|
7667
|
+
@logger.info(entry.to_json)
|
|
7668
|
+
end
|
|
7669
|
+
end
|
|
7670
|
+
|
|
7671
|
+
AUDIT = AuditLogger.new
|
|
7672
|
+
`, description: "Create Ruby audit logger", ruleId: "CONFIG-010" });
|
|
7673
|
+
} else if (lang === "go") {
|
|
7674
|
+
actions.push({ type: "create", filePath: "lib/audit.go", content: `package lib
|
|
7675
|
+
|
|
7676
|
+
import (
|
|
7677
|
+
"encoding/json"
|
|
7678
|
+
"log"
|
|
7679
|
+
"os"
|
|
7680
|
+
"time"
|
|
7681
|
+
)
|
|
7682
|
+
|
|
7683
|
+
type AuditEntry struct {
|
|
7684
|
+
UserID string "json:\\"userId\\""
|
|
7685
|
+
Action string "json:\\"action\\""
|
|
7686
|
+
Resource string "json:\\"resource\\""
|
|
7687
|
+
IPAddress string "json:\\"ipAddress\\""
|
|
7688
|
+
Timestamp string "json:\\"timestamp\\""
|
|
7689
|
+
Type string "json:\\"type\\""
|
|
7690
|
+
Metadata map[string]interface{} "json:\\"metadata,omitempty\\""
|
|
7691
|
+
}
|
|
7692
|
+
|
|
7693
|
+
var auditLogger = log.New(os.Stdout, "", 0)
|
|
7694
|
+
|
|
7695
|
+
func AuditLog(userID, action, resource, ipAddr string, metadata map[string]interface{}) {
|
|
7696
|
+
entry := AuditEntry{
|
|
7697
|
+
UserID: userID,
|
|
7698
|
+
Action: action,
|
|
7699
|
+
Resource: resource,
|
|
7700
|
+
IPAddress: ipAddr,
|
|
7701
|
+
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
7702
|
+
Type: "audit",
|
|
7703
|
+
Metadata: metadata,
|
|
7704
|
+
}
|
|
7705
|
+
data, _ := json.Marshal(entry)
|
|
7706
|
+
auditLogger.Println(string(data))
|
|
7707
|
+
}
|
|
7708
|
+
`, description: "Create Go audit logger", ruleId: "CONFIG-010" });
|
|
7709
|
+
} else if (lang === "java") {
|
|
7710
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/AuditLogger.java", content: `package com.example;
|
|
7711
|
+
|
|
7712
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
7713
|
+
import org.slf4j.Logger;
|
|
7714
|
+
import org.slf4j.LoggerFactory;
|
|
7715
|
+
import java.time.Instant;
|
|
7716
|
+
import java.util.Map;
|
|
7717
|
+
|
|
7718
|
+
public class AuditLogger {
|
|
7719
|
+
private static final Logger logger = LoggerFactory.getLogger("audit");
|
|
7720
|
+
private static final ObjectMapper mapper = new ObjectMapper();
|
|
7721
|
+
|
|
7722
|
+
public static void auditLog(String userId, String action, String resource, String ipAddress, Map<String, Object> metadata) {
|
|
7723
|
+
try {
|
|
7724
|
+
Map<String, Object> entry = Map.of(
|
|
7725
|
+
"userId", userId,
|
|
7726
|
+
"action", action,
|
|
7727
|
+
"resource", resource,
|
|
7728
|
+
"ipAddress", ipAddress,
|
|
7729
|
+
"timestamp", Instant.now().toString(),
|
|
7730
|
+
"type", "audit"
|
|
7731
|
+
);
|
|
7732
|
+
if (metadata != null) entry.putAll(metadata);
|
|
7733
|
+
logger.info(mapper.writeValueAsString(entry));
|
|
7734
|
+
} catch (Exception e) {
|
|
7735
|
+
logger.error("Audit log failed", e);
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
`, description: "Create Java audit logger", ruleId: "CONFIG-010" });
|
|
7740
|
+
} else if (lang === "php") {
|
|
7741
|
+
actions.push({ type: "create", filePath: "lib/audit_logger.php", content: `<?php
|
|
7742
|
+
|
|
7743
|
+
class AuditLogger
|
|
7744
|
+
{
|
|
7745
|
+
public static function log(string $userId, string $action, string $resource, string $ipAddress, array $metadata = []): void
|
|
7746
|
+
{
|
|
7747
|
+
$entry = array_merge([
|
|
7748
|
+
'userId' => $userId,
|
|
7749
|
+
'action' => $action,
|
|
7750
|
+
'resource' => $resource,
|
|
7751
|
+
'ipAddress' => $ipAddress,
|
|
7752
|
+
'timestamp' => gmdate('c'),
|
|
7753
|
+
'type' => 'audit',
|
|
7754
|
+
], $metadata);
|
|
7755
|
+
error_log(json_encode($entry));
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
`, description: "Create PHP audit logger", ruleId: "CONFIG-010" });
|
|
7759
|
+
} else if (lang === "rust") {
|
|
7760
|
+
actions.push({ type: "create", filePath: "src/logger.rs", content: `use serde_json::json;
|
|
7761
|
+
use tracing::{info, instrument};
|
|
7762
|
+
use chrono::Utc;
|
|
7763
|
+
|
|
7764
|
+
#[derive(Debug, serde::Serialize)]
|
|
7765
|
+
pub struct AuditEntry {
|
|
7766
|
+
pub user_id: String,
|
|
7767
|
+
pub action: String,
|
|
7768
|
+
pub resource: String,
|
|
7769
|
+
pub ip_address: String,
|
|
7770
|
+
pub timestamp: String,
|
|
7771
|
+
#[serde(rename = "type")]
|
|
7772
|
+
pub entry_type: String,
|
|
7773
|
+
}
|
|
7774
|
+
|
|
7775
|
+
pub fn audit_log(user_id: &str, action: &str, resource: &str, ip_address: &str) {
|
|
7776
|
+
let entry = AuditEntry {
|
|
7777
|
+
user_id: user_id.to_string(),
|
|
7778
|
+
action: action.to_string(),
|
|
7779
|
+
resource: resource.to_string(),
|
|
7780
|
+
ip_address: ip_address.to_string(),
|
|
7781
|
+
timestamp: Utc::now().to_rfc3339(),
|
|
7782
|
+
entry_type: "audit".to_string(),
|
|
7783
|
+
};
|
|
7784
|
+
info!("{}", serde_json::to_string(&entry).unwrap_or_default());
|
|
7785
|
+
}
|
|
7786
|
+
`, description: "Create Rust audit logger (tracing)", ruleId: "CONFIG-010" });
|
|
7215
7787
|
}
|
|
7216
7788
|
return actions;
|
|
7217
7789
|
}
|
|
@@ -7223,20 +7795,37 @@ function buildSecretsFix(root, f) {
|
|
|
7223
7795
|
const idx = (f.line || 1) - 1;
|
|
7224
7796
|
if (idx >= lines.length) return actions;
|
|
7225
7797
|
const line = lines[idx];
|
|
7798
|
+
const lang = detectProjectLanguage(root);
|
|
7226
7799
|
const match = line.match(/(\w+)\s*[:=]\s*['"]([^'"]+)['"]/);
|
|
7227
7800
|
if (match) {
|
|
7228
7801
|
const varName = match[1];
|
|
7229
7802
|
const value = match[2];
|
|
7230
|
-
|
|
7231
|
-
actions.push({ type: "append", filePath: envFile, content: `
|
|
7803
|
+
actions.push({ type: "append", filePath: ".env", content: `
|
|
7232
7804
|
${varName}=${value}
|
|
7233
7805
|
`, description: `Move ${varName} to .env`, ruleId: "SECRETS-001" });
|
|
7234
|
-
|
|
7806
|
+
let replacement;
|
|
7807
|
+
if (lang === "python") {
|
|
7808
|
+
replacement = line.replace(match[0], `${varName} = os.environ.get('${varName}')`);
|
|
7809
|
+
} else if (lang === "ruby") {
|
|
7810
|
+
replacement = line.replace(match[0], `${varName} = ENV['${varName}']`);
|
|
7811
|
+
} else if (lang === "go") {
|
|
7812
|
+
replacement = line.replace(match[0], `${varName} := os.Getenv("${varName}")`);
|
|
7813
|
+
} else if (lang === "java") {
|
|
7814
|
+
replacement = line.replace(match[0], `String ${varName} = System.getenv("${varName}")`);
|
|
7815
|
+
} else if (lang === "php") {
|
|
7816
|
+
replacement = line.replace(match[0], `$${varName} = getenv('${varName}')`);
|
|
7817
|
+
} else if (lang === "rust") {
|
|
7818
|
+
replacement = line.replace(match[0], `let ${varName} = std::env::var("${varName}").unwrap_or_default()`);
|
|
7819
|
+
} else {
|
|
7820
|
+
replacement = `${varName}: process.env.${varName}`;
|
|
7821
|
+
}
|
|
7822
|
+
actions.push({ type: "modify", filePath: f.file, search: line, replace: replacement, description: `Replace hardcoded ${varName} with env variable`, ruleId: "SECRETS-001" });
|
|
7235
7823
|
actions.push(...buildEnvGitignoreFix(root));
|
|
7236
7824
|
}
|
|
7237
7825
|
return actions;
|
|
7238
7826
|
}
|
|
7239
7827
|
function buildWeakHashFix(root, f) {
|
|
7828
|
+
const lang = detectProjectLanguage(root);
|
|
7240
7829
|
const content = readFileSafe(path3.join(root, f.file));
|
|
7241
7830
|
if (!content) return [];
|
|
7242
7831
|
const lines = content.split("\n");
|
|
@@ -7244,24 +7833,33 @@ function buildWeakHashFix(root, f) {
|
|
|
7244
7833
|
if (idx >= lines.length) return [];
|
|
7245
7834
|
const line = lines[idx];
|
|
7246
7835
|
let replacement = line;
|
|
7247
|
-
if (
|
|
7836
|
+
if (lang === "python") {
|
|
7837
|
+
replacement = line.replace(/hashlib\.md5\(/gi, "hashlib.sha256(").replace(/hashlib\.sha1\(/gi, "hashlib.sha256(");
|
|
7838
|
+
} else if (lang === "go") {
|
|
7839
|
+
replacement = line.replace(/md5\.New\(\)/gi, "sha256.New()").replace(/sha1\.New\(\)/gi, "sha256.New()");
|
|
7840
|
+
} else if (lang === "ruby") {
|
|
7841
|
+
replacement = line.replace(/Digest::MD5/gi, "Digest::SHA256").replace(/Digest::SHA1/gi, "Digest::SHA256");
|
|
7842
|
+
} else if (lang === "java") {
|
|
7843
|
+
replacement = line.replace(/MessageDigest\.getInstance\(["']MD5["']\)/gi, 'MessageDigest.getInstance("SHA-256")').replace(/MessageDigest\.getInstance\(["']SHA-1["']\)/gi, 'MessageDigest.getInstance("SHA-256")');
|
|
7844
|
+
} else if (lang === "php") {
|
|
7845
|
+
replacement = line.replace(/md5\(/gi, "hash('sha256', ").replace(/sha1\(/gi, "hash('sha256', ");
|
|
7846
|
+
} else if (lang === "rust") {
|
|
7847
|
+
replacement = line.replace(/md5::compute/gi, "sha2::Sha256::digest").replace(/use md5/gi, "use sha2::{Sha256, Digest}");
|
|
7848
|
+
} else {
|
|
7248
7849
|
replacement = line.replace(/createHash\s*\(\s*['"](?:md5|sha1)['"]\s*\)/, "createHash('sha256')");
|
|
7249
|
-
} else if (/hashlib\.md5\(/i.test(line)) {
|
|
7250
|
-
replacement = line.replace(/hashlib\.md5\(/gi, "hashlib.sha256(");
|
|
7251
|
-
} else if (/hashlib\.sha1\(/i.test(line)) {
|
|
7252
|
-
replacement = line.replace(/hashlib\.sha1\(/gi, "hashlib.sha256(");
|
|
7253
7850
|
}
|
|
7254
7851
|
if (replacement === line) return [];
|
|
7255
7852
|
return [{ type: "modify", filePath: f.file, search: line, replace: replacement, description: "Replace weak hash with SHA-256", ruleId: "CRYPTO-001" }];
|
|
7256
7853
|
}
|
|
7257
7854
|
function buildPasswordFix(root, _f) {
|
|
7258
|
-
const
|
|
7259
|
-
const
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7855
|
+
const lang = detectProjectLanguage(root);
|
|
7856
|
+
const actions = [];
|
|
7857
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
7858
|
+
const hasSrc = fs2.existsSync(path3.join(root, "src"));
|
|
7859
|
+
const authPath = hasSrc ? "src/lib/auth.ts" : "lib/auth.ts";
|
|
7860
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install argon2", ruleId: "CRYPTO-003" });
|
|
7861
|
+
if (!fs2.existsSync(path3.join(root, authPath))) {
|
|
7862
|
+
actions.push({ type: "create", filePath: authPath, content: `import argon2 from 'argon2';
|
|
7265
7863
|
|
|
7266
7864
|
export async function hashPassword(password: string): Promise<string> {
|
|
7267
7865
|
return argon2.hash(password, { type: argon2.argon2id });
|
|
@@ -7271,18 +7869,134 @@ export async function verifyPassword(hashedPassword: string, inputPassword: stri
|
|
|
7271
7869
|
return argon2.verify(hashedPassword, inputPassword);
|
|
7272
7870
|
}
|
|
7273
7871
|
`, description: "Create Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
7872
|
+
}
|
|
7873
|
+
} else if (lang === "python") {
|
|
7874
|
+
actions.push({ type: "create", filePath: "lib/auth.py", content: `import hashlib
|
|
7875
|
+
import os
|
|
7876
|
+
|
|
7877
|
+
def hash_password(password: str) -> str:
|
|
7878
|
+
salt = os.urandom(16)
|
|
7879
|
+
key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
|
|
7880
|
+
return salt.hex() + ':' + key.hex()
|
|
7881
|
+
|
|
7882
|
+
def verify_password(stored: str, provided: str) -> bool:
|
|
7883
|
+
salt_hex, key_hex = stored.split(':')
|
|
7884
|
+
salt = bytes.fromhex(salt_hex)
|
|
7885
|
+
new_key = hashlib.pbkdf2_hmac('sha256', provided.encode(), salt, 100000)
|
|
7886
|
+
return new_key.hex() == key_hex
|
|
7887
|
+
`, description: "Create Python password utility (PBKDF2-SHA256)", ruleId: "CRYPTO-003" });
|
|
7888
|
+
} else if (lang === "go") {
|
|
7889
|
+
actions.push({ type: "create", filePath: "lib/auth.go", content: `package lib
|
|
7890
|
+
|
|
7891
|
+
import (
|
|
7892
|
+
"crypto/rand"
|
|
7893
|
+
"crypto/subtle"
|
|
7894
|
+
"encoding/hex"
|
|
7895
|
+
"golang.org/x/crypto/argon2"
|
|
7896
|
+
)
|
|
7897
|
+
|
|
7898
|
+
func HashPassword(password string) (string, error) {
|
|
7899
|
+
salt := make([]byte, 16)
|
|
7900
|
+
if _, err := rand.Read(salt); err != nil {
|
|
7901
|
+
return "", err
|
|
7902
|
+
}
|
|
7903
|
+
hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
|
|
7904
|
+
return hex.EncodeToString(salt) + ":" + hex.EncodeToString(hash), nil
|
|
7905
|
+
}
|
|
7906
|
+
|
|
7907
|
+
func VerifyPassword(stored, provided string) (bool, error) {
|
|
7908
|
+
parts := strings.SplitN(stored, ":", 2)
|
|
7909
|
+
if len(parts) != 2 { return false, nil }
|
|
7910
|
+
salt, _ := hex.DecodeString(parts[0])
|
|
7911
|
+
storedHash, _ := hex.DecodeString(parts[1])
|
|
7912
|
+
providedHash := argon2.IDKey([]byte(provided), salt, 1, 64*1024, 4, 32)
|
|
7913
|
+
return subtle.ConstantTimeCompare(storedHash, providedHash) == 1, nil
|
|
7914
|
+
}
|
|
7915
|
+
`, description: "Create Go Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
7916
|
+
} else if (lang === "ruby") {
|
|
7917
|
+
actions.push({ type: "create", filePath: "lib/auth.rb", content: `require 'bcrypt'
|
|
7918
|
+
|
|
7919
|
+
def hash_password(password)
|
|
7920
|
+
BCrypt::Password.create(password)
|
|
7921
|
+
end
|
|
7922
|
+
|
|
7923
|
+
def verify_password(stored_hash, provided_password)
|
|
7924
|
+
BCrypt::Password.new(stored_hash) == provided_password
|
|
7925
|
+
end
|
|
7926
|
+
`, description: "Create Ruby BCrypt password utility", ruleId: "CRYPTO-003" });
|
|
7927
|
+
} else if (lang === "java") {
|
|
7928
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/PasswordUtil.java", content: `package com.example;
|
|
7929
|
+
|
|
7930
|
+
import javax.crypto.SecretKeyFactory;
|
|
7931
|
+
import javax.crypto.spec.PBEKeySpec;
|
|
7932
|
+
import java.security.SecureRandom;
|
|
7933
|
+
import java.util.Base64;
|
|
7934
|
+
|
|
7935
|
+
public class PasswordUtil {
|
|
7936
|
+
private static final int ITERATIONS = 100000;
|
|
7937
|
+
private static final int KEY_LENGTH = 256;
|
|
7938
|
+
private static final SecureRandom RANDOM = new SecureRandom();
|
|
7939
|
+
|
|
7940
|
+
public static String hashPassword(String password) throws Exception {
|
|
7941
|
+
byte[] salt = new byte[16];
|
|
7942
|
+
RANDOM.nextBytes(salt);
|
|
7943
|
+
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
|
|
7944
|
+
byte[] hash = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
|
7945
|
+
return Base64.getEncoder().encodeToString(salt) + ":" + Base64.getEncoder().encodeToString(hash);
|
|
7946
|
+
}
|
|
7947
|
+
|
|
7948
|
+
public static boolean verifyPassword(String stored, String provided) throws Exception {
|
|
7949
|
+
String[] parts = stored.split(":");
|
|
7950
|
+
byte[] salt = Base64.getDecoder().decode(parts[0]);
|
|
7951
|
+
byte[] storedHash = Base64.getDecoder().decode(parts[1]);
|
|
7952
|
+
PBEKeySpec spec = new PBEKeySpec(provided.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
|
|
7953
|
+
byte[] testHash = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
|
7954
|
+
return java.util.Arrays.equals(storedHash, testHash);
|
|
7955
|
+
}
|
|
7956
|
+
}
|
|
7957
|
+
`, description: "Create Java PBKDF2 password utility", ruleId: "CRYPTO-003" });
|
|
7958
|
+
} else if (lang === "php") {
|
|
7959
|
+
actions.push({ type: "create", filePath: "lib/auth.php", content: `<?php
|
|
7960
|
+
|
|
7961
|
+
function hash_password(string $password): string {
|
|
7962
|
+
return password_hash($password, PASSWORD_ARGON2ID);
|
|
7963
|
+
}
|
|
7964
|
+
|
|
7965
|
+
function verify_password(string $hash, string $password): bool {
|
|
7966
|
+
return password_verify($password, $hash);
|
|
7967
|
+
}
|
|
7968
|
+
`, description: "Create PHP Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
7969
|
+
} else if (lang === "rust") {
|
|
7970
|
+
actions.push({ type: "create", filePath: "src/auth.rs", content: `use argon2::{Argon2, Algorithm, Version, Params};
|
|
7971
|
+
use argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};
|
|
7972
|
+
use rand::rngs::OsRng;
|
|
7973
|
+
|
|
7974
|
+
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
|
7975
|
+
let salt = SaltString::generate(&mut OsRng);
|
|
7976
|
+
let params = Params::new(65536, 3, 4, Some(32))?;
|
|
7977
|
+
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
|
7978
|
+
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
|
|
7979
|
+
Ok(hash.to_string())
|
|
7980
|
+
}
|
|
7981
|
+
|
|
7982
|
+
pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
|
|
7983
|
+
let parsed = argon2::PasswordHash::new(hash)?;
|
|
7984
|
+
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())
|
|
7985
|
+
}
|
|
7986
|
+
`, description: "Create Rust Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
7274
7987
|
}
|
|
7275
7988
|
return actions;
|
|
7276
7989
|
}
|
|
7277
7990
|
function buildRateLimitFix(root) {
|
|
7278
|
-
const
|
|
7279
|
-
|
|
7280
|
-
const
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
return [
|
|
7284
|
-
|
|
7285
|
-
{ type: "
|
|
7991
|
+
const lang = detectProjectLanguage(root);
|
|
7992
|
+
const fw = detectWebFramework(root, lang);
|
|
7993
|
+
const actions = [];
|
|
7994
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
7995
|
+
const appFile = findMainAppFile(root);
|
|
7996
|
+
if (!appFile) return [];
|
|
7997
|
+
if (fw === "express") {
|
|
7998
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install express-rate-limit", ruleId: "AUTH-002" });
|
|
7999
|
+
actions.push({ type: "append", filePath: appFile, content: `
|
|
7286
8000
|
import rateLimit from 'express-rate-limit';
|
|
7287
8001
|
|
|
7288
8002
|
const limiter = rateLimit({
|
|
@@ -7292,29 +8006,91 @@ const limiter = rateLimit({
|
|
|
7292
8006
|
legacyHeaders: false,
|
|
7293
8007
|
});
|
|
7294
8008
|
app.use(limiter);
|
|
7295
|
-
`, description: "Add rate limiting (100 req/15min)", ruleId: "AUTH-002" }
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
{ type: "npm-install", filePath: "package.json", description: "Install @fastify/rate-limit", ruleId: "AUTH-002" },
|
|
7300
|
-
{ type: "append", filePath: appFile, content: `
|
|
8009
|
+
`, description: "Add rate limiting (100 req/15min)", ruleId: "AUTH-002" });
|
|
8010
|
+
} else if (fw === "fastify") {
|
|
8011
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install @fastify/rate-limit", ruleId: "AUTH-002" });
|
|
8012
|
+
actions.push({ type: "append", filePath: appFile, content: `
|
|
7301
8013
|
import rateLimit from '@fastify/rate-limit';
|
|
7302
8014
|
app.register(rateLimit, { max: 100, timeWindow: '15 minutes' });
|
|
7303
|
-
`, description: "Add rate limiting to Fastify", ruleId: "AUTH-002" }
|
|
7304
|
-
|
|
8015
|
+
`, description: "Add rate limiting to Fastify", ruleId: "AUTH-002" });
|
|
8016
|
+
}
|
|
8017
|
+
} else if (lang === "python") {
|
|
8018
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
8019
|
+
if (fw === "django") {
|
|
8020
|
+
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" });
|
|
8021
|
+
} else if (fw === "fastapi") {
|
|
8022
|
+
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" });
|
|
8023
|
+
} else if (fw === "flask") {
|
|
8024
|
+
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" });
|
|
8025
|
+
}
|
|
8026
|
+
} else if (lang === "ruby") {
|
|
8027
|
+
if (fw === "rails") {
|
|
8028
|
+
actions.push({ type: "append", filePath: "Gemfile", content: "\ngem 'rack-attack'\n", description: "Add rack-attack for rate limiting", ruleId: "AUTH-002" });
|
|
8029
|
+
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" });
|
|
8030
|
+
}
|
|
8031
|
+
} else if (lang === "go") {
|
|
8032
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
8033
|
+
actions.push({ type: "append", filePath: appFile, content: '\nimport (\n "net/http"\n "sync"\n "time"\n)\n\ntype rateLimiter struct {\n mu sync.Mutex\n visitors map[string][]time.Time\n limit int\n window time.Duration\n}\n\nfunc newRateLimiter(limit int, window time.Duration) *rateLimiter {\n return &rateLimiter{visitors: make(map[string][]time.Time), limit: limit, window: window}\n}\n\nfunc (rl *rateLimiter) allow(ip string) bool {\n rl.mu.Lock()\n defer rl.mu.Unlock()\n now := time.Now()\n windowStart := now.Add(-rl.window)\n var recent []time.Time\n for _, t := range rl.visitors[ip] {\n if t.After(windowStart) { recent = append(recent, t) }\n }\n rl.visitors[ip] = recent\n if len(recent) >= rl.limit { return false }\n rl.visitors[ip] = append(rl.visitors[ip], now)\n return true\n}\n\nvar limiter = newRateLimiter(100, 15*time.Minute)\n\nfunc rateLimitMiddleware(next http.Handler) http.Handler {\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n if !limiter.allow(r.RemoteAddr) {\n http.Error(w, "Too many requests", http.StatusTooManyRequests)\n return\n }\n next.ServeHTTP(w, r)\n })\n}\n', description: "Add Go rate limiter middleware", ruleId: "AUTH-002" });
|
|
8034
|
+
} else if (lang === "java") {
|
|
8035
|
+
if (fw === "spring") {
|
|
8036
|
+
actions.push({ type: "create", filePath: "src/main/java/com/example/RateLimitConfig.java", content: `package com.example;
|
|
8037
|
+
|
|
8038
|
+
import io.github.bucket4j.Bandwidth;
|
|
8039
|
+
import io.github.bucket4j.Bucket;
|
|
8040
|
+
import io.github.bucket4j.Refill;
|
|
8041
|
+
import org.springframework.stereotype.Component;
|
|
8042
|
+
import org.springframework.web.servlet.HandlerInterceptor;
|
|
8043
|
+
|
|
8044
|
+
import jakarta.servlet.http.HttpServletRequest;
|
|
8045
|
+
import jakarta.servlet.http.HttpServletResponse;
|
|
8046
|
+
import java.time.Duration;
|
|
8047
|
+
import java.util.Map;
|
|
8048
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
8049
|
+
|
|
8050
|
+
@Component
|
|
8051
|
+
public class RateLimitInterceptor implements HandlerInterceptor {
|
|
8052
|
+
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
|
8053
|
+
|
|
8054
|
+
private Bucket newBucket() {
|
|
8055
|
+
Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(15)));
|
|
8056
|
+
return Bucket.builder().addLimit(limit).build();
|
|
8057
|
+
}
|
|
8058
|
+
|
|
8059
|
+
@Override
|
|
8060
|
+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
|
8061
|
+
Bucket bucket = buckets.computeIfAbsent(request.getRemoteAddr(), k -> newBucket());
|
|
8062
|
+
if (bucket.tryConsume(1)) return true;
|
|
8063
|
+
response.setStatus(429);
|
|
8064
|
+
return false;
|
|
8065
|
+
}
|
|
8066
|
+
}
|
|
8067
|
+
`, description: "Create Spring rate limiter (bucket4j)", ruleId: "AUTH-002" });
|
|
8068
|
+
}
|
|
8069
|
+
} else if (lang === "php") {
|
|
8070
|
+
const appFile = findMainAppFile(root) || "public/index.php";
|
|
8071
|
+
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" });
|
|
8072
|
+
} else if (lang === "rust") {
|
|
8073
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
8074
|
+
if (fw === "actix") {
|
|
8075
|
+
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" });
|
|
8076
|
+
} else if (fw === "axum") {
|
|
8077
|
+
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" });
|
|
8078
|
+
} else {
|
|
8079
|
+
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" });
|
|
8080
|
+
}
|
|
7305
8081
|
}
|
|
7306
|
-
return
|
|
8082
|
+
return actions;
|
|
7307
8083
|
}
|
|
7308
8084
|
function buildSessionTimeoutFix(root) {
|
|
7309
|
-
const
|
|
7310
|
-
|
|
7311
|
-
const
|
|
7312
|
-
if (
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
8085
|
+
const lang = detectProjectLanguage(root);
|
|
8086
|
+
const fw = detectWebFramework(root, lang);
|
|
8087
|
+
const actions = [];
|
|
8088
|
+
if (lang === "typescript" || lang === "javascript") {
|
|
8089
|
+
const appFile = findMainAppFile(root);
|
|
8090
|
+
if (!appFile) return [];
|
|
8091
|
+
if (fw === "express") {
|
|
8092
|
+
actions.push({ type: "npm-install", filePath: "package.json", description: "Install express-session", ruleId: "AUTH-003" });
|
|
8093
|
+
actions.push({ type: "append", filePath: appFile, content: `
|
|
7318
8094
|
import session from 'express-session';
|
|
7319
8095
|
|
|
7320
8096
|
app.use(session({
|
|
@@ -7323,60 +8099,229 @@ app.use(session({
|
|
|
7323
8099
|
saveUninitialized: false,
|
|
7324
8100
|
cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 30 * 60 * 1000 },
|
|
7325
8101
|
}));
|
|
7326
|
-
`, description: "Add session with 30-min timeout", ruleId: "AUTH-003" }
|
|
7327
|
-
|
|
8102
|
+
`, description: "Add session with 30-min timeout", ruleId: "AUTH-003" });
|
|
8103
|
+
} else {
|
|
8104
|
+
actions.push({ type: "append", filePath: appFile, content: "\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n", description: "Add session timeout constant", ruleId: "AUTH-003" });
|
|
8105
|
+
}
|
|
8106
|
+
} else if (lang === "python") {
|
|
8107
|
+
if (fw === "django") {
|
|
8108
|
+
const settingsFile = findFileRecursive(root, "settings.py", ".") || "settings.py";
|
|
8109
|
+
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" });
|
|
8110
|
+
} else {
|
|
8111
|
+
const appFile = findMainAppFile(root) || "app.py";
|
|
8112
|
+
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" });
|
|
8113
|
+
}
|
|
8114
|
+
} else if (lang === "ruby") {
|
|
8115
|
+
if (fw === "rails") {
|
|
8116
|
+
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" });
|
|
8117
|
+
}
|
|
8118
|
+
} else if (lang === "go") {
|
|
8119
|
+
const appFile = findMainAppFile(root) || "main.go";
|
|
8120
|
+
actions.push({ type: "append", filePath: appFile, content: "\nconst sessionTimeout = 30 * time.Minute\n", description: "Add Go session timeout constant", ruleId: "AUTH-003" });
|
|
8121
|
+
} else if (lang === "java") {
|
|
8122
|
+
if (fw === "spring") {
|
|
8123
|
+
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" });
|
|
8124
|
+
}
|
|
8125
|
+
} else if (lang === "php") {
|
|
8126
|
+
if (fw === "laravel") {
|
|
8127
|
+
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" });
|
|
8128
|
+
} else {
|
|
8129
|
+
const appFile = findMainAppFile(root) || "public/index.php";
|
|
8130
|
+
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" });
|
|
8131
|
+
}
|
|
8132
|
+
} else if (lang === "rust") {
|
|
8133
|
+
const appFile = findMainAppFile(root) || "src/main.rs";
|
|
8134
|
+
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" });
|
|
8135
|
+
}
|
|
8136
|
+
return actions;
|
|
7328
8137
|
}
|
|
7329
8138
|
function buildCORSWildcardFix(root) {
|
|
8139
|
+
const lang = detectProjectLanguage(root);
|
|
7330
8140
|
const appFile = findMainAppFile(root);
|
|
7331
8141
|
if (!appFile) return [];
|
|
7332
8142
|
const content = readFileSafe(path3.join(root, appFile)) || "";
|
|
7333
8143
|
const actions = [];
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
8144
|
+
const wildcardPatterns = ["origin: '*'", "origin:'*'", 'origin:"*"', "Access-Control-Allow-Origin: *"];
|
|
8145
|
+
for (const pattern of wildcardPatterns) {
|
|
8146
|
+
if (!content.includes(pattern)) continue;
|
|
8147
|
+
if (lang === "python") {
|
|
8148
|
+
const replacement = pattern.includes("*'") || pattern.includes('*"') ? "origins=['http://localhost:3000']" : "origins=['http://localhost:3000']";
|
|
8149
|
+
actions.push({ type: "modify", filePath: appFile, search: pattern, replace: replacement, description: "Replace CORS wildcard", ruleId: "AUTH-004" });
|
|
8150
|
+
} else if (lang === "go") {
|
|
8151
|
+
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" });
|
|
8152
|
+
} else if (lang === "ruby") {
|
|
8153
|
+
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" });
|
|
8154
|
+
} else if (lang === "java") {
|
|
8155
|
+
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" });
|
|
8156
|
+
} else if (lang === "php") {
|
|
8157
|
+
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" });
|
|
8158
|
+
} else if (lang === "rust") {
|
|
8159
|
+
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" });
|
|
8160
|
+
} else {
|
|
8161
|
+
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" });
|
|
8162
|
+
}
|
|
7339
8163
|
}
|
|
7340
8164
|
return actions;
|
|
7341
8165
|
}
|
|
7342
8166
|
function buildTimestampsFix(root, f) {
|
|
7343
|
-
if (
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
8167
|
+
if (f.file.endsWith(".prisma")) {
|
|
8168
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
8169
|
+
if (!content) return [];
|
|
8170
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
8171
|
+
if (!modelMatch || modelMatch.length === 0) return [];
|
|
8172
|
+
const block = modelMatch[0];
|
|
8173
|
+
const closingBrace = block.lastIndexOf("}");
|
|
8174
|
+
if (closingBrace === -1) return [];
|
|
8175
|
+
const insertion = "\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt";
|
|
8176
|
+
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" }];
|
|
8177
|
+
}
|
|
8178
|
+
if (f.file.endsWith(".py")) {
|
|
8179
|
+
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" }];
|
|
8180
|
+
}
|
|
8181
|
+
if (f.file.endsWith(".rb")) {
|
|
8182
|
+
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" }];
|
|
8183
|
+
}
|
|
8184
|
+
if (f.file.endsWith(".go")) {
|
|
8185
|
+
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" }];
|
|
8186
|
+
}
|
|
8187
|
+
if (f.file.endsWith(".java")) {
|
|
8188
|
+
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" }];
|
|
8189
|
+
}
|
|
8190
|
+
if (f.file.endsWith(".php")) {
|
|
8191
|
+
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" }];
|
|
8192
|
+
}
|
|
8193
|
+
if (f.file.endsWith(".rs")) {
|
|
8194
|
+
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" }];
|
|
8195
|
+
}
|
|
8196
|
+
return [];
|
|
7353
8197
|
}
|
|
7354
8198
|
function buildSoftDeleteFix(root, f) {
|
|
7355
|
-
if (
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
8199
|
+
if (f.file.endsWith(".prisma")) {
|
|
8200
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
8201
|
+
if (!content) return [];
|
|
8202
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
8203
|
+
if (!modelMatch || modelMatch.length === 0) return [];
|
|
8204
|
+
const block = modelMatch[0];
|
|
8205
|
+
const closingBrace = block.lastIndexOf("}");
|
|
8206
|
+
if (closingBrace === -1) return [];
|
|
8207
|
+
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" }];
|
|
8208
|
+
}
|
|
8209
|
+
if (f.file.endsWith(".py")) {
|
|
8210
|
+
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" }];
|
|
8211
|
+
}
|
|
8212
|
+
if (f.file.endsWith(".go")) {
|
|
8213
|
+
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" }];
|
|
8214
|
+
}
|
|
8215
|
+
if (f.file.endsWith(".rs")) {
|
|
8216
|
+
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" }];
|
|
8217
|
+
}
|
|
8218
|
+
return [];
|
|
7364
8219
|
}
|
|
7365
8220
|
function buildUserAuditFix(root, f) {
|
|
7366
|
-
if (
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
8221
|
+
if (f.file.endsWith(".prisma")) {
|
|
8222
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
8223
|
+
if (!content) return [];
|
|
8224
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
8225
|
+
if (!modelMatch || modelMatch.length === 0) return [];
|
|
8226
|
+
const block = modelMatch[0];
|
|
8227
|
+
const closingBrace = block.lastIndexOf("}");
|
|
8228
|
+
if (closingBrace === -1) return [];
|
|
8229
|
+
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" }];
|
|
8230
|
+
}
|
|
8231
|
+
if (f.file.endsWith(".py")) {
|
|
8232
|
+
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" }];
|
|
8233
|
+
}
|
|
8234
|
+
if (f.file.endsWith(".rs")) {
|
|
8235
|
+
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" }];
|
|
8236
|
+
}
|
|
8237
|
+
return [];
|
|
7375
8238
|
}
|
|
7376
8239
|
function buildAuditModelFix(root) {
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
8240
|
+
if (fs2.existsSync(path3.join(root, "prisma/schema.prisma"))) {
|
|
8241
|
+
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" }];
|
|
8242
|
+
}
|
|
8243
|
+
const lang = detectProjectLanguage(root);
|
|
8244
|
+
if (lang === "python") {
|
|
8245
|
+
return [{ type: "create", filePath: "lib/models/audit.py", content: `from datetime import datetime
|
|
8246
|
+
from sqlalchemy import Column, Integer, String, DateTime, JSON
|
|
8247
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
8248
|
+
|
|
8249
|
+
Base = declarative_base()
|
|
8250
|
+
|
|
8251
|
+
class Audit(Base):
|
|
8252
|
+
__tablename__ = 'audit'
|
|
8253
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
8254
|
+
user_id = Column(String(255))
|
|
8255
|
+
action = Column(String(255))
|
|
8256
|
+
resource = Column(String(255))
|
|
8257
|
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
|
8258
|
+
ip_address = Column(String(45))
|
|
8259
|
+
metadata = Column(JSON)
|
|
8260
|
+
`, description: "Create Python Audit model (SQLAlchemy)", ruleId: "DB-004" }];
|
|
8261
|
+
}
|
|
8262
|
+
if (lang === "go") {
|
|
8263
|
+
return [{ type: "create", filePath: "lib/models/audit.go", content: `package models
|
|
8264
|
+
|
|
8265
|
+
import "time"
|
|
8266
|
+
|
|
8267
|
+
type Audit struct {
|
|
8268
|
+
ID uint \`json:"id" gorm:"primaryKey;autoIncrement"\`
|
|
8269
|
+
UserID string \`json:"userId"\`
|
|
8270
|
+
Action string \`json:"action"\`
|
|
8271
|
+
Resource string \`json:"resource"\`
|
|
8272
|
+
Timestamp time.Time \`json:"timestamp" gorm:"default:now()"\`
|
|
8273
|
+
IPAddress string \`json:"ipAddress"\`
|
|
8274
|
+
}
|
|
8275
|
+
`, description: "Create Go Audit model (GORM)", ruleId: "DB-004" }];
|
|
8276
|
+
}
|
|
8277
|
+
if (lang === "java") {
|
|
8278
|
+
return [{ type: "create", filePath: "src/main/java/com/example/Audit.java", content: `package com.example;
|
|
8279
|
+
|
|
8280
|
+
import jakarta.persistence.*;
|
|
8281
|
+
import java.time.Instant;
|
|
8282
|
+
|
|
8283
|
+
@Entity
|
|
8284
|
+
@Table(name = "audit")
|
|
8285
|
+
public class Audit {
|
|
8286
|
+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
8287
|
+
private Long id;
|
|
8288
|
+
private String userId;
|
|
8289
|
+
private String action;
|
|
8290
|
+
private String resource;
|
|
8291
|
+
private String ipAddress;
|
|
8292
|
+
@Column(columnDefinition = "jsonb")
|
|
8293
|
+
private String metadata;
|
|
8294
|
+
private Instant timestamp = Instant.now();
|
|
8295
|
+
}
|
|
8296
|
+
`, description: "Create Java Audit entity (JPA)", ruleId: "DB-004" }];
|
|
8297
|
+
}
|
|
8298
|
+
if (lang === "rust") {
|
|
8299
|
+
return [{ type: "create", filePath: "src/models/audit.rs", content: `use chrono::NaiveDateTime;
|
|
8300
|
+
|
|
8301
|
+
#[derive(Debug, Queryable, Serialize)]
|
|
8302
|
+
pub struct Audit {
|
|
8303
|
+
pub id: i32,
|
|
8304
|
+
pub user_id: String,
|
|
8305
|
+
pub action: String,
|
|
8306
|
+
pub resource: String,
|
|
8307
|
+
pub ip_address: String,
|
|
8308
|
+
pub timestamp: NaiveDateTime,
|
|
8309
|
+
}
|
|
8310
|
+
|
|
8311
|
+
// Diesel table definition:
|
|
8312
|
+
// table! {
|
|
8313
|
+
// audit (id) {
|
|
8314
|
+
// id -> Int4,
|
|
8315
|
+
// user_id -> Varchar,
|
|
8316
|
+
// action -> Varchar,
|
|
8317
|
+
// resource -> Varchar,
|
|
8318
|
+
// ip_address -> Varchar,
|
|
8319
|
+
// timestamp -> Timestamp,
|
|
8320
|
+
// }
|
|
8321
|
+
// }
|
|
8322
|
+
`, description: "Create Rust Audit model (Diesel)", ruleId: "DB-004" }];
|
|
8323
|
+
}
|
|
8324
|
+
return [];
|
|
7380
8325
|
}
|
|
7381
8326
|
function getNpmInstallsFromActions(actions) {
|
|
7382
8327
|
const installs = /* @__PURE__ */ new Set();
|
|
@@ -7395,6 +8340,36 @@ function getNpmInstallsFromActions(actions) {
|
|
|
7395
8340
|
return [...installs];
|
|
7396
8341
|
}
|
|
7397
8342
|
function buildEncryptionAtRestImpl(root, hasSrc) {
|
|
8343
|
+
const lang = detectProjectLanguage(root);
|
|
8344
|
+
if (lang === "rust") {
|
|
8345
|
+
return [
|
|
8346
|
+
{ type: "create", filePath: "src/encryption.rs", content: `use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
|
8347
|
+
use aes_gcm::aead::Aead;
|
|
8348
|
+
use rand::RngCore;
|
|
8349
|
+
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
|
8350
|
+
|
|
8351
|
+
pub fn encrypt(plaintext: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {
|
|
8352
|
+
let cipher = Aes256Gcm::new(key.into());
|
|
8353
|
+
let mut nonce_bytes = [0u8; 12];
|
|
8354
|
+
rand::thread_rng().fill_bytes(&mut nonce_bytes);
|
|
8355
|
+
let nonce = Nonce::from_slice(&nonce_bytes);
|
|
8356
|
+
let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())?;
|
|
8357
|
+
let mut combined = nonce_bytes.to_vec();
|
|
8358
|
+
combined.extend_from_slice(&ciphertext);
|
|
8359
|
+
Ok(BASE64.encode(&combined))
|
|
8360
|
+
}
|
|
8361
|
+
|
|
8362
|
+
pub fn decrypt(encoded: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {
|
|
8363
|
+
let combined = BASE64.decode(encoded).map_err(|_| aes_gcm::Error)?;
|
|
8364
|
+
let (nonce_bytes, ciphertext) = combined.split_at(12);
|
|
8365
|
+
let cipher = Aes256Gcm::new(key.into());
|
|
8366
|
+
let nonce = Nonce::from_slice(nonce_bytes);
|
|
8367
|
+
let plaintext = cipher.decrypt(nonce, ciphertext)?;
|
|
8368
|
+
String::from_utf8(plaintext).map_err(|_| aes_gcm::Error)
|
|
8369
|
+
}
|
|
8370
|
+
`, description: "Create Rust AES-256-GCM encryption utility", ruleId: "GDPR-ART32-002" }
|
|
8371
|
+
];
|
|
8372
|
+
}
|
|
7398
8373
|
const cryptoPath = hasSrc ? "src/lib/encryption.ts" : "lib/encryption.ts";
|
|
7399
8374
|
return [
|
|
7400
8375
|
{ type: "npm-install", filePath: "package.json", description: "Node.js crypto is built-in", ruleId: "GDPR-ART32-002" },
|
|
@@ -7433,14 +8408,45 @@ export function decrypt(ciphertext: string, secret: string): string {
|
|
|
7433
8408
|
];
|
|
7434
8409
|
}
|
|
7435
8410
|
function buildEncryptionInTransitImpl(root, _hasSrc) {
|
|
8411
|
+
const lang = detectProjectLanguage(root);
|
|
7436
8412
|
const appFile = findMainAppFile(root);
|
|
7437
8413
|
const actions = [];
|
|
8414
|
+
if (lang === "rust") {
|
|
8415
|
+
if (appFile) {
|
|
8416
|
+
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" });
|
|
8417
|
+
}
|
|
8418
|
+
return actions;
|
|
8419
|
+
}
|
|
7438
8420
|
if (appFile) {
|
|
7439
8421
|
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" });
|
|
7440
8422
|
}
|
|
7441
8423
|
return actions;
|
|
7442
8424
|
}
|
|
7443
8425
|
function buildUserIdentificationImpl(root, hasSrc) {
|
|
8426
|
+
const lang = detectProjectLanguage(root);
|
|
8427
|
+
if (lang === "rust") {
|
|
8428
|
+
const authPath2 = "src/auth.rs";
|
|
8429
|
+
if (fs2.existsSync(path3.join(root, authPath2))) return [];
|
|
8430
|
+
return [
|
|
8431
|
+
{ type: "create", filePath: authPath2, content: `use argon2::{Argon2, Algorithm, Version, Params};
|
|
8432
|
+
use argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};
|
|
8433
|
+
use rand::rngs::OsRng;
|
|
8434
|
+
|
|
8435
|
+
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
|
8436
|
+
let salt = SaltString::generate(&mut OsRng);
|
|
8437
|
+
let params = Params::new(65536, 3, 4, Some(32))?;
|
|
8438
|
+
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
|
8439
|
+
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
|
|
8440
|
+
Ok(hash.to_string())
|
|
8441
|
+
}
|
|
8442
|
+
|
|
8443
|
+
pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
|
|
8444
|
+
let parsed = argon2::PasswordHash::new(hash)?;
|
|
8445
|
+
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())
|
|
8446
|
+
}
|
|
8447
|
+
`, description: "Create Rust auth utility with Argon2id", ruleId: "GDPR-ART32-004" }
|
|
8448
|
+
];
|
|
8449
|
+
}
|
|
7444
8450
|
const authPath = hasSrc ? "src/lib/auth.ts" : "lib/auth.ts";
|
|
7445
8451
|
if (fs2.existsSync(path3.join(root, authPath))) return [];
|
|
7446
8452
|
return [
|
|
@@ -7458,6 +8464,29 @@ export async function verifyPassword(hashedPassword: string, inputPassword: stri
|
|
|
7458
8464
|
];
|
|
7459
8465
|
}
|
|
7460
8466
|
function buildIntegrityControlsImpl(root, hasSrc) {
|
|
8467
|
+
const lang = detectProjectLanguage(root);
|
|
8468
|
+
if (lang === "rust") {
|
|
8469
|
+
return [
|
|
8470
|
+
{ type: "create", filePath: "src/integrity.rs", content: `use sha2::{Sha256, Digest};
|
|
8471
|
+
|
|
8472
|
+
pub fn hash_data(data: &str) -> String {
|
|
8473
|
+
let mut hasher = Sha256::new();
|
|
8474
|
+
hasher.update(data.as_bytes());
|
|
8475
|
+
format!("{:x}", hasher.finalize())
|
|
8476
|
+
}
|
|
8477
|
+
|
|
8478
|
+
pub fn verify_integrity(data: &str, expected_hash: &str) -> bool {
|
|
8479
|
+
hash_data(data) == expected_hash
|
|
8480
|
+
}
|
|
8481
|
+
|
|
8482
|
+
pub fn generate_checksum(content: &[u8]) -> String {
|
|
8483
|
+
let mut hasher = Sha256::new();
|
|
8484
|
+
hasher.update(content);
|
|
8485
|
+
format!("{:x}", hasher.finalize())
|
|
8486
|
+
}
|
|
8487
|
+
`, description: "Create Rust integrity verification utility", ruleId: "GDPR-ART32-007" }
|
|
8488
|
+
];
|
|
8489
|
+
}
|
|
7461
8490
|
const integrityPath = hasSrc ? "src/lib/integrity.ts" : "lib/integrity.ts";
|
|
7462
8491
|
return [
|
|
7463
8492
|
{ type: "create", filePath: integrityPath, content: `import { createHash } from 'node:crypto';
|
|
@@ -7505,7 +8534,19 @@ echo "[$(date)] Cleaned up backups older than 30 days."
|
|
|
7505
8534
|
];
|
|
7506
8535
|
}
|
|
7507
8536
|
function buildSecurityTestingImpl(root) {
|
|
7508
|
-
const
|
|
8537
|
+
const lang = detectProjectLanguage(root);
|
|
8538
|
+
const setupSteps = lang === "rust" ? ` - uses: actions-rs/toolchain@v1
|
|
8539
|
+
with:
|
|
8540
|
+
toolchain: stable
|
|
8541
|
+
- run: cargo build
|
|
8542
|
+
- name: cargo audit
|
|
8543
|
+
run: cargo install cargo-audit && cargo audit` : ` - uses: actions/setup-node@v4
|
|
8544
|
+
with:
|
|
8545
|
+
node-version: '22'
|
|
8546
|
+
- run: npm ci
|
|
8547
|
+
- name: npm audit
|
|
8548
|
+
run: npm audit --audit-level=high
|
|
8549
|
+
continue-on-error: true`;
|
|
7509
8550
|
return [
|
|
7510
8551
|
{ type: "create", filePath: ".github/workflows/security-scan.yml", content: `name: Security Scan
|
|
7511
8552
|
on:
|
|
@@ -7521,16 +8562,49 @@ jobs:
|
|
|
7521
8562
|
runs-on: ubuntu-latest
|
|
7522
8563
|
steps:
|
|
7523
8564
|
- uses: actions/checkout@v4
|
|
7524
|
-
|
|
7525
|
-
with:
|
|
7526
|
-
node-version: '22'
|
|
7527
|
-
- run: npm ci
|
|
7528
|
-
- name: npm audit
|
|
7529
|
-
run: npm audit --audit-level=high
|
|
7530
|
-
continue-on-error: true
|
|
8565
|
+
${setupSteps}
|
|
7531
8566
|
- name: Run GESF compliance check
|
|
7532
8567
|
run: npx @greenarmor/ges audit --ci
|
|
7533
|
-
`, description: "Create security scanning GitHub Actions workflow", ruleId: "GDPR-ART32-009" }
|
|
8568
|
+
`, description: "Create security scanning GitHub Actions workflow", ruleId: "GDPR-ART32-009" },
|
|
8569
|
+
{ type: "create", filePath: ".github/workflows/sbom-scan.yml", content: `name: SBOM Generation & Scan
|
|
8570
|
+
on:
|
|
8571
|
+
push:
|
|
8572
|
+
branches: [main, master]
|
|
8573
|
+
pull_request:
|
|
8574
|
+
branches: [main, master]
|
|
8575
|
+
schedule:
|
|
8576
|
+
- cron: '0 6 * * 1'
|
|
8577
|
+
|
|
8578
|
+
jobs:
|
|
8579
|
+
sbom:
|
|
8580
|
+
runs-on: ubuntu-latest
|
|
8581
|
+
steps:
|
|
8582
|
+
- uses: actions/checkout@v4
|
|
8583
|
+
|
|
8584
|
+
- name: Generate SBOM with Syft
|
|
8585
|
+
uses: anchore/sbom-action@v0
|
|
8586
|
+
with:
|
|
8587
|
+
image: ""
|
|
8588
|
+
path: .
|
|
8589
|
+
format: cyclonedx-json
|
|
8590
|
+
output-file: sbom.json
|
|
8591
|
+
fail-build: false
|
|
8592
|
+
|
|
8593
|
+
- name: Scan SBOM for vulnerabilities with Grype
|
|
8594
|
+
uses: anchore/scan-action@v6
|
|
8595
|
+
with:
|
|
8596
|
+
sbom: sbom.json
|
|
8597
|
+
fail-build: true
|
|
8598
|
+
severity-cutoff: high
|
|
8599
|
+
|
|
8600
|
+
- name: Upload SBOM artifacts
|
|
8601
|
+
if: always()
|
|
8602
|
+
uses: actions/upload-artifact@v4
|
|
8603
|
+
with:
|
|
8604
|
+
name: sbom-artifacts
|
|
8605
|
+
path: sbom.json
|
|
8606
|
+
retention-days: 90
|
|
8607
|
+
`, description: "Create SBOM generation and scanning GitHub Actions workflow", ruleId: "GDPR-ART32-009" }
|
|
7534
8608
|
];
|
|
7535
8609
|
}
|
|
7536
8610
|
function generateDataInventory(projectName, projectType) {
|