Obbligazioni del Credential Scanner
Questa pagina documenta le quattro obbligazioni di sicurezza applicabili ad ogni PR che tocca
src/zenzic/core/. Una PR che risolve un bug senza soddisfare tutte e quattro verrà rifiutata
dall'Architecture Lead.
Queste regole esistono perché una security review ha dimostrato che quattro scelte di design
individualmente ragionevoli — ognuna corretta in isolamento — si sono composte in quattro
vettori di attacco distinti. Vedi docs/internal/security/shattered_mirror_report.md
per il post-mortem completo.
Obbligazione 1 — La Tassa di Sicurezza (Worker Timeout)
Qualsiasi PR che modifica l'uso di ProcessPoolExecutor in scanner.py deve preservare la
chiamata future.result(timeout=_WORKER_TIMEOUT_S). Il timeout corrente è 30 secondi.
# ✅ Forma richiesta — usa sempre submit() + wait(FIRST_COMPLETED) + result(timeout=...)
futures_map = {executor.submit(_worker, item): item[0] for item in work_items}
raw: list[IntegrityReport] = []
_pending: set[concurrent.futures.Future[IntegrityReport]] = set(futures_map)
while _pending:
done, _pending = concurrent.futures.wait(
_pending,
timeout=_WORKER_TIMEOUT_S,
return_when=concurrent.futures.FIRST_COMPLETED,
)
if not done:
# ZRT-002 deadlock guard: nessun worker completato entro la finestra di timeout
for fut in _pending:
raw.append(_make_timeout_report(futures_map[fut])) # finding Z902
fut.cancel()
break
for fut in done:
raw.append(fut.result())
# ❌ Vietato — blocca indefinitamente su ReDoS o worker in deadlock
raw = list(executor.map(_worker, work_items))
Il finding Z902 (WORKER_TIMEOUT) non è un crash — emerge nell'UI standard del report.
Un worker che va in timeout non termina la scansione; il coordinatore continua con i worker rimanenti.
Se la tua modifica richiede un timeout più lungo, aumenta _WORKER_TIMEOUT_S con un commento
che spieghi il costo e un benchmark che dimostri il worst-case input.
Obbligazione 2 — Il Protocollo Regex-Canary
Ogni entry [[custom_rules]] che specifica un pattern è soggetta al
Regex-Canary, un test di stress basato su POSIX SIGALRM che viene eseguito alla
costruzione di AdaptiveRuleEngine.
# _assert_regex_canary() in rules.py — eseguito automaticamente per ogni CustomRule
_CANARY_STRINGS = (
"a" * 30 + "b", # trigger classico (a+)+
"A" * 25 + "!", # variante maiuscola
"1" * 20 + "x", # variante numerica
)
_CANARY_TIMEOUT_S = 0.1 # 100 ms
Testa il tuo pattern prima del commit:
from pathlib import Path
from zenzic.core.rules import CustomRule, _assert_regex_canary
from zenzic.core.exceptions import PluginContractError
rule = CustomRule(
id="MY-001",
pattern=r"tuo-pattern-qui",
message="Trovato.",
severity="warning",
)
try:
_assert_regex_canary(rule)
print("✅ Canary superato — pattern sicuro per produzione")
except PluginContractError as e:
print(f"❌ Canary fallito — rischio ReDoS rilevato:\n{e}")
Pattern da evitare (trigger di backtracking catastrofico):
| Pattern | Perché pericoloso |
|---|---|
(a+)+ | Quantificatori annidati — percorsi esponenziali |
(a|aa)+ | Alternazione con sovrapposizione |
(a*)* | Star annidato — match vuoti infiniti |
.+foo.+bar | Multi-wildcard greedy con suffisso |
Pattern sempre sicuri:
| Pattern | Note |
|---|---|
TODO | Match letterale, O(n) |
^(DRAFT|WIP): | Alternazione ancorata, O(1) per posizione |
[A-Z]{3}-\d+ | Classi di caratteri delimitate |
\bfoo\b | Ancorato a word-boundary |
Nota piattaforma:
_assert_regex_canary()usasignal.SIGALRM, disponibile solo su sistemi POSIX (Linux, macOS). Su Windows il canary è un no-op. Il worker timeout (Obbligazione 1) è il backstop universale.
Obbligazione 3 — L'Invariante Dual-Stream
Lo stream del credential scanner e lo stream Content in ReferenceScanner.harvest() non
devono mai condividere un generator. Questa è la lezione architetturale di ZRT-001.
# ✅ CORRETTO — generator indipendenti, contratti di filtering indipendenti
with file_path.open(encoding="utf-8") as fh:
for lineno, line in enumerate(fh, start=1): # Credential scanner: TUTTE le righe
list(scan_line_for_secrets(line, file_path, lineno))
for lineno, line in _iter_content_lines(file_path): # Content: filtrato
...
# ❌ VIETATO — condividere un generator scarta silenziosamente il frontmatter dal credential scanner
with file_path.open(encoding="utf-8") as fh:
shared = _skip_frontmatter(fh)
for lineno, line in shared:
list(scan_line_for_secrets(...)) # ← cieco al frontmatter
for lineno, line in shared: # ← già esaurito
...
Baseline di performance: il dual-scan (riga raw + normalizzata) gira a circa
235 000 righe/secondo (12.74 ms mediana per 3 000 righe su 20 iterazioni). Se una PR
rifattorizza harvest() e il throughput CI scende sotto 100 000 righe/secondo, indagare
prima del merge.
Obbligazione 4 — Mutation Score ≥ 90% per Modifiche al Core
Qualsiasi PR che modifica src/zenzic/core/ deve mantenere o migliorare il mutation score
sul modulo interessato. La baseline corrente per rules.py è 86.7% (242/279 mutanti
eliminati). Target per rc1: ≥ 90%.
nox -s mutation
La sessione punta a rules.py, credentials.py e reporter.py. Qualsiasi PR che tocca
la funzione di conversione _map_credentials_to_finding(), il percorso della severity
SECURITY_BREACH in ZenzicReporter, o il routing del codice di uscita in cli.py
deve eliminare tutti e tre i mutanti obbligatori:
| Nome mutante | Cosa viene cambiato | Test che deve eliminarlo |
|---|---|---|
| The Invisible | severity="security_breach" → severity="warning" in _map_credentials_to_finding() | test_map_always_emits_security_breach_severity |
| The Amnesiac | _obfuscate_secret() restituisce raw invece della forma redatta | test_obfuscate_never_leaks_raw_secret |
| The Silencer | _map_credentials_to_finding() restituisce None invece di un Finding | test_pipeline_appends_breach_finding_to_list |
Validazione pickle di ResolutionContext: qualsiasi PR che aggiunge un campo a ResolutionContext
deve includere:
def test_resolution_context_is_pickleable():
import pickle
ctx = ResolutionContext(docs_root=Path("/docs"), source_file=Path("/docs/a.md"))
assert pickle.loads(pickle.dumps(ctx)) == ctx
Integrità dei report: un segreto rilevato ma non riportato correttamente è un bug CRITICO — indistinguibile da un segreto che non è mai stato rilevato.