@greenarmor/ges-mcp-server 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  GESF MCP Server — AI Compliance Assistant for GDPR, OWASP, NIST, and CIS frameworks.
4
4
 
5
- An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that provides compliance checking, policy generation, and risk assessment tools to any MCP-compatible AI code assistant.
5
+ An MCP (Model Context Protocol) server that provides compliance checking, policy generation, and risk assessment tools to any MCP-compatible AI code assistant.
6
6
 
7
7
  ## Tools
8
8
 
@@ -33,9 +33,7 @@ Add to `.vscode/mcp.json` in your project:
33
33
  }
34
34
  ```
35
35
 
36
- Or use the one-click install link:
37
-
38
- [Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22gesf%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40greenarmor%2Fges-mcp-server%22%5D%7D)
36
+ Or use the manual JSON configuration shown above.
39
37
 
40
38
  ### Claude Desktop
41
39
 
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { handleRequest } from "./server.js";
2
2
  export type { MCPRequest, MCPResponse } from "./server.js";
3
+ export { createAutoFixPlan, applyAutoFixAction, getNpmInstallsFromActions } from "./server.js";
4
+ export type { AutoFixAction, AutoFixResult } from "./server.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { handleRequest } from "./server.js";
2
+ export { createAutoFixPlan, applyAutoFixAction, getNpmInstallsFromActions } from "./server.js";
package/dist/server.d.ts CHANGED
@@ -1,4 +1,19 @@
1
1
  #!/usr/bin/env node
2
+ import type { Finding } from "@greenarmor/ges-audit-engine";
3
+ export type AutoFixAction = {
4
+ type: "create" | "modify" | "append" | "npm-install";
5
+ filePath: string;
6
+ content?: string;
7
+ search?: string;
8
+ replace?: string;
9
+ description: string;
10
+ ruleId: string;
11
+ };
12
+ export type AutoFixResult = {
13
+ applied: boolean;
14
+ action: AutoFixAction;
15
+ error?: string;
16
+ };
2
17
  export interface MCPRequest {
3
18
  jsonrpc: string;
4
19
  id?: number | string | null;
@@ -15,4 +30,10 @@ export interface MCPResponse {
15
30
  data?: unknown;
16
31
  };
17
32
  }
33
+ export declare function createAutoFixPlan(root: string, findings: Finding[], filterRuleIds?: Set<string>): {
34
+ actions: AutoFixAction[];
35
+ warnings: string[];
36
+ };
37
+ export declare function applyAutoFixAction(root: string, action: AutoFixAction): AutoFixResult;
38
+ export declare function getNpmInstallsFromActions(actions: AutoFixAction[]): string[];
18
39
  export declare function handleRequest(request: MCPRequest): MCPResponse | null;
package/dist/server.js CHANGED
@@ -595,7 +595,7 @@ function generateImplementationSteps(control) {
595
595
  }
596
596
  return steps;
597
597
  }
598
- function createAutoFixPlan(root, findings, filterRuleIds) {
598
+ export function createAutoFixPlan(root, findings, filterRuleIds) {
599
599
  const actions = [];
600
600
  const warnings = [];
601
601
  const processedRules = new Set();
@@ -669,7 +669,7 @@ function createAutoFixPlan(root, findings, filterRuleIds) {
669
669
  }
670
670
  return { actions, warnings };
671
671
  }
672
- function applyAutoFixAction(root, action) {
672
+ export function applyAutoFixAction(root, action) {
673
673
  const fullPath = path.join(root, action.filePath);
674
674
  try {
675
675
  switch (action.type) {
@@ -1045,32 +1045,10 @@ function buildHelmetFix(root) {
1045
1045
  else if (lang === "rust") {
1046
1046
  const appFile = findMainAppFile(root) || "src/main.rs";
1047
1047
  if (fw === "actix") {
1048
- actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: `use actix_web::{HttpResponse, dev::{ServiceRequest, Service, ServiceResponse}};
1049
-
1050
- pub fn add_security_headers(res: &mut HttpResponse) {
1051
- res.headers_mut().insert(("X-Content-Type-Options", "nosniff"));
1052
- res.headers_mut().insert(("X-Frame-Options", "DENY"));
1053
- res.headers_mut().insert(("X-XSS-Protection", "1; mode=block"));
1054
- res.headers_mut().insert(("Strict-Transport-Security", "max-age=31536000; includeSubDomains"));
1055
- res.headers_mut().insert(("Referrer-Policy", "strict-origin-when-cross-origin"));
1056
- res.headers_mut().insert(("Content-Security-Policy", "default-src 'self'"));
1057
- }
1058
- `, description: "Create Actix-web security headers middleware", ruleId: "CONFIG-001" });
1048
+ actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: "use actix_web::{HttpResponse, dev::{ServiceRequest, Service, ServiceResponse}};\n\npub fn add_security_headers(res: &mut HttpResponse) {\n res.headers_mut().insert((\"X-Content-Type-Options\", \"nosniff\"));\n res.headers_mut().insert((\"X-Frame-Options\", \"DENY\"));\n res.headers_mut().insert((\"X-XSS-Protection\", \"1; mode=block\"));\n res.headers_mut().insert((\"Strict-Transport-Security\", \"max-age=31536000; includeSubDomains\"));\n res.headers_mut().insert((\"Referrer-Policy\", \"strict-origin-when-cross-origin\"));\n res.headers_mut().insert((\"Content-Security-Policy\", \"default-src 'self'\"));\n}\n", description: "Create Actix-web security headers middleware", ruleId: "CONFIG-001" });
1059
1049
  }
1060
1050
  else if (fw === "axum") {
1061
- actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: `use axum::{http::HeaderValue, response::Response};
1062
-
1063
- pub async fn security_headers(mut res: Response) -> Response {
1064
- let headers = res.headers_mut();
1065
- headers.insert("X-Content-Type-Options", HeaderValue::from_static("nosniff"));
1066
- headers.insert("X-Frame-Options", HeaderValue::from_static("DENY"));
1067
- headers.insert("X-XSS-Protection", HeaderValue::from_static("1; mode=block"));
1068
- headers.insert("Strict-Transport-Security", HeaderValue::from_static("max-age=31536000; includeSubDomains"));
1069
- headers.insert("Referrer-Policy", HeaderValue::from_static("strict-origin-when-cross-origin"));
1070
- headers.insert("Content-Security-Policy", HeaderValue::from_static("default-src 'self'"));
1071
- res
1072
- }
1073
- `, description: "Create Axum security headers middleware", ruleId: "CONFIG-001" });
1051
+ actions.push({ type: "create", filePath: "src/middleware/security_headers.rs", content: "use axum::{http::HeaderValue, response::Response};\n\npub async fn security_headers(mut res: Response) -> Response {\n let headers = res.headers_mut();\n headers.insert(\"X-Content-Type-Options\", HeaderValue::from_static(\"nosniff\"));\n headers.insert(\"X-Frame-Options\", HeaderValue::from_static(\"DENY\"));\n headers.insert(\"X-XSS-Protection\", HeaderValue::from_static(\"1; mode=block\"));\n headers.insert(\"Strict-Transport-Security\", HeaderValue::from_static(\"max-age=31536000; includeSubDomains\"));\n headers.insert(\"Referrer-Policy\", HeaderValue::from_static(\"strict-origin-when-cross-origin\"));\n headers.insert(\"Content-Security-Policy\", HeaderValue::from_static(\"default-src 'self'\"));\n res\n}\n", description: "Create Axum security headers middleware", ruleId: "CONFIG-001" });
1074
1052
  }
1075
1053
  else {
1076
1054
  actions.push({ type: "append", filePath: appFile, content: "\n// GESF: Add security headers middleware\n// actix-web: use actix_web::middleware::DefaultHeaders\n// axum: use tower-http::set-header::SetResponseHeader\n// rocket: use rocket::fairing\n", description: "Add Rust security headers guidance", ruleId: "CONFIG-001" });
@@ -1089,23 +1067,23 @@ function buildCorsFix(root) {
1089
1067
  actions.push({ type: "npm-install", filePath: "package.json", description: "Install cors", ruleId: "CONFIG-002" });
1090
1068
  if (fw === "fastify") {
1091
1069
  actions.push({ type: "npm-install", filePath: "package.json", description: "Install @fastify/cors", ruleId: "CONFIG-002" });
1092
- actions.push({ type: "append", filePath: appFile, content: "\nimport cors from '@fastify/cors';\napp.register(cors, { origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'] });\n", description: "Add Fastify CORS", ruleId: "CONFIG-002" });
1070
+ actions.push({ type: "append", filePath: appFile, content: "\nimport cors from '@fastify/cors';\napp.register(cors, { origin: (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean) });\n", description: "Add Fastify CORS", ruleId: "CONFIG-002" });
1093
1071
  }
1094
1072
  else {
1095
- actions.push({ type: "append", filePath: appFile, content: "\nimport cors from 'cors';\napp.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'] }));\n", description: "Add CORS with configured origins", ruleId: "CONFIG-002" });
1073
+ actions.push({ type: "append", filePath: appFile, content: "\nimport cors from 'cors';\napp.use(cors({ origin: (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean) }));\n", description: "Add CORS with configured origins", ruleId: "CONFIG-002" });
1096
1074
  }
1097
1075
  }
1098
1076
  else if (lang === "python") {
1099
1077
  const appFile = findMainAppFile(root) || "app.py";
1100
1078
  if (fw === "django") {
1101
1079
  const settingsFile = findFileRecursive(root, "settings.py", ".") || "settings.py";
1102
- actions.push({ type: "append", filePath: settingsFile, content: "\nCORS_ALLOWED_ORIGINS = ['https://yourdomain.com']\nCORS_ALLOW_CREDENTIALS = True\n", description: "Add Django CORS settings", ruleId: "CONFIG-002" });
1080
+ actions.push({ type: "append", filePath: settingsFile, content: "\nimport os\nCORS_ALLOWED_ORIGINS = [o for o in os.environ.get('ALLOWED_ORIGINS', '').split(',') if o]\nCORS_ALLOW_CREDENTIALS = True\n", description: "Add Django CORS settings", ruleId: "CONFIG-002" });
1103
1081
  }
1104
1082
  else if (fw === "fastapi") {
1105
- actions.push({ type: "append", filePath: appFile, content: "\nfrom fastapi.middleware.cors import CORSMiddleware\napp.add_middleware(CORSMiddleware, allow_origins=['http://localhost:3000'], allow_credentials=True, allow_methods=['*'], allow_headers=['*'])\n", description: "Add FastAPI CORS middleware", ruleId: "CONFIG-002" });
1083
+ actions.push({ type: "append", filePath: appFile, content: "\nimport os\nfrom fastapi.middleware.cors import CORSMiddleware\napp.add_middleware(CORSMiddleware, allow_origins=[o for o in os.environ.get('ALLOWED_ORIGINS', '').split(',') if o], allow_credentials=True, allow_methods=['*'], allow_headers=['*'])\n", description: "Add FastAPI CORS middleware", ruleId: "CONFIG-002" });
1106
1084
  }
1107
1085
  else if (fw === "flask") {
1108
- actions.push({ type: "append", filePath: appFile, content: "\nfrom flask_cors import CORS\nCORS(app, origins=['http://localhost:3000'])\n", description: "Add Flask CORS", ruleId: "CONFIG-002" });
1086
+ actions.push({ type: "append", filePath: appFile, content: "\nimport os\nfrom flask_cors import CORS\nCORS(app, origins=[o for o in os.environ.get('ALLOWED_ORIGINS', '').split(',') if o])\n", description: "Add Flask CORS", ruleId: "CONFIG-002" });
1109
1087
  }
1110
1088
  else {
1111
1089
  actions.push({ type: "append", filePath: appFile, content: "\n# CORS: Configure allowed origins in production\n# pip install flask-cors or fastapi[all]\n", description: "Add CORS note", ruleId: "CONFIG-002" });
@@ -1113,7 +1091,7 @@ function buildCorsFix(root) {
1113
1091
  }
1114
1092
  else if (lang === "ruby") {
1115
1093
  if (fw === "rails") {
1116
- actions.push({ type: "append", filePath: "config/application.rb", content: "\nconfig.middleware.insert_before 0, Rack::Cors do\n allow do\n origins 'https://yourdomain.com'\n resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete]\n end\nend\n", description: "Add Rails CORS via Rack::Cors", ruleId: "CONFIG-002" });
1094
+ actions.push({ type: "append", filePath: "config/application.rb", content: "\nconfig.middleware.insert_before 0, Rack::Cors do\n allow do\n origins ENV.fetch('ALLOWED_ORIGINS', '').split(',').reject(&:empty?)\n resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete]\n end\nend\n", description: "Add Rails CORS via Rack::Cors", ruleId: "CONFIG-002" });
1117
1095
  }
1118
1096
  }
1119
1097
  else if (lang === "go") {
@@ -1122,35 +1100,16 @@ function buildCorsFix(root) {
1122
1100
  }
1123
1101
  else if (lang === "java") {
1124
1102
  if (fw === "spring") {
1125
- actions.push({ type: "create", filePath: "src/main/java/com/example/CorsConfig.java", content: `import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.UrlBasedCorsConfigurationSource;\nimport org.springframework.web.filter.CorsFilter;\n\n@Configuration\npublic class CorsConfig {\n @Bean\n public CorsFilter corsFilter() {\n CorsConfiguration config = new CorsConfiguration();\n config.addAllowedOrigin(\"https://yourdomain.com\");\n config.addAllowedHeader(\"*\");\n config.addAllowedMethod(\"*\");\n config.setAllowCredentials(true);\n UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\n source.registerCorsConfiguration(\"/**\", config);\n return new CorsFilter(source);\n }\n}\n`, description: "Create Spring CORS configuration", ruleId: "CONFIG-002" });
1103
+ actions.push({ type: "create", filePath: "src/main/java/com/example/CorsConfig.java", content: `import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.cors.CorsConfiguration;\nimport org.springframework.web.cors.UrlBasedCorsConfigurationSource;\nimport org.springframework.web.filter.CorsFilter;\n\n@Configuration\npublic class CorsConfig {\n @Bean\n public CorsFilter corsFilter() {\n CorsConfiguration config = new CorsConfiguration();\n config.addAllowedOrigin(System.getenv(\"ALLOWED_ORIGIN\"));\n config.addAllowedHeader(\"*\");\n config.addAllowedMethod(\"*\");\n config.setAllowCredentials(true);\n UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\n source.registerCorsConfiguration(\"/**\", config);\n return new CorsFilter(source);\n }\n}\n`, description: "Create Spring CORS configuration", ruleId: "CONFIG-002" });
1126
1104
  }
1127
1105
  }
1128
1106
  else if (lang === "rust") {
1129
1107
  const appFile = findMainAppFile(root) || "src/main.rs";
1130
1108
  if (fw === "actix") {
1131
- actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: `use actix_cors::Cors;
1132
- use actix_web::http::header;
1133
-
1134
- pub fn cors_config() -> Cors {
1135
- Cors::default()
1136
- .allowed_origin("http://localhost:3000")
1137
- .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
1138
- .allowed_headers(vec![header::CONTENT_TYPE, header::AUTHORIZATION])
1139
- .max_age(3600)
1140
- }
1141
- `, description: "Create Actix-web CORS configuration", ruleId: "CONFIG-002" });
1109
+ actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: "use actix_cors::Cors;\nuse actix_web::http::header;\n\npub fn cors_config() -> Cors {\n Cors::default()\n .allowed_origin(&std::env::var(\"ALLOWED_ORIGIN\").unwrap_or_default())\n .allowed_methods(vec![\"GET\", \"POST\", \"PUT\", \"DELETE\"])\n .allowed_headers(vec![header::CONTENT_TYPE, header::AUTHORIZATION])\n .max_age(3600)\n}\n", description: "Create Actix-web CORS configuration", ruleId: "CONFIG-002" });
1142
1110
  }
