change gitignore

This commit is contained in:
criss717 2026-08-10 19:08:57 +02:00
parent 2ed1a69672
commit ee7013b219
5 changed files with 644 additions and 0 deletions

2
.gitignore vendored
View file

@ -93,6 +93,8 @@ Thumbs.db
schema.graphql
.opencode/
.atl/
.codegraph/
# Root-only local data and reference checkouts
/.benchmarks/

View file

@ -0,0 +1,213 @@
# Technical Design: i18n Support — Phase 1
## Architecture Overview
```
┌─────────────────────────────────────────────────────────┐
│ Language Resolution │
│ --language > STRIX_LANGUAGE > config.json > LANG > en │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ strix/i18n.py │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ set_language │ │ get_language │ │ t(key, **kw) │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ _locales: dict[str, dict[str, str]] │ │
│ │ (lazy-loaded, cached, thread-safe) │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
strix/locales/ CLI args Agent prompt
{en,es}.json argparse Jinja injection
```
## Module Design: strix/i18n.py
```python
"""Internationalization support for Strix."""
from __future__ import annotations
import json
import logging
import os
import threading
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
# Supported languages — add new ones here + create matching JSON file
SUPPORTED_LANGUAGES: frozenset[str] = frozenset({"en", "es"})
# Module-level state
_language: str | None = None
_locales: dict[str, dict[str, str]] = {}
_lock = threading.Lock()
_locales_dir: Path = Path(__file__).parent / "locales"
def _detect_language() -> str:
"""Resolve language from the priority chain.
Priority:
1. _language (set by --language CLI flag or set_language())
2. STRIX_LANGUAGE env var
3. ~/.strix/cli-config.json "language" field
4. LANG / LC_ALL system locale (first 2 chars)
5. "en" default
"""
# 1. Explicitly set (CLI flag)
if _language is not None:
return _language
# 2. Environment variable
env_lang = os.environ.get("STRIX_LANGUAGE", "").strip().lower()
if env_lang:
return _normalize_lang(env_lang)
# 3. Config file
try:
config_path = Path.home() / ".strix" / "cli-config.json"
if config_path.exists():
data = json.loads(config_path.read_text(encoding="utf-8"))
config_lang = data.get("language", "").strip().lower()
if config_lang:
return _normalize_lang(config_lang)
except (json.JSONDecodeError, OSError):
pass
# 4. System locale
for var in ("LANG", "LC_ALL", "LC_MESSAGES"):
locale_val = os.environ.get(var, "")
if locale_val and len(locale_val) >= 2:
candidate = locale_val[:2].lower()
if candidate in SUPPORTED_LANGUAGES:
return candidate
# 5. Default
return "en"
def _normalize_lang(lang: str) -> str:
"""Normalize and validate a language code."""
lang = lang.strip().lower()[:2]
if lang not in SUPPORTED_LANGUAGES:
logger.warning("Unsupported language %r, falling back to 'en'", lang)
return "en"
return lang
def _load_locale(lang: str) -> dict[str, str]:
"""Load a locale JSON file. Thread-safe, cached."""
with _lock:
if lang in _locales:
return _locales[lang]
locale_file = _locales_dir / f"{lang}.json"
if not locale_file.exists():
logger.warning("Locale file not found: %s", locale_file)
_locales[lang] = {}
return {}
try:
data = json.loads(locale_file.read_text(encoding="utf-8"))
_locales[lang] = data if isinstance(data, dict) else {}
return _locales[lang]
except (json.JSONDecodeError, OSError) as exc:
logger.error("Failed to load locale %s: %s", lang, exc)
_locales[lang] = {}
return {}
def set_language(lang: str | None) -> None:
"""Set the active language. Called from CLI args parsing."""
global _language
_language = _normalize_lang(lang) if lang else None
def get_language() -> str:
"""Get the currently resolved language."""
return _detect_language()
def t(key: str, **kwargs: Any) -> str:
"""Translate a key to the active language.
Args:
key: Dot-separated translation key (e.g., "cli.scan_started")
**kwargs: Placeholder values for {name} interpolation
Returns:
Translated string with placeholders filled, or the key itself if not found.
"""
lang = get_language()
# Try active language first
locale = _load_locale(lang)
value = locale.get(key)
# Fallback to English
if value is None and lang != "en":
en_locale = _load_locale("en")
value = en_locale.get(key)
if value is not None:
logger.debug("Key %r not found in %s, using English fallback", key, lang)
# Last resort: return the key itself
if value is None:
logger.warning("Translation key not found: %s", key)
return key
# Interpolate placeholders
if kwargs:
try:
return value.format(**kwargs)
except KeyError as exc:
logger.warning("Missing placeholder %s in key %s", exc, key)
return value
return value
def get_language_directive() -> str:
"""Get the language directive for agent system prompts.
Returns empty string for English (no directive needed).
Returns an instruction block for other languages.
"""
lang = get_language()
if lang == "en":
return ""
lang_names = {
"es": "Spanish",
"fr": "French",
"de": "German",
"pt": "Portuguese",
"it": "Italian",
}
lang_name = lang_names.get(lang, lang)
return f"""LANGUAGE DIRECTIVE:
The user's preferred language is {lang_name}.
Write all natural-language findings, explanations, descriptions, impact assessments,
remediation steps, and recommendations in {lang_name}.
Keep the following UNCHANGED (do not translate):
- CVE identifiers (e.g., CVE-2025-XXXX)
- CWE identifiers (e.g., CWE-79)
- CVSS scores
- HTTP requests and headers
- URLs and domains
- Source code snippets
- Shell commands and payloads
- Technical product names
- File paths"""

View file

@ -0,0 +1,61 @@
# Proposal: Internationalization (i18n) Support — Phase 1
## Intent
Strix is hardcoded to English across Python CLI, Go TUI, and React viewer. Non-English security teams can't consume findings in their language, slowing triage and adoption. This adds an `i18n` capability so scans run in Spanish (others follow the same plumbing) without altering the engine.
## Scope
**In (Phase 1):** `Settings.language: str = "en"` (pydantic-settings), env `STRIX_LANGUAGE`, CLI `--language`/`-l`, persisted in `~/.strix/cli-config.json`. Resolution: `--language` > env > config > `LANG`/`LC_ALL` > `"en"`. Flat JSON dicts in `strix/locales/{lang}.json`. `strix/i18n.py` with `t("key")`. Spanish for argparse help, scan progress, errors. One Jinja variable in `system_prompt.jinja` directing the LLM to write findings/descriptions/recommendations in the target language (CVE/CWE/CVSS/code/commands unchanged). Lazy wrapper for argparse `help=` so locale is resolved at parse time.
**Out (later PRs):** Phase 2 report headings, Phase 3 Go TUI (~300 strings, 45 files), Phase 4 React viewer. SARIF and `vulnerabilities.json` stay English.
## Capabilities
**New:** `internationalization` — locale loading, resolution chain, prompt injection, `t()`.
**Modified:** None.
## Approach
Flat JSON dicts are portable — same source later serves Go (backend socket) and React (copy/fetch). Python side: one `Settings.language` field, small `i18n.py` with cache, one Jinja variable prepending a language directive to the agent system prompt. LLM body returns in target language; static CLI strings use `t()`. argparse `help=` uses a deferred callable so locale is available at parse time.
## Affected Areas
| Area | Impact | Change |
|------|--------|--------|
| `strix/config/settings.py` | Mod | `Settings.language` + env |
| `strix/interface/cli_args.py` | Mod | `--language` flag; lazy `help=` |
| `strix/locales/{en,es}.json` | New | Flat locale dicts |
| `strix/i18n.py` | New | `t()`, `set/get_language()` + cache |
| `strix/agents/prompts/system_prompt.jinja` | Mod | Inject `{{ language_directive }}` |
| `strix/agents/factory.py` | Mod | Pass language to Jinja |
| `strix/interface/utils.py` | Mod | Wrap CLI msgs in `t()` |
| `tests/` | Mod | Locale + prompt tests |
## Risks
| Risk | Mitigation |
|------|------------|
| LLM quality drops in non-English (M) | Directive preserves CVE/CWE/CVSS/code; model stays user-driven |
| argparse help evaluated at import (H) | Lazy callable wrapper at parse time |
| 3 ecosystems need coordinated i18n (M) | JSON portable; Phase 1 only touches Python |
| Locale drift between languages (L) | Keys generated from `en.json`; missing → English + warning |
| Locale leaks into SARIF/JSON (L) | Exports use fixed English keys; test covers |
## Rollback
Revert the merge. `Settings.language` defaults `"en"` and `t()` returns the key when no locale loaded — removing files and unhooking the Jinja directive restores prior behavior, no migration.
## Dependencies
`pydantic-settings` (already present). No new libs — stdlib `json` + existing Jinja2.
## Success Criteria
- [ ] `--language es` → Spanish findings; CVE/CWE/CVSS/code unchanged
- [ ] `STRIX_LANGUAGE=es` and `"language": "es"` in config match the flag
- [ ] `t('cli.scan_started')` returns Spanish under env, English otherwise
- [ ] `--help` shows translated text under `--language es`
- [ ] SARIF and `vulnerabilities.json` stay English regardless of locale
- [ ] `uv run pytest` and `make check-all` pass

View file

@ -0,0 +1,177 @@
# Internationalization Specification
## Purpose
Enable Strix to operate in multiple languages. Phase 1 delivers Spanish alongside English for CLI strings and LLM-generated findings, while preserving English for all machine-consumed artifacts (SARIF, vulnerabilities.json).
## Requirements
### Requirement: Locale Loading
The system SHALL load locale data from flat JSON files at `strix/locales/{lang}.json`. Locale files MUST be loaded lazily on first `t()` call or language resolution, then cached in memory for the process lifetime. Missing keys MUST fall back to the English value and log a warning.
#### Scenario: Load Spanish locale on first t() call
- GIVEN `strix/locales/es.json` exists with key `"cli.scan_started": "Escaneo iniciado"`
- WHEN `t("cli.scan_started")` is called with active language `"es"`
- THEN the function returns `"Escaneo iniciado"`
- AND the locale file is read from disk exactly once
#### Scenario: Missing key falls back to English
- GIVEN `strix/locales/es.json` does NOT contain key `"cli.unknown_key"`
- AND `strix/locales/en.json` contains `"cli.unknown_key": "Unknown key"`
- WHEN `t("cli.unknown_key")` is called with active language `"es"`
- THEN the function returns `"Unknown key"`
- AND a warning is logged
#### Scenario: Key missing from all locales
- GIVEN no locale file contains key `"cli.nonexistent"`
- WHEN `t("cli.nonexistent")` is called
- THEN the function returns the key string `"cli.nonexistent"`
### Requirement: Language Resolution Chain
The system SHALL determine the active language using this priority: `--language` CLI flag > `STRIX_LANGUAGE` env var > `~/.strix/cli-config.json` `"language"` field > `LANG`/`LC_ALL` system locale > `"en"` default. Unsupported languages MUST fall back to `"en"` with a warning. Initially supported: `en`, `es`.
#### Scenario: CLI flag takes highest priority
- GIVEN `STRIX_LANGUAGE=es` and `~/.strix/cli-config.json` contains `"language": "en"`
- WHEN the user runs `strix --language es --target example.com`
- THEN the active language is `"es"`
#### Scenario: Environment variable used when no CLI flag
- GIVEN no `--language` flag is provided
- AND `STRIX_LANGUAGE=es` is set
- WHEN strix starts
- THEN the active language is `"es"`
#### Scenario: Config file used when no flag or env
- GIVEN no `--language` flag, no `STRIX_LANGUAGE` env var
- AND `~/.strix/cli-config.json` contains `"language": "es"`
- WHEN strix starts
- THEN the active language is `"es"`
#### Scenario: System locale detection
- GIVEN no flag, env, or config language set
- AND `LANG=es_ES.UTF-8`
- WHEN strix starts
- THEN the active language is `"es"`
#### Scenario: Unsupported language falls back to English
- GIVEN `STRIX_LANGUAGE=fr` (unsupported)
- WHEN strix starts
- THEN the active language is `"en"`
- AND a warning is logged
### Requirement: CLI Integration
The system SHALL provide a `--language` / `-l` CLI flag via argparse. Help text for all argparse arguments MUST be evaluated lazily at parse time, not import time, so the active language is resolved before help strings are displayed. When `--language` is provided, the system SHOULD persist it to `~/.strix/cli-config.json`.
#### Scenario: --language flag sets active language
- GIVEN the user runs `strix --language es --target example.com`
- WHEN arguments are parsed
- THEN the active language is `"es"`
#### Scenario: Help text is translated
- GIVEN `STRIX_LANGUAGE=es`
- WHEN the user runs `strix --help`
- THEN help strings are displayed in Spanish
#### Scenario: Language persisted to config
- GIVEN the user runs `strix --language es --target example.com`
- WHEN the scan completes
- THEN `~/.strix/cli-config.json` contains `"language": "es"`
### Requirement: Agent Prompt Injection
The system SHALL inject a `{{ language_directive }}` Jinja variable into `system_prompt.jinja`. The directive MUST instruct the LLM to write findings, descriptions, and recommendations in the target language while preserving technical identifiers (CVE, CWE, CVSS, code snippets, commands) unchanged. The prompt factory MUST pass the resolved language context to the template.
#### Scenario: Spanish directive injected
- GIVEN active language is `"es"`
- WHEN `render_system_prompt()` is called
- THEN the rendered prompt contains an instruction to write in Spanish
- AND technical identifiers are explicitly excluded from translation
#### Scenario: English directive is no-op
- GIVEN active language is `"en"`
- WHEN `render_system_prompt()` is called
- THEN the language directive is empty or absent
### Requirement: t() Helper Contract
The system SHALL provide a `t(key: str, **kwargs) -> str` function. It MUST support `{placeholder}` interpolation via kwargs. It MUST be thread-safe and cache loaded locales. If a key is not found, it MUST return the key itself (graceful degradation).
#### Scenario: Placeholder interpolation
- GIVEN `en.json` contains `"cli.scan_target": "Scanning {target}"`
- WHEN `t("cli.scan_target", target="example.com")` is called
- THEN the function returns `"Scanning example.com"`
#### Scenario: Thread-safe concurrent access
- GIVEN multiple threads call `t()` simultaneously
- WHEN locales are not yet loaded
- THEN the locale is loaded exactly once
- AND all threads receive correct translations
### Requirement: Spanish Translations
The system SHALL ship `strix/locales/es.json` with Spanish translations for Phase 1 strings: CLI argparse help, scan progress messages, error messages, and auth flow messages. Keys MUST be dot-separated paths matching the English source.
#### Scenario: All Phase 1 keys translated
- GIVEN `strix/locales/en.json` contains N keys
- WHEN `strix/locales/es.json` is loaded
- THEN it contains translations for all N keys
#### Scenario: Key structure consistency
- GIVEN `en.json` has key `"cli.scan_started"`
- THEN `es.json` MUST have the same key `"cli.scan_started"`
## Locale Key Structure
```json
// en.json
{
"cli.target_help": "Target to test: URL, repository, local directory path, domain name, IP address...",
"cli.instruction_help": "Custom instructions for the penetration test.",
"cli.scan_started": "Starting scan against {target}",
"cli.scan_completed": "Scan completed. {count} vulnerabilities found.",
"cli.error_no_target": "No target specified. Use --target or --target-list.",
"cli.error_invalid_target": "Invalid target: {target}",
"cli.auth_login_prompt": "Enter your API key",
"cli.auth_login_success": "Authentication successful",
"cli.auth_login_failure": "Authentication failed: {reason}",
"cli.progress_recon": "Performing reconnaissance...",
"cli.progress_scanning": "Scanning {target}...",
"cli.progress_reporting": "Generating report..."
}
// es.json
{
"cli.target_help": "Objetivo a probar: URL, repositorio, directorio local, dominio, dirección IP...",
"cli.instruction_help": "Instrucciones personalizadas para la prueba de penetración.",
"cli.scan_started": "Iniciando escaneo contra {target}",
"cli.scan_completed": "Escaneo completado. {count} vulnerabilidades encontradas.",
"cli.error_no_target": "No se especificó objetivo. Use --target o --target-list.",
"cli.error_invalid_target": "Objetivo inválido: {target}",
"cli.auth_login_prompt": "Ingrese su clave API",
"cli.auth_login_success": "Autenticación exitosa",
"cli.auth_login_failure": "Autenticación fallida: {reason}",
"cli.progress_recon": "Realizando reconocimiento...",
"cli.progress_scanning": "Escaneando {target}...",
"cli.progress_reporting": "Generando informe..."
}
```

View file

@ -0,0 +1,191 @@
# Tasks: i18n Support — Phase 1
## Review Workload Forecast
- **Estimated changed lines**: ~250 (well under 400-line budget)
- **Chained PRs recommended**: No
- **Decision needed before apply**: No
---
## Task 1: Add language field to Settings
**File**: `strix/config/settings.py`
**Description**: Add `language: str` field to the `Settings` class with `STRIX_LANGUAGE` env var alias.
**Changes**:
```python
class Settings(BaseSettings):
# ... existing fields ...
language: str = Field(default="en", alias="STRIX_LANGUAGE")
```
**Acceptance**:
- [ ] `Settings(language="es").language == "es"`
- [ ] `STRIX_LANGUAGE=es` env var is picked up
- [ ] Default is `"en"`
**Dependencies**: None
---
## Task 2: Add --language CLI flag
**File**: `strix/interface/cli_args.py`
**Description**: Add `--language` / `-l` argument to argparse. Call `set_language()` after parsing.
**Changes**:
1. Add argument before `parse_arguments()` returns:
```python
parser.add_argument(
"-l", "--language",
type=str,
default=None,
help="Language for UI and agent responses (e.g., 'en', 'es'). Default: auto-detect.",
)
```
2. After `args = parser.parse_args()`, add:
```python
from strix.i18n import set_language
if args.language:
set_language(args.language)
```
**Acceptance**:
- [ ] `strix --language es --help` shows help in Spanish
- [ ] `strix -l es` works
- [ ] No `--language` flag → auto-detection from env/config/locale
**Dependencies**: Task 1
---
## Task 3: Inject language directive into agent prompts
**Files**:
- `strix/agents/prompt.py`
- `strix/agents/prompts/system_prompt.jinja`
**Description**: Pass `language_directive` to Jinja template and render it.
**Changes in prompt.py** (`render_system_prompt` function):
```python
from strix.i18n import get_language_directive
# Inside render_system_prompt(), before env.get_template().render():
language_directive = get_language_directive()
# Add to render() call:
rendered = env.get_template("system_prompt.jinja").render(
# ... existing params ...
language_directive=language_directive,
)
```
**Changes in system_prompt.jinja** (add near the top, after initial instructions):
```jinja
{% if language_directive %}
{{ language_directive }}
{% endif %}
```
**Acceptance**:
- [ ] With `language="es"`, rendered prompt contains "Spanish" instruction
- [ ] With `language="en"`, no directive injected (empty string)
- [ ] CVE/CWE/CVSS preservation mentioned in directive
**Dependencies**: None (parallel with Task 2)
---
## Task 4: Integrate t() into main CLI messages
**File**: `strix/interface/main.py`
**Description**: Replace hardcoded English strings with `t()` calls for key user-facing messages.
**Changes**:
```python
from strix.i18n import t
# Replace strings like:
# print("Starting scan...")
# With:
# print(t("cli.scan_started", target=target))
```
Key strings to translate:
- Scan start/complete messages
- Error messages for missing targets
- Progress indicators
**Acceptance**:
- [ ] `strix --language es -t example.com` shows Spanish progress messages
- [ ] `strix -t example.com` shows English (default)
- [ ] No runtime errors from t() calls
**Dependencies**: Task 1, Task 2
---
## Task 5: Add tests
**File**: `tests/test_i18n.py`
**Description**: Test the i18n module: translation, fallback, language resolution, directive generation.
**Test cases**:
```python
def test_t_returns_english_by_default()
def test_t_returns_spanish_when_language_set()
def test_t_falls_back_to_english_for_missing_key()
def test_t_returns_key_for_completely_missing_key()
def test_t_interpolates_placeholders()
def test_set_language_normalizes()
def test_get_language_directive_empty_for_english()
def test_get_language_directive_contains_language_name()
def test_locale_files_are_valid_json()
def test_all_en_keys_exist_in_es()
```
**Acceptance**:
- [ ] `uv run pytest tests/test_i18n.py -v` passes
- [ ] All locale keys validated
**Dependencies**: Task 1-4
---
## Task 6: Verify with make check-all
**Description**: Run full quality suite to ensure no regressions.
**Commands**:
```bash
make check-all # ruff, mypy, bandit
uv run pytest # all tests
```
**Acceptance**:
- [ ] `make check-all` passes
- [ ] `uv run pytest` passes
- [ ] No new warnings or errors
**Dependencies**: Task 1-5
---
## Implementation Order
```
Task 1 (Settings) ──┐
├──> Task 2 (CLI flag) ──┐
Task 3 (Jinja) ─────┘ ├──> Task 4 (Main.py) ──> Task 5 (Tests) ──> Task 6 (Verify)
│
└──> Task 3 (parallel)
```
Tasks 1 and 3 can be done in parallel. Task 2 depends on Task 1. Task 4 depends on Task 2. Task 5 depends on all. Task 6 is final verification.