mirror of
https://github.com/usestrix/strix.git
synced 2026-08-28 05:25:00 +00:00
Merge 4264af7a9e into cbb0f57058
This commit is contained in:
commit
5040b81b3e
9 changed files with 272 additions and 0 deletions
|
|
@ -36,6 +36,10 @@ for asset in viewer_static.rglob('*'):
|
|||
rel_path = asset.relative_to(project_root)
|
||||
datas.append((str(asset), str(rel_path.parent)))
|
||||
|
||||
for json_locale in strix_root.rglob('locales/*.json'):
|
||||
rel_path = json_locale.relative_to(project_root)
|
||||
datas.append((str(json_locale), str(rel_path.parent)))
|
||||
|
||||
datas += collect_data_files('tiktoken')
|
||||
datas += collect_data_files('tiktoken_ext')
|
||||
|
||||
|
|
@ -112,6 +116,7 @@ hiddenimports = [
|
|||
|
||||
# Strix modules
|
||||
'strix',
|
||||
'strix.i18n',
|
||||
'strix.interface',
|
||||
'strix.interface.main',
|
||||
'strix.interface.cli',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
You are an advanced AI application security validation agent. Your purpose is to perform authorized security verification, reproduce and validate weaknesses on in-scope assets, and help remediate real security issues.
|
||||
{% if language_directive %}
|
||||
<language_directive>
|
||||
{{ language_directive }}
|
||||
</language_directive>
|
||||
{% endif %}
|
||||
You follow all instructions and rules provided to you exactly as written in the system prompt at all times.
|
||||
{% if is_root %}
|
||||
<root_agent_directive>
|
||||
|
|
|
|||
|
|
@ -147,6 +147,10 @@ class ViewerSettings(BaseSettings):
|
|||
class Settings(BaseSettings):
|
||||
model_config = _BASE_CONFIG
|
||||
|
||||
language: str = Field(
|
||||
default="en",
|
||||
validation_alias=AliasChoices("STRIX_LANGUAGE", "language"),
|
||||
)
|
||||
llm: LlmSettings = Field(default_factory=LlmSettings)
|
||||
dedupe: DedupeSettings = Field(default_factory=DedupeSettings)
|
||||
runtime: RuntimeSettings = Field(default_factory=RuntimeSettings)
|
||||
|
|
|
|||
145
strix/i18n.py
Normal file
145
strix/i18n.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
"""Robust Internationalization (i18n) Engine for Strix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import threading
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
SUPPORTED_LANGUAGES: Set[str] = {"en", "es", "id"}
|
||||
DEFAULT_LANGUAGE: str = "en"
|
||||
|
||||
# ContextVar for async execution & multi-tenant thread safety
|
||||
_current_language: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
||||
"strix_current_language", default=None
|
||||
)
|
||||
|
||||
_locales_lock = threading.Lock()
|
||||
_locales_cache: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
|
||||
def _normalize_lang(lang: Optional[str]) -> str:
|
||||
"""Normalize language tags (e.g. 'es_ES.UTF-8' -> 'es')."""
|
||||
if not lang:
|
||||
return DEFAULT_LANGUAGE
|
||||
clean = lang.strip().lower().split("_")[0].split("-")[0]
|
||||
return clean if clean in SUPPORTED_LANGUAGES else DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def set_language(lang: Optional[str]) -> str:
|
||||
"""Set language preference for the current async execution context."""
|
||||
normalized = _normalize_lang(lang)
|
||||
_current_language.set(normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def get_language() -> str:
|
||||
"""Get active language preference or auto-detect from environment/config."""
|
||||
lang = _current_language.get()
|
||||
if lang is not None:
|
||||
return lang
|
||||
return _detect_language()
|
||||
|
||||
|
||||
def _detect_language() -> str:
|
||||
"""Detect language via STRIX_LANGUAGE, config file, or system LANG/LC_ALL."""
|
||||
# 1. Environment Variable STRIX_LANGUAGE
|
||||
env_lang = os.getenv("STRIX_LANGUAGE")
|
||||
if env_lang:
|
||||
return _normalize_lang(env_lang)
|
||||
|
||||
# 2. Config File ~/.strix/cli-config.json
|
||||
try:
|
||||
cfg_path = Path.home() / ".strix" / "cli-config.json"
|
||||
if cfg_path.exists():
|
||||
data = json.loads(cfg_path.read_text(encoding="utf-8"))
|
||||
cfg_lang = data.get("env", {}).get("STRIX_LANGUAGE") or data.get("language")
|
||||
if cfg_lang:
|
||||
return _normalize_lang(cfg_lang)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. System Locale (LC_ALL takes precedence over LANG)
|
||||
sys_lang = os.getenv("LC_ALL") or os.getenv("LANG")
|
||||
if sys_lang:
|
||||
return _normalize_lang(sys_lang)
|
||||
|
||||
return DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
def _load_locale(lang: str) -> Dict[str, str]:
|
||||
"""Load locale JSON file with thread-safe caching, type validation, and user overrides."""
|
||||
with _locales_lock:
|
||||
if lang in _locales_cache:
|
||||
return _locales_cache[lang]
|
||||
|
||||
translations: Dict[str, str] = {}
|
||||
|
||||
def _safe_merge(source_data: Any) -> None:
|
||||
if isinstance(source_data, dict):
|
||||
for k, v in source_data.items():
|
||||
if isinstance(k, str) and isinstance(v, str):
|
||||
translations[k] = v
|
||||
|
||||
# Built-in locale
|
||||
builtin_path = Path(__file__).parent / "locales" / f"{lang}.json"
|
||||
if builtin_path.exists():
|
||||
try:
|
||||
_safe_merge(json.loads(builtin_path.read_text(encoding="utf-8")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# User override locale (~/.strix/locales/{lang}.json)
|
||||
user_path = Path.home() / ".strix" / "locales" / f"{lang}.json"
|
||||
if user_path.exists():
|
||||
try:
|
||||
_safe_merge(json.loads(user_path.read_text(encoding="utf-8")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with _locales_lock:
|
||||
_locales_cache[lang] = translations
|
||||
return translations
|
||||
|
||||
|
||||
def t(key: str, **kwargs: Any) -> str:
|
||||
"""Translate key into active language with safe keyword interpolation."""
|
||||
lang = get_language()
|
||||
locale = _load_locale(lang)
|
||||
|
||||
msg = locale.get(key)
|
||||
if msg is None and lang != DEFAULT_LANGUAGE:
|
||||
msg = _load_locale(DEFAULT_LANGUAGE).get(key)
|
||||
if msg is None:
|
||||
msg = key
|
||||
|
||||
if kwargs and "{" in msg and "}" in msg:
|
||||
try:
|
||||
msg = msg.format(**kwargs)
|
||||
except (KeyError, ValueError, IndexError):
|
||||
for k, v in kwargs.items():
|
||||
msg = msg.replace(f"{{{k}}}", str(v))
|
||||
return msg
|
||||
|
||||
|
||||
def get_language_directive() -> str:
|
||||
"""Generate prompt directive for LLM agents when target language is non-English."""
|
||||
lang = get_language()
|
||||
if lang == "en":
|
||||
return ""
|
||||
|
||||
lang_names = {
|
||||
"es": "Spanish",
|
||||
"id": "Indonesian",
|
||||
}
|
||||
target_name = lang_names.get(lang, lang.upper())
|
||||
|
||||
return (
|
||||
f"IMPORTANT LANGUAGE INSTRUCTION:\n"
|
||||
f"Write all human-readable findings, executive summaries, descriptions, and remediation steps in {target_name}.\n"
|
||||
f"Do NOT translate technical identifiers, including CVE IDs, CWE IDs, CVSS scores, "
|
||||
f"source code snippets, file paths, or shell commands."
|
||||
)
|
||||
|
|
@ -52,6 +52,13 @@ def _positive_int(value: str) -> int:
|
|||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
# Pre-resolve language before argparse if set in sys.argv
|
||||
for idx, arg in enumerate(sys.argv[:-1]):
|
||||
if arg in ("--language", "-l") and idx + 1 < len(sys.argv):
|
||||
from strix.i18n import set_language
|
||||
set_language(sys.argv[idx + 1])
|
||||
break
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Strix Multi-Agent Cybersecurity Penetration Testing Tool",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
|
|
@ -114,6 +121,12 @@ Examples:
|
|||
help="Update strix to the latest version and exit. Self-updates the "
|
||||
"standalone binary install; for pip/pipx/uv installs, prints the "
|
||||
"matching upgrade command instead.",
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
"-l",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Language preference for CLI messages and audit reports (e.g. en, es, id)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
|
|
|||
11
strix/locales/en.json
Normal file
11
strix/locales/en.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"cli.description": "Strix - Autonomous AI Security Penetration Testing Agent",
|
||||
"cli.scan_started": "Starting scan on target: {target}",
|
||||
"cli.scan_completed": "Scan completed for target: {target}",
|
||||
"cli.vulnerabilities_found": "Found {count} vulnerability(ies)",
|
||||
"report.title": "Security Audit Executive Report",
|
||||
"report.description": "Description",
|
||||
"report.evidence": "Evidence",
|
||||
"report.impact": "Impact",
|
||||
"report.remediation": "Remediation Steps"
|
||||
}
|
||||
11
strix/locales/es.json
Normal file
11
strix/locales/es.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"cli.description": "Strix - Agente Autónomo de Pruebas de Penetración de Seguridad con IA",
|
||||
"cli.scan_started": "Iniciando escaneo en el objetivo: {target}",
|
||||
"cli.scan_completed": "Escaneo completado para el objetivo: {target}",
|
||||
"cli.vulnerabilities_found": "Se encontraron {count} vulnerabilidad(es)",
|
||||
"report.title": "Informe Ejecutivo de Auditoría de Seguridad",
|
||||
"report.description": "Descripción",
|
||||
"report.evidence": "Evidencia",
|
||||
"report.impact": "Impacto",
|
||||
"report.remediation": "Pasos de Remediación"
|
||||
}
|
||||
11
strix/locales/id.json
Normal file
11
strix/locales/id.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"cli.description": "Strix - Agen Uji Penetrasi Keamanan Berbasis AI Otonom",
|
||||
"cli.scan_started": "Memulai pemindaian pada target: {target}",
|
||||
"cli.scan_completed": "Pemindaian selesai untuk target: {target}",
|
||||
"cli.vulnerabilities_found": "Ditemukan {count} kerentanan",
|
||||
"report.title": "Laporan Eksekutif Audit Keamanan",
|
||||
"report.description": "Deskripsi",
|
||||
"report.evidence": "Bukti",
|
||||
"report.impact": "Dampak",
|
||||
"report.remediation": "Langkah Perbaikan"
|
||||
}
|
||||
67
tests/test_i18n.py
Normal file
67
tests/test_i18n.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""Comprehensive tests for the Strix i18n engine."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
import strix.i18n as i18n
|
||||
from strix.i18n import get_language, get_language_directive, set_language, t
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_i18n():
|
||||
i18n._current_language.set(None)
|
||||
with i18n._locales_lock:
|
||||
i18n._locales_cache.clear()
|
||||
yield
|
||||
i18n._current_language.set(None)
|
||||
|
||||
|
||||
def test_set_and_get_language():
|
||||
set_language("es")
|
||||
assert get_language() == "es"
|
||||
set_language("id")
|
||||
assert get_language() == "id"
|
||||
|
||||
|
||||
def test_fallback_unsupported_language():
|
||||
set_language("fr")
|
||||
assert get_language() == "en"
|
||||
|
||||
|
||||
def test_translation_formatting():
|
||||
set_language("id")
|
||||
result = t("cli.scan_started", target="example.com")
|
||||
assert "example.com" in result
|
||||
assert "Memulai" in result
|
||||
|
||||
|
||||
def test_safe_interpolation_on_missing_key():
|
||||
set_language("en")
|
||||
result = t("cli.scan_started") # target missing
|
||||
assert "target" in result or isinstance(result, str)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_contextvar_isolation():
|
||||
"""Verify concurrent async tasks maintain isolated language contexts."""
|
||||
|
||||
async def task_es():
|
||||
set_language("es")
|
||||
await asyncio.sleep(0.01)
|
||||
assert get_language() == "es"
|
||||
|
||||
async def task_id():
|
||||
set_language("id")
|
||||
await asyncio.sleep(0.01)
|
||||
assert get_language() == "id"
|
||||
|
||||
await asyncio.gather(task_es(), task_id())
|
||||
|
||||
|
||||
def test_language_directive():
|
||||
set_language("es")
|
||||
directive = get_language_directive()
|
||||
assert "Spanish" in directive
|
||||
assert "CVE" in directive
|
||||
Loading…
Add table
Reference in a new issue