1143
1111
  else if (fw === "axum") {
1144
- actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: `use tower_http::cors::{CorsLayer, Any};
1145
- use http::Method;
1146
-
1147
- pub fn cors_layer() -> CorsLayer {
1148
- CorsLayer::new()
1149
- .allow_origin(["http://localhost:3000".parse().unwrap()])
1150
- .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
1151
- .allow_headers(Any)
1152
- }
1153
- `, description: "Create Axum CORS layer", ruleId: "CONFIG-002" });
1112
+ actions.push({ type: "create", filePath: "src/middleware/cors.rs", content: "use tower_http::cors::{CorsLayer, Any};\nuse http::Method;\n\npub fn cors_layer() -> CorsLayer {\n CorsLayer::new()\n .allow_origin([std::env::var(\"ALLOWED_ORIGIN\").unwrap_or_default().parse().unwrap()])\n .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])\n .allow_headers(Any)\n}\n", description: "Create Axum CORS layer", ruleId: "CONFIG-002" });
1154
1113
  }
1155
1114
  else {
1156
1115
  actions.push({ type: "append", filePath: appFile, content: "\n// GESF CORS: Configure allowed origins\n// actix-web: cargo add actix-cors\n// axum: cargo add tower-http --features cors\n// rocket: cargo add rocket_cors\n", description: "Add Rust CORS guidance", ruleId: "CONFIG-002" });
@@ -1228,33 +1187,7 @@ function buildLoggingFix(root) {
1228
1187
  actions.push({ type: "create", filePath: "lib/audit_logger.php", content: `<?php\n\nclass AuditLogger\n{\n public static function log(string $userId, string $action, string $resource, string $ipAddress, array $metadata = []): void\n {\n $entry = array_merge([\n 'userId' => $userId,\n 'action' => $action,\n 'resource' => $resource,\n 'ipAddress' => $ipAddress,\n 'timestamp' => gmdate('c'),\n 'type' => 'audit',\n ], $metadata);\n error_log(json_encode($entry));\n }\n}\n`, description: "Create PHP audit logger", ruleId: "CONFIG-010" });
1229
1188
  }
1230
1189
  else if (lang === "rust") {
1231
- actions.push({ type: "create", filePath: "src/logger.rs", content: `use serde_json::json;
1232
- use tracing::{info, instrument};
1233
- use chrono::Utc;
1234
-
1235
- #[derive(Debug, serde::Serialize)]
1236
- pub struct AuditEntry {
1237
- pub user_id: String,
1238
- pub action: String,
1239
- pub resource: String,
1240
- pub ip_address: String,
1241
- pub timestamp: String,
1242
- #[serde(rename = "type")]
1243
- pub entry_type: String,
1244
- }
1245
-
1246
- pub fn audit_log(user_id: &str, action: &str, resource: &str, ip_address: &str) {
1247
- let entry = AuditEntry {
1248
- user_id: user_id.to_string(),
1249
- action: action.to_string(),
1250
- resource: resource.to_string(),
1251
- ip_address: ip_address.to_string(),
1252
- timestamp: Utc::now().to_rfc3339(),
1253
- entry_type: "audit".to_string(),
1254
- };
1255
- info!("{}", serde_json::to_string(&entry).unwrap_or_default());
1256
- }
1257
- `, description: "Create Rust audit logger (tracing)", ruleId: "CONFIG-010" });
1190
+ actions.push({ type: "create", filePath: "src/logger.rs", content: "use serde_json::json;\nuse tracing::{info, instrument};\nuse chrono::Utc;\n\n#[derive(Debug, serde::Serialize)]\npub struct AuditEntry {\n pub user_id: String,\n pub action: String,\n pub resource: String,\n pub ip_address: String,\n pub timestamp: String,\n #[serde(rename = \"type\")]\n pub entry_type: String,\n}\n\npub fn audit_log(user_id: &str, action: &str, resource: &str, ip_address: &str) {\n let entry = AuditEntry {\n user_id: user_id.to_string(),\n action: action.to_string(),\n resource: resource.to_string(),\n ip_address: ip_address.to_string(),\n timestamp: Utc::now().to_rfc3339(),\n entry_type: \"audit\".to_string(),\n };\n info!(\"{}\", serde_json::to_string(&entry).unwrap_or_default());\n}\n", description: "Create Rust audit logger (tracing)", ruleId: "CONFIG-010" });
1258
1191
  }
1259
1192
  return actions;
1260
1193
  }
