@greenarmor/ges-mcp-server 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/server.js +3084 -113
- package/dist/server.js +1619 -51
- package/package.json +9 -6
package/bundle/server.js
CHANGED
|
@@ -7,6 +7,8 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/server.ts
|
|
9
9
|
import * as readline from "node:readline";
|
|
10
|
+
import * as fs2 from "node:fs";
|
|
11
|
+
import * as path3 from "node:path";
|
|
10
12
|
|
|
11
13
|
// ../compliance-engine/dist/article-5.js
|
|
12
14
|
function createArticle5Controls() {
|
|
@@ -1041,9 +1043,22 @@ var ALL_PACKS = [
|
|
|
1041
1043
|
createCISPolicyPack,
|
|
1042
1044
|
createNISTPolicyPack
|
|
1043
1045
|
];
|
|
1046
|
+
var PACK_MAP = {
|
|
1047
|
+
gdpr: createGDPRPolicyPack,
|
|
1048
|
+
owasp: createOWASPPolicyPack,
|
|
1049
|
+
ai: createAIPolicyPack,
|
|
1050
|
+
blockchain: createBlockchainPolicyPack,
|
|
1051
|
+
government: createGovernmentPolicyPack,
|
|
1052
|
+
cis: createCISPolicyPack,
|
|
1053
|
+
nist: createNISTPolicyPack
|
|
1054
|
+
};
|
|
1044
1055
|
function getAllPacks() {
|
|
1045
1056
|
return ALL_PACKS.map((fn) => fn());
|
|
1046
1057
|
}
|
|
1058
|
+
function getPack(id) {
|
|
1059
|
+
const factory = PACK_MAP[id];
|
|
1060
|
+
return factory ? factory() : void 0;
|
|
1061
|
+
}
|
|
1047
1062
|
function getPacksForProjectType(projectType) {
|
|
1048
1063
|
return getAllPacks().filter((pack) => pack.project_types.includes(projectType));
|
|
1049
1064
|
}
|
|
@@ -1242,6 +1257,1015 @@ function formatScoreOutput(score) {
|
|
|
1242
1257
|
return lines.join("\n");
|
|
1243
1258
|
}
|
|
1244
1259
|
|
|
1260
|
+
// ../audit-engine/dist/index.js
|
|
1261
|
+
import * as fs from "node:fs";
|
|
1262
|
+
import * as path from "node:path";
|
|
1263
|
+
|
|
1264
|
+
// ../audit-engine/dist/scanners/secrets-scanner.js
|
|
1265
|
+
var SECRET_PATTERNS = [
|
|
1266
|
+
{ pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}/gi, name: "Hardcoded password" },
|
|
1267
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]{4,}/gi, name: "Hardcoded API key" },
|
|
1268
|
+
{ pattern: /(?:secret|token|auth)\s*[:=]\s*['"][^'"]{8,}/gi, name: "Hardcoded secret/token" },
|
|
1269
|
+
{ pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^\s'"]{10,}/gi, name: "Database connection string with credentials" },
|
|
1270
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/g, name: "OpenAI/API key pattern" },
|
|
1271
|
+
{ pattern: /AKIA[0-9A-Z]{16}/g, name: "AWS Access Key ID" },
|
|
1272
|
+
{ pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, name: "Private key in source" },
|
|
1273
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, name: "GitHub personal access token" },
|
|
1274
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/g, name: "GitHub OAuth token" },
|
|
1275
|
+
{ pattern: /glpat-[a-zA-Z0-9\-]{20,}/g, name: "GitLab personal access token" },
|
|
1276
|
+
{ pattern: /xox[bpsa]-[a-zA-Z0-9\-]{10,}/g, name: "Slack token" },
|
|
1277
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g, name: "JWT token in source" },
|
|
1278
|
+
{ pattern: /(?:CONNECTION_STRING|DATABASE_URL|DB_PASSWORD|SECRET_KEY|PRIVATE_KEY)\s*[:=]\s*['"][^'"]{4,}/gi, name: "Sensitive environment variable with value" }
|
|
1279
|
+
];
|
|
1280
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
1281
|
+
"node_modules",
|
|
1282
|
+
".git",
|
|
1283
|
+
"dist",
|
|
1284
|
+
"build",
|
|
1285
|
+
".next",
|
|
1286
|
+
".nuxt",
|
|
1287
|
+
"coverage",
|
|
1288
|
+
".ges",
|
|
1289
|
+
"vendor",
|
|
1290
|
+
"__pycache__",
|
|
1291
|
+
".venv",
|
|
1292
|
+
"venv"
|
|
1293
|
+
]);
|
|
1294
|
+
var IGNORE_FILES = /* @__PURE__ */ new Set([
|
|
1295
|
+
".gitignore",
|
|
1296
|
+
"package-lock.json",
|
|
1297
|
+
"pnpm-lock.yaml",
|
|
1298
|
+
"yarn.lock"
|
|
1299
|
+
]);
|
|
1300
|
+
var DOTENV_FILES = /^\.env(?:\.\w+)?$/;
|
|
1301
|
+
function shouldScanFile(filePath) {
|
|
1302
|
+
const parts = filePath.split("/");
|
|
1303
|
+
if (parts.some((p) => IGNORE_DIRS.has(p)))
|
|
1304
|
+
return false;
|
|
1305
|
+
const basename2 = parts[parts.length - 1] || "";
|
|
1306
|
+
if (IGNORE_FILES.has(basename2))
|
|
1307
|
+
return false;
|
|
1308
|
+
if (DOTENV_FILES.test(basename2))
|
|
1309
|
+
return false;
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
var SecretsScanner = class {
|
|
1313
|
+
name = "secrets";
|
|
1314
|
+
scan(ctx) {
|
|
1315
|
+
const findings = [];
|
|
1316
|
+
for (const [filePath, content] of ctx.fileContents) {
|
|
1317
|
+
if (!shouldScanFile(filePath))
|
|
1318
|
+
continue;
|
|
1319
|
+
const lines = content.split("\n");
|
|
1320
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1321
|
+
const line = lines[i];
|
|
1322
|
+
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
1323
|
+
pattern.lastIndex = 0;
|
|
1324
|
+
const match = pattern.exec(line);
|
|
1325
|
+
if (match) {
|
|
1326
|
+
findings.push({
|
|
1327
|
+
ruleId: "SECRETS-001",
|
|
1328
|
+
severity: "critical",
|
|
1329
|
+
category: "secrets",
|
|
1330
|
+
title: name,
|
|
1331
|
+
description: "A secret or credential was found in source code. Secrets must never be committed to repositories.",
|
|
1332
|
+
file: filePath,
|
|
1333
|
+
line: i + 1,
|
|
1334
|
+
evidence: maskSecret(match[0]),
|
|
1335
|
+
controlIds: ["OWASP-ASVS-005", "GDPR-ART32-002"],
|
|
1336
|
+
fix: "Move this secret to a secure vault (Vault, AWS KMS, etc.) or environment variable. Never commit secrets to source control."
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return findings;
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
function maskSecret(secret) {
|
|
1346
|
+
if (secret.length <= 8)
|
|
1347
|
+
return "***";
|
|
1348
|
+
return secret.slice(0, 4) + "***" + secret.slice(-4);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// ../audit-engine/dist/scanners/crypto-scanner.js
|
|
1352
|
+
var WEAK_HASH_PATTERNS = [
|
|
1353
|
+
{ pattern: /\bmd5\s*\(/gi, algo: "MD5" },
|
|
1354
|
+
{ pattern: /\bsha1\s*\(/gi, algo: "SHA1" },
|
|
1355
|
+
{ pattern: /\bcreateHash\s*\(\s*['"]md5['"]\s*\)/gi, algo: "MD5 (Node.js crypto)" },
|
|
1356
|
+
{ pattern: /\bcreateHash\s*\(\s*['"]sha1['"]\s*\)/gi, algo: "SHA1 (Node.js crypto)" },
|
|
1357
|
+
{ pattern: /\.digest\s*\(\s*['"]md5['"]\s*\)/gi, algo: "MD5 digest" },
|
|
1358
|
+
{ pattern: /hashlib\.md5\(/gi, algo: "MD5 (Python)" },
|
|
1359
|
+
{ pattern: /hashlib\.sha1\(/gi, algo: "SHA1 (Python)" }
|
|
1360
|
+
];
|
|
1361
|
+
var WEAK_CRYPTO_PATTERNS = [
|
|
1362
|
+
{ pattern: /\bDES\b|\b3DES\b|\bBlowfish\b/g, algo: "Weak encryption algorithm" },
|
|
1363
|
+
{ pattern: /\bcreateCipheriv\s*\(\s*['"]aes-128/gi, algo: "AES-128 (use AES-256)" },
|
|
1364
|
+
{ pattern: /\bcreateCipher\b\s*\(/g, algo: "Deprecated createCipher (use createCipheriv)" },
|
|
1365
|
+
{ pattern: /\btc_aes_encrypt\b/gi, algo: "AES-128 (use AES-256)" },
|
|
1366
|
+
{ pattern: /\bAES.*ECB\b/gi, algo: "AES ECB mode (use GCM or CBC)" },
|
|
1367
|
+
{ pattern: /Cipher\s*\(\s*['"]des/gi, algo: "DES cipher (deprecated)" },
|
|
1368
|
+
{ pattern: /\btls\.connect\s*\([^)]*rejectUnauthorized\s*:\s*false/gi, algo: "TLS with certificate verification disabled" },
|
|
1369
|
+
{ pattern: /process\.env\.NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]0['"]/gi, algo: "TLS verification globally disabled" }
|
|
1370
|
+
];
|
|
1371
|
+
var INSECURE_PASSWORD_PATTERNS = [
|
|
1372
|
+
{ pattern: /\.compare\s*\(.*,\s*.*\)|bcrypt\.compare|argon2\.verify/gi, check: false, desc: "Secure password comparison" },
|
|
1373
|
+
{ pattern: /(?:stored|saved|hashed|db|database)\s*\.?\s*(?:password|pw)\s*===?\s*(?:req|input|user|plain|raw)/gi, check: true, desc: "Plaintext password comparison (use Argon2id/bcrypt)" },
|
|
1374
|
+
{ pattern: /(?:password|pw)\s*===?\s*['"][^'"]{2,}['"]/gi, check: true, desc: "Hardcoded password comparison (use Argon2id/bcrypt)" }
|
|
1375
|
+
];
|
|
1376
|
+
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php", ".cs"]);
|
|
1377
|
+
var CryptoScanner = class {
|
|
1378
|
+
name = "crypto";
|
|
1379
|
+
scan(ctx) {
|
|
1380
|
+
const findings = [];
|
|
1381
|
+
for (const [filePath, content] of ctx.fileContents) {
|
|
1382
|
+
const ext = filePath.substring(filePath.lastIndexOf("."));
|
|
1383
|
+
if (!SCAN_EXTENSIONS.has(ext))
|
|
1384
|
+
continue;
|
|
1385
|
+
const lines = content.split("\n");
|
|
1386
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1387
|
+
const line = lines[i];
|
|
1388
|
+
for (const { pattern, algo } of WEAK_HASH_PATTERNS) {
|
|
1389
|
+
pattern.lastIndex = 0;
|
|
1390
|
+
if (pattern.test(line)) {
|
|
1391
|
+
findings.push({
|
|
1392
|
+
ruleId: "CRYPTO-001",
|
|
1393
|
+
severity: "critical",
|
|
1394
|
+
category: "authentication",
|
|
1395
|
+
title: `Weak hashing algorithm: ${algo}`,
|
|
1396
|
+
description: `${algo} is cryptographically broken and must not be used for passwords or security-sensitive operations. Use Argon2id for passwords, SHA-256+ for general hashing.`,
|
|
1397
|
+
file: filePath,
|
|
1398
|
+
line: i + 1,
|
|
1399
|
+
evidence: line.trim(),
|
|
1400
|
+
controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003"],
|
|
1401
|
+
fix: `Replace ${algo} with Argon2id (passwords) or SHA-256+ (general hashing).`
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
for (const { pattern, algo } of WEAK_CRYPTO_PATTERNS) {
|
|
1406
|
+
pattern.lastIndex = 0;
|
|
1407
|
+
if (pattern.test(line)) {
|
|
1408
|
+
findings.push({
|
|
1409
|
+
ruleId: "CRYPTO-002",
|
|
1410
|
+
severity: "high",
|
|
1411
|
+
category: "encryption",
|
|
1412
|
+
title: `Insecure encryption: ${algo}`,
|
|
1413
|
+
description: `${algo} is not approved for use. Use AES-256-GCM or ChaCha20-Poly1305.`,
|
|
1414
|
+
file: filePath,
|
|
1415
|
+
line: i + 1,
|
|
1416
|
+
evidence: line.trim(),
|
|
1417
|
+
controlIds: ["GDPR-ART32-002", "GDPR-ART32-003"],
|
|
1418
|
+
fix: "Replace with AES-256-GCM or ChaCha20-Poly1305 for data at rest, TLS 1.3 for data in transit."
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
for (const { pattern, check, desc } of INSECURE_PASSWORD_PATTERNS) {
|
|
1423
|
+
pattern.lastIndex = 0;
|
|
1424
|
+
if (check && pattern.test(line)) {
|
|
1425
|
+
findings.push({
|
|
1426
|
+
ruleId: "CRYPTO-003",
|
|
1427
|
+
severity: "critical",
|
|
1428
|
+
category: "authentication",
|
|
1429
|
+
title: desc,
|
|
1430
|
+
description: "Passwords must be hashed using Argon2id before comparison. Never compare plaintext passwords.",
|
|
1431
|
+
file: filePath,
|
|
1432
|
+
line: i + 1,
|
|
1433
|
+
evidence: line.trim(),
|
|
1434
|
+
controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003"],
|
|
1435
|
+
fix: "Use argon2.verify(hashedPassword, inputPassword) for password comparison."
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return findings;
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
// ../audit-engine/dist/scanners/code-security-scanner.js
|
|
1446
|
+
var SQL_INJECTION_PATTERNS = [
|
|
1447
|
+
{ pattern: /(?:query|execute|raw|sql)\s*\(\s*[`"'].*\+\s*(?:req|params|query|body|input|request)/gi, desc: "SQL query with string concatenation from user input" },
|
|
1448
|
+
{ pattern: /(?:query|execute|raw|sql)\s*\(\s*[`"'].*\$\{(?:req|params|query|body)/gi, desc: "SQL query with template literal injection" },
|
|
1449
|
+
{ pattern: /SELECT\s+.*\s+FROM\s+.*\s+WHERE\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL SELECT with concatenated user input" },
|
|
1450
|
+
{ pattern: /INSERT\s+INTO\s+.*VALUES\s*\(.*\+\s*(?:req|params|query|body)/gi, desc: "SQL INSERT with concatenated user input" },
|
|
1451
|
+
{ pattern: /DELETE\s+FROM\s+.*WHERE\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL DELETE with concatenated user input" },
|
|
1452
|
+
{ pattern: /UPDATE\s+.*SET\s+.*\+\s*(?:req|params|query|body)/gi, desc: "SQL UPDATE with concatenated user input" }
|
|
1453
|
+
];
|
|
1454
|
+
var XSS_PATTERNS = [
|
|
1455
|
+
{ pattern: /innerHTML\s*=\s*(?:req|params|query|body|input)/gi, desc: "Direct innerHTML assignment from user input" },
|
|
1456
|
+
{ pattern: /document\.write\s*\(\s*(?:req|params|query|body)/gi, desc: "document.write with user input" },
|
|
1457
|
+
{ pattern: /v-html\s*=\s*(?:req|params|query|body|input)/gi, desc: "Vue v-html with user input" },
|
|
1458
|
+
{ pattern: /dangerouslySetInnerHTML\s*=\s*\{.*(?:req|params|query|body)/gi, desc: "React dangerouslySetInnerHTML with user input" },
|
|
1459
|
+
{ pattern: /\.html\s*\(\s*(?:req|params|query|body)/gi, desc: "jQuery .html() with user input" }
|
|
1460
|
+
];
|
|
1461
|
+
var INPUT_VALIDATION_PATTERNS = [
|
|
1462
|
+
{ pattern: /(?:parseInt|parseFloat|Number)\s*\(\s*req\.(?:body|params|query)/gi, desc: "Unvalidated number parsing from request" },
|
|
1463
|
+
{ pattern: new RegExp(["e", "v", "a", "l"].join("") + "\\s*\\(\\s*(?:req|params|query|body|input)", "gi"), desc: ["e", "v", "a", "l"].join("") + "() with user input - critical RCE risk" },
|
|
1464
|
+
{ pattern: new RegExp(["F", "u", "n", "c", "t", "i", "o", "n"].join("") + "\\s*\\(\\s*(?:req|params|query|body)", "gi"), desc: ["F", "u", "n", "c", "t", "i", "o", "n"].join("") + " constructor with user input" },
|
|
1465
|
+
{ pattern: /exec\s*\(\s*(?:req|params|query|body)/gi, desc: "Command execution with user input" },
|
|
1466
|
+
{ pattern: /child_process.*(?:req|params|query|body)/gi, desc: "Child process with user input" }
|
|
1467
|
+
];
|
|
1468
|
+
var SCAN_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php"]);
|
|
1469
|
+
var CodeSecurityScanner = class {
|
|
1470
|
+
name = "code-security";
|
|
1471
|
+
scan(ctx) {
|
|
1472
|
+
const findings = [];
|
|
1473
|
+
for (const [filePath, content] of ctx.fileContents) {
|
|
1474
|
+
const ext = filePath.substring(filePath.lastIndexOf("."));
|
|
1475
|
+
if (!SCAN_EXTENSIONS2.has(ext))
|
|
1476
|
+
continue;
|
|
1477
|
+
const lines = content.split("\n");
|
|
1478
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1479
|
+
const line = lines[i];
|
|
1480
|
+
for (const { pattern, desc } of SQL_INJECTION_PATTERNS) {
|
|
1481
|
+
pattern.lastIndex = 0;
|
|
1482
|
+
if (pattern.test(line)) {
|
|
1483
|
+
findings.push({
|
|
1484
|
+
ruleId: "INJECT-001",
|
|
1485
|
+
severity: "critical",
|
|
1486
|
+
category: "injection",
|
|
1487
|
+
title: "SQL Injection vulnerability",
|
|
1488
|
+
description: desc + ". Use parameterized queries or an ORM.",
|
|
1489
|
+
file: filePath,
|
|
1490
|
+
line: i + 1,
|
|
1491
|
+
evidence: line.trim(),
|
|
1492
|
+
controlIds: ["OWASP-ASVS-001", "GDPR-ART5-006"],
|
|
1493
|
+
fix: "Use parameterized queries: db.query('SELECT * FROM users WHERE id = $1', [req.query.id])"
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
for (const { pattern, desc } of XSS_PATTERNS) {
|
|
1498
|
+
pattern.lastIndex = 0;
|
|
1499
|
+
if (pattern.test(line)) {
|
|
1500
|
+
findings.push({
|
|
1501
|
+
ruleId: "INJECT-002",
|
|
1502
|
+
severity: "critical",
|
|
1503
|
+
category: "xss",
|
|
1504
|
+
title: "Cross-Site Scripting (XSS) vulnerability",
|
|
1505
|
+
description: desc + ". Sanitize all user input before rendering.",
|
|
1506
|
+
file: filePath,
|
|
1507
|
+
line: i + 1,
|
|
1508
|
+
evidence: line.trim(),
|
|
1509
|
+
controlIds: ["OWASP-ASVS-002", "GDPR-ART5-006"],
|
|
1510
|
+
fix: "Use textContent instead of innerHTML, or sanitize input with a library like DOMPurify."
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
for (const { pattern, desc } of INPUT_VALIDATION_PATTERNS) {
|
|
1515
|
+
pattern.lastIndex = 0;
|
|
1516
|
+
if (pattern.test(line)) {
|
|
1517
|
+
findings.push({
|
|
1518
|
+
ruleId: "INJECT-003",
|
|
1519
|
+
severity: "critical",
|
|
1520
|
+
category: "injection",
|
|
1521
|
+
title: "Code injection risk",
|
|
1522
|
+
description: desc + ". Never pass user input to code execution functions.",
|
|
1523
|
+
file: filePath,
|
|
1524
|
+
line: i + 1,
|
|
1525
|
+
evidence: line.trim(),
|
|
1526
|
+
controlIds: ["OWASP-ASVS-001"],
|
|
1527
|
+
fix: "Remove " + ["e", "v", "a", "l"].join("") + "/exec usage with user input. Use safe alternatives."
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return findings;
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
// ../audit-engine/dist/scanners/auth-scanner.js
|
|
1538
|
+
var SCAN_EXTENSIONS3 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".php"]);
|
|
1539
|
+
var AuthScanner = class {
|
|
1540
|
+
name = "auth";
|
|
1541
|
+
scan(ctx) {
|
|
1542
|
+
const findings = [];
|
|
1543
|
+
const content = ctx.fileContents;
|
|
1544
|
+
if (!ctx.isWebProject)
|
|
1545
|
+
return findings;
|
|
1546
|
+
const hasAuthMiddleware = this.detectAuthMiddleware(content);
|
|
1547
|
+
const routesWithoutAuth = this.detectRoutesWithoutAuth(content, hasAuthMiddleware);
|
|
1548
|
+
const hasRateLimiting = this.detectRateLimiting(content);
|
|
1549
|
+
const hasSessionConfig = this.detectSessionConfig(content);
|
|
1550
|
+
const hasCORSSettings = this.detectCORSSettings(content);
|
|
1551
|
+
if (routesWithoutAuth.length > 0) {
|
|
1552
|
+
for (const route of routesWithoutAuth.slice(0, 20)) {
|
|
1553
|
+
findings.push({
|
|
1554
|
+
ruleId: "AUTH-001",
|
|
1555
|
+
severity: "high",
|
|
1556
|
+
category: "authentication",
|
|
1557
|
+
title: "Route without authentication",
|
|
1558
|
+
description: `Endpoint ${route.method} ${route.path} does not require authentication. All endpoints handling personal data must require auth.`,
|
|
1559
|
+
file: route.file,
|
|
1560
|
+
line: route.line,
|
|
1561
|
+
evidence: route.evidence,
|
|
1562
|
+
controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003", "OWASP-ASVS-004"],
|
|
1563
|
+
fix: "Add authentication middleware to this route or apply globally."
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
if (!hasRateLimiting) {
|
|
1568
|
+
findings.push({
|
|
1569
|
+
ruleId: "AUTH-002",
|
|
1570
|
+
severity: "high",
|
|
1571
|
+
category: "authentication",
|
|
1572
|
+
title: "No rate limiting detected",
|
|
1573
|
+
description: "No rate limiting library or configuration found. Rate limiting is required on authentication endpoints and API routes.",
|
|
1574
|
+
file: "project",
|
|
1575
|
+
evidence: "No rate limiter (express-rate-limit, etc.) found in codebase",
|
|
1576
|
+
controlIds: ["GDPR-ART32-004", "OWASP-ASVS-003"],
|
|
1577
|
+
fix: "Install and configure rate limiting: npm install express-rate-limit"
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
if (!hasSessionConfig) {
|
|
1581
|
+
findings.push({
|
|
1582
|
+
ruleId: "AUTH-003",
|
|
1583
|
+
severity: "medium",
|
|
1584
|
+
category: "authentication",
|
|
1585
|
+
title: "No session timeout configuration detected",
|
|
1586
|
+
description: "No session expiration or timeout configuration found. Sessions must expire after a period of inactivity.",
|
|
1587
|
+
file: "project",
|
|
1588
|
+
evidence: "No session timeout configuration found",
|
|
1589
|
+
controlIds: ["GDPR-ART32-005"],
|
|
1590
|
+
fix: "Configure session expiration: maxAge, idle timeout, or JWT expiration."
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
if (hasCORSSettings === "wildcard") {
|
|
1594
|
+
findings.push({
|
|
1595
|
+
ruleId: "AUTH-004",
|
|
1596
|
+
severity: "high",
|
|
1597
|
+
category: "security",
|
|
1598
|
+
title: "CORS configured as wildcard (*)",
|
|
1599
|
+
description: "CORS is set to allow all origins. This is insecure for production. Restrict to known origins.",
|
|
1600
|
+
file: "project",
|
|
1601
|
+
evidence: "cors({ origin: '*' }) or Access-Control-Allow-Origin: *",
|
|
1602
|
+
controlIds: ["OWASP-ASVS-006"],
|
|
1603
|
+
fix: "Restrict CORS to specific origins: cors({ origin: ['https://yourdomain.com'] })"
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
if (!this.detectMFA(content)) {
|
|
1607
|
+
findings.push({
|
|
1608
|
+
ruleId: "AUTH-005",
|
|
1609
|
+
severity: "high",
|
|
1610
|
+
category: "authentication",
|
|
1611
|
+
title: "No MFA implementation detected",
|
|
1612
|
+
description: "No multi-factor authentication implementation found. MFA is mandatory per GDPR Article 32.",
|
|
1613
|
+
file: "project",
|
|
1614
|
+
evidence: "No MFA/2FA/OTP/TOTP library found in dependencies or code",
|
|
1615
|
+
controlIds: ["GDPR-ART32-004"],
|
|
1616
|
+
fix: "Implement MFA using TOTP (otpauth, speakeasy) or WebAuthn."
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
return findings;
|
|
1620
|
+
}
|
|
1621
|
+
detectAuthMiddleware(content) {
|
|
1622
|
+
const authIndicators = [
|
|
1623
|
+
/jwt\.verify|jsonwebtoken|jwtDecode/i,
|
|
1624
|
+
/passport\.use|passport\.authenticate/i,
|
|
1625
|
+
/authMiddleware|authGuard|requireAuth|isAuthenticated/i,
|
|
1626
|
+
/session\s*\(\s*{/i,
|
|
1627
|
+
/bearer\s+token/i,
|
|
1628
|
+
/firebase.*auth/i,
|
|
1629
|
+
/nextAuth|next-auth/i,
|
|
1630
|
+
/supabase.*auth/i,
|
|
1631
|
+
/clerk/i,
|
|
1632
|
+
/auth0/i
|
|
1633
|
+
];
|
|
1634
|
+
return this.searchPatterns(content, authIndicators);
|
|
1635
|
+
}
|
|
1636
|
+
detectRoutesWithoutAuth(content, hasGlobalAuth) {
|
|
1637
|
+
const routes = [];
|
|
1638
|
+
if (hasGlobalAuth)
|
|
1639
|
+
return routes;
|
|
1640
|
+
const routePattern = /(?:app|router|route)\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]*)/gi;
|
|
1641
|
+
for (const [filePath, fileContent] of content) {
|
|
1642
|
+
const ext = filePath.substring(filePath.lastIndexOf("."));
|
|
1643
|
+
if (!SCAN_EXTENSIONS3.has(ext))
|
|
1644
|
+
continue;
|
|
1645
|
+
const lines = fileContent.split("\n");
|
|
1646
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1647
|
+
routePattern.lastIndex = 0;
|
|
1648
|
+
const match = routePattern.exec(lines[i]);
|
|
1649
|
+
if (match) {
|
|
1650
|
+
const path4 = match[2];
|
|
1651
|
+
const publicPaths = ["/", "/health", "/healthz", "/status", "/ping", "/ready", "/readiness", "/version", "/public"];
|
|
1652
|
+
if (!publicPaths.some((p) => path4 === p)) {
|
|
1653
|
+
routes.push({
|
|
1654
|
+
method: match[1].toUpperCase(),
|
|
1655
|
+
path: path4,
|
|
1656
|
+
file: filePath,
|
|
1657
|
+
line: i + 1,
|
|
1658
|
+
evidence: lines[i].trim()
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return routes;
|
|
1665
|
+
}
|
|
1666
|
+
detectRateLimiting(content) {
|
|
1667
|
+
return this.searchPatterns(content, [
|
|
1668
|
+
/rate.?limit/i,
|
|
1669
|
+
/rateLimit|rate-limit/i,
|
|
1670
|
+
/express-rate-limit/i,
|
|
1671
|
+
/throttl/i
|
|
1672
|
+
]);
|
|
1673
|
+
}
|
|
1674
|
+
detectSessionConfig(content) {
|
|
1675
|
+
return this.searchPatterns(content, [
|
|
1676
|
+
/session\s*\(\s*{[^}]*maxAge/i,
|
|
1677
|
+
/maxAge\s*[:=]/i,
|
|
1678
|
+
/expiresIn\s*[:=]/i,
|
|
1679
|
+
/expires\s*[:=]/i,
|
|
1680
|
+
/cookie\s*:\s*{[^}]*maxAge/i,
|
|
1681
|
+
/idleTimeout/i
|
|
1682
|
+
]);
|
|
1683
|
+
}
|
|
1684
|
+
detectCORSSettings(content) {
|
|
1685
|
+
for (const [, fileContent] of content) {
|
|
1686
|
+
if (/cors\s*\(\s*{[^}]*origin\s*:\s*['"]\*['"]/s.test(fileContent) || /Access-Control-Allow-Origin\s*:\s*\*/i.test(fileContent)) {
|
|
1687
|
+
return "wildcard";
|
|
1688
|
+
}
|
|
1689
|
+
if (/cors\s*\(/i.test(fileContent) || /Access-Control-Allow/i.test(fileContent)) {
|
|
1690
|
+
return "configured";
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return "none";
|
|
1694
|
+
}
|
|
1695
|
+
detectMFA(content) {
|
|
1696
|
+
return this.searchPatterns(content, [
|
|
1697
|
+
/mfa|multi.?factor|2fa|two.?factor/i,
|
|
1698
|
+
/totp|otpauth|speakeasy|otplib/i,
|
|
1699
|
+
/webauthn|fido2|passkey/i,
|
|
1700
|
+
/authenticator/i
|
|
1701
|
+
]);
|
|
1702
|
+
}
|
|
1703
|
+
searchPatterns(content, patterns) {
|
|
1704
|
+
for (const [, fileContent] of content) {
|
|
1705
|
+
for (const pattern of patterns) {
|
|
1706
|
+
pattern.lastIndex = 0;
|
|
1707
|
+
if (pattern.test(fileContent))
|
|
1708
|
+
return true;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return false;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
// ../audit-engine/dist/scanners/config-scanner.js
|
|
1716
|
+
var ConfigScanner = class {
|
|
1717
|
+
name = "config";
|
|
1718
|
+
scan(ctx) {
|
|
1719
|
+
const findings = [];
|
|
1720
|
+
this.checkPackageJson(ctx, findings);
|
|
1721
|
+
this.checkEnvFiles(ctx, findings);
|
|
1722
|
+
this.checkDockerConfig(ctx, findings);
|
|
1723
|
+
this.checkTLSConfig(ctx, findings);
|
|
1724
|
+
this.checkGitignore(ctx, findings);
|
|
1725
|
+
this.checkLoggingConfig(ctx, findings);
|
|
1726
|
+
return findings;
|
|
1727
|
+
}
|
|
1728
|
+
checkPackageJson(ctx, findings) {
|
|
1729
|
+
const pkgContent = ctx.fileContents.get("package.json");
|
|
1730
|
+
if (!pkgContent)
|
|
1731
|
+
return;
|
|
1732
|
+
try {
|
|
1733
|
+
const pkg2 = JSON.parse(pkgContent);
|
|
1734
|
+
const deps = { ...pkg2.dependencies, ...pkg2.devDependencies };
|
|
1735
|
+
if (deps.helmet === void 0 && (deps.express || deps.koa || deps.fastify)) {
|
|
1736
|
+
findings.push({
|
|
1737
|
+
ruleId: "CONFIG-001",
|
|
1738
|
+
severity: "high",
|
|
1739
|
+
category: "security",
|
|
1740
|
+
title: "Missing security headers (no helmet)",
|
|
1741
|
+
description: "No helmet middleware detected for HTTP framework. Security headers protect against XSS, clickjacking, and other attacks.",
|
|
1742
|
+
file: "package.json",
|
|
1743
|
+
evidence: "helmet not in dependencies",
|
|
1744
|
+
controlIds: ["OWASP-ASVS-002", "OWASP-ASVS-006"],
|
|
1745
|
+
fix: "npm install helmet && app.use(helmet())"
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
if (deps.cors === void 0 && (deps.express || deps.fastify)) {
|
|
1749
|
+
findings.push({
|
|
1750
|
+
ruleId: "CONFIG-002",
|
|
1751
|
+
severity: "medium",
|
|
1752
|
+
category: "security",
|
|
1753
|
+
title: "No CORS configuration",
|
|
1754
|
+
description: "No CORS package found. Unrestricted CORS can expose your API to cross-origin attacks.",
|
|
1755
|
+
file: "package.json",
|
|
1756
|
+
evidence: "cors not in dependencies",
|
|
1757
|
+
controlIds: ["OWASP-ASVS-006"],
|
|
1758
|
+
fix: "npm install cors and configure allowed origins explicitly."
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
const auditDeps = ["lodash", "axios", "underscore"];
|
|
1762
|
+
for (const dep of auditDeps) {
|
|
1763
|
+
if (deps[dep]) {
|
|
1764
|
+
findings.push({
|
|
1765
|
+
ruleId: "CONFIG-003",
|
|
1766
|
+
severity: "medium",
|
|
1767
|
+
category: "dependencies",
|
|
1768
|
+
title: `Dependency review needed: ${dep}`,
|
|
1769
|
+
description: `${dep} is a commonly exploited dependency. Ensure you are running the latest version with no known vulnerabilities.`,
|
|
1770
|
+
file: "package.json",
|
|
1771
|
+
evidence: `${dep}: ${deps[dep]}`,
|
|
1772
|
+
controlIds: ["CIS-004", "OWASP-ASVS-005"],
|
|
1773
|
+
fix: "Run npm audit regularly. Update to latest version. Consider automated dependency scanning."
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
checkEnvFiles(ctx, findings) {
|
|
1781
|
+
for (const [filePath, content] of ctx.fileContents) {
|
|
1782
|
+
if (filePath !== ".env" && !filePath.endsWith("/.env") && !filePath.startsWith(".env."))
|
|
1783
|
+
continue;
|
|
1784
|
+
if (filePath.includes("example") || filePath.includes("template"))
|
|
1785
|
+
continue;
|
|
1786
|
+
const lines = content.split("\n");
|
|
1787
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1788
|
+
const line = lines[i].trim();
|
|
1789
|
+
if (!line || line.startsWith("#"))
|
|
1790
|
+
continue;
|
|
1791
|
+
if (/\b(PASSWORD|SECRET|KEY|TOKEN|PRIVATE)\b.*=\s*[^\s]/i.test(line) && !line.includes("your_") && !line.includes("changeme") && !line.includes("xxx")) {
|
|
1792
|
+
findings.push({
|
|
1793
|
+
ruleId: "CONFIG-004",
|
|
1794
|
+
severity: "critical",
|
|
1795
|
+
category: "secrets",
|
|
1796
|
+
title: "Secret with value in .env file",
|
|
1797
|
+
description: "A .env file contains actual secret values. Ensure .env files are in .gitignore and never committed.",
|
|
1798
|
+
file: filePath,
|
|
1799
|
+
line: i + 1,
|
|
1800
|
+
evidence: line.split("=")[0] + "=***",
|
|
1801
|
+
controlIds: ["OWASP-ASVS-005", "GDPR-ART32-002"],
|
|
1802
|
+
fix: "Ensure .env is in .gitignore. Use a secrets management solution for production."
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
checkDockerConfig(ctx, findings) {
|
|
1809
|
+
const dockerfile = ctx.fileContents.get("Dockerfile");
|
|
1810
|
+
if (dockerfile) {
|
|
1811
|
+
if (/USER\s+root/i.test(dockerfile) || !/USER\s+/i.test(dockerfile)) {
|
|
1812
|
+
findings.push({
|
|
1813
|
+
ruleId: "CONFIG-005",
|
|
1814
|
+
severity: "medium",
|
|
1815
|
+
category: "infrastructure",
|
|
1816
|
+
title: "Docker running as root",
|
|
1817
|
+
description: "Container may be running as root. Use a non-root user for security.",
|
|
1818
|
+
file: "Dockerfile",
|
|
1819
|
+
evidence: "No non-root USER directive found",
|
|
1820
|
+
controlIds: ["CIS-003"],
|
|
1821
|
+
fix: "Add: USER node (or other non-root user) to your Dockerfile."
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
if (/\bENV\b.*(?:PASSWORD|SECRET|KEY|TOKEN)\s*=\s*\S+/i.test(dockerfile)) {
|
|
1825
|
+
findings.push({
|
|
1826
|
+
ruleId: "CONFIG-006",
|
|
1827
|
+
severity: "critical",
|
|
1828
|
+
category: "secrets",
|
|
1829
|
+
title: "Secret in Dockerfile ENV",
|
|
1830
|
+
description: "Secrets must not be baked into Docker images.",
|
|
1831
|
+
file: "Dockerfile",
|
|
1832
|
+
evidence: "ENV with secret value",
|
|
1833
|
+
controlIds: ["OWASP-ASVS-005"],
|
|
1834
|
+
fix: "Use Docker secrets or environment variables at runtime instead."
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
checkTLSConfig(ctx, findings) {
|
|
1840
|
+
for (const [filePath, content] of ctx.fileContents) {
|
|
1841
|
+
if (!filePath.includes(".env") && !filePath.includes("config"))
|
|
1842
|
+
continue;
|
|
1843
|
+
if (/\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0['"]?/i.test(content)) {
|
|
1844
|
+
findings.push({
|
|
1845
|
+
ruleId: "CONFIG-007",
|
|
1846
|
+
severity: "critical",
|
|
1847
|
+
category: "encryption",
|
|
1848
|
+
title: "TLS verification disabled",
|
|
1849
|
+
description: "NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification, enabling MITM attacks.",
|
|
1850
|
+
file: filePath,
|
|
1851
|
+
evidence: "NODE_TLS_REJECT_UNAUTHORIZED=0",
|
|
1852
|
+
controlIds: ["GDPR-ART32-003", "OWASP-ASVS-006"],
|
|
1853
|
+
fix: "Remove NODE_TLS_REJECT_UNAUTHORIZED=0. Fix the certificate issue instead."
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
checkGitignore(ctx, findings) {
|
|
1859
|
+
const gitignore = ctx.fileContents.get(".gitignore");
|
|
1860
|
+
if (!gitignore) {
|
|
1861
|
+
findings.push({
|
|
1862
|
+
ruleId: "CONFIG-008",
|
|
1863
|
+
severity: "high",
|
|
1864
|
+
category: "security",
|
|
1865
|
+
title: "No .gitignore file",
|
|
1866
|
+
description: "No .gitignore found. Secrets and build artifacts may be committed accidentally.",
|
|
1867
|
+
file: ".gitignore",
|
|
1868
|
+
evidence: "File not found",
|
|
1869
|
+
controlIds: ["OWASP-ASVS-005"],
|
|
1870
|
+
fix: "Create .gitignore with node_modules/, .env, dist/, *.key, etc."
|
|
1871
|
+
});
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
const required = [".env", "node_modules"];
|
|
1875
|
+
for (const pattern of required) {
|
|
1876
|
+
if (!gitignore.includes(pattern)) {
|
|
1877
|
+
findings.push({
|
|
1878
|
+
ruleId: "CONFIG-009",
|
|
1879
|
+
severity: "high",
|
|
1880
|
+
category: "security",
|
|
1881
|
+
title: `.gitignore missing ${pattern}`,
|
|
1882
|
+
description: `${pattern} should be in .gitignore to prevent accidental commits.`,
|
|
1883
|
+
file: ".gitignore",
|
|
1884
|
+
evidence: `${pattern} not found in .gitignore`,
|
|
1885
|
+
controlIds: ["OWASP-ASVS-005"],
|
|
1886
|
+
fix: `Add ${pattern} to .gitignore.`
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
checkLoggingConfig(ctx, findings) {
|
|
1892
|
+
const hasLogging = this.searchContent(ctx, [
|
|
1893
|
+
/winston|pino|bunyan|morgan|helmet/i,
|
|
1894
|
+
/logging|logger/i,
|
|
1895
|
+
/auditLog|audit_log/i
|
|
1896
|
+
]);
|
|
1897
|
+
if (!hasLogging) {
|
|
1898
|
+
findings.push({
|
|
1899
|
+
ruleId: "CONFIG-010",
|
|
1900
|
+
severity: "high",
|
|
1901
|
+
category: "audit",
|
|
1902
|
+
title: "No logging framework detected",
|
|
1903
|
+
description: "No logging library or audit logging found. Audit logging is mandatory for GDPR compliance.",
|
|
1904
|
+
file: "project",
|
|
1905
|
+
evidence: "No logging library (winston, pino, etc.) found",
|
|
1906
|
+
controlIds: ["GDPR-ART32-006", "OWASP-ASVS-004"],
|
|
1907
|
+
fix: "Install a logging library (winston or pino) and implement structured audit logging."
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
searchContent(ctx, patterns) {
|
|
1912
|
+
for (const [, content] of ctx.fileContents) {
|
|
1913
|
+
for (const pattern of patterns) {
|
|
1914
|
+
pattern.lastIndex = 0;
|
|
1915
|
+
if (pattern.test(content))
|
|
1916
|
+
return true;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return false;
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
// ../audit-engine/dist/scanners/database-scanner.js
|
|
1924
|
+
var DB_SCHEMA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1925
|
+
".prisma",
|
|
1926
|
+
".sql"
|
|
1927
|
+
]);
|
|
1928
|
+
var DB_SCHEMA_FILENAMES = [
|
|
1929
|
+
/(?:^|[\/\\])(?:schema|migration|knexfile|drizzle\.config|database\.conf)/i
|
|
1930
|
+
];
|
|
1931
|
+
var DB_DIR_INDICATORS = [
|
|
1932
|
+
/[\/\\](?:migrations?|models?|entities?|repositories?|schemas?|db|database)[\/\\]/i
|
|
1933
|
+
];
|
|
1934
|
+
var ORM_ENTITY_PATTERNS = {
|
|
1935
|
+
".ts": /(?:@Entity|@Table|@Schema|BaseModel)\s*\(|(?:Model|Entity|Schema)\s+extends\s+|Schema\s*=\s*new\s+mongoose\.Schema/i,
|
|
1936
|
+
".js": /(?:@Entity|@Table|@Schema|BaseModel)\s*\(/,
|
|
1937
|
+
".py": /class\s+\w+\s*\(\s*(?:models\.Model|Base|declarative_base)\)/i,
|
|
1938
|
+
".rb": /class\s+\w+\s*<\s*(?:ApplicationRecord|ActiveRecord::Base)/i,
|
|
1939
|
+
".go": /type\s+\w+\s+struct\s*\{[\s\S]*?gorm/i,
|
|
1940
|
+
".java": /@Entity\s*(?:public\s+)?class/i,
|
|
1941
|
+
".php": /class\s+\w+\s+extends\s+(?:Model|Eloquent|Doctrine)/i
|
|
1942
|
+
};
|
|
1943
|
+
var MIGRATION_DIR_PATTERN = /[\/\\]migrations?[\/\\]/i;
|
|
1944
|
+
function isDatabaseSchemaFile(filePath, content) {
|
|
1945
|
+
const ext = filePath.substring(filePath.lastIndexOf("."));
|
|
1946
|
+
const basename2 = filePath.substring(filePath.lastIndexOf("/") + 1);
|
|
1947
|
+
if (ext === ".prisma")
|
|
1948
|
+
return true;
|
|
1949
|
+
if (MIGRATION_DIR_PATTERN.test(filePath))
|
|
1950
|
+
return false;
|
|
1951
|
+
if (DB_SCHEMA_EXTENSIONS.has(ext))
|
|
1952
|
+
return true;
|
|
1953
|
+
for (const pattern of DB_SCHEMA_FILENAMES) {
|
|
1954
|
+
if (pattern.test(basename2))
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
for (const pattern of DB_DIR_INDICATORS) {
|
|
1958
|
+
if (pattern.test(filePath))
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1961
|
+
const ormPattern = ORM_ENTITY_PATTERNS[ext];
|
|
1962
|
+
if (ormPattern && ormPattern.test(content))
|
|
1963
|
+
return true;
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
var DatabaseScanner = class {
|
|
1967
|
+
name = "database";
|
|
1968
|
+
scan(ctx) {
|
|
1969
|
+
const findings = [];
|
|
1970
|
+
this.checkSchemaPatterns(ctx, findings);
|
|
1971
|
+
this.checkORMConfig(ctx, findings);
|
|
1972
|
+
return findings;
|
|
1973
|
+
}
|
|
1974
|
+
checkSchemaPatterns(ctx, findings) {
|
|
1975
|
+
for (const [filePath, content] of ctx.fileContents) {
|
|
1976
|
+
if (!isDatabaseSchemaFile(filePath, content))
|
|
1977
|
+
continue;
|
|
1978
|
+
const isPrisma = filePath.endsWith(".prisma");
|
|
1979
|
+
const hasTimestamps = isPrisma ? /\b(?:createdAt|created_at)\b.*(?:DateTime|timestamp)/i.test(content) : /\b(?:timestamps|created_at|createdAt|createdDate|date_created|timecreated|createdTime)\s*[:\(]/i.test(content);
|
|
1980
|
+
const hasSoftDelete = /\b(?:deleted_at|deletedAt|softDelete|paranoid|is_deleted|isDeleted|deleted|active)\s*[:\(]/i.test(content) || isPrisma && /\b(?:deletedAt|deleted_at)\s+DateTime/i.test(content);
|
|
1981
|
+
const hasUserAudit = /\b(?:created_by|createdBy|updated_by|updatedBy|owner_id|author_id)\s*[:\(]/i.test(content) || isPrisma && /\b(?:createdBy|updatedBy|ownerId|authorId)\s+String/i.test(content);
|
|
1982
|
+
const hasSchemaDef = /\b(?:model|schema|entity|table|struct|class)\b.*\{/i.test(content) || /\bCREATE\s+TABLE\b/i.test(content) || /@(?:Entity|Table|Schema)\b/.test(content);
|
|
1983
|
+
if (!hasSchemaDef)
|
|
1984
|
+
continue;
|
|
1985
|
+
if (!hasTimestamps) {
|
|
1986
|
+
findings.push({
|
|
1987
|
+
ruleId: "DB-001",
|
|
1988
|
+
severity: "high",
|
|
1989
|
+
category: "database",
|
|
1990
|
+
title: "Missing audit timestamps in schema",
|
|
1991
|
+
description: "Database schema does not include created_at/updated_at timestamps. These are mandatory for audit trails.",
|
|
1992
|
+
file: filePath,
|
|
1993
|
+
evidence: "No created_at/updated_at columns detected",
|
|
1994
|
+
controlIds: ["GDPR-ART32-006"],
|
|
1995
|
+
fix: "Add created_at and updated_at columns. In Prisma: add DateTime fields, in Sequelize: timestamps: true, in Django: auto_now_add=True."
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
if (!hasSoftDelete) {
|
|
1999
|
+
findings.push({
|
|
2000
|
+
ruleId: "DB-002",
|
|
2001
|
+
severity: "medium",
|
|
2002
|
+
category: "database",
|
|
2003
|
+
title: "Missing soft delete pattern",
|
|
2004
|
+
description: "No deleted_at column or soft delete pattern found. Hard deletes prevent audit trail and data recovery.",
|
|
2005
|
+
file: filePath,
|
|
2006
|
+
evidence: "No deleted_at/softDelete pattern detected",
|
|
2007
|
+
controlIds: ["GDPR-ART32-007"],
|
|
2008
|
+
fix: "Add deleted_at column or soft delete flag. In Prisma: DeletedAt DateTime?, in Sequelize: paranoid: true, in Django: SoftDeleteModel."
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
if (!hasUserAudit) {
|
|
2012
|
+
findings.push({
|
|
2013
|
+
ruleId: "DB-003",
|
|
2014
|
+
severity: "medium",
|
|
2015
|
+
category: "database",
|
|
2016
|
+
title: "Missing user audit columns",
|
|
2017
|
+
description: "No created_by/updated_by columns found. Track who makes changes for accountability.",
|
|
2018
|
+
file: filePath,
|
|
2019
|
+
evidence: "No created_by/updated_by columns detected",
|
|
2020
|
+
controlIds: ["GDPR-ART32-006"],
|
|
2021
|
+
fix: "Add created_by and updated_by columns to track which user made changes."
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
checkORMConfig(ctx, findings) {
|
|
2027
|
+
const prismaSchema = ctx.fileContents.get("prisma/schema.prisma");
|
|
2028
|
+
if (prismaSchema) {
|
|
2029
|
+
if (!/@@map/i.test(prismaSchema) && !/model\s+Audit/i.test(prismaSchema)) {
|
|
2030
|
+
findings.push({
|
|
2031
|
+
ruleId: "DB-004",
|
|
2032
|
+
severity: "medium",
|
|
2033
|
+
category: "database",
|
|
2034
|
+
title: "No Audit model in Prisma schema",
|
|
2035
|
+
description: "Consider adding an Audit model for immutable audit logging.",
|
|
2036
|
+
file: "prisma/schema.prisma",
|
|
2037
|
+
evidence: "No Audit model found",
|
|
2038
|
+
controlIds: ["GDPR-ART32-006"],
|
|
2039
|
+
fix: "Add model Audit { id Int @id @default(autoincrement()) userId String action String resource String timestamp DateTime @default(now()) ipAddress String }"
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// ../audit-engine/dist/index.js
|
|
2047
|
+
var IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
2048
|
+
"node_modules",
|
|
2049
|
+
".git",
|
|
2050
|
+
"dist",
|
|
2051
|
+
"build",
|
|
2052
|
+
".next",
|
|
2053
|
+
".nuxt",
|
|
2054
|
+
"coverage",
|
|
2055
|
+
".ges",
|
|
2056
|
+
"vendor",
|
|
2057
|
+
"__pycache__",
|
|
2058
|
+
".venv",
|
|
2059
|
+
"venv",
|
|
2060
|
+
".turbo",
|
|
2061
|
+
".cache",
|
|
2062
|
+
"reports",
|
|
2063
|
+
"compliance",
|
|
2064
|
+
"security",
|
|
2065
|
+
"controls",
|
|
2066
|
+
"policies",
|
|
2067
|
+
"checklists",
|
|
2068
|
+
"docs",
|
|
2069
|
+
"bundle",
|
|
2070
|
+
".crush",
|
|
2071
|
+
".vscode",
|
|
2072
|
+
".idea"
|
|
2073
|
+
]);
|
|
2074
|
+
var SKIP_PATHS = [
|
|
2075
|
+
"/audit-engine/src/"
|
|
2076
|
+
];
|
|
2077
|
+
var IGNORE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2078
|
+
".png",
|
|
2079
|
+
".jpg",
|
|
2080
|
+
".jpeg",
|
|
2081
|
+
".gif",
|
|
2082
|
+
".webp",
|
|
2083
|
+
".svg",
|
|
2084
|
+
".ico",
|
|
2085
|
+
".woff",
|
|
2086
|
+
".woff2",
|
|
2087
|
+
".ttf",
|
|
2088
|
+
".eot",
|
|
2089
|
+
".mp4",
|
|
2090
|
+
".mp3",
|
|
2091
|
+
".zip",
|
|
2092
|
+
".gz",
|
|
2093
|
+
".tar",
|
|
2094
|
+
".lock",
|
|
2095
|
+
".map",
|
|
2096
|
+
".wasm"
|
|
2097
|
+
]);
|
|
2098
|
+
function collectFiles(root) {
|
|
2099
|
+
const files = [];
|
|
2100
|
+
function walk(dir) {
|
|
2101
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2102
|
+
for (const entry of entries) {
|
|
2103
|
+
if (IGNORE_DIRS2.has(entry.name))
|
|
2104
|
+
continue;
|
|
2105
|
+
const fullPath = path.join(dir, entry.name);
|
|
2106
|
+
if (entry.isDirectory()) {
|
|
2107
|
+
walk(fullPath);
|
|
2108
|
+
} else if (entry.isFile()) {
|
|
2109
|
+
const ext = path.extname(entry.name);
|
|
2110
|
+
if (!IGNORE_EXTENSIONS.has(ext)) {
|
|
2111
|
+
const rel = path.relative(root, fullPath).replace(/\\/g, "/");
|
|
2112
|
+
if (!SKIP_PATHS.some((skip) => rel.includes(skip))) {
|
|
2113
|
+
files.push(rel);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
walk(root);
|
|
2120
|
+
return files;
|
|
2121
|
+
}
|
|
2122
|
+
function readFiles(root, files) {
|
|
2123
|
+
const contents = /* @__PURE__ */ new Map();
|
|
2124
|
+
const MAX_FILE_SIZE = 1024 * 1024;
|
|
2125
|
+
for (const file of files) {
|
|
2126
|
+
try {
|
|
2127
|
+
const fullPath = path.join(root, file);
|
|
2128
|
+
const stat = fs.statSync(fullPath);
|
|
2129
|
+
if (stat.size > MAX_FILE_SIZE)
|
|
2130
|
+
continue;
|
|
2131
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
2132
|
+
contents.set(file, content);
|
|
2133
|
+
} catch {
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return contents;
|
|
2137
|
+
}
|
|
2138
|
+
function detectWebProject(fileContents) {
|
|
2139
|
+
const webPatterns = [
|
|
2140
|
+
/from\s+['"]express['"]/,
|
|
2141
|
+
/require\s*\(\s*['"]express['"]\s*\)/,
|
|
2142
|
+
/from\s+['"]fastify['"]/,
|
|
2143
|
+
/require\s*\(\s*['"]fastify['"]\s*\)/,
|
|
2144
|
+
/from\s+['"]koa['"]/,
|
|
2145
|
+
/require\s*\(\s*['"]koa['"]\s*\)/,
|
|
2146
|
+
/from\s+['"]hono['"]/,
|
|
2147
|
+
/require\s*\(\s*['"]hono['"]\s*\)/,
|
|
2148
|
+
/from\s+['"]@nestjs/,
|
|
2149
|
+
/from\s+['"]next['"]/,
|
|
2150
|
+
/from\s+['"]nuxt['"]/,
|
|
2151
|
+
/from\s+['"]@sveltejs/,
|
|
2152
|
+
/from\s+['"]@remix-run/,
|
|
2153
|
+
/from\s+['"]@angular/,
|
|
2154
|
+
/from\s+['"]vue['"]/,
|
|
2155
|
+
/import\s+django/,
|
|
2156
|
+
/from\s+flask\s+import/,
|
|
2157
|
+
/from\s+fastapi\s+import/,
|
|
2158
|
+
/from\s+sanic\s+import/,
|
|
2159
|
+
/from\s+aiohttp\s+import/,
|
|
2160
|
+
/import\s+tornado/,
|
|
2161
|
+
/use\s+gin\.Default\(\)|gin\.New\(\)/,
|
|
2162
|
+
/fiber\.New\(\)/,
|
|
2163
|
+
/echo\.New\(\)/,
|
|
2164
|
+
/mux\.NewRouter\(\)/,
|
|
2165
|
+
/chi\.NewRouter\(\)/,
|
|
2166
|
+
/iris\.New\(\)/,
|
|
2167
|
+
/use\s+Actix\s*Web/,
|
|
2168
|
+
/use\s+rocket/,
|
|
2169
|
+
/use\s+warp/,
|
|
2170
|
+
/use\s+axum/,
|
|
2171
|
+
/Rails\.application/,
|
|
2172
|
+
/ActionController::Base/,
|
|
2173
|
+
/Sinatra::Base/,
|
|
2174
|
+
/import\s+io\.express/,
|
|
2175
|
+
/import\s+io\.ktor/,
|
|
2176
|
+
/import\s+spark\.Spark/,
|
|
2177
|
+
/@SpringBootApplication/,
|
|
2178
|
+
/@Controller/,
|
|
2179
|
+
/@RestController/,
|
|
2180
|
+
/use\s+Rocketeer/,
|
|
2181
|
+
/Route::get|Route::post/,
|
|
2182
|
+
/use\s+Illuminate/,
|
|
2183
|
+
/using\s+Microsoft\.AspNetCore/,
|
|
2184
|
+
/using\s+Nancy/,
|
|
2185
|
+
/ControllerBase/,
|
|
2186
|
+
/createServer\s*\(\s*.*request\b/,
|
|
2187
|
+
/http\.createServer/,
|
|
2188
|
+
/router\.(get|post|put|delete|patch)\s*\(/
|
|
2189
|
+
];
|
|
2190
|
+
for (const [, content] of fileContents) {
|
|
2191
|
+
for (const pattern of webPatterns) {
|
|
2192
|
+
if (pattern.test(content))
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
for (const [filePath, content] of fileContents) {
|
|
2197
|
+
if (filePath === "package.json") {
|
|
2198
|
+
try {
|
|
2199
|
+
const pkg2 = JSON.parse(content);
|
|
2200
|
+
const allDeps = { ...pkg2.dependencies, ...pkg2.devDependencies };
|
|
2201
|
+
if (allDeps.express || allDeps.fastify || allDeps.koa || allDeps.hono || allDeps.next || allDeps.nuxt || allDeps["@nestjs/core"] || allDeps["@sveltejs/kit"] || allDeps["@remix-run/node"] || allDeps["@angular/core"] || allDeps.vue) {
|
|
2202
|
+
return true;
|
|
2203
|
+
}
|
|
2204
|
+
} catch {
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (filePath === "requirements.txt" || filePath === "pyproject.toml") {
|
|
2208
|
+
if (/django|flask|fastapi|sanic|aiohttp|tornado|starlette/i.test(content))
|
|
2209
|
+
return true;
|
|
2210
|
+
}
|
|
2211
|
+
if (filePath === "go.mod") {
|
|
2212
|
+
if (/gin-gonic|fiber|echo|chi|gorilla\/mux|iris/i.test(content))
|
|
2213
|
+
return true;
|
|
2214
|
+
}
|
|
2215
|
+
if (filePath === "Cargo.toml") {
|
|
2216
|
+
if (/actix-web|rocket|warp|axum|tide/i.test(content))
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2219
|
+
if (filePath === "Gemfile") {
|
|
2220
|
+
if (/rails|sinatra|hanami/i.test(content))
|
|
2221
|
+
return true;
|
|
2222
|
+
}
|
|
2223
|
+
if (filePath === "pom.xml" || filePath === "build.gradle") {
|
|
2224
|
+
if (/spring-boot|ktor|sparkjava|quarkus/i.test(content))
|
|
2225
|
+
return true;
|
|
2226
|
+
}
|
|
2227
|
+
if (filePath === "composer.json") {
|
|
2228
|
+
try {
|
|
2229
|
+
const pkg2 = JSON.parse(content);
|
|
2230
|
+
const allDeps = { ...pkg2.require, ...pkg2["require-dev"] };
|
|
2231
|
+
if (allDeps["laravel/framework"] || allDeps["symfony/symfony"] || allDeps["slim/slim"])
|
|
2232
|
+
return true;
|
|
2233
|
+
} catch {
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return false;
|
|
2238
|
+
}
|
|
2239
|
+
function runAudit(root) {
|
|
2240
|
+
const files = collectFiles(root);
|
|
2241
|
+
const fileContents = readFiles(root, files);
|
|
2242
|
+
const isWebProject = detectWebProject(fileContents);
|
|
2243
|
+
const ctx = { root, files, fileContents, isWebProject };
|
|
2244
|
+
const scanners = [
|
|
2245
|
+
new SecretsScanner(),
|
|
2246
|
+
new CryptoScanner(),
|
|
2247
|
+
new CodeSecurityScanner(),
|
|
2248
|
+
new AuthScanner(),
|
|
2249
|
+
new ConfigScanner(),
|
|
2250
|
+
new DatabaseScanner()
|
|
2251
|
+
];
|
|
2252
|
+
const allFindings = [];
|
|
2253
|
+
for (const scanner of scanners) {
|
|
2254
|
+
allFindings.push(...scanner.scan(ctx));
|
|
2255
|
+
}
|
|
2256
|
+
return { findings: allFindings, scannedFiles: files.length };
|
|
2257
|
+
}
|
|
2258
|
+
function deduplicateFindings(findings) {
|
|
2259
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2260
|
+
return findings.filter((f) => {
|
|
2261
|
+
const key = `${f.ruleId}:${f.file}:${f.line || ""}:${f.evidence}`;
|
|
2262
|
+
if (seen.has(key))
|
|
2263
|
+
return false;
|
|
2264
|
+
seen.add(key);
|
|
2265
|
+
return true;
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
1245
2269
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
1246
2270
|
var external_exports = {};
|
|
1247
2271
|
__export(external_exports, {
|
|
@@ -1720,8 +2744,8 @@ function getErrorMap() {
|
|
|
1720
2744
|
|
|
1721
2745
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
|
|
1722
2746
|
var makeIssue = (params) => {
|
|
1723
|
-
const { data, path:
|
|
1724
|
-
const fullPath = [...
|
|
2747
|
+
const { data, path: path4, errorMaps, issueData } = params;
|
|
2748
|
+
const fullPath = [...path4, ...issueData.path || []];
|
|
1725
2749
|
const fullIssue = {
|
|
1726
2750
|
...issueData,
|
|
1727
2751
|
path: fullPath
|
|
@@ -1837,11 +2861,11 @@ var errorUtil;
|
|
|
1837
2861
|
|
|
1838
2862
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
|
|
1839
2863
|
var ParseInputLazyPath = class {
|
|
1840
|
-
constructor(parent, value,
|
|
2864
|
+
constructor(parent, value, path4, key) {
|
|
1841
2865
|
this._cachedPath = [];
|
|
1842
2866
|
this.parent = parent;
|
|
1843
2867
|
this.data = value;
|
|
1844
|
-
this._path =
|
|
2868
|
+
this._path = path4;
|
|
1845
2869
|
this._key = key;
|
|
1846
2870
|
}
|
|
1847
2871
|
get path() {
|
|
@@ -5388,9 +6412,9 @@ var ReportOptionsSchema = external_exports.object({
|
|
|
5388
6412
|
// ../core/dist/constants/index.js
|
|
5389
6413
|
import { createRequire } from "node:module";
|
|
5390
6414
|
import * as url from "node:url";
|
|
5391
|
-
import * as
|
|
6415
|
+
import * as path2 from "node:path";
|
|
5392
6416
|
var __filename = url.fileURLToPath(import.meta.url);
|
|
5393
|
-
var __dirname =
|
|
6417
|
+
var __dirname = path2.dirname(__filename);
|
|
5394
6418
|
var require2 = createRequire(import.meta.url);
|
|
5395
6419
|
var pkg = require2("../../package.json");
|
|
5396
6420
|
var GESF_VERSION = pkg.version;
|
|
@@ -5399,17 +6423,27 @@ var GESF_VERSION = pkg.version;
|
|
|
5399
6423
|
var TOOLS = [
|
|
5400
6424
|
{
|
|
5401
6425
|
name: "check_compliance",
|
|
5402
|
-
description: "Check GDPR compliance status for a project",
|
|
6426
|
+
description: "Check GDPR compliance status for a project. Returns compliance scores per framework (GDPR, OWASP, CIS, NIST) with grades and control breakdown.",
|
|
5403
6427
|
inputSchema: {
|
|
5404
6428
|
type: "object",
|
|
5405
6429
|
properties: {
|
|
5406
|
-
project_type: { type: "string", description: "Project type" }
|
|
6430
|
+
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)" }
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
},
|
|
6434
|
+
{
|
|
6435
|
+
name: "check_project_status",
|
|
6436
|
+
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'.",
|
|
6437
|
+
inputSchema: {
|
|
6438
|
+
type: "object",
|
|
6439
|
+
properties: {
|
|
6440
|
+
project_path: { type: "string", description: "Absolute path to the project root. Defaults to current working directory." }
|
|
5407
6441
|
}
|
|
5408
6442
|
}
|
|
5409
6443
|
},
|
|
5410
6444
|
{
|
|
5411
6445
|
name: "list_missing_controls",
|
|
5412
|
-
description: "Show missing compliance controls",
|
|
6446
|
+
description: "Show missing or failed compliance controls for a given framework. Returns control ID, severity, name, and implementation guidance.",
|
|
5413
6447
|
inputSchema: {
|
|
5414
6448
|
type: "object",
|
|
5415
6449
|
properties: {
|
|
@@ -5419,11 +6453,72 @@ var TOOLS = [
|
|
|
5419
6453
|
},
|
|
5420
6454
|
framework: {
|
|
5421
6455
|
type: "string",
|
|
5422
|
-
description: "Framework name (GDPR, OWASP,
|
|
6456
|
+
description: "Framework name (GDPR, OWASP, CIS, NIST)"
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6460
|
+
},
|
|
6461
|
+
{
|
|
6462
|
+
name: "list_framework_controls",
|
|
6463
|
+
description: "List all controls for a given framework with their status, severity, category, and implementation guidance. Useful for understanding the full control landscape.",
|
|
6464
|
+
inputSchema: {
|
|
6465
|
+
type: "object",
|
|
6466
|
+
properties: {
|
|
6467
|
+
framework: {
|
|
6468
|
+
type: "string",
|
|
6469
|
+
description: "Framework name (GDPR, OWASP, CIS, NIST, AI, blockchain, government)"
|
|
6470
|
+
},
|
|
6471
|
+
status_filter: {
|
|
6472
|
+
type: "string",
|
|
6473
|
+
description: "Filter by status (pass, fail, warning, not-implemented, not-applicable). Omit to show all."
|
|
5423
6474
|
}
|
|
5424
6475
|
}
|
|
5425
6476
|
}
|
|
5426
6477
|
},
|
|
6478
|
+
{
|
|
6479
|
+
name: "run_audit",
|
|
6480
|
+
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.",
|
|
6481
|
+
inputSchema: {
|
|
6482
|
+
type: "object",
|
|
6483
|
+
properties: {
|
|
6484
|
+
project_path: { type: "string", description: "Absolute path to the project root to audit." }
|
|
6485
|
+
}
|
|
6486
|
+
}
|
|
6487
|
+
},
|
|
6488
|
+
{
|
|
6489
|
+
name: "generate_compliance_report",
|
|
6490
|
+
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.",
|
|
6491
|
+
inputSchema: {
|
|
6492
|
+
type: "object",
|
|
6493
|
+
properties: {
|
|
6494
|
+
project_type: { type: "string", description: "Project type" },
|
|
6495
|
+
project_name: { type: "string", description: "Project name" },
|
|
6496
|
+
frameworks: { type: "string", description: "Comma-separated framework names (GDPR,OWASP,CIS,NIST)" }
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
},
|
|
6500
|
+
{
|
|
6501
|
+
name: "generate_audit_report",
|
|
6502
|
+
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.",
|
|
6503
|
+
inputSchema: {
|
|
6504
|
+
type: "object",
|
|
6505
|
+
properties: {
|
|
6506
|
+
project_path: { type: "string", description: "Absolute path to the project root to audit and report on." },
|
|
6507
|
+
project_name: { type: "string", description: "Project name for the report title." }
|
|
6508
|
+
}
|
|
6509
|
+
}
|
|
6510
|
+
},
|
|
6511
|
+
{
|
|
6512
|
+
name: "fix_recommendation",
|
|
6513
|
+
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.",
|
|
6514
|
+
inputSchema: {
|
|
6515
|
+
type: "object",
|
|
6516
|
+
properties: {
|
|
6517
|
+
control_id: { type: "string", description: "Control ID to get fix guidance for (e.g. GDPR-ART32-001, OWASP-AUTH-001)" },
|
|
6518
|
+
finding_title: { type: "string", description: "Title of a specific audit finding to get fix guidance for." }
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
},
|
|
5427
6522
|
{
|
|
5428
6523
|
name: "generate_retention_policy",
|
|
5429
6524
|
description: "Generate a data retention policy template",
|
|
@@ -5463,25 +6558,1145 @@ var TOOLS = [
|
|
|
5463
6558
|
project_name: { type: "string", description: "Project name" }
|
|
5464
6559
|
}
|
|
5465
6560
|
}
|
|
5466
|
-
}
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
6561
|
+
},
|
|
6562
|
+
{
|
|
6563
|
+
name: "generate_data_inventory",
|
|
6564
|
+
description: "Generate a data inventory document listing data categories, classifications, retention periods, and legal basis. Required for GDPR Article 30 compliance.",
|
|
6565
|
+
inputSchema: {
|
|
6566
|
+
type: "object",
|
|
6567
|
+
properties: {
|
|
6568
|
+
project_name: { type: "string", description: "Project name" },
|
|
6569
|
+
project_type: { type: "string", description: "Project type" }
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
},
|
|
6573
|
+
{
|
|
6574
|
+
name: "generate_processing_records",
|
|
6575
|
+
description: "Generate Article 30 Records of Processing Activities (ROPA). Documents all processing activities, purposes, data categories, recipients, and retention periods.",
|
|
6576
|
+
inputSchema: {
|
|
6577
|
+
type: "object",
|
|
6578
|
+
properties: {
|
|
6579
|
+
project_name: { type: "string", description: "Project name" },
|
|
6580
|
+
controller_name: { type: "string", description: "Data controller organization name" }
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
},
|
|
6584
|
+
{
|
|
6585
|
+
name: "auto_fix",
|
|
6586
|
+
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.",
|
|
6587
|
+
inputSchema: {
|
|
6588
|
+
type: "object",
|
|
6589
|
+
properties: {
|
|
6590
|
+
project_path: { type: "string", description: "Absolute path to the project root." },
|
|
6591
|
+
dry_run: { type: "boolean", description: "If true, show what would be fixed without making changes. Default: false." },
|
|
6592
|
+
rule_ids: { type: "string", description: "Comma-separated rule IDs to fix (e.g. 'CONFIG-001,AUTH-002'). Omit to fix all auto-fixable issues." }
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
6595
|
+
},
|
|
6596
|
+
{
|
|
6597
|
+
name: "apply_control_override",
|
|
6598
|
+
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.",
|
|
6599
|
+
inputSchema: {
|
|
6600
|
+
type: "object",
|
|
6601
|
+
properties: {
|
|
6602
|
+
project_path: { type: "string", description: "Absolute path to the project root." },
|
|
6603
|
+
control_id: { type: "string", description: "Control ID to override (e.g. GDPR-ART32-004)" },
|
|
6604
|
+
status: { type: "string", description: "New status: 'not-applicable' or 'pass'" },
|
|
6605
|
+
reason: { type: "string", description: "Reason for the override" }
|
|
6606
|
+
}
|
|
6607
|
+
}
|
|
6608
|
+
},
|
|
6609
|
+
{
|
|
6610
|
+
name: "implement_control",
|
|
6611
|
+
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.",
|
|
6612
|
+
inputSchema: {
|
|
6613
|
+
type: "object",
|
|
6614
|
+
properties: {
|
|
6615
|
+
project_path: { type: "string", description: "Absolute path to the project root." },
|
|
6616
|
+
control_id: { type: "string", description: "Control ID to implement (e.g. GDPR-ART32-002, GDPR-ART32-006, AUTH-002)" }
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
];
|
|
6621
|
+
function send(message) {
|
|
6622
|
+
process.stdout.write(JSON.stringify(message) + "\n");
|
|
6623
|
+
}
|
|
6624
|
+
function resolveProjectPath(projectPath) {
|
|
6625
|
+
return projectPath || process.cwd();
|
|
6626
|
+
}
|
|
6627
|
+
function readJsonFileSafe(filePath) {
|
|
6628
|
+
try {
|
|
6629
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
6630
|
+
return JSON.parse(content);
|
|
6631
|
+
} catch {
|
|
6632
|
+
return null;
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
function loadProjectConfig(projectPath) {
|
|
6636
|
+
const gesDir = path3.join(projectPath, ".ges");
|
|
6637
|
+
const config = readJsonFileSafe(path3.join(gesDir, "config.json"));
|
|
6638
|
+
const score = readJsonFileSafe(path3.join(gesDir, "score.json"));
|
|
6639
|
+
const overrides = readJsonFileSafe(path3.join(gesDir, "control-overrides.json"));
|
|
6640
|
+
return {
|
|
6641
|
+
config,
|
|
6642
|
+
score,
|
|
6643
|
+
overrides: Array.isArray(overrides) ? overrides : []
|
|
6644
|
+
};
|
|
6645
|
+
}
|
|
6646
|
+
function applyControlOverrides(controls, overrides) {
|
|
6647
|
+
if (overrides.length === 0) return controls;
|
|
6648
|
+
const overrideMap = new Map(overrides.map((o) => [o.control_id, o]));
|
|
6649
|
+
return controls.map((control) => {
|
|
6650
|
+
const override = overrideMap.get(control.id);
|
|
6651
|
+
if (!override) return control;
|
|
6652
|
+
return {
|
|
6653
|
+
...control,
|
|
6654
|
+
status: override.status,
|
|
6655
|
+
checks: control.checks.map((check) => ({ ...check, status: override.status }))
|
|
6656
|
+
};
|
|
6657
|
+
});
|
|
6658
|
+
}
|
|
6659
|
+
function updateControlsFromFindings(controls, findings) {
|
|
6660
|
+
return controls.map((control) => {
|
|
6661
|
+
if (control.status === "pass" || control.status === "not-applicable") return control;
|
|
6662
|
+
const relevantFindings = findings.filter((f) => f.controlIds && f.controlIds.includes(control.id));
|
|
6663
|
+
if (relevantFindings.length === 0) return control;
|
|
6664
|
+
const hasCritical = relevantFindings.some((f) => f.severity === "critical" || f.severity === "high");
|
|
6665
|
+
return {
|
|
6666
|
+
...control,
|
|
6667
|
+
status: hasCritical ? "fail" : "warning",
|
|
6668
|
+
checks: control.checks.map((check) => ({
|
|
6669
|
+
...check,
|
|
6670
|
+
status: hasCritical ? "fail" : "warning"
|
|
6671
|
+
}))
|
|
6672
|
+
};
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
function getControlsForProject(projectType, frameworks) {
|
|
6676
|
+
const projectPacks = getPacksForProjectType(projectType);
|
|
6677
|
+
const packIds = new Set(projectPacks.map((p) => p.id));
|
|
6678
|
+
const fwLower = new Set(frameworks.map((f) => f.toLowerCase()));
|
|
6679
|
+
const allPacks = getAllPacks();
|
|
6680
|
+
for (const p of allPacks) {
|
|
6681
|
+
if (fwLower.has(p.id)) packIds.add(p.id);
|
|
6682
|
+
}
|
|
6683
|
+
return allPacks.filter((p) => packIds.has(p.id)).flatMap((p) => p.controls);
|
|
6684
|
+
}
|
|
6685
|
+
function generateFullComplianceReport(projectName, projectType, frameworks, findings, overrides) {
|
|
6686
|
+
const controls = getControlsForProject(projectType, frameworks);
|
|
6687
|
+
const overriddenControls = applyControlOverrides(controls, overrides || []);
|
|
6688
|
+
const auditedControls = findings ? updateControlsFromFindings(overriddenControls, findings) : overriddenControls;
|
|
6689
|
+
const score = generateScoreFile(auditedControls, frameworks, findings);
|
|
6690
|
+
const sections = [];
|
|
6691
|
+
sections.push(`# Compliance Report - ${projectName}`);
|
|
6692
|
+
sections.push(`
|
|
6693
|
+
Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
6694
|
+
sections.push(`Project Type: ${projectType}`);
|
|
6695
|
+
sections.push(`Frameworks: ${frameworks.join(", ")}
|
|
6696
|
+
`);
|
|
6697
|
+
sections.push("## Executive Summary\n");
|
|
6698
|
+
sections.push(`**Overall Score: ${score.overall}% (Grade: ${score.overall_grade})**
|
|
6699
|
+
`);
|
|
6700
|
+
sections.push("| Framework | Score | Grade | Passed | Failed | Warnings | Critical Failures |");
|
|
6701
|
+
sections.push("|-----------|-------|-------|--------|--------|----------|-------------------|");
|
|
6702
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
6703
|
+
sections.push(`| ${fw} | ${data.score}% | ${data.grade} | ${data.passed_controls} | ${data.failed_controls} | ${data.warning_controls} | ${data.critical_failures} |`);
|
|
6704
|
+
}
|
|
6705
|
+
if (findings && findings.length > 0) {
|
|
6706
|
+
sections.push(`
|
|
6707
|
+
**Security Findings**: ${findings.length} total`);
|
|
6708
|
+
const crit = findings.filter((f) => f.severity === "critical").length;
|
|
6709
|
+
const high = findings.filter((f) => f.severity === "high").length;
|
|
6710
|
+
sections.push(`- Critical: ${crit}, High: ${high}`);
|
|
6711
|
+
}
|
|
6712
|
+
if (score.audit_impact) {
|
|
6713
|
+
const ai = score.audit_impact;
|
|
6714
|
+
sections.push(`
|
|
6715
|
+
**Audit Impact**: -${ai.total_deduction}% deduction`);
|
|
6716
|
+
}
|
|
6717
|
+
if (findings && findings.length > 0) {
|
|
6718
|
+
sections.push("\n## Security Findings\n");
|
|
6719
|
+
const grouped = {};
|
|
6720
|
+
for (const f of findings) {
|
|
6721
|
+
if (!grouped[f.category]) grouped[f.category] = [];
|
|
6722
|
+
grouped[f.category].push(f);
|
|
6723
|
+
}
|
|
6724
|
+
for (const [category, categoryFindings] of Object.entries(grouped)) {
|
|
6725
|
+
sections.push(`### ${category.charAt(0).toUpperCase() + category.slice(1)}
|
|
6726
|
+
`);
|
|
6727
|
+
sections.push("| Severity | Title | File | Fix |");
|
|
6728
|
+
sections.push("|----------|-------|------|-----|");
|
|
6729
|
+
for (const f of categoryFindings) {
|
|
6730
|
+
const loc = f.file !== "project" ? `${f.file}${f.line ? `:${f.line}` : ""}` : "project-wide";
|
|
6731
|
+
sections.push(`| ${f.severity} | ${f.title} | ${loc} | ${f.fix.slice(0, 80)} |`);
|
|
6732
|
+
}
|
|
6733
|
+
sections.push("");
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
sections.push("\n## Compliance Details\n");
|
|
6737
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
6738
|
+
sections.push(`### ${fw} - ${data.score}% (Grade: ${data.grade})
|
|
6739
|
+
`);
|
|
6740
|
+
sections.push(`- Total Controls: ${data.total_controls}`);
|
|
6741
|
+
sections.push(`- Passed: ${data.passed_controls}`);
|
|
6742
|
+
sections.push(`- Failed: ${data.failed_controls}`);
|
|
6743
|
+
sections.push(`- Warnings: ${data.warning_controls}`);
|
|
6744
|
+
sections.push(`- Not Implemented: ${data.not_implemented}`);
|
|
6745
|
+
sections.push(`- Critical Failures: ${data.critical_failures}`);
|
|
6746
|
+
const sb = data.severity_breakdown;
|
|
6747
|
+
sections.push("\n**Severity Breakdown:**");
|
|
6748
|
+
sections.push("| Level | Total | Passed | Failed | Warning | Not Implemented |");
|
|
6749
|
+
sections.push("|-------|-------|--------|--------|---------|-----------------|");
|
|
6750
|
+
if (sb.critical.total > 0) sections.push(`| Critical | ${sb.critical.total} | ${sb.critical.passed} | ${sb.critical.failed} | ${sb.critical.warning} | ${sb.critical.not_implemented} |`);
|
|
6751
|
+
if (sb.high.total > 0) sections.push(`| High | ${sb.high.total} | ${sb.high.passed} | ${sb.high.failed} | ${sb.high.warning} | ${sb.high.not_implemented} |`);
|
|
6752
|
+
if (sb.medium.total > 0) sections.push(`| Medium | ${sb.medium.total} | ${sb.medium.passed} | ${sb.medium.failed} | ${sb.medium.warning} | ${sb.medium.not_implemented} |`);
|
|
6753
|
+
if (sb.low.total > 0) sections.push(`| Low | ${sb.low.total} | ${sb.low.passed} | ${sb.low.failed} | ${sb.low.warning} | ${sb.low.not_implemented} |`);
|
|
6754
|
+
sections.push("");
|
|
6755
|
+
}
|
|
6756
|
+
sections.push(generateRecommendations(auditedControls, findings));
|
|
6757
|
+
return sections.join("\n");
|
|
6758
|
+
}
|
|
6759
|
+
function generateRecommendations(controls, findings) {
|
|
6760
|
+
const lines = ["## Recommendations\n"];
|
|
6761
|
+
const failedControls = controls.filter((c) => c.status === "fail");
|
|
6762
|
+
const criticalFails = failedControls.filter((c) => c.severity === "critical");
|
|
6763
|
+
const highFails = failedControls.filter((c) => c.severity === "high");
|
|
6764
|
+
const warningControls = controls.filter((c) => c.status === "warning");
|
|
6765
|
+
const notImplemented = controls.filter((c) => c.status === "not-implemented");
|
|
6766
|
+
if (criticalFails.length > 0) {
|
|
6767
|
+
lines.push("### Critical Actions Required\n");
|
|
6768
|
+
for (const c of criticalFails) {
|
|
6769
|
+
lines.push(`**${c.id}** (${c.severity}): ${c.name}`);
|
|
6770
|
+
lines.push(` Category: ${c.category}`);
|
|
6771
|
+
lines.push(` Guidance: ${c.implementation_guidance}`);
|
|
6772
|
+
lines.push(` Fix: Use \`fix_recommendation\` tool with control_id="${c.id}" for detailed steps.
|
|
6773
|
+
`);
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
6776
|
+
if (highFails.length > 0) {
|
|
6777
|
+
lines.push("### High Priority Actions\n");
|
|
6778
|
+
for (const c of highFails) {
|
|
6779
|
+
lines.push(`**${c.id}** (${c.severity}): ${c.name}`);
|
|
6780
|
+
lines.push(` Category: ${c.category}`);
|
|
6781
|
+
lines.push(` Guidance: ${c.implementation_guidance}
|
|
6782
|
+
`);
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
if (findings && findings.length > 0) {
|
|
6786
|
+
const critFindings = findings.filter((f) => f.severity === "critical");
|
|
6787
|
+
const highFindings = findings.filter((f) => f.severity === "high");
|
|
6788
|
+
if (critFindings.length > 0) {
|
|
6789
|
+
lines.push("### Immediate Security Fixes\n");
|
|
6790
|
+
for (const f of critFindings) {
|
|
6791
|
+
lines.push(`- **[${f.severity.toUpperCase()}] ${f.title}** (${f.file}${f.line ? `:${f.line}` : ""})`);
|
|
6792
|
+
lines.push(` Evidence: ${f.evidence}`);
|
|
6793
|
+
lines.push(` Fix: ${f.fix}`);
|
|
6794
|
+
if (f.controlIds && f.controlIds.length > 0) {
|
|
6795
|
+
lines.push(` Related controls: ${f.controlIds.join(", ")}`);
|
|
6796
|
+
}
|
|
6797
|
+
lines.push("");
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
if (highFindings.length > 0 && critFindings.length === 0) {
|
|
6801
|
+
lines.push("### Security Fixes Needed\n");
|
|
6802
|
+
for (const f of highFindings) {
|
|
6803
|
+
lines.push(`- **[${f.severity.toUpperCase()}] ${f.title}** (${f.file}${f.line ? `:${f.line}` : ""})`);
|
|
6804
|
+
lines.push(` Fix: ${f.fix}
|
|
6805
|
+
`);
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
if (warningControls.length > 0) {
|
|
6810
|
+
lines.push("### Warnings to Address\n");
|
|
6811
|
+
for (const c of warningControls.slice(0, 10)) {
|
|
6812
|
+
lines.push(`- **${c.id}** (${c.severity}): ${c.name} \u2014 ${c.implementation_guidance.split(".")[0]}`);
|
|
6813
|
+
}
|
|
6814
|
+
if (warningControls.length > 10) {
|
|
6815
|
+
lines.push(`- ... and ${warningControls.length - 10} more warnings`);
|
|
6816
|
+
}
|
|
6817
|
+
lines.push("");
|
|
6818
|
+
}
|
|
6819
|
+
if (notImplemented.length > 0) {
|
|
6820
|
+
lines.push("### Not Yet Implemented\n");
|
|
6821
|
+
lines.push(`${notImplemented.length} controls have not been implemented yet. Priority order:
|
|
6822
|
+
`);
|
|
6823
|
+
const sorted = [...notImplemented].sort((a, b) => {
|
|
6824
|
+
const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
6825
|
+
return (sevOrder[a.severity] ?? 4) - (sevOrder[b.severity] ?? 4);
|
|
6826
|
+
});
|
|
6827
|
+
for (const c of sorted.slice(0, 10)) {
|
|
6828
|
+
lines.push(`- **${c.id}** (${c.severity}): ${c.name}`);
|
|
6829
|
+
lines.push(` ${c.implementation_guidance.split(".")[0]}`);
|
|
6830
|
+
}
|
|
6831
|
+
if (notImplemented.length > 10) {
|
|
6832
|
+
lines.push(`
|
|
6833
|
+
... and ${notImplemented.length - 10} more not-implemented controls.`);
|
|
6834
|
+
}
|
|
6835
|
+
lines.push("");
|
|
6836
|
+
}
|
|
6837
|
+
const totalIssues = failedControls.length + warningControls.length + notImplemented.length;
|
|
6838
|
+
if (totalIssues === 0) {
|
|
6839
|
+
lines.push("**All controls are passing.** No recommendations at this time. Continue monitoring with regular audits.");
|
|
6840
|
+
} else {
|
|
6841
|
+
lines.push(`**Summary**: ${totalIssues} total issues (${criticalFails.length} critical, ${highFails.length} high, ${warningControls.length} warnings, ${notImplemented.length} not-implemented).`);
|
|
6842
|
+
lines.push("\nUse the `fix_recommendation` tool with a specific control_id to get step-by-step implementation guidance for any issue.");
|
|
6843
|
+
}
|
|
6844
|
+
return lines.join("\n");
|
|
6845
|
+
}
|
|
6846
|
+
function generateFixGuidance(controlId, findingTitle) {
|
|
6847
|
+
const allControls = getAllPacks().flatMap((p) => p.controls);
|
|
6848
|
+
const control = allControls.find((c) => c.id === controlId);
|
|
6849
|
+
const lines = [];
|
|
6850
|
+
lines.push(`# Fix Guidance: ${controlId}
|
|
6851
|
+
`);
|
|
6852
|
+
if (control) {
|
|
6853
|
+
lines.push(`## Control: ${control.name}`);
|
|
6854
|
+
lines.push(`**Framework**: ${control.framework}`);
|
|
6855
|
+
lines.push(`**Category**: ${control.category}`);
|
|
6856
|
+
lines.push(`**Severity**: ${control.severity}`);
|
|
6857
|
+
lines.push(`**Current Status**: ${control.status}`);
|
|
6858
|
+
lines.push(`**Article**: ${control.article || "N/A"}
|
|
6859
|
+
`);
|
|
6860
|
+
lines.push(`### Description
|
|
6861
|
+
${control.description}
|
|
6862
|
+
`);
|
|
6863
|
+
lines.push(`### Implementation Guidance
|
|
6864
|
+
${control.implementation_guidance}
|
|
6865
|
+
`);
|
|
6866
|
+
lines.push("### Implementation Steps\n");
|
|
6867
|
+
const steps = generateImplementationSteps(control);
|
|
6868
|
+
for (let i = 0; i < steps.length; i++) {
|
|
6869
|
+
lines.push(`${i + 1}. ${steps[i]}`);
|
|
6870
|
+
}
|
|
6871
|
+
lines.push("\n### Verification\n");
|
|
6872
|
+
lines.push("After implementing the fix:");
|
|
6873
|
+
lines.push("1. Run `ges audit` to verify the finding no longer appears");
|
|
6874
|
+
lines.push("2. Run `ges score` to see the updated compliance score");
|
|
6875
|
+
lines.push("3. If the control is not applicable to your project, add it to `.ges/control-overrides.json`:");
|
|
6876
|
+
lines.push("```json");
|
|
6877
|
+
lines.push('[\n {\n "control_id": "' + controlId + '",\n "status": "not-applicable",\n "reason": "Explain why this control does not apply"\n }\n]');
|
|
6878
|
+
lines.push("```");
|
|
6879
|
+
} else {
|
|
6880
|
+
lines.push(`Control **${controlId}** not found in any framework pack.`);
|
|
6881
|
+
lines.push("\nAvailable control IDs:");
|
|
6882
|
+
const grouped = {};
|
|
6883
|
+
for (const c of allControls) {
|
|
6884
|
+
if (!grouped[c.framework]) grouped[c.framework] = [];
|
|
6885
|
+
grouped[c.framework].push(` ${c.id}: ${c.name} (${c.severity})`);
|
|
6886
|
+
}
|
|
6887
|
+
for (const [fw, ids] of Object.entries(grouped)) {
|
|
6888
|
+
lines.push(`
|
|
6889
|
+
**${fw}:**`);
|
|
6890
|
+
lines.push(ids.join("\n"));
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
6893
|
+
if (findingTitle) {
|
|
6894
|
+
lines.push(`
|
|
6895
|
+
### Finding: ${findingTitle}
|
|
6896
|
+
`);
|
|
6897
|
+
lines.push("To fix this specific finding:");
|
|
6898
|
+
lines.push("1. Locate the file mentioned in the finding");
|
|
6899
|
+
lines.push("2. Apply the fix suggested in the finding details");
|
|
6900
|
+
lines.push("3. Run `ges audit` to verify the fix");
|
|
6901
|
+
}
|
|
6902
|
+
return lines.join("\n");
|
|
6903
|
+
}
|
|
6904
|
+
function generateImplementationSteps(control) {
|
|
6905
|
+
const steps = [];
|
|
6906
|
+
const category = control.category;
|
|
6907
|
+
const id = control.id;
|
|
6908
|
+
if (category === "encryption") {
|
|
6909
|
+
steps.push("Install an encryption library: `npm install crypto-js` or use Node.js built-in `crypto` module");
|
|
6910
|
+
steps.push("Implement AES-256-GCM encryption for data at rest");
|
|
6911
|
+
steps.push("Ensure TLS 1.2+ is configured for all data in transit");
|
|
6912
|
+
steps.push("Add encryption key management (use environment variables or a vault service)");
|
|
6913
|
+
steps.push("Verify encryption is applied to all sensitive data fields in your database schema");
|
|
6914
|
+
} else if (category === "authentication") {
|
|
6915
|
+
steps.push("Implement Argon2id password hashing: `npm install argon2`");
|
|
6916
|
+
steps.push("Add multi-factor authentication (MFA) support");
|
|
6917
|
+
steps.push("Implement session expiration (recommended: 15-30 minutes of inactivity)");
|
|
6918
|
+
steps.push("Add rate limiting to authentication endpoints: `npm install express-rate-limit`");
|
|
6919
|
+
steps.push("Configure CORS to restrict origins (never use `*` in production)");
|
|
6920
|
+
} else if (category === "authorization") {
|
|
6921
|
+
steps.push("Implement Role-Based Access Control (RBAC) with defined roles and permissions");
|
|
6922
|
+
steps.push("Apply the principle of least privilege to all user roles");
|
|
6923
|
+
steps.push("Configure deny-by-default access control policies");
|
|
6924
|
+
steps.push("Add authorization middleware to all protected routes");
|
|
6925
|
+
steps.push("Document the access control matrix in your compliance documentation");
|
|
6926
|
+
} else if (category === "audit") {
|
|
6927
|
+
steps.push("Implement audit logging middleware that captures: userId, action, resource, timestamp, ipAddress");
|
|
6928
|
+
steps.push("Store audit logs in a separate, append-only data store");
|
|
6929
|
+
steps.push("Ensure logs are immutable (no update or delete operations)");
|
|
6930
|
+
steps.push("Add logging for: authentication, authorization, data exports, role changes, admin actions");
|
|
6931
|
+
steps.push("Configure log retention policy (minimum 1 year for compliance)");
|
|
6932
|
+
} else if (category === "secrets") {
|
|
6933
|
+
steps.push("Audit all source files for hardcoded secrets: `ges scan` or `npx gitleaks detect`");
|
|
6934
|
+
steps.push("Move all secrets to environment variables or a secrets manager (Vault, AWS KMS, etc.)");
|
|
6935
|
+
steps.push("Add secrets to `.gitignore` (.env files, key files, certificate files)");
|
|
6936
|
+
steps.push("Implement secret rotation policy (rotate every 90 days minimum)");
|
|
6937
|
+
steps.push("Add pre-commit hooks to prevent secrets from being committed: `npx gitleaks protect --staged`");
|
|
6938
|
+
} else if (category === "security-testing") {
|
|
6939
|
+
steps.push("Set up automated security scanning in CI/CD (Trivy, Semgrep, npm audit)");
|
|
6940
|
+
steps.push("Add dependency scanning to detect vulnerable packages");
|
|
6941
|
+
steps.push("Implement static application security testing (SAST)");
|
|
6942
|
+
steps.push("Schedule regular penetration testing (quarterly recommended)");
|
|
6943
|
+
steps.push("Create a security testing checklist and integrate into your development workflow");
|
|
6944
|
+
} else if (category === "privacy") {
|
|
6945
|
+
steps.push("Implement data minimization - only collect data that is necessary");
|
|
6946
|
+
steps.push("Add privacy-by-design principles to your development process");
|
|
6947
|
+
steps.push("Implement data subject rights endpoints (access, rectification, erasure, portability)");
|
|
6948
|
+
steps.push("Create and publish a privacy policy");
|
|
6949
|
+
steps.push("Conduct a Privacy Impact Assessment (PIA) for high-risk processing");
|
|
6950
|
+
} else if (category === "data-protection") {
|
|
6951
|
+
steps.push("Classify all data into categories: public, internal, confidential, restricted");
|
|
6952
|
+
steps.push("Apply appropriate protection controls based on classification");
|
|
6953
|
+
steps.push("Implement data retention policies with automated deletion");
|
|
6954
|
+
steps.push("Add data access logging for all restricted and confidential data");
|
|
6955
|
+
steps.push("Create a data inventory documenting all personal data processing activities");
|
|
6956
|
+
} else if (category === "access-control") {
|
|
6957
|
+
steps.push("Review and document all user roles and their permissions");
|
|
6958
|
+
steps.push("Implement the principle of least privilege");
|
|
6959
|
+
steps.push("Add separation of duties for critical operations");
|
|
6960
|
+
steps.push("Implement regular access reviews (quarterly recommended)");
|
|
6961
|
+
steps.push("Automate provisioning and deprovisioning of access");
|
|
6962
|
+
} else if (category === "incident-response") {
|
|
6963
|
+
steps.push("Create an incident response plan with defined severity levels and escalation paths");
|
|
6964
|
+
steps.push("Define communication templates for GDPR breach notification (72-hour requirement)");
|
|
6965
|
+
steps.push("Set up incident detection and alerting (monitoring, SIEM)");
|
|
6966
|
+
steps.push("Conduct regular incident response tabletop exercises");
|
|
6967
|
+
steps.push("Document lessons learned after each incident");
|
|
6968
|
+
} else if (category === "vulnerability-management") {
|
|
6969
|
+
steps.push("Implement automated vulnerability scanning in CI/CD pipeline");
|
|
6970
|
+
steps.push("Set up dependency scanning (npm audit, Dependabot, Snyk)");
|
|
6971
|
+
steps.push("Define SLA for fixing vulnerabilities based on severity (critical: 24h, high: 7d)");
|
|
6972
|
+
steps.push("Maintain a vulnerability register with tracking");
|
|
6973
|
+
steps.push("Regularly review and update dependencies");
|
|
6974
|
+
} else if (category === "configuration") {
|
|
6975
|
+
steps.push("Review and harden all service configurations");
|
|
6976
|
+
steps.push("Implement security headers (helmet for Node.js: `npm install helmet`)");
|
|
6977
|
+
steps.push("Configure proper CORS policies");
|
|
6978
|
+
steps.push("Ensure containers do not run as root");
|
|
6979
|
+
steps.push("Remove all default credentials and configurations");
|
|
6980
|
+
} else {
|
|
6981
|
+
steps.push(`Review the control requirements: ${control.description}`);
|
|
6982
|
+
steps.push(`Follow the implementation guidance: ${control.implementation_guidance}`);
|
|
6983
|
+
steps.push("Implement the required controls based on your project's architecture");
|
|
6984
|
+
steps.push("Test the implementation thoroughly");
|
|
6985
|
+
steps.push("Document the implementation in your compliance documentation");
|
|
6986
|
+
}
|
|
6987
|
+
if (id.includes("AI") || id.includes("ai-")) {
|
|
6988
|
+
steps.push("");
|
|
6989
|
+
steps.push("**AI-Specific Considerations:**");
|
|
6990
|
+
steps.push("- Implement prompt logging and monitoring");
|
|
6991
|
+
steps.push("- Add PII detection for all inputs and outputs");
|
|
6992
|
+
steps.push("- Rate limit AI API calls to prevent abuse");
|
|
6993
|
+
steps.push("- Validate all AI outputs before presenting to users");
|
|
6994
|
+
steps.push("- Classify data before sending to AI providers");
|
|
6995
|
+
}
|
|
6996
|
+
if (id.includes("BLOCK") || id.includes("blockchain")) {
|
|
6997
|
+
steps.push("");
|
|
6998
|
+
steps.push("**Blockchain-Specific Considerations:**");
|
|
6999
|
+
steps.push("- Never store plaintext personal data on-chain");
|
|
7000
|
+
steps.push("- Store only hashes, CIDs, or encrypted references on-chain");
|
|
7001
|
+
steps.push("- Implement key rotation procedures");
|
|
7002
|
+
steps.push("- Use cryptographic signatures for all on-chain transactions");
|
|
7003
|
+
steps.push("- Maintain immutable audit trails off-chain");
|
|
7004
|
+
}
|
|
7005
|
+
return steps;
|
|
7006
|
+
}
|
|
7007
|
+
function createAutoFixPlan(root, findings, filterRuleIds) {
|
|
7008
|
+
const actions = [];
|
|
7009
|
+
const warnings = [];
|
|
7010
|
+
const processedRules = /* @__PURE__ */ new Set();
|
|
7011
|
+
for (const f of findings) {
|
|
7012
|
+
if (filterRuleIds && !filterRuleIds.has(f.ruleId)) continue;
|
|
7013
|
+
const key = `${f.ruleId}:${f.file}`;
|
|
7014
|
+
if (processedRules.has(key)) continue;
|
|
7015
|
+
processedRules.add(key);
|
|
7016
|
+
switch (f.ruleId) {
|
|
7017
|
+
case "CONFIG-001":
|
|
7018
|
+
actions.push(...buildHelmetFix(root));
|
|
7019
|
+
break;
|
|
7020
|
+
case "CONFIG-002":
|
|
7021
|
+
actions.push(...buildCorsFix(root));
|
|
7022
|
+
break;
|
|
7023
|
+
case "CONFIG-004":
|
|
7024
|
+
actions.push(...buildEnvGitignoreFix(root));
|
|
7025
|
+
break;
|
|
7026
|
+
case "CONFIG-005":
|
|
7027
|
+
actions.push(...buildDockerNonRootFix(root));
|
|
7028
|
+
break;
|
|
7029
|
+
case "CONFIG-007":
|
|
7030
|
+
actions.push(...buildTLSFix(root, f));
|
|
7031
|
+
break;
|
|
7032
|
+
case "CONFIG-008":
|
|
7033
|
+
actions.push(...buildGitignoreCreateFix(root));
|
|
7034
|
+
break;
|
|
7035
|
+
case "CONFIG-009":
|
|
7036
|
+
actions.push(...buildGitignoreEntryFix(root, f));
|
|
7037
|
+
break;
|
|
7038
|
+
case "CONFIG-010":
|
|
7039
|
+
actions.push(...buildLoggingFix(root));
|
|
7040
|
+
break;
|
|
7041
|
+
case "SECRETS-001":
|
|
7042
|
+
actions.push(...buildSecretsFix(root, f));
|
|
7043
|
+
warnings.push(`[SECRETS-001] Secret in ${f.file}:${f.line}. Verify .env is in .gitignore and never committed.`);
|
|
7044
|
+
break;
|
|
7045
|
+
case "CRYPTO-001":
|
|
7046
|
+
actions.push(...buildWeakHashFix(root, f));
|
|
7047
|
+
warnings.push("[CRYPTO-001] For passwords, use Argon2id instead of SHA-256.");
|
|
7048
|
+
break;
|
|
7049
|
+
case "CRYPTO-003":
|
|
7050
|
+
actions.push(...buildPasswordFix(root, f));
|
|
7051
|
+
break;
|
|
7052
|
+
case "AUTH-002":
|
|
7053
|
+
actions.push(...buildRateLimitFix(root));
|
|
7054
|
+
break;
|
|
7055
|
+
case "AUTH-003":
|
|
7056
|
+
actions.push(...buildSessionTimeoutFix(root));
|
|
7057
|
+
break;
|
|
7058
|
+
case "AUTH-004":
|
|
7059
|
+
actions.push(...buildCORSWildcardFix(root));
|
|
7060
|
+
break;
|
|
7061
|
+
case "DB-001":
|
|
7062
|
+
actions.push(...buildTimestampsFix(root, f));
|
|
7063
|
+
break;
|
|
7064
|
+
case "DB-002":
|
|
7065
|
+
actions.push(...buildSoftDeleteFix(root, f));
|
|
7066
|
+
break;
|
|
7067
|
+
case "DB-003":
|
|
7068
|
+
actions.push(...buildUserAuditFix(root, f));
|
|
7069
|
+
break;
|
|
7070
|
+
case "DB-004":
|
|
7071
|
+
actions.push(...buildAuditModelFix(root));
|
|
7072
|
+
break;
|
|
7073
|
+
default:
|
|
7074
|
+
warnings.push(`[${f.severity.toUpperCase()}] ${f.title} in ${f.file}${f.line ? `:${f.line}` : ""}: Manual fix required.`);
|
|
7075
|
+
}
|
|
7076
|
+
}
|
|
7077
|
+
return { actions, warnings };
|
|
7078
|
+
}
|
|
7079
|
+
function applyAutoFixAction(root, action) {
|
|
7080
|
+
const fullPath = path3.join(root, action.filePath);
|
|
7081
|
+
try {
|
|
7082
|
+
switch (action.type) {
|
|
7083
|
+
case "create": {
|
|
7084
|
+
if (fs2.existsSync(fullPath)) {
|
|
7085
|
+
return { applied: false, action, error: "File already exists" };
|
|
7086
|
+
}
|
|
7087
|
+
const dir = path3.dirname(fullPath);
|
|
7088
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
7089
|
+
fs2.writeFileSync(fullPath, action.content || "", "utf-8");
|
|
7090
|
+
return { applied: true, action };
|
|
7091
|
+
}
|
|
7092
|
+
case "modify": {
|
|
7093
|
+
if (!fs2.existsSync(fullPath)) {
|
|
7094
|
+
return { applied: false, action, error: "File not found" };
|
|
7095
|
+
}
|
|
7096
|
+
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
7097
|
+
if (action.search && !content.includes(action.search)) {
|
|
7098
|
+
return { applied: false, action, error: "Search string not found" };
|
|
7099
|
+
}
|
|
7100
|
+
fs2.writeFileSync(fullPath, content.replace(action.search || "", action.replace || ""), "utf-8");
|
|
7101
|
+
return { applied: true, action };
|
|
7102
|
+
}
|
|
7103
|
+
case "append": {
|
|
7104
|
+
const dir = path3.dirname(fullPath);
|
|
7105
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
7106
|
+
fs2.appendFileSync(fullPath, action.content || "", "utf-8");
|
|
7107
|
+
return { applied: true, action };
|
|
7108
|
+
}
|
|
7109
|
+
case "npm-install": {
|
|
7110
|
+
return { applied: true, action };
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
} catch (err) {
|
|
7114
|
+
return { applied: false, action, error: err instanceof Error ? err.message : String(err) };
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
function findMainAppFile(root) {
|
|
7118
|
+
const candidates = ["src/index.ts", "src/index.js", "src/app.ts", "src/app.js", "src/server.ts", "src/server.js", "src/main.ts", "src/main.js", "index.ts", "index.js", "app.ts", "app.js"];
|
|
7119
|
+
for (const c of candidates) {
|
|
7120
|
+
if (fs2.existsSync(path3.join(root, c))) return c;
|
|
7121
|
+
}
|
|
7122
|
+
return null;
|
|
7123
|
+
}
|
|
7124
|
+
function hasDep(root, dep) {
|
|
7125
|
+
const pkg2 = readJsonFileSafe(path3.join(root, "package.json"));
|
|
7126
|
+
if (!pkg2) return false;
|
|
7127
|
+
const deps = { ...pkg2.dependencies, ...pkg2.devDependencies };
|
|
7128
|
+
return dep in deps;
|
|
7129
|
+
}
|
|
7130
|
+
function readFileSafe(filePath) {
|
|
7131
|
+
try {
|
|
7132
|
+
return fs2.readFileSync(filePath, "utf-8");
|
|
7133
|
+
} catch {
|
|
7134
|
+
return null;
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
function buildHelmetFix(root) {
|
|
7138
|
+
const appFile = findMainAppFile(root);
|
|
7139
|
+
if (!appFile) return [];
|
|
7140
|
+
const actions = [
|
|
7141
|
+
{ type: "npm-install", filePath: "package.json", description: "Install helmet", ruleId: "CONFIG-001" }
|
|
7142
|
+
];
|
|
7143
|
+
const content = readFileSafe(path3.join(root, appFile));
|
|
7144
|
+
if (content && content.includes("const app = express()")) {
|
|
7145
|
+
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" });
|
|
7146
|
+
} else {
|
|
7147
|
+
actions.push({ type: "append", filePath: appFile, content: "\nimport helmet from 'helmet';\napp.use(helmet());\n", description: "Add helmet import and middleware", ruleId: "CONFIG-001" });
|
|
7148
|
+
}
|
|
7149
|
+
return actions;
|
|
7150
|
+
}
|
|
7151
|
+
function buildCorsFix(root) {
|
|
7152
|
+
const appFile = findMainAppFile(root);
|
|
7153
|
+
if (!appFile) return [];
|
|
7154
|
+
return [
|
|
7155
|
+
{ type: "npm-install", filePath: "package.json", description: "Install cors", ruleId: "CONFIG-002" },
|
|
7156
|
+
{ 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" }
|
|
7157
|
+
];
|
|
7158
|
+
}
|
|
7159
|
+
function buildEnvGitignoreFix(root) {
|
|
7160
|
+
const gi = fs2.existsSync(path3.join(root, ".gitignore")) ? ".gitignore" : null;
|
|
7161
|
+
if (!gi) return buildGitignoreCreateFix(root);
|
|
7162
|
+
const content = readFileSafe(path3.join(root, gi)) || "";
|
|
7163
|
+
if (content.includes(".env")) return [];
|
|
7164
|
+
return [{ type: "append", filePath: ".gitignore", content: "\n.env\n.env.*\n!.env.example\n", description: "Add .env to .gitignore", ruleId: "CONFIG-004" }];
|
|
7165
|
+
}
|
|
7166
|
+
function buildDockerNonRootFix(root) {
|
|
7167
|
+
if (!fs2.existsSync(path3.join(root, "Dockerfile"))) return [];
|
|
7168
|
+
return [{ type: "append", filePath: "Dockerfile", content: "\nUSER node\n", description: "Add non-root USER to Dockerfile", ruleId: "CONFIG-005" }];
|
|
7169
|
+
}
|
|
7170
|
+
function buildTLSFix(root, f) {
|
|
7171
|
+
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
|
+
}
|
|
7173
|
+
function buildGitignoreCreateFix(root) {
|
|
7174
|
+
return [{ type: "create", filePath: ".gitignore", content: "node_modules/\n.env\n.env.*\n!.env.example\ndist/\nbuild/\n*.key\n*.pem\ncoverage/\n.DS_Store\n", description: "Create .gitignore with security entries", ruleId: "CONFIG-008" }];
|
|
7175
|
+
}
|
|
7176
|
+
function buildGitignoreEntryFix(root, f) {
|
|
7177
|
+
const entry = f.fix.replace("Add ", "").replace(" to .gitignore.", "");
|
|
7178
|
+
if (!fs2.existsSync(path3.join(root, ".gitignore"))) return buildGitignoreCreateFix(root);
|
|
7179
|
+
return [{ type: "append", filePath: ".gitignore", content: `
|
|
7180
|
+
${entry}
|
|
7181
|
+
`, description: `Add ${entry} to .gitignore`, ruleId: "CONFIG-009" }];
|
|
7182
|
+
}
|
|
7183
|
+
function buildLoggingFix(root) {
|
|
7184
|
+
const appFile = findMainAppFile(root);
|
|
7185
|
+
const hasSrc = fs2.existsSync(path3.join(root, "src"));
|
|
7186
|
+
const loggerPath = hasSrc ? "src/lib/logger.ts" : "lib/logger.ts";
|
|
7187
|
+
const actions = [
|
|
7188
|
+
{ type: "npm-install", filePath: "package.json", description: "Install pino logger", ruleId: "CONFIG-010" },
|
|
7189
|
+
{ type: "create", filePath: loggerPath, content: `import pino from 'pino';
|
|
7190
|
+
|
|
7191
|
+
const logger = pino({
|
|
7192
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
7193
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
7194
|
+
});
|
|
7195
|
+
|
|
7196
|
+
interface AuditLogParams {
|
|
7197
|
+
userId: string;
|
|
7198
|
+
action: string;
|
|
7199
|
+
resource: string;
|
|
7200
|
+
ipAddress: string;
|
|
7201
|
+
metadata?: Record<string, unknown>;
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
export function auditLog(params: AuditLogParams): void {
|
|
7205
|
+
logger.info({ ...params, timestamp: new Date().toISOString(), type: 'audit' });
|
|
7206
|
+
}
|
|
7207
|
+
|
|
7208
|
+
export default logger;
|
|
7209
|
+
`, description: "Create structured logger with audit logging", ruleId: "CONFIG-010" }
|
|
7210
|
+
];
|
|
7211
|
+
if (appFile) {
|
|
7212
|
+
actions.push({ type: "append", filePath: appFile, content: `
|
|
7213
|
+
import logger from './${hasSrc ? "lib/logger" : hasSrc ? "src/lib/logger" : "lib/logger"}';
|
|
7214
|
+
`, description: "Import logger", ruleId: "CONFIG-010" });
|
|
7215
|
+
}
|
|
7216
|
+
return actions;
|
|
7217
|
+
}
|
|
7218
|
+
function buildSecretsFix(root, f) {
|
|
7219
|
+
const actions = [];
|
|
7220
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
7221
|
+
if (!content) return actions;
|
|
7222
|
+
const lines = content.split("\n");
|
|
7223
|
+
const idx = (f.line || 1) - 1;
|
|
7224
|
+
if (idx >= lines.length) return actions;
|
|
7225
|
+
const line = lines[idx];
|
|
7226
|
+
const match = line.match(/(\w+)\s*[:=]\s*['"]([^'"]+)['"]/);
|
|
7227
|
+
if (match) {
|
|
7228
|
+
const varName = match[1];
|
|
7229
|
+
const value = match[2];
|
|
7230
|
+
const envFile = fs2.existsSync(path3.join(root, ".env")) ? ".env" : ".env";
|
|
7231
|
+
actions.push({ type: "append", filePath: envFile, content: `
|
|
7232
|
+
${varName}=${value}
|
|
7233
|
+
`, description: `Move ${varName} to .env`, ruleId: "SECRETS-001" });
|
|
7234
|
+
actions.push({ type: "modify", filePath: f.file, search: line, replace: `${varName}: process.env.${varName}`, description: `Replace hardcoded ${varName}`, ruleId: "SECRETS-001" });
|
|
7235
|
+
actions.push(...buildEnvGitignoreFix(root));
|
|
7236
|
+
}
|
|
7237
|
+
return actions;
|
|
7238
|
+
}
|
|
7239
|
+
function buildWeakHashFix(root, f) {
|
|
7240
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
7241
|
+
if (!content) return [];
|
|
7242
|
+
const lines = content.split("\n");
|
|
7243
|
+
const idx = (f.line || 1) - 1;
|
|
7244
|
+
if (idx >= lines.length) return [];
|
|
7245
|
+
const line = lines[idx];
|
|
7246
|
+
let replacement = line;
|
|
7247
|
+
if (/createHash\s*\(\s*['"](?:md5|sha1)['"]\s*\)/.test(line)) {
|
|
7248
|
+
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
|
+
}
|
|
7254
|
+
if (replacement === line) return [];
|
|
7255
|
+
return [{ type: "modify", filePath: f.file, search: line, replace: replacement, description: "Replace weak hash with SHA-256", ruleId: "CRYPTO-001" }];
|
|
7256
|
+
}
|
|
7257
|
+
function buildPasswordFix(root, _f) {
|
|
7258
|
+
const hasSrc = fs2.existsSync(path3.join(root, "src"));
|
|
7259
|
+
const authPath = hasSrc ? "src/lib/auth.ts" : "lib/auth.ts";
|
|
7260
|
+
const actions = [
|
|
7261
|
+
{ type: "npm-install", filePath: "package.json", description: "Install argon2", ruleId: "CRYPTO-003" }
|
|
7262
|
+
];
|
|
7263
|
+
if (!fs2.existsSync(path3.join(root, authPath))) {
|
|
7264
|
+
actions.push({ type: "create", filePath: authPath, content: `import argon2 from 'argon2';
|
|
7265
|
+
|
|
7266
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
7267
|
+
return argon2.hash(password, { type: argon2.argon2id });
|
|
7268
|
+
}
|
|
7269
|
+
|
|
7270
|
+
export async function verifyPassword(hashedPassword: string, inputPassword: string): Promise<boolean> {
|
|
7271
|
+
return argon2.verify(hashedPassword, inputPassword);
|
|
7272
|
+
}
|
|
7273
|
+
`, description: "Create Argon2id password utility", ruleId: "CRYPTO-003" });
|
|
7274
|
+
}
|
|
7275
|
+
return actions;
|
|
7276
|
+
}
|
|
7277
|
+
function buildRateLimitFix(root) {
|
|
7278
|
+
const appFile = findMainAppFile(root);
|
|
7279
|
+
if (!appFile) return [];
|
|
7280
|
+
const isExpress = hasDep(root, "express");
|
|
7281
|
+
const isFastify = hasDep(root, "fastify");
|
|
7282
|
+
if (isExpress) {
|
|
7283
|
+
return [
|
|
7284
|
+
{ type: "npm-install", filePath: "package.json", description: "Install express-rate-limit", ruleId: "AUTH-002" },
|
|
7285
|
+
{ type: "append", filePath: appFile, content: `
|
|
7286
|
+
import rateLimit from 'express-rate-limit';
|
|
7287
|
+
|
|
7288
|
+
const limiter = rateLimit({
|
|
7289
|
+
windowMs: 15 * 60 * 1000,
|
|
7290
|
+
max: 100,
|
|
7291
|
+
standardHeaders: true,
|
|
7292
|
+
legacyHeaders: false,
|
|
7293
|
+
});
|
|
7294
|
+
app.use(limiter);
|
|
7295
|
+
`, description: "Add rate limiting (100 req/15min)", ruleId: "AUTH-002" }
|
|
7296
|
+
];
|
|
7297
|
+
} else if (isFastify) {
|
|
7298
|
+
return [
|
|
7299
|
+
{ type: "npm-install", filePath: "package.json", description: "Install @fastify/rate-limit", ruleId: "AUTH-002" },
|
|
7300
|
+
{ type: "append", filePath: appFile, content: `
|
|
7301
|
+
import rateLimit from '@fastify/rate-limit';
|
|
7302
|
+
app.register(rateLimit, { max: 100, timeWindow: '15 minutes' });
|
|
7303
|
+
`, description: "Add rate limiting to Fastify", ruleId: "AUTH-002" }
|
|
7304
|
+
];
|
|
7305
|
+
}
|
|
7306
|
+
return [];
|
|
7307
|
+
}
|
|
7308
|
+
function buildSessionTimeoutFix(root) {
|
|
7309
|
+
const appFile = findMainAppFile(root);
|
|
7310
|
+
if (!appFile) return [];
|
|
7311
|
+
const isExpress = hasDep(root, "express");
|
|
7312
|
+
if (!isExpress) return [{ type: "append", filePath: appFile, content: "\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n", description: "Add session timeout constant", ruleId: "AUTH-003" }];
|
|
7313
|
+
const content = readFileSafe(path3.join(root, appFile)) || "";
|
|
7314
|
+
if (content.includes("session(")) return [];
|
|
7315
|
+
return [
|
|
7316
|
+
{ type: "npm-install", filePath: "package.json", description: "Install express-session", ruleId: "AUTH-003" },
|
|
7317
|
+
{ type: "append", filePath: appFile, content: `
|
|
7318
|
+
import session from 'express-session';
|
|
7319
|
+
|
|
7320
|
+
app.use(session({
|
|
7321
|
+
secret: process.env.SESSION_SECRET || 'change-me-in-production',
|
|
7322
|
+
resave: false,
|
|
7323
|
+
saveUninitialized: false,
|
|
7324
|
+
cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 30 * 60 * 1000 },
|
|
7325
|
+
}));
|
|
7326
|
+
`, description: "Add session with 30-min timeout", ruleId: "AUTH-003" }
|
|
7327
|
+
];
|
|
7328
|
+
}
|
|
7329
|
+
function buildCORSWildcardFix(root) {
|
|
7330
|
+
const appFile = findMainAppFile(root);
|
|
7331
|
+
if (!appFile) return [];
|
|
7332
|
+
const content = readFileSafe(path3.join(root, appFile)) || "";
|
|
7333
|
+
const actions = [];
|
|
7334
|
+
if (content.includes("origin: '*'")) {
|
|
7335
|
+
actions.push({ type: "modify", filePath: appFile, search: "origin: '*'", replace: "origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']", description: "Replace CORS wildcard", ruleId: "AUTH-004" });
|
|
7336
|
+
}
|
|
7337
|
+
if (content.includes('origin:"*"')) {
|
|
7338
|
+
actions.push({ type: "modify", filePath: appFile, search: 'origin:"*"', replace: "origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']", description: "Replace CORS wildcard", ruleId: "AUTH-004" });
|
|
7339
|
+
}
|
|
7340
|
+
return actions;
|
|
7341
|
+
}
|
|
7342
|
+
function buildTimestampsFix(root, f) {
|
|
7343
|
+
if (!f.file.endsWith(".prisma")) return [];
|
|
7344
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
7345
|
+
if (!content) return [];
|
|
7346
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
7347
|
+
if (!modelMatch || modelMatch.length === 0) return [];
|
|
7348
|
+
const block = modelMatch[0];
|
|
7349
|
+
const closingBrace = block.lastIndexOf("}");
|
|
7350
|
+
if (closingBrace === -1) return [];
|
|
7351
|
+
const insertion = "\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt";
|
|
7352
|
+
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" }];
|
|
7353
|
+
}
|
|
7354
|
+
function buildSoftDeleteFix(root, f) {
|
|
7355
|
+
if (!f.file.endsWith(".prisma")) return [];
|
|
7356
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
7357
|
+
if (!content) return [];
|
|
7358
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
7359
|
+
if (!modelMatch || modelMatch.length === 0) return [];
|
|
7360
|
+
const block = modelMatch[0];
|
|
7361
|
+
const closingBrace = block.lastIndexOf("}");
|
|
7362
|
+
if (closingBrace === -1) return [];
|
|
7363
|
+
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" }];
|
|
7364
|
+
}
|
|
7365
|
+
function buildUserAuditFix(root, f) {
|
|
7366
|
+
if (!f.file.endsWith(".prisma")) return [];
|
|
7367
|
+
const content = readFileSafe(path3.join(root, f.file));
|
|
7368
|
+
if (!content) return [];
|
|
7369
|
+
const modelMatch = content.match(/model\s+\w+\s*\{[^}]*\}/g);
|
|
7370
|
+
if (!modelMatch || modelMatch.length === 0) return [];
|
|
7371
|
+
const block = modelMatch[0];
|
|
7372
|
+
const closingBrace = block.lastIndexOf("}");
|
|
7373
|
+
if (closingBrace === -1) return [];
|
|
7374
|
+
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" }];
|
|
7375
|
+
}
|
|
7376
|
+
function buildAuditModelFix(root) {
|
|
7377
|
+
const content = readFileSafe(path3.join(root, "prisma/schema.prisma"));
|
|
7378
|
+
if (!content) return [];
|
|
7379
|
+
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" }];
|
|
7380
|
+
}
|
|
7381
|
+
function getNpmInstallsFromActions(actions) {
|
|
7382
|
+
const installs = /* @__PURE__ */ new Set();
|
|
7383
|
+
for (const a of actions) {
|
|
7384
|
+
if (a.type !== "npm-install") continue;
|
|
7385
|
+
const map = {
|
|
7386
|
+
"CONFIG-001": "helmet",
|
|
7387
|
+
"CONFIG-002": "cors",
|
|
7388
|
+
"CONFIG-010": "pino",
|
|
7389
|
+
"CRYPTO-003": "argon2",
|
|
7390
|
+
"AUTH-002": "express-rate-limit",
|
|
7391
|
+
"AUTH-003": "express-session"
|
|
7392
|
+
};
|
|
7393
|
+
if (map[a.ruleId]) installs.add(map[a.ruleId]);
|
|
7394
|
+
}
|
|
7395
|
+
return [...installs];
|
|
7396
|
+
}
|
|
7397
|
+
function buildEncryptionAtRestImpl(root, hasSrc) {
|
|
7398
|
+
const cryptoPath = hasSrc ? "src/lib/encryption.ts" : "lib/encryption.ts";
|
|
7399
|
+
return [
|
|
7400
|
+
{ type: "npm-install", filePath: "package.json", description: "Node.js crypto is built-in", ruleId: "GDPR-ART32-002" },
|
|
7401
|
+
{ type: "create", filePath: cryptoPath, content: `import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
7402
|
+
|
|
7403
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
7404
|
+
const IV_LENGTH = 16;
|
|
7405
|
+
const TAG_LENGTH = 16;
|
|
7406
|
+
|
|
7407
|
+
function deriveKey(secret: string, salt: Buffer): Buffer {
|
|
7408
|
+
return scryptSync(secret, salt, 32);
|
|
7409
|
+
}
|
|
7410
|
+
|
|
7411
|
+
export function encrypt(plaintext: string, secret: string): string {
|
|
7412
|
+
const salt = randomBytes(16);
|
|
7413
|
+
const key = deriveKey(secret, salt);
|
|
7414
|
+
const iv = randomBytes(IV_LENGTH);
|
|
7415
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
7416
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
7417
|
+
const tag = cipher.getAuthTag();
|
|
7418
|
+
return Buffer.concat([salt, iv, tag, encrypted]).toString('base64');
|
|
7419
|
+
}
|
|
7420
|
+
|
|
7421
|
+
export function decrypt(ciphertext: string, secret: string): string {
|
|
7422
|
+
const data = Buffer.from(ciphertext, 'base64');
|
|
7423
|
+
const salt = data.subarray(0, 16);
|
|
7424
|
+
const iv = data.subarray(16, 32);
|
|
7425
|
+
const tag = data.subarray(32, 48);
|
|
7426
|
+
const encrypted = data.subarray(48);
|
|
7427
|
+
const key = deriveKey(secret, salt);
|
|
7428
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
7429
|
+
decipher.setAuthTag(tag);
|
|
7430
|
+
return decipher.update(encrypted) + decipher.final('utf8');
|
|
7431
|
+
}
|
|
7432
|
+
`, description: "Create AES-256-GCM encryption utility", ruleId: "GDPR-ART32-002" }
|
|
7433
|
+
];
|
|
7434
|
+
}
|
|
7435
|
+
function buildEncryptionInTransitImpl(root, _hasSrc) {
|
|
7436
|
+
const appFile = findMainAppFile(root);
|
|
7437
|
+
const actions = [];
|
|
7438
|
+
if (appFile) {
|
|
7439
|
+
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
|
+
}
|
|
7441
|
+
return actions;
|
|
7442
|
+
}
|
|
7443
|
+
function buildUserIdentificationImpl(root, hasSrc) {
|
|
7444
|
+
const authPath = hasSrc ? "src/lib/auth.ts" : "lib/auth.ts";
|
|
7445
|
+
if (fs2.existsSync(path3.join(root, authPath))) return [];
|
|
7446
|
+
return [
|
|
7447
|
+
{ type: "npm-install", filePath: "package.json", description: "Install argon2 for password hashing", ruleId: "GDPR-ART32-004" },
|
|
7448
|
+
{ type: "create", filePath: authPath, content: `import argon2 from 'argon2';
|
|
7449
|
+
|
|
7450
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
7451
|
+
return argon2.hash(password, { type: argon2.argon2id });
|
|
7452
|
+
}
|
|
7453
|
+
|
|
7454
|
+
export async function verifyPassword(hashedPassword: string, inputPassword: string): Promise<boolean> {
|
|
7455
|
+
return argon2.verify(hashedPassword, inputPassword);
|
|
7456
|
+
}
|
|
7457
|
+
`, description: "Create auth utility with Argon2id", ruleId: "GDPR-ART32-004" }
|
|
7458
|
+
];
|
|
7459
|
+
}
|
|
7460
|
+
function buildIntegrityControlsImpl(root, hasSrc) {
|
|
7461
|
+
const integrityPath = hasSrc ? "src/lib/integrity.ts" : "lib/integrity.ts";
|
|
7462
|
+
return [
|
|
7463
|
+
{ type: "create", filePath: integrityPath, content: `import { createHash } from 'node:crypto';
|
|
7464
|
+
|
|
7465
|
+
export function hashData(data: string): string {
|
|
7466
|
+
return createHash('sha256').update(data).digest('hex');
|
|
7467
|
+
}
|
|
7468
|
+
|
|
7469
|
+
export function verifyIntegrity(data: string, expectedHash: string): boolean {
|
|
7470
|
+
return hashData(data) === expectedHash;
|
|
7471
|
+
}
|
|
7472
|
+
|
|
7473
|
+
export function generateChecksum(content: string): string {
|
|
7474
|
+
return createHash('sha256').update(content).digest('base64');
|
|
7475
|
+
}
|
|
7476
|
+
`, description: "Create integrity verification utility", ruleId: "GDPR-ART32-007" }
|
|
7477
|
+
];
|
|
7478
|
+
}
|
|
7479
|
+
function buildBackupPolicyImpl(root, _hasSrc) {
|
|
7480
|
+
return [
|
|
7481
|
+
{ type: "create", filePath: "scripts/backup.sh", content: `#!/bin/bash
|
|
7482
|
+
set -euo pipefail
|
|
7483
|
+
|
|
7484
|
+
BACKUP_DIR="\${BACKUP_DIR:-./backups}"
|
|
7485
|
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
7486
|
+
BACKUP_FILE="$BACKUP_DIR/backup_$TIMESTAMP.tar.gz.gpg"
|
|
7487
|
+
ENCRYPTION_KEY="'''\${BACKUP_ENCRYPTION_KEY:-change-me}'''"
|
|
7488
|
+
|
|
7489
|
+
mkdir -p "$BACKUP_DIR"
|
|
7490
|
+
|
|
7491
|
+
echo "[$(date)] Starting backup..."
|
|
7492
|
+
|
|
7493
|
+
if command -v pg_dump &> /dev/null; then
|
|
7494
|
+
pg_dump "$DATABASE_URL" | gpg --symmetric --cipher-algo AES256 --batch --passphrase "$ENCRYPTION_KEY" -o "$BACKUP_FILE"
|
|
7495
|
+
echo "[$(date)] Database backup completed: $BACKUP_FILE"
|
|
7496
|
+
fi
|
|
7497
|
+
|
|
7498
|
+
tar 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
|
|
7499
|
+
|
|
7500
|
+
echo "[$(date)] Backup completed."
|
|
7501
|
+
|
|
7502
|
+
find "$BACKUP_DIR" -name "*.gpg" -mtime +30 -delete
|
|
7503
|
+
echo "[$(date)] Cleaned up backups older than 30 days."
|
|
7504
|
+
`, description: "Create encrypted backup script", ruleId: "GDPR-ART32-008" }
|
|
7505
|
+
];
|
|
7506
|
+
}
|
|
7507
|
+
function buildSecurityTestingImpl(root) {
|
|
7508
|
+
const ghDir = path3.join(root, ".github/workflows");
|
|
7509
|
+
return [
|
|
7510
|
+
{ type: "create", filePath: ".github/workflows/security-scan.yml", content: `name: Security Scan
|
|
7511
|
+
on:
|
|
7512
|
+
push:
|
|
7513
|
+
branches: [main, master]
|
|
7514
|
+
pull_request:
|
|
7515
|
+
branches: [main, master]
|
|
7516
|
+
schedule:
|
|
7517
|
+
- cron: '0 6 * * 1'
|
|
7518
|
+
|
|
7519
|
+
jobs:
|
|
7520
|
+
security:
|
|
7521
|
+
runs-on: ubuntu-latest
|
|
7522
|
+
steps:
|
|
7523
|
+
- uses: actions/checkout@v4
|
|
7524
|
+
- uses: actions/setup-node@v4
|
|
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
|
|
7531
|
+
- name: Run GESF compliance check
|
|
7532
|
+
run: npx @greenarmor/ges audit --ci
|
|
7533
|
+
`, description: "Create security scanning GitHub Actions workflow", ruleId: "GDPR-ART32-009" }
|
|
7534
|
+
];
|
|
7535
|
+
}
|
|
7536
|
+
function generateDataInventory(projectName, projectType) {
|
|
7537
|
+
const webCategories = [
|
|
7538
|
+
{ category: "User Profiles", type: "Personal", classification: "Restricted", retention: "Account + 30 days", basis: "Contract (Art. 6(1)(b))" },
|
|
7539
|
+
{ category: "Email Addresses", type: "Personal", classification: "Confidential", retention: "Account + 30 days", basis: "Contract (Art. 6(1)(b))" },
|
|
7540
|
+
{ category: "Authentication Credentials", type: "Personal", classification: "Restricted", retention: "Session duration", basis: "Contract (Art. 6(1)(b))" },
|
|
7541
|
+
{ category: "IP Addresses", type: "Personal", classification: "Internal", retention: "30 days", basis: "Legitimate interest (Art. 6(1)(f))" },
|
|
7542
|
+
{ category: "Session Data", type: "Operational", classification: "Internal", retention: "Session duration", basis: "Contract (Art. 6(1)(b))" },
|
|
7543
|
+
{ category: "Audit Logs", type: "Operational", classification: "Internal", retention: "1 year", basis: "Legal obligation (Art. 6(1)(c))" }
|
|
7544
|
+
];
|
|
7545
|
+
const aiCategories = [
|
|
7546
|
+
{ category: "AI Prompts", type: "Personal", classification: "Confidential", retention: "90 days", basis: "Legitimate interest (Art. 6(1)(f))" },
|
|
7547
|
+
{ category: "AI Outputs", type: "Personal", classification: "Internal", retention: "30 days", basis: "Legitimate interest (Art. 6(1)(f))" },
|
|
7548
|
+
{ category: "Training Data References", type: "Personal", classification: "Restricted", retention: "Duration of use", basis: "Consent (Art. 6(1)(a))" }
|
|
7549
|
+
];
|
|
7550
|
+
const blockchainCategories = [
|
|
7551
|
+
{ category: "Wallet Addresses", type: "Pseudonymous", classification: "Public", retention: "Indefinite (on-chain)", basis: "Contract (Art. 6(1)(b))" },
|
|
7552
|
+
{ category: "Transaction History", type: "Pseudonymous", classification: "Public", retention: "Indefinite (on-chain)", basis: "Contract (Art. 6(1)(b))" },
|
|
7553
|
+
{ category: "KYC Data", type: "Personal", classification: "Restricted", retention: "5 years", basis: "Legal obligation (Art. 6(1)(c))" }
|
|
7554
|
+
];
|
|
7555
|
+
let categories = webCategories;
|
|
7556
|
+
if (projectType.includes("ai")) categories = [...webCategories, ...aiCategories];
|
|
7557
|
+
if (projectType.includes("blockchain") || projectType.includes("wallet")) categories = [...webCategories, ...blockchainCategories];
|
|
7558
|
+
if (projectType.includes("healthcare")) {
|
|
7559
|
+
categories = [...webCategories, { category: "Health Records", type: "Special Category", classification: "Restricted", retention: "10 years", basis: "Legal obligation (Art. 6(1)(c) + Art. 9)" }];
|
|
7560
|
+
}
|
|
7561
|
+
if (projectType.includes("photo")) {
|
|
7562
|
+
categories = [...webCategories, { category: "Photos/Images", type: "Personal", classification: "Restricted", retention: "Account + 30 days", basis: "Contract (Art. 6(1)(b))" }];
|
|
7563
|
+
}
|
|
7564
|
+
const lines = [
|
|
7565
|
+
`# Data Inventory - ${projectName}
|
|
7566
|
+
`,
|
|
7567
|
+
`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7568
|
+
`,
|
|
7569
|
+
`## Data Categories
|
|
7570
|
+
`,
|
|
7571
|
+
`| Category | Type | Classification | Retention | Legal Basis |`,
|
|
7572
|
+
`|----------|------|---------------|-----------|-------------|`
|
|
7573
|
+
];
|
|
7574
|
+
for (const cat of categories) {
|
|
7575
|
+
lines.push(`| ${cat.category} | ${cat.type} | ${cat.classification} | ${cat.retention} | ${cat.basis} |`);
|
|
7576
|
+
}
|
|
7577
|
+
lines.push("");
|
|
7578
|
+
lines.push("## Data Classification Rules\n");
|
|
7579
|
+
lines.push("| Classification | Encryption | Access Controls | Audit Logging |");
|
|
7580
|
+
lines.push("|---------------|-----------|-----------------|---------------|");
|
|
7581
|
+
lines.push("| Public | Not required | Not required | Not required |");
|
|
7582
|
+
lines.push("| Internal | Not required | Required | Recommended |");
|
|
7583
|
+
lines.push("| Confidential | Required | Required | Required |");
|
|
7584
|
+
lines.push("| Restricted | Required | Required + MFA | Required + Immutable |");
|
|
7585
|
+
lines.push("");
|
|
7586
|
+
lines.push("## Data Subject Rights Implementation\n");
|
|
7587
|
+
lines.push("- [ ] Right of access (Article 15) - API endpoint or process implemented");
|
|
7588
|
+
lines.push("- [ ] Right to rectification (Article 16) - Update process documented");
|
|
7589
|
+
lines.push("- [ ] Right to erasure (Article 17) - Deletion process with verification");
|
|
7590
|
+
lines.push("- [ ] Right to restriction (Article 18) - Mark-and-hold process");
|
|
7591
|
+
lines.push("- [ ] Right to data portability (Article 20) - Export in machine-readable format");
|
|
7592
|
+
lines.push("- [ ] Right to object (Article 21) - Opt-out mechanism");
|
|
7593
|
+
lines.push("");
|
|
7594
|
+
lines.push("## Third-Party Processors\n");
|
|
7595
|
+
lines.push("| Processor | Data Shared | Purpose | DPA Signed | Location |");
|
|
7596
|
+
lines.push("|-----------|------------|---------|------------|----------|");
|
|
7597
|
+
lines.push("| [Cloud Provider] | [Data categories] | [Purpose] | [Yes/No] | [Country] |");
|
|
7598
|
+
lines.push("");
|
|
7599
|
+
lines.push("## Cross-Border Transfers\n");
|
|
7600
|
+
lines.push("| Transfer From | Transfer To | Safeguard |");
|
|
7601
|
+
lines.push("|--------------|------------|-----------|");
|
|
7602
|
+
lines.push("| [EU] | [Country] | [SCCs / Adequacy Decision / BCRs] |");
|
|
7603
|
+
return lines.join("\n");
|
|
7604
|
+
}
|
|
7605
|
+
function generateProcessingRecords(projectName, controllerName) {
|
|
7606
|
+
const lines = [
|
|
7607
|
+
`# Records of Processing Activities (ROPA) - ${projectName}
|
|
7608
|
+
`,
|
|
7609
|
+
`**Controller**: ${controllerName}`,
|
|
7610
|
+
`**Date**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
7611
|
+
`**Document Reference**: ROPA-${projectName.replace(/\s+/g, "-").toUpperCase()}-001
|
|
7612
|
+
`,
|
|
7613
|
+
`## Article 30(1) \u2014 Controller Records
|
|
7614
|
+
`
|
|
7615
|
+
];
|
|
7616
|
+
const activities = [
|
|
7617
|
+
{
|
|
7618
|
+
name: "User Account Management",
|
|
7619
|
+
purpose: "Provision and manage user accounts",
|
|
7620
|
+
categories: "Identity data, contact data, authentication data",
|
|
7621
|
+
recipients: "Internal systems, identity provider",
|
|
7622
|
+
transfers: "None (or specify if applicable)",
|
|
7623
|
+
retention: "Account lifetime + 30 days post-deletion",
|
|
7624
|
+
security: "Encryption at rest (AES-256-GCM), TLS 1.2+ in transit, MFA, RBAC"
|
|
7625
|
+
},
|
|
7626
|
+
{
|
|
7627
|
+
name: "Service Delivery",
|
|
7628
|
+
purpose: "Deliver core product/service functionality",
|
|
7629
|
+
categories: "Usage data, preferences, content data",
|
|
7630
|
+
recipients: "Internal systems, CDN provider",
|
|
7631
|
+
transfers: "None (or specify)",
|
|
7632
|
+
retention: "Account lifetime",
|
|
7633
|
+
security: "Encryption, access controls, audit logging"
|
|
7634
|
+
},
|
|
7635
|
+
{
|
|
7636
|
+
name: "Communication",
|
|
7637
|
+
purpose: "Service notifications, support, marketing (with consent)",
|
|
7638
|
+
categories: "Email addresses, communication preferences",
|
|
7639
|
+
recipients: "Email service provider",
|
|
7640
|
+
transfers: "None (or specify)",
|
|
7641
|
+
retention: "Until consent withdrawal or account closure",
|
|
7642
|
+
security: "Encryption, access controls"
|
|
7643
|
+
},
|
|
7644
|
+
{
|
|
7645
|
+
name: "Analytics and Monitoring",
|
|
7646
|
+
purpose: "Service improvement and security monitoring",
|
|
7647
|
+
categories: "Usage data, IP addresses, device information",
|
|
7648
|
+
recipients: "Analytics provider, monitoring systems",
|
|
7649
|
+
transfers: "None (or specify)",
|
|
7650
|
+
retention: "12 months for analytics, 1 year for security logs",
|
|
7651
|
+
security: "Pseudonymization, access controls, aggregated reporting"
|
|
7652
|
+
},
|
|
7653
|
+
{
|
|
7654
|
+
name: "Legal Compliance",
|
|
7655
|
+
purpose: "Meet regulatory and legal obligations",
|
|
7656
|
+
categories: "Identity data, transaction records, audit logs",
|
|
7657
|
+
recipients: "Legal authorities (upon request), auditors",
|
|
7658
|
+
transfers: "As required by law",
|
|
7659
|
+
retention: "Per legal requirements (typically 5-7 years)",
|
|
7660
|
+
security: "Encryption, access controls, immutable audit trail"
|
|
7661
|
+
}
|
|
7662
|
+
];
|
|
7663
|
+
for (const activity of activities) {
|
|
7664
|
+
lines.push(`### ${activity.name}
|
|
7665
|
+
`);
|
|
7666
|
+
lines.push(`- **Purpose**: ${activity.purpose}`);
|
|
7667
|
+
lines.push(`- **Categories of Data Subjects**: Users, customers, employees`);
|
|
7668
|
+
lines.push(`- **Categories of Personal Data**: ${activity.categories}`);
|
|
7669
|
+
lines.push(`- **Categories of Recipients**: ${activity.recipients}`);
|
|
7670
|
+
lines.push(`- **International Transfers**: ${activity.transfers}`);
|
|
7671
|
+
lines.push(`- **Retention Period**: ${activity.retention}`);
|
|
7672
|
+
lines.push(`- **Technical and Organizational Measures**: ${activity.security}`);
|
|
7673
|
+
lines.push(`- **Legal Basis**: Contract (Art. 6(1)(b)), Legitimate Interest (Art. 6(1)(f))
|
|
7674
|
+
`);
|
|
7675
|
+
}
|
|
7676
|
+
lines.push("## Data Protection Officer\n");
|
|
7677
|
+
lines.push("- **Name**: [DPO Name or N/A if not required]");
|
|
7678
|
+
lines.push("- **Contact**: [DPO Contact Details]");
|
|
7679
|
+
lines.push("");
|
|
7680
|
+
lines.push("## Review History\n");
|
|
7681
|
+
lines.push("| Date | Reviewer | Changes |");
|
|
7682
|
+
lines.push("|------|----------|---------|");
|
|
7683
|
+
lines.push(`| ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]} | Initial | Created ROPA |`);
|
|
7684
|
+
return lines.join("\n");
|
|
7685
|
+
}
|
|
7686
|
+
function handleRequest(request) {
|
|
7687
|
+
const isNotification = request.id === void 0 || request.id === null;
|
|
7688
|
+
if (request.method === "initialize") {
|
|
7689
|
+
return {
|
|
7690
|
+
jsonrpc: "2.0",
|
|
7691
|
+
id: request.id,
|
|
7692
|
+
result: {
|
|
7693
|
+
protocolVersion: "2024-11-05",
|
|
7694
|
+
capabilities: { tools: {} },
|
|
7695
|
+
serverInfo: {
|
|
7696
|
+
name: "gesf-mcp-server",
|
|
7697
|
+
version: GESF_VERSION
|
|
7698
|
+
}
|
|
7699
|
+
}
|
|
5485
7700
|
};
|
|
5486
7701
|
}
|
|
5487
7702
|
if (request.method === "notifications/initialized") {
|
|
@@ -5509,97 +7724,853 @@ function handleRequest(request) {
|
|
|
5509
7724
|
const toolName = request.params?.name || "";
|
|
5510
7725
|
const args = request.params?.arguments || {};
|
|
5511
7726
|
let resultText;
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
7727
|
+
try {
|
|
7728
|
+
switch (toolName) {
|
|
7729
|
+
case "check_compliance": {
|
|
7730
|
+
const projectType = args.project_type || "saas";
|
|
7731
|
+
const packs = getPacksForProjectType(projectType);
|
|
7732
|
+
const controls = packs.flatMap((p) => p.controls);
|
|
7733
|
+
const frameworks = [...new Set(controls.map((c) => c.framework))];
|
|
7734
|
+
const score = generateScoreFile(controls, frameworks);
|
|
7735
|
+
resultText = formatScoreOutput(score);
|
|
7736
|
+
break;
|
|
7737
|
+
}
|
|
7738
|
+
case "check_project_status": {
|
|
7739
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
7740
|
+
const { config, score, overrides } = loadProjectConfig(projectPath);
|
|
7741
|
+
if (!config) {
|
|
7742
|
+
resultText = `No GESF project found at ${projectPath}. Run 'ges init' first to initialize the project.`;
|
|
7743
|
+
break;
|
|
7744
|
+
}
|
|
7745
|
+
const lines = [];
|
|
7746
|
+
lines.push(`# Project Status: ${config.project_name || "Unknown"}
|
|
7747
|
+
`);
|
|
7748
|
+
lines.push(`**Path**: ${projectPath}`);
|
|
7749
|
+
lines.push(`**Type**: ${config.project_type || "Unknown"}`);
|
|
7750
|
+
lines.push(`**Initialized**: ${config.created_at || "Unknown"}`);
|
|
7751
|
+
lines.push(`**Frameworks**: ${Array.isArray(config.frameworks) ? config.frameworks.join(", ") : "Unknown"}`);
|
|
7752
|
+
if (overrides.length > 0) {
|
|
7753
|
+
const naCount = overrides.filter((o) => o.status === "not-applicable").length;
|
|
7754
|
+
const passCount = overrides.filter((o) => o.status === "pass").length;
|
|
7755
|
+
lines.push(`**Control Overrides**: ${overrides.length} (${naCount} not-applicable, ${passCount} pre-verified)`);
|
|
7756
|
+
}
|
|
7757
|
+
if (score) {
|
|
7758
|
+
lines.push(`
|
|
7759
|
+
## Compliance Score
|
|
7760
|
+
`);
|
|
7761
|
+
lines.push(`**Overall: ${score.overall}% (Grade: ${score.overall_grade})**
|
|
7762
|
+
`);
|
|
7763
|
+
lines.push("| Framework | Score | Grade | Passed | Failed | Warnings | Critical |");
|
|
7764
|
+
lines.push("|-----------|-------|-------|--------|--------|----------|----------|");
|
|
7765
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
7766
|
+
lines.push(`| ${fw} | ${data.score}% | ${data.grade} | ${data.passed_controls} | ${data.failed_controls} | ${data.warning_controls} | ${data.critical_failures} |`);
|
|
7767
|
+
}
|
|
7768
|
+
if (score.audit_impact) {
|
|
7769
|
+
const ai = score.audit_impact;
|
|
7770
|
+
lines.push(`
|
|
7771
|
+
**Audit Impact**: -${ai.total_deduction}% (${ai.critical_findings} critical, ${ai.high_findings} high, ${ai.medium_findings} medium, ${ai.low_findings} low findings)`);
|
|
7772
|
+
}
|
|
7773
|
+
lines.push(`
|
|
7774
|
+
Last evaluated: ${score.evaluated_at}`);
|
|
7775
|
+
} else {
|
|
7776
|
+
lines.push("\nNo compliance score found. Run 'ges audit' then 'ges score'.");
|
|
7777
|
+
}
|
|
7778
|
+
const controlsDir = path3.join(projectPath, "controls");
|
|
7779
|
+
if (fs2.existsSync(controlsDir)) {
|
|
7780
|
+
const controlFiles = fs2.readdirSync(controlsDir).filter((f) => f.endsWith(".json"));
|
|
7781
|
+
if (controlFiles.length > 0) {
|
|
7782
|
+
lines.push(`
|
|
7783
|
+
**Control Files**: ${controlFiles.join(", ")}`);
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
resultText = lines.join("\n");
|
|
7787
|
+
break;
|
|
7788
|
+
}
|
|
7789
|
+
case "list_missing_controls": {
|
|
7790
|
+
const framework = args.framework || "GDPR";
|
|
7791
|
+
const projectType = args.project_type;
|
|
7792
|
+
let controls;
|
|
7793
|
+
if (projectType) {
|
|
7794
|
+
const packs = getPacksForProjectType(projectType);
|
|
7795
|
+
controls = packs.flatMap((p) => p.controls);
|
|
7796
|
+
} else {
|
|
7797
|
+
controls = getAllPacks().flatMap((p) => p.controls);
|
|
7798
|
+
}
|
|
7799
|
+
const missing = controls.filter(
|
|
7800
|
+
(c) => c.framework.toLowerCase() === framework.toLowerCase() && c.status !== "pass"
|
|
7801
|
+
);
|
|
7802
|
+
if (missing.length === 0) {
|
|
7803
|
+
resultText = `All ${framework} controls are passing. No missing controls found.`;
|
|
7804
|
+
} else {
|
|
7805
|
+
const lines = [`# Missing Controls - ${framework}
|
|
7806
|
+
`];
|
|
7807
|
+
const critical = missing.filter((c) => c.severity === "critical");
|
|
7808
|
+
const high = missing.filter((c) => c.severity === "high");
|
|
7809
|
+
const medium = missing.filter((c) => c.severity === "medium");
|
|
7810
|
+
const low = missing.filter((c) => c.severity === "low");
|
|
7811
|
+
lines.push(`**Total**: ${missing.length} (${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low)
|
|
7812
|
+
`);
|
|
7813
|
+
for (const group of [
|
|
7814
|
+
{ label: "Critical", items: critical },
|
|
7815
|
+
{ label: "High", items: high },
|
|
7816
|
+
{ label: "Medium", items: medium },
|
|
7817
|
+
{ label: "Low", items: low }
|
|
7818
|
+
]) {
|
|
7819
|
+
if (group.items.length > 0) {
|
|
7820
|
+
lines.push(`## ${group.label} Severity
|
|
7821
|
+
`);
|
|
7822
|
+
for (const c of group.items) {
|
|
7823
|
+
lines.push(`**${c.id}**: ${c.name}`);
|
|
7824
|
+
lines.push(` Status: ${c.status} | Category: ${c.category}`);
|
|
7825
|
+
lines.push(` ${c.implementation_guidance.split(".")[0]}
|
|
7826
|
+
`);
|
|
7827
|
+
}
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
lines.push(`
|
|
7831
|
+
Use \`fix_recommendation\` with a control_id to get detailed implementation guidance.`);
|
|
7832
|
+
resultText = lines.join("\n");
|
|
7833
|
+
}
|
|
7834
|
+
break;
|
|
7835
|
+
}
|
|
7836
|
+
case "list_framework_controls": {
|
|
7837
|
+
const framework = args.framework || "GDPR";
|
|
7838
|
+
const statusFilter = args.status_filter;
|
|
7839
|
+
let allControls;
|
|
7840
|
+
const pack = getPack(framework.toLowerCase());
|
|
7841
|
+
if (pack) {
|
|
7842
|
+
allControls = pack.controls;
|
|
7843
|
+
} else {
|
|
7844
|
+
allControls = getAllPacks().flatMap((p) => p.controls);
|
|
7845
|
+
}
|
|
7846
|
+
const filtered = framework.toLowerCase() !== "all" ? allControls.filter((c) => c.framework.toLowerCase() === framework.toLowerCase()) : allControls;
|
|
7847
|
+
const controls = statusFilter ? filtered.filter((c) => c.status === statusFilter) : filtered;
|
|
7848
|
+
if (controls.length === 0) {
|
|
7849
|
+
resultText = statusFilter ? `No ${framework} controls with status '${statusFilter}' found.` : `No controls found for framework '${framework}'. Available: GDPR, OWASP, CIS, NIST, AI, blockchain, government.`;
|
|
7850
|
+
} else {
|
|
7851
|
+
const lines = [`# ${framework} Controls (${controls.length} total${statusFilter ? `, filtered by: ${statusFilter}` : ""})
|
|
7852
|
+
`];
|
|
7853
|
+
lines.push("| ID | Name | Severity | Category | Status |");
|
|
7854
|
+
lines.push("|----|------|----------|----------|--------|");
|
|
7855
|
+
for (const c of controls) {
|
|
7856
|
+
lines.push(`| ${c.id} | ${c.name} | ${c.severity} | ${c.category} | ${c.status} |`);
|
|
7857
|
+
}
|
|
7858
|
+
lines.push(`
|
|
7859
|
+
### Summary`);
|
|
7860
|
+
const byStatus = {};
|
|
7861
|
+
for (const c of controls) {
|
|
7862
|
+
byStatus[c.status] = (byStatus[c.status] || 0) + 1;
|
|
7863
|
+
}
|
|
7864
|
+
for (const [status, count] of Object.entries(byStatus)) {
|
|
7865
|
+
lines.push(`- ${status}: ${count}`);
|
|
7866
|
+
}
|
|
7867
|
+
resultText = lines.join("\n");
|
|
7868
|
+
}
|
|
7869
|
+
break;
|
|
7870
|
+
}
|
|
7871
|
+
case "run_audit": {
|
|
7872
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
7873
|
+
if (!fs2.existsSync(projectPath)) {
|
|
7874
|
+
resultText = `Project path does not exist: ${projectPath}`;
|
|
7875
|
+
break;
|
|
7876
|
+
}
|
|
7877
|
+
if (!fs2.existsSync(path3.join(projectPath, ".ges"))) {
|
|
7878
|
+
resultText = `GESF not initialized at ${projectPath}. Run 'ges init' first.`;
|
|
7879
|
+
break;
|
|
7880
|
+
}
|
|
7881
|
+
const { findings: rawFindings, scannedFiles } = runAudit(projectPath);
|
|
7882
|
+
const findings = deduplicateFindings(rawFindings);
|
|
7883
|
+
const projectConfig = loadProjectConfig(projectPath);
|
|
7884
|
+
const config = projectConfig.config;
|
|
7885
|
+
const frameworks = config?.frameworks || ["GDPR", "OWASP"];
|
|
7886
|
+
const projectType = config?.project_type || "generic-web-application";
|
|
7887
|
+
const controls = getControlsForProject(projectType, frameworks);
|
|
7888
|
+
const overriddenControls = applyControlOverrides(controls, projectConfig.overrides);
|
|
7889
|
+
const auditedControls = updateControlsFromFindings(overriddenControls, findings);
|
|
7890
|
+
const score = generateScoreFile(auditedControls, frameworks, findings);
|
|
7891
|
+
const critical = findings.filter((f) => f.severity === "critical");
|
|
7892
|
+
const high = findings.filter((f) => f.severity === "high");
|
|
7893
|
+
const medium = findings.filter((f) => f.severity === "medium");
|
|
7894
|
+
const low = findings.filter((f) => f.severity === "low");
|
|
7895
|
+
const lines = [];
|
|
7896
|
+
lines.push(`# Security Audit Report
|
|
7897
|
+
`);
|
|
7898
|
+
lines.push(`**Scanned**: ${scannedFiles} files`);
|
|
7899
|
+
lines.push(`**Findings**: ${findings.length} total (${critical.length} critical, ${high.length} high, ${medium.length} medium, ${low.length} low)
|
|
7900
|
+
`);
|
|
7901
|
+
if (findings.length > 0) {
|
|
7902
|
+
const grouped = {};
|
|
7903
|
+
for (const f of findings) {
|
|
7904
|
+
if (!grouped[f.category]) grouped[f.category] = [];
|
|
7905
|
+
grouped[f.category].push(f);
|
|
7906
|
+
}
|
|
7907
|
+
const categoryOrder = ["secrets", "encryption", "authentication", "injection", "xss", "security", "database", "config", "infrastructure", "dependencies"];
|
|
7908
|
+
for (const cat of categoryOrder) {
|
|
7909
|
+
if (!grouped[cat]) continue;
|
|
7910
|
+
lines.push(`## ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
7911
|
+
`);
|
|
7912
|
+
for (const f of grouped[cat]) {
|
|
7913
|
+
const loc = f.file !== "project" ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : " (project-wide)";
|
|
7914
|
+
lines.push(`- **[${f.severity.toUpperCase()}] ${f.title}**${loc}`);
|
|
7915
|
+
lines.push(` Evidence: ${f.evidence.slice(0, 150)}`);
|
|
7916
|
+
lines.push(` Fix: ${f.fix}`);
|
|
7917
|
+
if (f.controlIds && f.controlIds.length > 0) {
|
|
7918
|
+
lines.push(` Controls: ${f.controlIds.join(", ")}`);
|
|
7919
|
+
}
|
|
7920
|
+
lines.push("");
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
} else {
|
|
7924
|
+
lines.push("**No security findings detected.** All scanned files are clean.\n");
|
|
7925
|
+
}
|
|
7926
|
+
lines.push("## Compliance Score\n");
|
|
7927
|
+
lines.push(`**Overall: ${score.overall}% (Grade: ${score.overall_grade})**
|
|
7928
|
+
`);
|
|
7929
|
+
for (const [fw, data] of Object.entries(score.frameworks)) {
|
|
7930
|
+
lines.push(`- ${fw}: ${data.score}% (${data.grade}) \u2014 ${data.passed_controls}/${data.total_controls} controls passed, ${data.critical_failures} critical failures`);
|
|
7931
|
+
}
|
|
7932
|
+
if (projectConfig.overrides.length > 0) {
|
|
7933
|
+
lines.push(`
|
|
7934
|
+
*Note: ${projectConfig.overrides.length} control overrides applied.*`);
|
|
7935
|
+
}
|
|
7936
|
+
resultText = lines.join("\n");
|
|
7937
|
+
break;
|
|
7938
|
+
}
|
|
7939
|
+
case "generate_compliance_report": {
|
|
7940
|
+
const projectName = args.project_name || "Project";
|
|
7941
|
+
const projectType = args.project_type || "saas";
|
|
7942
|
+
const frameworksStr = args.frameworks || "GDPR,OWASP";
|
|
7943
|
+
const frameworks = frameworksStr.split(",").map((f) => f.trim());
|
|
7944
|
+
resultText = generateFullComplianceReport(projectName, projectType, frameworks);
|
|
7945
|
+
break;
|
|
7946
|
+
}
|
|
7947
|
+
case "generate_audit_report": {
|
|
7948
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
7949
|
+
if (!args.project_path && !fs2.existsSync(path3.join(projectPath, ".ges"))) {
|
|
7950
|
+
const projectName2 = args.project_name || "Project";
|
|
7951
|
+
const projectType2 = "generic-web-application";
|
|
7952
|
+
resultText = generateFullComplianceReport(projectName2, projectType2, ["GDPR", "OWASP"]);
|
|
7953
|
+
resultText += "\n\n**Note: No project path specified and no .ges/ directory found. Showing default compliance report. Provide project_path for actual audit results.**";
|
|
7954
|
+
break;
|
|
7955
|
+
}
|
|
7956
|
+
if (!fs2.existsSync(projectPath)) {
|
|
7957
|
+
resultText = `Project path does not exist: ${projectPath}`;
|
|
7958
|
+
break;
|
|
7959
|
+
}
|
|
7960
|
+
const projectName = args.project_name || path3.basename(projectPath);
|
|
7961
|
+
const projectConfig = loadProjectConfig(projectPath);
|
|
7962
|
+
const config = projectConfig.config;
|
|
7963
|
+
const projectType = config?.project_type || "generic-web-application";
|
|
7964
|
+
const frameworks = config?.frameworks || ["GDPR", "OWASP"];
|
|
7965
|
+
const { findings: rawFindings, scannedFiles } = runAudit(projectPath);
|
|
7966
|
+
const findings = deduplicateFindings(rawFindings);
|
|
7967
|
+
resultText = generateFullComplianceReport(projectName, projectType, frameworks, findings, projectConfig.overrides);
|
|
7968
|
+
resultText += `
|
|
5535
7969
|
|
|
5536
|
-
|
|
7970
|
+
**Audit Details**: Scanned ${scannedFiles} files. ${findings.length} findings detected.`;
|
|
7971
|
+
break;
|
|
7972
|
+
}
|
|
7973
|
+
case "fix_recommendation": {
|
|
7974
|
+
const controlId = args.control_id || "";
|
|
7975
|
+
const findingTitle = args.finding_title;
|
|
7976
|
+
if (!controlId && !findingTitle) {
|
|
7977
|
+
resultText = "Please provide either a control_id (e.g. 'GDPR-ART32-001') or a finding_title to get fix guidance.";
|
|
7978
|
+
break;
|
|
7979
|
+
}
|
|
7980
|
+
resultText = generateFixGuidance(controlId, findingTitle);
|
|
7981
|
+
break;
|
|
7982
|
+
}
|
|
7983
|
+
case "generate_retention_policy": {
|
|
7984
|
+
const name = args.project_name || "Project";
|
|
7985
|
+
resultText = `# Data Retention Policy - ${name}
|
|
5537
7986
|
|
|
5538
|
-
|
|
5539
|
-
|----------|--------|---------------|
|
|
5540
|
-
| User data | Account + 30 days | Contract |
|
|
5541
|
-
| Audit logs | 1 year | Legal obligation |
|
|
5542
|
-
| Session data | Session duration | Operational |
|
|
7987
|
+
## 1. Purpose
|
|
5543
7988
|
|
|
5544
|
-
|
|
5545
|
-
break;
|
|
5546
|
-
}
|
|
5547
|
-
case "generate_incident_response": {
|
|
5548
|
-
const name = args.project_name || "Project";
|
|
5549
|
-
resultText = `# Incident Response Plan - ${name}
|
|
7989
|
+
This policy defines the retention periods for all data categories processed by ${name}.
|
|
5550
7990
|
|
|
5551
|
-
##
|
|
5552
|
-
- P1 (Critical): 15 min response
|
|
5553
|
-
- P2 (High): 1 hour response
|
|
5554
|
-
- P3 (Medium): 4 hour response
|
|
7991
|
+
## 2. Retention Periods
|
|
5555
7992
|
|
|
5556
|
-
|
|
5557
|
-
|
|
7993
|
+
| Category | Period | Justification | Legal Basis |
|
|
7994
|
+
|----------|--------|---------------|-------------|
|
|
7995
|
+
| User account data | Account lifetime + 30 days | Contract fulfillment | Art. 6(1)(b) |
|
|
7996
|
+
| Email addresses | Account lifetime + 30 days | Communication | Art. 6(1)(b) |
|
|
7997
|
+
| Authentication data | Session duration | Security | Art. 6(1)(f) |
|
|
7998
|
+
| IP addresses | 30 days | Security monitoring | Art. 6(1)(f) |
|
|
7999
|
+
| Audit logs | 1 year | Legal obligation | Art. 6(1)(c) |
|
|
8000
|
+
| Session data | Session duration | Operational | Art. 6(1)(b) |
|
|
8001
|
+
| Marketing consent | Until withdrawal | Consent | Art. 6(1)(a) |
|
|
8002
|
+
| Support tickets | 2 years | Quality assurance | Art. 6(1)(f) |
|
|
5558
8003
|
|
|
5559
|
-
##
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
##
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
8004
|
+
## 3. Deletion Process
|
|
8005
|
+
|
|
8006
|
+
1. Automated deletion: Data past retention period is flagged for deletion
|
|
8007
|
+
2. Deletion verification: Monthly audit of deletion jobs
|
|
8008
|
+
3. Backup purge: Backups containing expired data are purged within 90 days
|
|
8009
|
+
4. Deletion log: All deletions are logged with timestamp and scope
|
|
8010
|
+
|
|
8011
|
+
## 4. Exceptions
|
|
8012
|
+
|
|
8013
|
+
- Data subject to legal hold: Retained until hold is lifted
|
|
8014
|
+
- Data required for ongoing legal proceedings: Retained until proceedings conclude
|
|
8015
|
+
- Anonymized data may be retained indefinitely for statistical purposes
|
|
8016
|
+
|
|
8017
|
+
## 5. Data Subject Rights
|
|
8018
|
+
|
|
8019
|
+
- Users can request early deletion via the data subject rights process
|
|
8020
|
+
- Right to erasure (Article 17) requests are processed within 30 days
|
|
8021
|
+
- Verification of identity is required before any deletion
|
|
8022
|
+
|
|
8023
|
+
## 6. Review Schedule
|
|
8024
|
+
|
|
8025
|
+
This policy is reviewed quarterly and updated as needed.
|
|
8026
|
+
|
|
8027
|
+
Last reviewed: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
8028
|
+
break;
|
|
8029
|
+
}
|
|
8030
|
+
case "generate_incident_response": {
|
|
8031
|
+
const name = args.project_name || "Project";
|
|
8032
|
+
resultText = `# Incident Response Plan - ${name}
|
|
8033
|
+
|
|
8034
|
+
## 1. Severity Levels
|
|
8035
|
+
|
|
8036
|
+
| Level | Response Time | Examples |
|
|
8037
|
+
|-------|--------------|----------|
|
|
8038
|
+
| P1 (Critical) | 15 minutes | Data breach, system compromise, ransomware |
|
|
8039
|
+
| P2 (High) | 1 hour | Unauthorized access, vulnerability exploitation |
|
|
8040
|
+
| P3 (Medium) | 4 hours | Suspicious activity, policy violation |
|
|
8041
|
+
| P4 (Low) | 24 hours | Minor misconfiguration, informational findings |
|
|
8042
|
+
|
|
8043
|
+
## 2. Response Team
|
|
8044
|
+
|
|
8045
|
+
| Role | Responsibility |
|
|
8046
|
+
|------|---------------|
|
|
8047
|
+
| Incident Commander | Overall coordination and decision making |
|
|
8048
|
+
| Security Lead | Technical investigation and containment |
|
|
8049
|
+
| Communications Lead | Internal and external notifications |
|
|
8050
|
+
| Legal Advisor | Regulatory and legal compliance |
|
|
8051
|
+
| DPO (if applicable) | GDPR compliance and data subject notification |
|
|
8052
|
+
|
|
8053
|
+
## 3. Response Process
|
|
8054
|
+
|
|
8055
|
+
### Phase 1: Detection & Identification
|
|
8056
|
+
- Alert triggered by monitoring, user report, or external notification
|
|
8057
|
+
- Initial assessment of scope and severity
|
|
8058
|
+
- Assign severity level (P1-P4)
|
|
8059
|
+
|
|
8060
|
+
### Phase 2: Containment
|
|
8061
|
+
- Isolate affected systems
|
|
8062
|
+
- Preserve evidence for forensic analysis
|
|
8063
|
+
- Implement temporary controls
|
|
8064
|
+
|
|
8065
|
+
### Phase 3: Eradication
|
|
8066
|
+
- Identify root cause
|
|
8067
|
+
- Remove threat from all systems
|
|
8068
|
+
- Patch vulnerabilities
|
|
8069
|
+
|
|
8070
|
+
### Phase 4: Recovery
|
|
8071
|
+
- Restore systems from verified backups
|
|
8072
|
+
- Verify system integrity
|
|
8073
|
+
- Resume normal operations with enhanced monitoring
|
|
8074
|
+
|
|
8075
|
+
### Phase 5: Post-Incident Review
|
|
8076
|
+
- Document timeline and actions taken
|
|
8077
|
+
- Identify lessons learned
|
|
8078
|
+
- Update security controls and processes
|
|
8079
|
+
- Update this plan if needed
|
|
8080
|
+
|
|
8081
|
+
## 4. GDPR Breach Notification
|
|
8082
|
+
|
|
8083
|
+
**72-hour rule**: If a breach is likely to result in a risk to data subjects:
|
|
8084
|
+
1. Notify supervisory authority within 72 hours (Article 33)
|
|
8085
|
+
2. If high risk: Notify affected data subjects without undue delay (Article 34)
|
|
8086
|
+
3. Document all actions in the breach register
|
|
8087
|
+
|
|
8088
|
+
### Notification Template
|
|
8089
|
+
- Nature of the breach
|
|
8090
|
+
- Categories and approximate number of data subjects
|
|
8091
|
+
- Likely consequences
|
|
8092
|
+
- Measures taken or proposed
|
|
8093
|
+
|
|
8094
|
+
## 5. Communication Templates
|
|
8095
|
+
|
|
8096
|
+
### Internal Notification
|
|
8097
|
+
Subject: [P-level] Security Incident - [Brief Description]
|
|
8098
|
+
- What: [Description]
|
|
8099
|
+
- When: [Detection time]
|
|
8100
|
+
- Impact: [Known impact]
|
|
8101
|
+
- Actions: [Current containment measures]
|
|
8102
|
+
- Next update: [Time]
|
|
8103
|
+
|
|
8104
|
+
### Regulatory Notification
|
|
8105
|
+
Addressed to: [Supervisory Authority]
|
|
8106
|
+
- DPO contact: [Name, email, phone]
|
|
8107
|
+
- Breach description: [Details]
|
|
8108
|
+
- Affected individuals: [Number and categories]
|
|
8109
|
+
- Measures taken: [Containment and remediation]
|
|
8110
|
+
|
|
8111
|
+
## 6. Testing
|
|
8112
|
+
|
|
8113
|
+
- Tabletop exercises: Quarterly
|
|
8114
|
+
- Full simulation: Annually
|
|
8115
|
+
- Plan review: After each incident and at least semi-annually
|
|
8116
|
+
|
|
8117
|
+
Last reviewed: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
8118
|
+
break;
|
|
8119
|
+
}
|
|
8120
|
+
case "generate_risk_assessment": {
|
|
8121
|
+
const name = args.project_name || "Project";
|
|
8122
|
+
resultText = `# Risk Assessment - ${name}
|
|
8123
|
+
|
|
8124
|
+
## 1. Methodology
|
|
8125
|
+
|
|
8126
|
+
Risk assessment follows the GESF methodology based on ISO 27005 and NIST SP 800-30.
|
|
8127
|
+
|
|
8128
|
+
Risk Score = Likelihood \xD7 Impact
|
|
8129
|
+
|
|
8130
|
+
| Rating | Score |
|
|
8131
|
+
|--------|-------|
|
|
8132
|
+
| Critical | 5 |
|
|
8133
|
+
| High | 4 |
|
|
8134
|
+
| Medium | 3 |
|
|
8135
|
+
| Low | 2 |
|
|
8136
|
+
| Negligible | 1 |
|
|
8137
|
+
|
|
8138
|
+
## 2. Risk Register
|
|
8139
|
+
|
|
8140
|
+
| ID | Risk | Likelihood | Impact | Score | Mitigation | Residual |
|
|
8141
|
+
|----|------|-----------|--------|-------|------------|----------|
|
|
8142
|
+
| R001 | Data breach (external) | Medium (3) | Critical (5) | 15 | Encryption, MFA, WAF, pen testing | Medium |
|
|
8143
|
+
| R002 | Insider threat | Low (2) | High (4) | 8 | RBAC, audit logging, DLP | Low |
|
|
8144
|
+
| R003 | Data loss | Low (2) | Critical (5) | 10 | Backups, DR plan, replication | Low |
|
|
8145
|
+
| R004 | Ransomware | Low (2) | Critical (5) | 10 | Backups, EDR, email filtering | Low |
|
|
8146
|
+
| R005 | Supply chain attack | Medium (3) | High (4) | 12 | Dependency scanning, SBOM, vendor assessment | Medium |
|
|
8147
|
+
| R006 | Misconfiguration | Medium (3) | High (4) | 12 | IaC scanning, security review, hardening | Medium |
|
|
8148
|
+
| R007 | Credential compromise | Medium (3) | High (4) | 12 | MFA, password policy, monitoring | Low |
|
|
8149
|
+
| R008 | DDoS attack | Low (2) | Medium (3) | 6 | CDN, rate limiting, WAF | Low |
|
|
8150
|
+
| R009 | Non-compliance (GDPR) | Medium (3) | High (4) | 12 | Regular audits, compliance scanning | Low |
|
|
8151
|
+
| R010 | Third-party data breach | Medium (3) | High (4) | 12 | DPA requirements, vendor assessment | Medium |
|
|
8152
|
+
|
|
8153
|
+
## 3. Risk Treatment Plan
|
|
8154
|
+
|
|
8155
|
+
| ID | Treatment | Owner | Deadline | Status |
|
|
8156
|
+
|----|-----------|-------|----------|--------|
|
|
8157
|
+
| R001 | Implement WAF + annual pen testing | Security Lead | Quarterly | In progress |
|
|
8158
|
+
| R002 | Deploy DLP solution | Security Lead | Q2 | Planned |
|
|
8159
|
+
| R003 | Test DR plan monthly | Platform Lead | Monthly | In progress |
|
|
8160
|
+
| R005 | Automate dependency scanning | DevOps | Q1 | In progress |
|
|
8161
|
+
| R006 | Implement IaC security scanning | DevOps | Q2 | Planned |
|
|
8162
|
+
| R007 | Enforce MFA for all users | Security Lead | Q1 | Done |
|
|
8163
|
+
| R009 | Monthly compliance audits | Compliance Lead | Monthly | In progress |
|
|
8164
|
+
|
|
8165
|
+
## 4. Acceptance Criteria
|
|
8166
|
+
|
|
8167
|
+
Risks with residual score > 12 require executive sign-off.
|
|
8168
|
+
All critical risks must have active mitigation plans.
|
|
8169
|
+
|
|
8170
|
+
## 5. Review Schedule
|
|
8171
|
+
|
|
8172
|
+
- Full assessment: Annually
|
|
8173
|
+
- Risk register review: Quarterly
|
|
8174
|
+
- After any significant change or incident
|
|
8175
|
+
|
|
8176
|
+
Last reviewed: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
8177
|
+
break;
|
|
8178
|
+
}
|
|
8179
|
+
case "generate_dpa": {
|
|
8180
|
+
const name = args.project_name || "Project";
|
|
8181
|
+
resultText = `# Data Processing Agreement - ${name}
|
|
8182
|
+
|
|
8183
|
+
## 1. Parties
|
|
8184
|
+
|
|
8185
|
+
**Controller**: [Company Name]
|
|
8186
|
+
Address: [Address]
|
|
8187
|
+
DPO: [Name, Email]
|
|
8188
|
+
|
|
8189
|
+
**Processor**: [Service Provider Name]
|
|
8190
|
+
Address: [Address]
|
|
8191
|
+
DPO: [Name, Email]
|
|
8192
|
+
|
|
8193
|
+
## 2. Subject Matter and Duration
|
|
8194
|
+
|
|
8195
|
+
This Agreement governs the processing of personal data by the Processor on behalf of the Controller in connection with the provision of services for **${name}**.
|
|
8196
|
+
|
|
8197
|
+
**Duration**: Effective from the date of signature until termination of the underlying service agreement.
|
|
8198
|
+
|
|
8199
|
+
## 3. Details of Processing
|
|
8200
|
+
|
|
8201
|
+
| Category | Type | Purpose |
|
|
8202
|
+
|----------|------|--------|
|
|
8203
|
+
| User data | Personal | Service delivery |
|
|
8204
|
+
| Authentication data | Personal | Access control |
|
|
8205
|
+
| Usage data | Operational | Analytics |
|
|
8206
|
+
| Communication data | Personal | Support |
|
|
8207
|
+
|
|
8208
|
+
## 4. Obligations of the Processor
|
|
8209
|
+
|
|
8210
|
+
The Processor shall:
|
|
8211
|
+
|
|
8212
|
+
1. Process data only on documented instructions from the Controller
|
|
8213
|
+
2. Ensure confidentiality of all persons authorized to process personal data
|
|
8214
|
+
3. Implement appropriate technical and organizational measures (Article 32)
|
|
8215
|
+
4. Not engage sub-processors without prior authorization
|
|
8216
|
+
5. Assist the Controller in responding to data subject rights requests
|
|
8217
|
+
6. Assist the Controller in ensuring compliance with Articles 32-36
|
|
8218
|
+
7. Delete or return all personal data upon termination
|
|
8219
|
+
8. Make available all information necessary to demonstrate compliance
|
|
8220
|
+
9. Allow and contribute to audits conducted by the Controller or mandated auditor
|
|
8221
|
+
|
|
8222
|
+
## 5. Security Measures (Article 32)
|
|
8223
|
+
|
|
8224
|
+
- Encryption of personal data at rest (AES-256-GCM)
|
|
8225
|
+
- Encryption of personal data in transit (TLS 1.2+)
|
|
8226
|
+
- Access controls with principle of least privilege
|
|
8227
|
+
- Regular security testing and vulnerability assessments
|
|
8228
|
+
- Incident response plan with 72-hour notification
|
|
8229
|
+
- Audit logging with immutable records
|
|
8230
|
+
- Regular backup and disaster recovery testing
|
|
8231
|
+
|
|
8232
|
+
## 6. Sub-Processors
|
|
8233
|
+
|
|
8234
|
+
| Sub-Processor | Purpose | Location |
|
|
8235
|
+
|-------------|---------|----------|
|
|
8236
|
+
| [Cloud Provider] | Hosting | [Country] |
|
|
8237
|
+
| [Email Provider] | Communications | [Country] |
|
|
8238
|
+
|
|
8239
|
+
The Controller authorizes the use of the above sub-processors. Any changes will be notified 30 days in advance.
|
|
8240
|
+
|
|
8241
|
+
## 7. Data Breach Notification
|
|
8242
|
+
|
|
8243
|
+
The Processor shall notify the Controller within 24 hours of becoming aware of a personal data breach, providing:
|
|
8244
|
+
- Nature of the breach including categories and approximate numbers
|
|
8245
|
+
- Name and contact details of the DPO
|
|
8246
|
+
- Likely consequences of the breach
|
|
8247
|
+
- Measures taken or proposed to address the breach
|
|
8248
|
+
|
|
8249
|
+
## 8. Data Subject Rights
|
|
8250
|
+
|
|
8251
|
+
The Processor shall assist the Controller in fulfilling its obligations to respond to data subject requests for:
|
|
8252
|
+
- Access (Article 15)
|
|
8253
|
+
- Rectification (Article 16)
|
|
8254
|
+
- Erasure (Article 17)
|
|
8255
|
+
- Restriction (Article 18)
|
|
8256
|
+
- Data portability (Article 20)
|
|
8257
|
+
- Objection (Article 21)
|
|
8258
|
+
|
|
8259
|
+
## 9. International Transfers
|
|
8260
|
+
|
|
8261
|
+
Any transfer of personal data outside the EEA shall be subject to:
|
|
8262
|
+
- Adequacy decision by the European Commission, OR
|
|
8263
|
+
- Standard Contractual Clauses (SCCs), OR
|
|
8264
|
+
- Binding Corporate Rules (BCRs)
|
|
8265
|
+
|
|
8266
|
+
## 10. Termination
|
|
8267
|
+
|
|
8268
|
+
Upon termination:
|
|
8269
|
+
1. Processor shall return all personal data to the Controller within 30 days
|
|
8270
|
+
2. If return is not possible, Processor shall delete all personal data
|
|
8271
|
+
3. Processor shall certify deletion in writing
|
|
8272
|
+
|
|
8273
|
+
## 11. Liability and Indemnification
|
|
8274
|
+
|
|
8275
|
+
Each party's liability shall be governed by the underlying service agreement and applicable GDPR provisions.
|
|
8276
|
+
|
|
8277
|
+
## 12. Governing Law
|
|
8278
|
+
|
|
8279
|
+
This Agreement shall be governed by [Applicable Jurisdiction].
|
|
8280
|
+
|
|
8281
|
+
---
|
|
8282
|
+
|
|
8283
|
+
**Signed:**
|
|
8284
|
+
|
|
8285
|
+
Controller: _________________________ Date: ____________
|
|
8286
|
+
|
|
8287
|
+
Processor: _________________________ Date: ____________`;
|
|
8288
|
+
break;
|
|
8289
|
+
}
|
|
8290
|
+
case "generate_data_inventory": {
|
|
8291
|
+
const projectName = args.project_name || "Project";
|
|
8292
|
+
const projectType = args.project_type || "saas";
|
|
8293
|
+
resultText = generateDataInventory(projectName, projectType);
|
|
8294
|
+
break;
|
|
8295
|
+
}
|
|
8296
|
+
case "generate_processing_records": {
|
|
8297
|
+
const projectName = args.project_name || "Project";
|
|
8298
|
+
const controllerName = args.controller_name || "[Organization Name]";
|
|
8299
|
+
resultText = generateProcessingRecords(projectName, controllerName);
|
|
8300
|
+
break;
|
|
8301
|
+
}
|
|
8302
|
+
case "auto_fix": {
|
|
8303
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
8304
|
+
const dryRun = args.dry_run === "true";
|
|
8305
|
+
const ruleFilter = args.rule_ids ? new Set(args.rule_ids.split(",").map((r) => r.trim())) : void 0;
|
|
8306
|
+
if (!fs2.existsSync(projectPath)) {
|
|
8307
|
+
resultText = `Project path does not exist: ${projectPath}`;
|
|
8308
|
+
break;
|
|
8309
|
+
}
|
|
8310
|
+
const { findings: rawFindings, scannedFiles } = runAudit(projectPath);
|
|
8311
|
+
const findings = deduplicateFindings(rawFindings);
|
|
8312
|
+
if (findings.length === 0) {
|
|
8313
|
+
resultText = `# Auto-Fix Report
|
|
8314
|
+
|
|
8315
|
+
**Project**: ${projectPath}
|
|
8316
|
+
**Scanned**: ${scannedFiles} files
|
|
8317
|
+
|
|
8318
|
+
No issues found. Project is clean!`;
|
|
8319
|
+
break;
|
|
8320
|
+
}
|
|
8321
|
+
const { actions, warnings } = createAutoFixPlan(projectPath, findings, ruleFilter);
|
|
8322
|
+
if (actions.length === 0) {
|
|
8323
|
+
const lines2 = [
|
|
8324
|
+
`# Auto-Fix Report
|
|
8325
|
+
`,
|
|
8326
|
+
`**Project**: ${projectPath}`,
|
|
8327
|
+
`**Scanned**: ${scannedFiles} files`,
|
|
8328
|
+
`**Findings**: ${findings.length}
|
|
8329
|
+
`,
|
|
8330
|
+
`## No Auto-Fixable Issues
|
|
8331
|
+
`,
|
|
8332
|
+
`All ${findings.length} findings require manual review:
|
|
8333
|
+
`
|
|
8334
|
+
];
|
|
8335
|
+
for (const w of warnings) lines2.push(`- ${w}`);
|
|
8336
|
+
for (const f of findings.slice(0, 10)) {
|
|
8337
|
+
lines2.push(`- [${f.severity.toUpperCase()}] ${f.title} (${f.file}${f.line ? `:${f.line}` : ""})`);
|
|
8338
|
+
}
|
|
8339
|
+
resultText = lines2.join("\n");
|
|
8340
|
+
break;
|
|
8341
|
+
}
|
|
8342
|
+
const npmInstalls = getNpmInstallsFromActions(actions);
|
|
8343
|
+
const lines = [
|
|
8344
|
+
`# Auto-Fix Report
|
|
8345
|
+
`,
|
|
8346
|
+
`**Project**: ${projectPath}`,
|
|
8347
|
+
`**Scanned**: ${scannedFiles} files`,
|
|
8348
|
+
`**Findings**: ${findings.length} total`,
|
|
8349
|
+
`**Auto-fixable**: ${actions.length} actions`,
|
|
8350
|
+
`**Manual review**: ${warnings.length} items`,
|
|
8351
|
+
dryRun ? `**Mode**: DRY RUN (no changes applied)
|
|
8352
|
+
` : ""
|
|
8353
|
+
];
|
|
8354
|
+
if (dryRun) {
|
|
8355
|
+
lines.push("## Planned Actions (dry run)\n");
|
|
8356
|
+
} else {
|
|
8357
|
+
lines.push("## Applied Fixes\n");
|
|
8358
|
+
}
|
|
8359
|
+
let applied = 0;
|
|
8360
|
+
let failed = 0;
|
|
8361
|
+
for (const action of actions) {
|
|
8362
|
+
if (dryRun) {
|
|
8363
|
+
lines.push(`- [${action.type}] ${action.filePath}: ${action.description}`);
|
|
8364
|
+
applied++;
|
|
8365
|
+
} else {
|
|
8366
|
+
const result = applyAutoFixAction(projectPath, action);
|
|
8367
|
+
if (result.applied) {
|
|
8368
|
+
applied++;
|
|
8369
|
+
lines.push(`- \u2713 [${action.type}] ${action.filePath}: ${action.description}`);
|
|
8370
|
+
} else {
|
|
8371
|
+
failed++;
|
|
8372
|
+
lines.push(`- \u2717 [${action.type}] ${action.filePath}: ${action.description} \u2014 ${result.error}`);
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
8376
|
+
lines.push(`
|
|
8377
|
+
## Summary
|
|
8378
|
+
`);
|
|
8379
|
+
lines.push(`- Actions applied: ${applied}${failed > 0 ? ` (${failed} failed)` : ""}`);
|
|
8380
|
+
if (npmInstalls.length > 0) {
|
|
8381
|
+
lines.push(`
|
|
8382
|
+
## npm Packages to Install
|
|
8383
|
+
`);
|
|
8384
|
+
lines.push("```bash");
|
|
8385
|
+
lines.push(`npm install ${npmInstalls.join(" ")}`);
|
|
8386
|
+
lines.push("```\n");
|
|
8387
|
+
lines.push("Or if using pnpm:");
|
|
8388
|
+
lines.push("```bash");
|
|
8389
|
+
lines.push(`pnpm add ${npmInstalls.join(" ")}`);
|
|
8390
|
+
lines.push("```");
|
|
8391
|
+
}
|
|
8392
|
+
if (warnings.length > 0) {
|
|
8393
|
+
lines.push(`
|
|
8394
|
+
## Manual Review Required
|
|
8395
|
+
`);
|
|
8396
|
+
for (const w of warnings) lines.push(`- ${w}`);
|
|
8397
|
+
}
|
|
8398
|
+
lines.push(`
|
|
8399
|
+
## Next Steps`);
|
|
8400
|
+
lines.push("1. Install the npm packages listed above");
|
|
8401
|
+
lines.push("2. Review all changes with `git diff`");
|
|
8402
|
+
lines.push("3. Run `ges audit` to verify fixes");
|
|
8403
|
+
lines.push("4. Address remaining manual review items");
|
|
8404
|
+
lines.push("5. Use `fix_recommendation` tool for detailed guidance on manual items");
|
|
8405
|
+
resultText = lines.join("\n");
|
|
8406
|
+
break;
|
|
8407
|
+
}
|
|
8408
|
+
case "apply_control_override": {
|
|
8409
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
8410
|
+
const controlId = args.control_id || "";
|
|
8411
|
+
const status = args.status || "not-applicable";
|
|
8412
|
+
const reason = args.reason || "";
|
|
8413
|
+
if (!controlId) {
|
|
8414
|
+
resultText = "Error: control_id is required.";
|
|
8415
|
+
break;
|
|
8416
|
+
}
|
|
8417
|
+
if (!["not-applicable", "pass"].includes(status)) {
|
|
8418
|
+
resultText = `Error: status must be 'not-applicable' or 'pass'. Got: ${status}`;
|
|
8419
|
+
break;
|
|
8420
|
+
}
|
|
8421
|
+
if (!fs2.existsSync(path3.join(projectPath, ".ges"))) {
|
|
8422
|
+
resultText = `Error: No .ges/ directory at ${projectPath}. Run 'ges init' first.`;
|
|
8423
|
+
break;
|
|
8424
|
+
}
|
|
8425
|
+
const overridePath = path3.join(projectPath, ".ges", "control-overrides.json");
|
|
8426
|
+
let overrides = [];
|
|
8427
|
+
if (fs2.existsSync(overridePath)) {
|
|
8428
|
+
const parsed = readJsonFileSafe(overridePath);
|
|
8429
|
+
if (Array.isArray(parsed)) overrides = parsed;
|
|
8430
|
+
}
|
|
8431
|
+
const existing = overrides.findIndex((o) => o.control_id === controlId);
|
|
8432
|
+
if (existing >= 0) {
|
|
8433
|
+
overrides[existing] = { control_id: controlId, status, reason };
|
|
8434
|
+
} else {
|
|
8435
|
+
overrides.push({ control_id: controlId, status, reason });
|
|
8436
|
+
}
|
|
8437
|
+
const dir = path3.dirname(overridePath);
|
|
8438
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
8439
|
+
fs2.writeFileSync(overridePath, JSON.stringify(overrides, null, 2), "utf-8");
|
|
8440
|
+
const lines = [
|
|
8441
|
+
`# Control Override Applied
|
|
8442
|
+
`,
|
|
8443
|
+
`**Control**: ${controlId}`,
|
|
8444
|
+
`**Status**: ${status}`,
|
|
8445
|
+
`**Reason**: ${reason || "(none provided)"}`,
|
|
8446
|
+
`**File**: ${overridePath}`,
|
|
8447
|
+
`**Total overrides**: ${overrides.length}
|
|
8448
|
+
`,
|
|
8449
|
+
`The override will take effect on the next \`ges audit\` or \`ges score\` run.`,
|
|
8450
|
+
`
|
|
8451
|
+
Run \`ges audit\` then \`ges score\` to see the updated compliance score.`
|
|
8452
|
+
];
|
|
8453
|
+
resultText = lines.join("\n");
|
|
8454
|
+
break;
|
|
8455
|
+
}
|
|
8456
|
+
case "implement_control": {
|
|
8457
|
+
const projectPath = resolveProjectPath(args.project_path);
|
|
8458
|
+
const controlId = args.control_id || "";
|
|
8459
|
+
if (!controlId) {
|
|
8460
|
+
resultText = "Error: control_id is required. Example: GDPR-ART32-002, GDPR-ART32-006, AUTH-002";
|
|
8461
|
+
break;
|
|
8462
|
+
}
|
|
8463
|
+
if (!fs2.existsSync(projectPath)) {
|
|
8464
|
+
resultText = `Error: Project path does not exist: ${projectPath}`;
|
|
8465
|
+
break;
|
|
8466
|
+
}
|
|
8467
|
+
const hasSrc = fs2.existsSync(path3.join(projectPath, "src"));
|
|
8468
|
+
const appFile = findMainAppFile(projectPath);
|
|
8469
|
+
const lines = [`# Implement Control: ${controlId}
|
|
8470
|
+
`];
|
|
8471
|
+
const actions = [];
|
|
8472
|
+
const controlMap = {
|
|
8473
|
+
"GDPR-ART32-002": {
|
|
8474
|
+
name: "Encryption at Rest",
|
|
8475
|
+
actions: buildEncryptionAtRestImpl(projectPath, hasSrc),
|
|
8476
|
+
warnings: ["Configure encryption keys via environment variables or a vault service."]
|
|
8477
|
+
},
|
|
8478
|
+
"GDPR-ART32-003": {
|
|
8479
|
+
name: "Encryption in Transit",
|
|
8480
|
+
actions: buildEncryptionInTransitImpl(projectPath, hasSrc),
|
|
8481
|
+
warnings: ["Ensure your server/infrastructure is configured with TLS certificates."]
|
|
8482
|
+
},
|
|
8483
|
+
"GDPR-ART32-004": {
|
|
8484
|
+
name: "Unique User Identification",
|
|
8485
|
+
actions: buildUserIdentificationImpl(projectPath, hasSrc),
|
|
8486
|
+
warnings: ["Integrate the auth middleware into your routes."]
|
|
8487
|
+
},
|
|
8488
|
+
"GDPR-ART32-005": {
|
|
8489
|
+
name: "Automatic Session Timeout",
|
|
8490
|
+
actions: buildSessionTimeoutFix(projectPath),
|
|
8491
|
+
warnings: []
|
|
8492
|
+
},
|
|
8493
|
+
"GDPR-ART32-006": {
|
|
8494
|
+
name: "Audit Logging",
|
|
8495
|
+
actions: buildLoggingFix(projectPath),
|
|
8496
|
+
warnings: ["Use auditLog() for all security-relevant actions."]
|
|
8497
|
+
},
|
|
8498
|
+
"GDPR-ART32-007": {
|
|
8499
|
+
name: "Integrity Controls",
|
|
8500
|
+
actions: buildIntegrityControlsImpl(projectPath, hasSrc),
|
|
8501
|
+
warnings: ["Apply integrity hashing to all critical data flows."]
|
|
8502
|
+
},
|
|
8503
|
+
"GDPR-ART32-008": {
|
|
8504
|
+
name: "Backup and Recovery",
|
|
8505
|
+
actions: buildBackupPolicyImpl(projectPath, hasSrc),
|
|
8506
|
+
warnings: ["Test your backup recovery process monthly."]
|
|
8507
|
+
},
|
|
8508
|
+
"GDPR-ART32-009": {
|
|
8509
|
+
name: "Regular Security Testing",
|
|
8510
|
+
actions: buildSecurityTestingImpl(projectPath),
|
|
8511
|
+
warnings: ["Schedule regular security scans in CI/CD."]
|
|
8512
|
+
}
|
|
8513
|
+
};
|
|
8514
|
+
const plan = controlMap[controlId];
|
|
8515
|
+
if (!plan) {
|
|
8516
|
+
resultText = `Control ${controlId} does not have an auto-implementation. Use \`fix_recommendation\` for manual guidance.
|
|
8517
|
+
|
|
8518
|
+
Available auto-implementations: ${Object.keys(controlMap).join(", ")}`;
|
|
8519
|
+
break;
|
|
8520
|
+
}
|
|
8521
|
+
lines.push(`**Control**: ${plan.name}
|
|
8522
|
+
`);
|
|
8523
|
+
for (const action of plan.actions) {
|
|
8524
|
+
const result = applyAutoFixAction(projectPath, action);
|
|
8525
|
+
if (result.applied) {
|
|
8526
|
+
lines.push(`- \u2713 [${action.type}] ${action.filePath}: ${action.description}`);
|
|
8527
|
+
} else if (result.error === "File already exists") {
|
|
8528
|
+
lines.push(`- \u2192 [${action.type}] ${action.filePath}: Already exists (skipped)`);
|
|
8529
|
+
} else {
|
|
8530
|
+
lines.push(`- \u2717 [${action.type}] ${action.filePath}: ${result.error}`);
|
|
8531
|
+
}
|
|
8532
|
+
}
|
|
8533
|
+
const npmInstalls = getNpmInstallsFromActions(plan.actions);
|
|
8534
|
+
if (npmInstalls.length > 0) {
|
|
8535
|
+
lines.push(`
|
|
8536
|
+
## Install Dependencies
|
|
8537
|
+
`);
|
|
8538
|
+
lines.push("```bash");
|
|
8539
|
+
lines.push(`npm install ${npmInstalls.join(" ")}`);
|
|
8540
|
+
lines.push("```");
|
|
8541
|
+
}
|
|
8542
|
+
if (plan.warnings.length > 0) {
|
|
8543
|
+
lines.push(`
|
|
8544
|
+
## Notes`);
|
|
8545
|
+
for (const w of plan.warnings) lines.push(`- ${w}`);
|
|
8546
|
+
}
|
|
8547
|
+
lines.push(`
|
|
8548
|
+
## Next Steps`);
|
|
8549
|
+
lines.push("1. Install any npm packages listed above");
|
|
8550
|
+
lines.push("2. Import and integrate the generated files into your app");
|
|
8551
|
+
lines.push("3. Run `ges audit` to verify the control is now passing");
|
|
8552
|
+
lines.push(`4. Or use \`apply_control_override\` with control_id="${controlId}" if verified manually`);
|
|
8553
|
+
resultText = lines.join("\n");
|
|
8554
|
+
break;
|
|
8555
|
+
}
|
|
8556
|
+
default:
|
|
8557
|
+
return {
|
|
8558
|
+
jsonrpc: "2.0",
|
|
8559
|
+
id: request.id,
|
|
8560
|
+
error: { code: -32601, message: `Unknown tool: ${toolName}` }
|
|
8561
|
+
};
|
|
5596
8562
|
}
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
8563
|
+
} catch (err) {
|
|
8564
|
+
return {
|
|
8565
|
+
jsonrpc: "2.0",
|
|
8566
|
+
id: request.id,
|
|
8567
|
+
result: {
|
|
8568
|
+
content: [{
|
|
8569
|
+
type: "text",
|
|
8570
|
+
text: `Error executing tool '${toolName}': ${err instanceof Error ? err.message : String(err)}. Check your parameters and try again.`
|
|
8571
|
+
}]
|
|
8572
|
+
}
|
|
8573
|
+
};
|
|
5603
8574
|
}
|
|
5604
8575
|
return {
|
|
5605
8576
|
jsonrpc: "2.0",
|