Architecture
Note de déploiement V1. Le nommage de la liste d'entités ci-dessous utilise les anciens préfixes « case_* » pour plusieurs tables ; les noms réels du schéma V1 sont :
cases,investigation_runs,investigation_events,investigation_iocs,investigation_assets,investigation_links,investigation_outbox,proposals. Le nom de tablecasesreste inchangé pour la rétrocompatibilité, mais toutes les tables enfant par enquête utilisent le préfixeinvestigation_*. Parmi celles-ci, les tables cases / investigation_runs / investigation_events sont sollicitées par l'orchestrateur actuel ;proposalsetinvestigation_outboxsont présentes dans le schéma, mais la partie exécuteur qui les consomme figure sur la feuille de route. Lisez cette page comme l'intention architecturale ; consultezsrc/soctalk/core/ir/models.pypour le schéma exact.
1. Entités principales
Forme minimale. Les listes complètes de colonnes vivent dans la migration ; seuls les champs porteurs sont nommés ici.
alerts raw ingest from adapter; AI-triaged
cases investigation unit; one run at a time
case_runs a single AI execution span against a case
case_events ordered event inbox per case (immutable)
proposals AI-proposed actions awaiting human gate
execution_log append-only audit of all meaningful actions
notes markdown / evidence blocks
iocs typed artifacts; carry external_context
case_iocs, case_assets bridge tables
case_links related-case edges (shared IOC / asset / rule)
case_outbox outbound work for executors and exportsChaque ligne porteuse de contenu porte tenant_id, visibility et created_at. La RLS s'applique par tenancy.
2. Modèle de visibilité
Classes (enum) :
mssp_only default; internal reasoning, raw tool output, hypotheses
customer_safe approved for customer view
system lifecycle and state-change events, always visible
tool_output classified per-tool at registration timeRègles :
visibilityest une colonne présente sur chaque ligne visible par l'utilisateur (messages, notes, proposals, enregistrements tool_output, entrées de la chronologie, champs du panneau de faits).- Par défaut à l'insertion, la valeur est
mssp_only. La promotion verscustomer_safeest une opération explicite. - Les requêtes du portail client filtrent au niveau de la politique RLS, et non au rendu. Une session client-viewer ne peut pas lire les lignes
mssp_only, même via du SQL brut. - Les proposals ont une visibilité au niveau du champ :
{action, outcome}peut êtrecustomer_safetandis que{rationale, blast_radius}restemssp_only. Rendu sous forme de deux projections. - Chaque promotion de visibilité émet une entrée
execution_logavec l'acteur et la justification.
Refus-par-défaut de la promotion : les politiques peuvent rétrograder la visibilité mais ne peuvent pas la relever sans une action explicite d'un principal autorisé.
3. Cycle de vie d'un run
États :
active run consuming events and taking steps
waiting_on_gate a proposal is pending; run does not mutate state
halted_budget budget exceeded; requires analyst resume
paused analyst-paused
completed case closed
failed unrecoverable error; requires analyst resume or restartTransitions :
active → waiting_on_gate on proposal created (status = proposed)
waiting_on_gate → active on proposal approved/rejected (new event)
active → halted_budget on budget exceeded
halted_budget → active on analyst resume (grants new budget)
active → paused on analyst pause
paused → active on analyst resume
active → completed on case close
* → failed on uncaught error, preserved for diagnosisInvariants :
- Au plus un run par case dans l'état
active | waiting_on_gate | halted_budget | paused. Imposé via un index unique partiel surcase_runs(case_id) WHERE status IN (...). - Compteurs de budget sur le run :
tokens_used,dollars_used,tool_calls_used,wall_clock_ms. Imposés côté serveur ; avertissement souple à 75 %, arrêt strict à 100 %. - Un run
waiting_on_gatene traite pas les événements de l'inbox, hormis les événements de résolution de gate (proposal.approved / .rejected).
4. Inbox d'événements, ordonnancement, coalescence, idempotence
Tout le travail entrant pour un case atterrit dans case_events :
event_id uuid PK
case_id FK
run_id FK nullable
seq bigint, case-scoped monotonic (sequence)
kind enum (alert_ingested, tool_result,
proposal_approved, proposal_rejected,
analyst_message, analyst_correction,
budget_warning, external_signal, ...)
payload jsonb
causation_event_id uuid nullable (which event caused this one)
correlation_id uuid (spans a causally-related fan-out)
idempotency_key text unique per case
created_at timestamptzRègles :
seqest émis par une séquence à portée case lors de l'insertion. Les consommateurs lisent strictement dans l'ordre deseq.idempotency_keyest unique parcase_id. Une insertion en double est silencieusement ignorée (renvoie la ligne existante).- Coalescence : avant l'insertion, les événements correspondant à
(case_id, kind, payload.signature, window)fusionnent en une seule ligne. La signature est spécifique au kind (alert : empreinte de IOC + règle + asset ; tool_result : tool_id + hash des params). causation_event_idrelie cause → effet pour le rejeu.correlation_idregroupe les événements issus d'un même déclencheur externe ou d'une action d'analyste.- Les événements sont immuables. Les mises à jour s'expriment comme des événements ultérieurs.
Exemple de rafale : 100 alertes hôte similaires en 5 minutes se coalescent en un seul événement alert_ingested portant une liste asset_ids: [...]. Le run le traite une seule fois.
5. Cycle de vie d'une proposal et contrat d'exécution
États :
draft being composed by the AI
proposed submitted to human gate
approved human approved (with typed reason if required)
rejected human rejected (reason required)
executing outbox picked up; executor running
executed action complete, result recorded
rolled_back post-execution reversal (rare, analyst-initiated)
failed executor errorIdempotence :
proposal.idempotency_key = sha256(case_id || action_type ||
canonical_json(params))Les proposals en double dans une fenêtre active (par défaut 15 minutes) sont rejetées à l'insertion. Garantit que l'AI ne peut pas déclencher deux fois, même en cas de re-run.
Comportement du gate :
- Sur
proposed: le run transitionne verswaiting_on_gate. - Sur
approved: insère une ligne danscase_outboxaveckind = 'execute_proposal',idempotency_key = proposal.idempotency_key. Émetproposal_approveddanscase_events. Le run reprend. - Sur
rejected: émetproposal_rejectedavec motif danscase_events. Le run reprend. Aucune ligne d'outbox.
Exécution :
- Un worker exécuteur distinct consomme
case_outboxet effectue l'action. - En cas de succès : enregistre
execute_proposal_resultdanscase_events, met à jour la proposal →executed, écrit une entréeexecution_log. - En cas d'échec : enregistre l'erreur, met à jour la proposal →
failed, écrit une entréeexecution_log. Le run peut proposer une nouvelle tentative. - Exactement-une-fois via
idempotency_key: les lignes d'outbox aux clés dupliquées sont rejetées. Les workers exécuteurs réclament les lignes avec un bail (p. ex.FOR UPDATE SKIP LOCKED).
Le run AI n'exécute pas d'effets de bord en ligne. Tout passe par l'outbox.
6. Schéma et invariants du journal d'exécution
En ajout seul (append-only), distinct de la conversation :
log_id uuid PK
case_id FK
run_id FK nullable
actor_kind enum (ai, human, system, executor)
actor_id text
kind enum (tool_call, proposal_state_change,
approval, override, visibility_promotion,
correction_applied, policy_bound,
export_emitted, ...)
subject_type enum (case, proposal, ioc, asset, note, ...)
subject_id text
before jsonb nullable
after jsonb nullable
versions jsonb (model_id, prompt_version, template_version,
policy_version at time of action)
ts timestamptz default now()Invariants :
- Aucune UPDATE ni DELETE autorisée depuis les rôles applicatifs. Seuls INSERT + SELECT. Imposé au niveau des octrois de rôles Postgres.
- Chaque changement d'état de proposal, chaque appel d'outil, chaque approbation, chaque override analyste d'une décision AI, chaque changement de visibilité, chaque correction, chaque dispatch d'outbox écrit une ligne.
versionscapture la pile qui a produit l'action. Requis pour la reproductibilité et la calibration a posteriori.- La conversation est une vue rendue d'un sous-ensemble d'événements ; ce n'est pas l'audit. Détruire ou compacter la conversation ne détruit pas l'audit.
7. Autorité du panneau de faits et flux de correction
L'état structuré du case (hypothèses, IOC, assets, résumé de chronologie, confiance, directives actives) est une sortie de reducer sur case_events. Il n'est jamais muté directement par la conversation.
Règles :
- Les messages de conversation n'écrivent pas d'état structuré.
- Les mises à jour AI de l'état structuré se font via des événements émis par l'AI (
hypothesis_updated,ioc_added,asset_linked). - Les modifications de l'analyste dans le panneau de faits émettent des événements
analyst_correction. Le reducer les applique. L'AI consomme la correction comme prochain événement d'inbox et re-raisonne à partir de l'état corrigé. - Le panneau de faits est cohérent à terme (eventually consistent) avec
case_events. Une projection matérialisée (table ou vue) est maintenue ; les lectures peuvent l'atteindre directement. - Les corrections directes du journal d'exécution sont interdites ; les corrections s'expriment comme de nouveaux événements assortis d'un pointeur vers celui qui est corrigé.
8. Taxonomie des capacités d'outils
Chaque outil est enregistré avec une classe de capacité, une politique d'approbation par défaut et un modèle de coût.
Classes de capacité :
read_local inspect SocTalk state only
read_external_silent no target footprint (feeds, cached intel, vector)
read_external_attributed trace at target (SIEM query, EDR read)
write_sandbox footprint without target mutation (detonation)
write_external target state change (block, isolate, notify)Politique d'approbation par défaut par classe :
read_local → autonomous
read_external_silent → autonomous
read_external_attributed → analyst_approve
write_sandbox → analyst_approve
write_external → typed_reasonModèle de coût par outil : {tokens_est, dollars_est, wall_ms_est, footprint}. Le budget du run en suit la somme.
9. Préséance des politiques
Les politiques sont fusionnées dans cet ordre, le plus bas l'emportant sur le plus haut :
1. install default (shipped in chart, read-only in v1)
2. tenant override (MSSP sets per customer)
3. case template (phishing, ransomware, etc.)
4. case-local override (set for this one case by analyst)Pour chaque clé de politique (approbation d'outil, auto-close, promotion de visibilité, modèles de réponse, budget), la valeur effective est celle de la portée la plus profonde qui la définit.
Invariants :
- La promotion de visibilité n'est jamais réglée sur
permissivepar défaut à la portée install. Par défaut, « promotion explicite requise ». - Une politique de tenant ne peut pas outrepasser un plafond strict de niveau install (p. ex.
max_tokens_per_case). - Les overrides case-local sont limités au case et ne persistent pas vers les cases futurs.
10. Sémantique d'auto-close / réouverture
Auto-close pour les faux positifs à haute confiance :
Trigger:
AI assessment = fp, confidence ≥ policy.auto_close_threshold
AND policy.auto_close_enabled is true for the tenant
AND no active directive prevents auto-close
Action:
case.status = 'auto_closed_fp'
case.reopen_window_until = now() + policy.reopen_window
case.reopen_signature = {
ioc_fingerprints: [...],
asset_ids: [...],
time_window: {start, end}
}
run transitions to completed
execution_log row writtenRéouverture :
Trigger:
new case_events row with kind ∈ {alert_ingested, external_signal}
whose signature intersects a case's reopen_signature
where case.status = 'auto_closed_fp'
AND now() < case.reopen_window_until
Action:
case.status = 'active'
emit reopened event into case_events
new run created
execution_log row written
conversation receives a system message noting the reopenCoupe-circuit :
IntegrationConfig.auto_close_enabledpar tenant (par défaut : activé).CaseTemplate.auto_close_disabledpar type de case.
11. Contrat d'export TheHive (basé sur outbox, unidirectionnel)
Réplique les cases, IOC et notes sélectionnées vers TheHive lorsque le tenant a thehive_export_enabled. N'accepte jamais de changements entrants.
Ligne d'outbox (dans case_outbox) :
id uuid PK
kind 'export.thehive.case' | 'export.thehive.ioc' | ...
external_system 'thehive'
external_ref TheHive object id (filled on first successful mirror)
object_type case | ioc | note
object_id internal subject id
idempotency_key sha256(object_type || object_id || state_hash)
payload jsonb
export_status pending | in_flight | succeeded | failed | skipped
attempts int
last_error text nullable
next_attempt_at timestamptz
created_at, updated_atRègles :
- Un changement d'état sur un objet répliqué met en file une ligne d'export avec une
idempotency_keyfraîche (qui intègre le hash d'état). - Le worker réclame avec
FOR UPDATE SKIP LOCKED. En cas de succès, enregistreexternal_ref(en créant ou mettant à jour côté TheHive selon le besoin) et écrit dans execution_log. - Les webhooks entrants de TheHive ne sont acceptés que pour les cases de tableau de bord en lecture seule (pas en v1). Toute tentative d'accepter un état entrant est explicitement rejetée et journalisée.
- Aucune boucle de réconciliation — TheHive est un miroir en aval, la source de vérité est SocTalk.
- Les exports en échec réessaient avec un backoff exponentiel jusqu'à un plafond ; l'échec permanent remonte sur le panneau de santé des intégrations.
12. Tests obligatoires et invariants
La suite de tests (unitaire + intégration) doit couvrir :
- Immuabilité du journal d'exécution. Les UPDATE et DELETE sur
execution_logdepuis le rôle applicatif échouent au niveau Postgres. - Un seul run actif par case. Les tentatives concurrentes de créer un second run actif échouent avec une violation de contrainte d'unicité.
- Idempotence des proposals. Soumettre deux proposals avec la même clé d'idempotence dans la fenêtre : la seconde est rejetée.
- Comportement de pause-gate. Un run avec une proposal
proposedne consomme pas les événements non-gate de son inbox. - Outbox exactement-une-fois. Deux workers réclamant la même ligne d'outbox aboutissent à un succès et un no-op.
- Application de la visibilité. Une session client-viewer ne peut pas sélectionner de lignes
mssp_onlydans aucune table, même en SQL brut. - Promotion de visibilité journalisée. Chaque promotion de
mssp_onlyverscustomer_safeproduit une ligneexecution_log. - Flux de correction. Un événement de correction analyste produit un nouvel événement que le reducer applique ; la projection du panneau de faits reflète la correction.
- Réouverture après auto-close. Un événement correspondant à une reopen_signature dans la fenêtre rouvre le case et démarre un nouveau run.
- Idempotence de l'export TheHive. Re-lancer un export pour un objet dont l'état n'a pas changé est un no-op (même idempotency_key).
- Politique d'approbation d'outil. Un appel d'outil
write_externalsans approbation typed_reason ne peut pas atteindre l'exécuteur. - Préséance des politiques. L'override case-local l'emporte sur le tenant qui l'emporte sur l'install pour la même clé de politique.
13. Hors de cette spécification
- Modèles de composants, comportement visuel, analyse de la barre de commandes → le chantier de l'UI de conversation.
- Corrélation de campagnes, scoring, mécanique cross-tenant → le chantier des campagnes.
- Bibliothèque de prompts, contenu du registre d'outils LLM, politique de version de modèle → le chantier du runtime LLM (LLM runtime), séparé, quand nous y arriverons.