@@ -1364,23 +1297,7 @@ function buildPasswordFix(root, _f) {
1364
1297
  actions.push({ type: "create", filePath: "lib/auth.php", content: `<?php\n\nfunction hash_password(string $password): string {\n return password_hash($password, PASSWORD_ARGON2ID);\n}\n\nfunction verify_password(string $hash, string $password): bool {\n return password_verify($password, $hash);\n}\n`, description: "Create PHP Argon2id password utility", ruleId: "CRYPTO-003" });
1365
1298
  }
1366
1299
  else if (lang === "rust") {
1367
- actions.push({ type: "create", filePath: "src/auth.rs", content: `use argon2::{Argon2, Algorithm, Version, Params};
1368
- use argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};
1369
- use rand::rngs::OsRng;
1370
-
1371
- pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
1372
- let salt = SaltString::generate(&mut OsRng);
1373
- let params = Params::new(65536, 3, 4, Some(32))?;
1374
- let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
1375
- let hash = argon2.hash_password(password.as_bytes(), &salt)?;
1376
- Ok(hash.to_string())
1377
- }
1378
-
1379
- pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
1380
- let parsed = argon2::PasswordHash::new(hash)?;
1381
- Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())
1382
- }
1383
- `, description: "Create Rust Argon2id password utility", ruleId: "CRYPTO-003" });
1300
+ actions.push({ type: "create", filePath: "src/auth.rs", content: "use argon2::{Argon2, Algorithm, Version, Params};\nuse argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};\nuse rand::rngs::OsRng;\n\npub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {\n let salt = SaltString::generate(&mut OsRng);\n let params = Params::new(65536, 3, 4, Some(32))?;\n let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);\n let hash = argon2.hash_password(password.as_bytes(), &salt)?;\n Ok(hash.to_string())\n}\n\npub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {\n let parsed = argon2::PasswordHash::new(hash)?;\n Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())\n}\n", description: "Create Rust Argon2id password utility", ruleId: "CRYPTO-003" });
1384
1301
  }
