skillguard/artifacts/api-server/src/lib/ruleCatalogI18n.ts
amertensreplit 2236ad179d Add DE/EN/ES multilingual support to SkillGuard (Task #49)
German is source of truth; EN/ES fully translated with no German residue.
Auto-detects browser language (fallback German), persists choice, language
switcher on all pages, localized formats/Clerk/legal. Scans store their language.

Backend (T001-T003): language column on scans, openapi+codegen, ruleCatalogI18n,
language threaded scans route -> analyzeSkill -> runStaticRule -> AI calls.
Route/AI error messages localized via expanded i18n MESSAGES + reqLang(req)
(?lang query -> Accept-Language header -> "de"). No German left in routes.

Frontend (T004-T005): react-i18next framework, LanguageSwitcher, locale-aware
format.ts, Clerk localizations. All page/component strings externalized to
de/en/es locale area files across catalog, education, scan form/report/compare,
history, dashboard, admin, legal pages.

T006 verification + review-fix follow-up (this session):
- Applied formatNumber to all visible metrics in scan-report (risk score,
  severity counts, security/privacy) and scan-compare (risk score, file count,
  diff counts); PDF/HTML export numbers formatted via Intl.NumberFormat(lng).
- Fixed leftover `@workspace/n` import alias in i18n/index.ts -> real package
  `@workspace/api-client-react` (was failing workspace typecheck).
- Verified: full `pnpm run typecheck` green; api-server tests 72/72 pass;
  curl confirms localized error responses (de/en/es) on scans route.

Deviations: AI connection-test prompts left in German intentionally (sent to
the model, not user-facing). proposeFollowUpTasks already created #52.

Replit-Task-Id: 9f137230-db11-45dc-9276-4e5cbcceff03
2026-06-13 09:05:57 +00:00

397 lines
16 KiB
TypeScript

import { RULE_CATALOG } from "./ruleCatalog";
export type Lang = "de" | "en" | "es";
export const SUPPORTED_LANGS: Lang[] = ["de", "en", "es"];
export function normalizeLang(value: string | null | undefined): Lang {
return value === "en" || value === "es" || value === "de" ? value : "de";
}
export type RuleText = {
category: string;
title: string;
description: string;
remediation: string;
};
// German is the canonical source kept inline in RULE_CATALOG. This map only
// carries the English and Spanish overrides; de falls back to the catalog.
const OVERRIDES: Record<string, { en: RuleText; es: RuleText }> = {
"SEC-REVERSE-SHELL": {
en: {
category: "Code execution",
title: "Reverse shell / interactive shell",
description:
"The skill contains patterns typical of reverse shells or of establishing interactive shell connections to a remote host.",
remediation:
"Remove any code that opens shell connections to external hosts. Such patterns are practically never required in legitimate skills.",
},
es: {
category: "Ejecución de código",
title: "Shell inversa / shell interactiva",
description:
"El skill contiene patrones típicos de shells inversas o del establecimiento de conexiones de shell interactivas con un host remoto.",
remediation:
"Elimine cualquier código que abra conexiones de shell con hosts externos. Estos patrones prácticamente nunca son necesarios en skills legítimos.",
},
},
"SEC-REMOTE-EXEC": {
en: {
category: "Code execution",
title: "Download-and-execute from the network",
description:
"Content is downloaded from the internet and piped straight into an interpreter (e.g. curl | bash). This allows uncontrolled execution of foreign code.",
remediation:
"Never pipe code directly into a shell. Review and version downloaded artifacts before running them.",
},
es: {
category: "Ejecución de código",
title: "Descargar y ejecutar desde la red",
description:
"Se descarga contenido de internet y se pasa directamente a un intérprete (p. ej. curl | bash). Esto permite la ejecución incontrolada de código ajeno.",
remediation:
"Nunca canalice código directamente a una shell. Revise y versione los artefactos descargados antes de ejecutarlos.",
},
},
"SEC-DESTRUCTIVE": {
en: {
category: "Destructive operations",
title: "Destructive file or disk operation",
description:
"Potentially destructive commands were detected (recursive deletion, overwriting disks, formatting, fork bomb).",
remediation:
"Restrict delete operations to clearly scoped paths and avoid operations at the root, home or device level.",
},
es: {
category: "Operaciones destructivas",
title: "Operación destructiva de archivos o disco",
description:
"Se detectaron comandos potencialmente destructivos (borrado recursivo, sobrescritura de discos, formateo, fork bomb).",
remediation:
"Limite las operaciones de borrado a rutas claramente delimitadas y evite operaciones a nivel de raíz, home o dispositivo.",
},
},
"SEC-PRIV-ESC": {
en: {
category: "Privilege escalation",
title: "Privilege escalation / insecure permissions",
description:
"The skill tries to gain elevated privileges or sets insecure file permissions (sudo, chmod 777, setuid, chown root).",
remediation:
"Avoid sudo and overly broad permissions. Grant only the minimum privileges that are strictly necessary.",
},
es: {
category: "Escalada de privilegios",
title: "Escalada de privilegios / permisos inseguros",
description:
"El skill intenta obtener privilegios elevados o establece permisos de archivo inseguros (sudo, chmod 777, setuid, chown root).",
remediation:
"Evite sudo y permisos demasiado amplios. Conceda únicamente los privilegios mínimos estrictamente necesarios.",
},
},
"SEC-PERSISTENCE": {
en: {
category: "Persistence",
title: "Persistence mechanism",
description:
"The skill may set up persistent mechanisms (cron jobs, systemd services, shell profile hooks, SSH keys).",
remediation:
"Persistence should only happen with explicit consent. Verify whether creating autostart entries is really necessary.",
},
es: {
category: "Persistencia",
title: "Mecanismo de persistencia",
description:
"El skill podría configurar mecanismos persistentes (cron jobs, servicios systemd, hooks del perfil de shell, claves SSH).",
remediation:
"La persistencia solo debe producirse con consentimiento explícito. Compruebe si crear entradas de inicio automático es realmente necesario.",
},
},
"SEC-OBFUSCATION": {
en: {
category: "Obfuscation",
title: "Obfuscated or dynamically executed code",
description:
"Indications of obfuscated code or dynamic execution were found (base64 decoding with execution, eval/exec, hex escapes).",
remediation:
"Avoid dynamic code execution and obfuscation. Code should be readable in plain text.",
},
es: {
category: "Ofuscación",
title: "Código ofuscado o ejecutado dinámicamente",
description:
"Se encontraron indicios de código ofuscado o ejecución dinámica (decodificación base64 con ejecución, eval/exec, escapes hexadecimales).",
remediation:
"Evite la ejecución dinámica de código y la ofuscación. El código debe ser legible en texto plano.",
},
},
"SEC-SUPPLY-CHAIN": {
en: {
category: "Supply chain",
title: "Insecure package or source installation",
description:
"Packages are installed from untrusted sources (direct URLs, git+ sources, external apt repositories or keys).",
remediation:
"Install packages only from trusted, versioned sources and avoid installing directly from URLs.",
},
es: {
category: "Cadena de suministro",
title: "Instalación insegura de paquetes o fuentes",
description:
"Se instalan paquetes desde fuentes no confiables (URLs directas, fuentes git+, repositorios o claves apt externos).",
remediation:
"Instale paquetes solo desde fuentes confiables y versionadas y evite instalar directamente desde URLs.",
},
},
"SEC-NETWORK": {
en: {
category: "Network",
title: "Outbound network access",
description:
"The skill establishes outbound network connections. This is not necessarily malicious but should be assessed.",
remediation:
"Make sure the contacted endpoints are expected and trustworthy.",
},
es: {
category: "Red",
title: "Acceso de red saliente",
description:
"El skill establece conexiones de red salientes. Esto no es necesariamente malicioso, pero debe evaluarse.",
remediation:
"Asegúrese de que los endpoints contactados sean esperados y confiables.",
},
},
"PRIV-SECRET-ACCESS": {
en: {
category: "Access to secrets",
title: "Access to credentials or secrets",
description:
"The skill accesses typical secret storage locations (.env, SSH keys, cloud credentials, .netrc, environment variables).",
remediation:
"Avoid accessing secret files. If required, document the purpose and scope transparently.",
},
es: {
category: "Acceso a secretos",
title: "Acceso a credenciales o secretos",
description:
"El skill accede a ubicaciones típicas de almacenamiento de secretos (.env, claves SSH, credenciales de la nube, .netrc, variables de entorno).",
remediation:
"Evite acceder a archivos de secretos. Si es necesario, documente el propósito y el alcance de forma transparente.",
},
},
"PRIV-EXFILTRATION": {
en: {
category: "Data exfiltration",
title: "Possible data exfiltration",
description:
"Data is sent to external endpoints (HTTP POST with payloads, file uploads). Combined with secret access this is highly critical.",
remediation:
"Do not send local data to external servers without explicit, documented consent.",
},
es: {
category: "Fuga de datos",
title: "Posible exfiltración de datos",
description:
"Se envían datos a endpoints externos (HTTP POST con datos, subida de archivos). Combinado con acceso a secretos esto es altamente crítico.",
remediation:
"No envíe datos locales a servidores externos sin consentimiento explícito y documentado.",
},
},
"PRIV-PROMPT-INJECTION": {
en: {
category: "Prompt injection",
title: "Prompt injection / instruction manipulation",
description:
"The instructions contain wording that manipulates agent behaviour (ignore previous instructions, deceive the user, bypass safety guardrails).",
remediation:
"Remove manipulative instructions. A skill must not instruct the agent to bypass safety rules or deceive the user.",
},
es: {
category: "Inyección de prompts",
title: "Inyección de prompts / manipulación de instrucciones",
description:
"Las instrucciones contienen formulaciones que manipulan el comportamiento del agente (ignorar instrucciones previas, engañar al usuario, eludir las barreras de seguridad).",
remediation:
"Elimine las instrucciones manipuladoras. Un skill no debe indicar al agente que eluda las reglas de seguridad ni que engañe al usuario.",
},
},
"PRIV-HIDDEN-INSTRUCTIONS": {
en: {
category: "Hidden content",
title: "Hidden or invisible instructions",
description:
"Invisible Unicode characters or hidden comments were found in the text that could conceal instructions from humans.",
remediation:
"Remove invisible control characters and hidden comments. All instructions must be visible to humans.",
},
es: {
category: "Contenido oculto",
title: "Instrucciones ocultas o invisibles",
description:
"Se encontraron caracteres Unicode invisibles o comentarios ocultos en el texto que podrían ocultar instrucciones a las personas.",
remediation:
"Elimine los caracteres de control invisibles y los comentarios ocultos. Todas las instrucciones deben ser visibles para las personas.",
},
},
"PRIV-PII": {
en: {
category: "Data protection / GDPR",
title: "Collection of personal data",
description:
"The skill refers to collecting or processing personal or sensitive data (passwords, credit cards, ID documents, date of birth).",
remediation:
"Only collect personal data with a legal basis and document the purpose, scope and storage in accordance with the GDPR.",
},
es: {
category: "Protección de datos / RGPD",
title: "Recopilación de datos personales",
description:
"El skill hace referencia a la recopilación o el tratamiento de datos personales o sensibles (contraseñas, tarjetas de crédito, documentos de identidad, fecha de nacimiento).",
remediation:
"Recopile datos personales únicamente con una base legal y documente el propósito, el alcance y el almacenamiento conforme al RGPD.",
},
},
"PRIV-AGENT-TAMPERING": {
en: {
category: "System compromise",
title: "Tampering with the agent or other skills",
description:
"The skill may try to modify or delete the agent, its memory or other skills/configurations.",
remediation:
"A skill must not modify the agent, other skills, memory or configuration files.",
},
es: {
category: "Compromiso del sistema",
title: "Manipulación del agente o de otros skills",
description:
"El skill podría intentar modificar o eliminar el agente, su memoria u otros skills/configuraciones.",
remediation:
"Un skill no debe modificar el agente, otros skills, la memoria ni los archivos de configuración.",
},
},
"PRIV-OVERREACH": {
en: {
category: "Permissions",
title: "Excessive permission request",
description:
"The skill requests very broad or unrestricted permissions.",
remediation:
"Request only the minimum necessary permissions (principle of least privilege).",
},
es: {
category: "Permisos",
title: "Solicitud excesiva de permisos",
description:
"El skill solicita permisos muy amplios o sin restricciones.",
remediation:
"Solicite únicamente los permisos mínimos necesarios (principio de mínimo privilegio).",
},
},
"AI-PROMPT-INJECTION": {
en: {
category: "AI analysis",
title: "AI: Covert prompt injection",
description:
"Semantic AI analysis for covert or subtle attempts to manipulate agent behaviour that static rules do not catch.",
remediation:
"Manually review the spots flagged by the AI and remove manipulative content.",
},
es: {
category: "Análisis con IA",
title: "IA: Inyección de prompts encubierta",
description:
"Análisis semántico con IA para detectar intentos encubiertos o sutiles de manipular el comportamiento del agente que las reglas estáticas no captan.",
remediation:
"Revise manualmente los puntos marcados por la IA y elimine el contenido manipulador.",
},
},
"AI-MALICIOUS-INTENT": {
en: {
category: "AI analysis",
title: "AI: Malicious intent in the code",
description:
"Semantic AI analysis for malicious or hidden functionality in the code that goes beyond pure pattern matching.",
remediation:
"Manually review the flagged code sections for malicious intent.",
},
es: {
category: "Análisis con IA",
title: "IA: Intención maliciosa en el código",
description:
"Análisis semántico con IA para detectar funcionalidad maliciosa u oculta en el código que va más allá de la mera coincidencia de patrones.",
remediation:
"Revise manualmente las secciones de código marcadas en busca de intención maliciosa.",
},
},
"AI-DATA-PRIVACY": {
en: {
category: "AI analysis",
title: "AI: Data protection risk",
description:
"Semantic AI analysis for data protection risks and possible leakage of personal data.",
remediation:
"Assess the flagged data protection risks and ensure GDPR compliance.",
},
es: {
category: "Análisis con IA",
title: "IA: Riesgo de protección de datos",
description:
"Análisis semántico con IA para detectar riesgos de protección de datos y posible fuga de datos personales.",
remediation:
"Evalúe los riesgos de protección de datos marcados y garantice el cumplimiento del RGPD.",
},
},
};
// Built lazily to avoid a circular module-init crash: ruleCatalog imports this
// file, so RULE_CATALOG is in the temporal dead zone while this module first
// evaluates. Touching it only inside a function defers access until runtime.
let deById: Map<string, RuleText> | null = null;
function getDeById(): Map<string, RuleText> {
if (!deById) {
deById = new Map(
RULE_CATALOG.map((r) => [
r.ruleId,
{
category: r.category,
title: r.title,
description: r.description,
remediation: r.remediation,
} satisfies RuleText,
]),
);
}
return deById;
}
/** Localized text for a rule. Falls back to the German catalog if unknown. */
export function localizeRule(ruleId: string, lang: Lang): RuleText {
const de = getDeById().get(ruleId) ?? {
category: "",
title: ruleId,
description: "",
remediation: "",
};
if (lang === "de") return de;
const override = OVERRIDES[ruleId];
return override ? override[lang] : de;
}
// Generated snippet text used by the hidden-instructions heuristic, localized so
// findings never leak German into EN/ES reports.
export const INVISIBLE_CHAR_SNIPPET_DE =
"Unsichtbares Steuerzeichen in dieser Zeile erkannt.";
const INVISIBLE_CHAR_SNIPPET: Record<Lang, string> = {
de: INVISIBLE_CHAR_SNIPPET_DE,
en: "Invisible control character detected on this line.",
es: "Carácter de control invisible detectado en esta línea.",
};
export function localizeSnippet(snippet: string, lang: Lang): string {
if (snippet === INVISIBLE_CHAR_SNIPPET_DE) {
return INVISIBLE_CHAR_SNIPPET[lang];
}
return snippet;
}