1385
1302
  return actions;
1386
1303
  }
@@ -1514,15 +1431,15 @@ function buildCORSWildcardFix(root) {
1514
1431
  continue;
1515
1432
  if (lang === "python") {
1516
1433
  const replacement = pattern.includes("*'") || pattern.includes('*"')
1517
- ? "origins=['http://localhost:3000']"
1518
- : "origins=['http://localhost:3000']";
1434
+ ? "origins=[o for o in __import__('os').environ.get('ALLOWED_ORIGINS', '').split(',') if o]"
1435
+ : "origins=[o for o in __import__('os').environ.get('ALLOWED_ORIGINS', '').split(',') if o]";
1519
1436
  actions.push({ type: "modify", filePath: appFile, search: pattern, replace: replacement, description: "Replace CORS wildcard", ruleId: "AUTH-004" });
1520
1437
  }
1521
1438
  else if (lang === "go") {
1522
1439
  actions.push({ type: "modify", filePath: appFile, search: pattern, replace: 'w.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN"))', description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
1523
1440
  }
1524
1441
  else if (lang === "ruby") {
1525
- actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "origins ENV.fetch('ALLOWED_ORIGINS', 'http://localhost:3000').split(',')", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
1442
+ actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "origins ENV.fetch('ALLOWED_ORIGINS', '').split(',').reject(&:empty?)", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
1526
1443
  }
1527
1444
  else if (lang === "java") {
1528
1445
  actions.push({ type: "modify", filePath: appFile, search: pattern, replace: 'config.addAllowedOrigin(System.getenv("ALLOWED_ORIGIN"))', description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
@@ -1531,10 +1448,10 @@ function buildCORSWildcardFix(root) {
1531
1448
  actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "$response->headers->set('Access-Control-Allow-Origin', getenv('ALLOWED_ORIGIN'))", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
1532
1449
  }
1533
1450
  else if (lang === "rust") {
1534
- actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "allowed_origin(std::env::var(\"ALLOWED_ORIGIN\").unwrap_or(\"http://localhost:3000\".to_string()))", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
1451
+ actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "allowed_origin(std::env::var(\"ALLOWED_ORIGIN\").unwrap_or_default())", description: "Replace CORS wildcard with env var", ruleId: "AUTH-004" });
1535
1452
  }
1536
1453
  else {
1537
- actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']", description: "Replace CORS wildcard", ruleId: "AUTH-004" });
1454
+ actions.push({ type: "modify", filePath: appFile, search: pattern, replace: "origin: (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean)", description: "Replace CORS wildcard", ruleId: "AUTH-004" });
1538
1455
  }
1539
1456
  }
1540
1457
  return actions;
@@ -1636,34 +1553,11 @@ function buildAuditModelFix(root) {
1636
1553
  return [{ type: "create", filePath: "src/main/java/com/example/Audit.java", content: `package com.example;\n\nimport jakarta.persistence.*;\nimport java.time.Instant;\n\n@Entity\n@Table(name = "audit")\npublic class Audit {\n @Id @GeneratedValue(strategy = GenerationType.IDENTITY)\n private Long id;\n private String userId;\n private String action;\n private String resource;\n private String ipAddress;\n @Column(columnDefinition = "jsonb")\n private String metadata;\n private Instant timestamp = Instant.now();\n}\n`, description: "Create Java Audit entity (JPA)", ruleId: "DB-004" }];
1637
1554
  }
1638
1555
  if (lang === "rust") {
1639
- return [{ type: "create", filePath: "src/models/audit.rs", content: `use chrono::NaiveDateTime;
1640
-
1641
- #[derive(Debug, Queryable, Serialize)]
1642
- pub struct Audit {
1643
- pub id: i32,
1644
- pub user_id: String,
1645
- pub action: String,
1646
- pub resource: String,
1647
- pub ip_address: String,
1648
- pub timestamp: NaiveDateTime,
1649
- }
1650
-
1651
- // Diesel table definition:
1652
- // table! {
1653
- // audit (id) {
1654
- // id -> Int4,
1655
- // user_id -> Varchar,
1656
- // action -> Varchar,
1657
- // resource -> Varchar,
1658
- // ip_address -> Varchar,
1659
- // timestamp -> Timestamp,
1660
- // }
1661
- // }
1662
- `, description: "Create Rust Audit model (Diesel)", ruleId: "DB-004" }];
1556
+ return [{ type: "create", filePath: "src/models/audit.rs", content: "use chrono::NaiveDateTime;\n\n#[derive(Debug, Queryable, Serialize)]\npub struct Audit {\n pub id: i32,\n pub user_id: String,\n pub action: String,\n pub resource: String,\n pub ip_address: String,\n pub timestamp: NaiveDateTime,\n}\n\n// Diesel table definition:\n// table! {\n// audit (id) {\n// id -> Int4,\n// user_id -> Varchar,\n// action -> Varchar,\n// resource -> Varchar,\n// ip_address -> Varchar,\n// timestamp -> Timestamp,\n// }\n// }\n", description: "Create Rust Audit model (Diesel)", ruleId: "DB-004" }];
1663
1557
  }
1664
1558
  return [];
1665
1559
  }
1666
- function getNpmInstallsFromActions(actions) {
1560
+ export function getNpmInstallsFromActions(actions) {
1667
1561
  const installs = new Set();
1668
1562
  for (const a of actions) {
1669
1563
  if (a.type !== "npm-install")
@@ -1685,31 +1579,7 @@ function buildEncryptionAtRestImpl(root, hasSrc) {
1685
1579
  const lang = detectProjectLanguage(root);
1686
1580
  if (lang === "rust") {
1687
1581
  return [
1688
- { type: "create", filePath: "src/encryption.rs", content: `use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
1689
- use aes_gcm::aead::Aead;
1690
- use rand::RngCore;
1691
- use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
1692
-
1693
- pub fn encrypt(plaintext: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {
1694
- let cipher = Aes256Gcm::new(key.into());
1695
- let mut nonce_bytes = [0u8; 12];
1696
- rand::thread_rng().fill_bytes(&mut nonce_bytes);
1697
- let nonce = Nonce::from_slice(&nonce_bytes);
1698
- let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())?;
1699
- let mut combined = nonce_bytes.to_vec();
1700
- combined.extend_from_slice(&ciphertext);
1701
- Ok(BASE64.encode(&combined))
1702
- }
1703
-
1704
- pub fn decrypt(encoded: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {
1705
- let combined = BASE64.decode(encoded).map_err(|_| aes_gcm::Error)?;
1706
- let (nonce_bytes, ciphertext) = combined.split_at(12);
1707
- let cipher = Aes256Gcm::new(key.into());
1708
- let nonce = Nonce::from_slice(nonce_bytes);
1709
- let plaintext = cipher.decrypt(nonce, ciphertext)?;
1710
- String::from_utf8(plaintext).map_err(|_| aes_gcm::Error)
1711
- }
1712
- `, description: "Create Rust AES-256-GCM encryption utility", ruleId: "GDPR-ART32-002" },
1582
+ { type: "create", filePath: "src/encryption.rs", content: "use aes_gcm::{Aes256Gcm, KeyInit, Nonce};\nuse aes_gcm::aead::Aead;\nuse rand::RngCore;\nuse base64::{Engine, engine::general_purpose::STANDARD as BASE64};\n\npub fn encrypt(plaintext: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {\n let cipher = Aes256Gcm::new(key.into());\n let mut nonce_bytes = [0u8; 12];\n rand::thread_rng().fill_bytes(&mut nonce_bytes);\n let nonce = Nonce::from_slice(&nonce_bytes);\n let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())?;\n let mut combined = nonce_bytes.to_vec();\n combined.extend_from_slice(&ciphertext);\n Ok(BASE64.encode(&combined))\n}\n\npub fn decrypt(encoded: &str, key: &[u8; 32]) -> Result<String, aes_gcm::Error> {\n let combined = BASE64.decode(encoded).map_err(|_| aes_gcm::Error)?;\n let (nonce_bytes, ciphertext) = combined.split_at(12);\n let cipher = Aes256Gcm::new(key.into());\n let nonce = Nonce::from_slice(nonce_bytes);\n let plaintext = cipher.decrypt(nonce, ciphertext)?;\n String::from_utf8(plaintext).map_err(|_| aes_gcm::Error)\n}\n", description: "Create Rust AES-256-GCM encryption utility", ruleId: "GDPR-ART32-002" },
1713
1583
  ];
1714
1584
  }
1715
1585
  const cryptoPath = hasSrc ? "src/lib/encryption.ts" : "lib/encryption.ts";
@@ -1729,7 +1599,7 @@ function buildEncryptionInTransitImpl(root, _hasSrc) {
1729
1599
  return actions;
1730
1600
  }
1731
1601
  if (appFile) {
1732
- actions.push({ type: "append", filePath: appFile, content: "\nif (process.env.NODE_ENV === 'production') {\n app.use((req, res, next) => {\n if (req.headers['x-forwarded-proto'] === 'http') {\n return res.redirect(301, `https://${req.headers.host}${req.url}`);\n }\n next();\n });\n}\n", description: "Add HTTPS redirect middleware", ruleId: "GDPR-ART32-003" });
1602
+ 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 const secureProto = 'https';\n return res.redirect(301, `${secureProto}://${req.headers.host}${req.url}`);\n }\n next();\n });\n}\n", description: "Add HTTPS redirect middleware", ruleId: "GDPR-ART32-003" });
1733
1603
  }
1734
1604
  return actions;
1735
1605
  }
@@ -1740,23 +1610,7 @@ function buildUserIdentificationImpl(root, hasSrc) {
1740
1610
  if (fs.existsSync(path.join(root, authPath)))
1741
1611
  return [];
1742
1612
  return [
1743
- { type: "create", filePath: authPath, content: `use argon2::{Argon2, Algorithm, Version, Params};
1744
- use argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};
1745
- use rand::rngs::OsRng;
1746
-
1747
- pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
1748
- let salt = SaltString::generate(&mut OsRng);
1749
- let params = Params::new(65536, 3, 4, Some(32))?;
1750
- let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
1751
- let hash = argon2.hash_password(password.as_bytes(), &salt)?;
1752
- Ok(hash.to_string())
1753
- }
1754
-
1755
- pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
1756
- let parsed = argon2::PasswordHash::new(hash)?;
1757
- Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())
1758
- }
1759
- `, description: "Create Rust auth utility with Argon2id", ruleId: "GDPR-ART32-004" },
1613
+ { type: "create", filePath: authPath, content: "use argon2::{Argon2, Algorithm, Version, Params};\nuse argon2::password_hash::{SaltString, PasswordHasher, PasswordVerifier};\nuse rand::rngs::OsRng;\n\npub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {\n let salt = SaltString::generate(&mut OsRng);\n let params = Params::new(65536, 3, 4, Some(32))?;\n let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);\n let hash = argon2.hash_password(password.as_bytes(), &salt)?;\n Ok(hash.to_string())\n}\n\npub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {\n let parsed = argon2::PasswordHash::new(hash)?;\n Ok(Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok())\n}\n", description: "Create Rust auth utility with Argon2id", ruleId: "GDPR-ART32-004" },
1760
1614
  ];
1761
1615
  }
1762
1616
  const authPath = hasSrc ? "src/lib/auth.ts" : "lib/auth.ts";
@@ -1771,24 +1625,7 @@ function buildIntegrityControlsImpl(root, hasSrc) {
1771
1625
  const lang = detectProjectLanguage(root);
1772
1626
  if (lang === "rust") {
1773
1627
  return [
1774
- { type: "create", filePath: "src/integrity.rs", content: `use sha2::{Sha256, Digest};
1775
-
1776
- pub fn hash_data(data: &str) -> String {
1777
- let mut hasher = Sha256::new();
1778
- hasher.update(data.as_bytes());
1779
- format!("{:x}", hasher.finalize())
1780
- }
1781
-
1782
- pub fn verify_integrity(data: &str, expected_hash: &str) -> bool {
1783
- hash_data(data) == expected_hash
1784
- }
1785
-
1786
- pub fn generate_checksum(content: &[u8]) -> String {
1787
- let mut hasher = Sha256::new();
1788
- hasher.update(content);
1789
- format!("{:x}", hasher.finalize())
1790
- }
1791
- `, description: "Create Rust integrity verification utility", ruleId: "GDPR-ART32-007" },
1628
+ { type: "create", filePath: "src/integrity.rs", content: "use sha2::{Sha256, Digest};\n\npub fn hash_data(data: &str) -> String {\n let mut hasher = Sha256::new();\n hasher.update(data.as_bytes());\n format!(\"{:x}\", hasher.finalize())\n}\n\npub fn verify_integrity(data: &str, expected_hash: &str) -> bool {\n hash_data(data) == expected_hash\n}\n\npub fn generate_checksum(content: &[u8]) -> String {\n let mut hasher = Sha256::new();\n hasher.update(content);\n format!(\"{:x}\", hasher.finalize())\n}\n", description: "Create Rust integrity verification utility", ruleId: "GDPR-ART32-007" },
1792
1629
  ];
1793
1630
  }
1794
1631
  const integrityPath = hasSrc ? "src/lib/integrity.ts" : "lib/integrity.ts";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { handleRequest } from "./server.js";
3
+ function req(method, params, id = 1) {
4
+ return { jsonrpc: "2.0", id, method, params };
5
+ }
6
+ function callTool(name, args = {}, id = 1) {
7
+ return req("tools/call", { name, arguments: args }, id);
8
+ }
9
+ function getResultText(response) {
10
+ const r = response;
11
+ return r.result?.content?.[0]?.text ?? "";
12
+ }
13
+ describe("MCP Protocol", () => {
14
+ it("responds to initialize", () => {
15
+ const res = handleRequest(req("initialize"));
16
+ expect(res).not.toBeNull();
17
+ const result = res.result;
18
+ expect(result.protocolVersion).toBe("2024-11-05");
19
+ expect(result.serverInfo.name).toBe("gesf-mcp-server");
20
+ });
21
+ it("returns null for notifications/initialized", () => {
22
+ const res = handleRequest({ jsonrpc: "2.0", method: "notifications/initialized" });
23
+ expect(res).toBeNull();
24
+ });
25
+ it("returns null for notifications/cancelled", () => {
26
+ const res = handleRequest({ jsonrpc: "2.0", method: "notifications/cancelled" });
27
+ expect(res).toBeNull();
28
+ });
29
+ it("responds to ping", () => {
30
+ const res = handleRequest(req("ping"));
31
+ expect(res).not.toBeNull();
32
+ expect(res.result).toBeDefined();
33
+ });
34
+ it("returns null for ping notification", () => {
35
+ const res = handleRequest({ jsonrpc: "2.0", method: "ping" });
36
+ expect(res).toBeNull();
37
+ });
38
+ it("responds to tools/list with 17 tools", () => {
39
+ const res = handleRequest(req("tools/list"));
40
+ const tools = res.result.tools;
41
+ expect(tools.length).toBe(17);
42
+ });
43
+ it("returns error for unknown method", () => {
44
+ const res = handleRequest(req("unknown/method"));
45
+ expect(res.error).toBeDefined();
46
+ });
47
+ });
48
+ describe("tools/list content", () => {
49
+ it("includes all expected tool names", () => {
50
+ const res = handleRequest(req("tools/list"));
51
+ const tools = res.result.tools;
52
+ const names = tools.map((t) => t.name);
53
+ expect(names).toContain("check_compliance");
54
+ expect(names).toContain("check_project_status");
55
+ expect(names).toContain("list_missing_controls");
56
+ expect(names).toContain("list_framework_controls");
57
+ expect(names).toContain("run_audit");
58
+ expect(names).toContain("generate_compliance_report");
59
+ expect(names).toContain("generate_audit_report");
60
+ expect(names).toContain("fix_recommendation");
61
+ expect(names).toContain("auto_fix");
62
+ expect(names).toContain("implement_control");
63
+ expect(names).toContain("apply_control_override");
64
+ expect(names).toContain("generate_retention_policy");
65
+ expect(names).toContain("generate_incident_response");
66
+ expect(names).toContain("generate_risk_assessment");
67
+ expect(names).toContain("generate_dpa");
68
+ expect(names).toContain("generate_data_inventory");
69
+ expect(names).toContain("generate_processing_records");
70
+ });
71
+ });
72
+ describe("check_compliance tool", () => {
73
+ it("returns compliance score output", () => {
74
+ const res = handleRequest(callTool("check_compliance", { project_type: "saas" }));
75
+ const text = getResultText(res);
76
+ expect(text.length).toBeGreaterThan(0);
77
+ expect(text).toContain("GDPR");
78
+ });
79
+ });
80
+ describe("list_missing_controls tool", () => {
81
+ it("returns missing controls for GDPR", () => {
82
+ const res = handleRequest(callTool("list_missing_controls", { framework: "GDPR" }));
83
+ const text = getResultText(res);
84
+ expect(text.length).toBeGreaterThan(0);
85
+ });
86
+ });
87
+ describe("list_framework_controls tool", () => {
88
+ it("returns all GDPR controls", () => {
89
+ const res = handleRequest(callTool("list_framework_controls", { framework: "GDPR" }));
90
+ const text = getResultText(res);
91
+ expect(text.length).toBeGreaterThan(0);
92
+ });
93
+ });
94
+ describe("generate_retention_policy tool", () => {
95
+ it("generates a retention policy", () => {
96
+ const res = handleRequest(callTool("generate_retention_policy", { project_name: "TestApp" }));
97
+ const text = getResultText(res);
98
+ expect(text.length).toBeGreaterThan(0);
99
+ expect(text).toContain("Retention");
100
+ });
101
+ });
102
+ describe("generate_incident_response tool", () => {
103
+ it("generates an incident response plan", () => {
104
+ const res = handleRequest(callTool("generate_incident_response", { project_name: "TestApp" }));
105
+ const text = getResultText(res);
106
+ expect(text.length).toBeGreaterThan(0);
107
+ expect(text).toContain("Incident");
108
+ });
109
+ });
110
+ describe("generate_risk_assessment tool", () => {
111
+ it("generates a risk assessment", () => {
112
+ const res = handleRequest(callTool("generate_risk_assessment", { project_name: "TestApp" }));
113
+ const text = getResultText(res);
114
+ expect(text.length).toBeGreaterThan(0);
115
+ expect(text).toContain("Risk");
116
+ });
117
+ });
118
+ describe("generate_dpa tool", () => {
119
+ it("generates a DPA", () => {
120
+ const res = handleRequest(callTool("generate_dpa", { project_name: "TestApp" }));
121
+ const text = getResultText(res);
122
+ expect(text.length).toBeGreaterThan(0);
123
+ expect(text).toContain("Data Processing");
124
+ });
125
+ });
126
+ describe("generate_data_inventory tool", () => {
127
+ it("generates a data inventory", () => {
128
+ const res = handleRequest(callTool("generate_data_inventory", { project_name: "TestApp" }));
129
+ const text = getResultText(res);
130
+ expect(text.length).toBeGreaterThan(0);
131
+ });
132
+ });
133
+ describe("generate_processing_records tool", () => {
134
+ it("generates processing records", () => {
135
+ const res = handleRequest(callTool("generate_processing_records", { project_name: "TestApp" }));
136
+ const text = getResultText(res);
137
+ expect(text.length).toBeGreaterThan(0);
138
+ });
139
+ });
140
+ describe("fix_recommendation tool", () => {
141
+ it("returns guidance for a control ID", () => {
142
+ const res = handleRequest(callTool("fix_recommendation", { control_id: "GDPR-ART32-002" }));
143
+ const text = getResultText(res);
144
+ expect(text.length).toBeGreaterThan(0);
145
+ });
146
+ });
147
+ describe("unknown tool", () => {
148
+ it("returns error for unknown tool name", () => {
149
+ const res = handleRequest(callTool("nonexistent_tool"));
150
+ expect(res.error).toBeDefined();
151
+ });
152
+ });