Architecture¶
Cargo workspace¶
Le projet est un cargo workspace de 14 packages, plus 1 crate nightly exclu (sigmacatch-ebpf) :
sigmacatch/
├── Cargo.toml # Racine workspace
├── sigmacatch-win/ # Binaires Windows (lib + 1 bin)
│ └── src/
│ ├── lib.rs # module channels (stubs no-op hors Windows)
│ ├── main_winevt.rs # bin `sigmacatch-channel` : collecteur Winevt multi-channel
│ ├── channels.rs # Collecteur Winevt (EvtQueryW/EvtNext/EvtRender, multi-channel)
│ └── cli.rs # Sous-commandes de diagnostic : check-filter, list-rules
├── sigmacatch-lnx/ # Binaires Linux (lib + 3 bins, feature-gated)
│ └── src/
│ ├── lib.rs # Module gates : auditd, builtin (syslog), sysmon (tail), ebpf
│ ├── entry.rs # Pipeline Linux partagé `LinuxCollector` + `run()`
│ ├── main_base.rs # bin `sigmacatch-linux` (wrapper fin sur entry::run)
│ ├── main_sysmon.rs # bin `sigmacatch-linux-sysmon` (wrapper fin, + tail sysmon)
│ ├── main_ebpf.rs # bin `sigmacatch-linux-ebpf` (wrapper fin, + eBPF natif)
│ ├── auditd.rs # Collecteur auditd (tail /var/log/audit/audit.log, groupement par event id)
│ ├── syslog.rs # Collecteur syslog builtin (central /var/log/messages → authpriv + cron, RFC3164)
│ ├── sysmon.rs # Collecteur Sysmon-for-Linux (tail syslog, feature `sysmon`)
│ ├── sysmon_parse.rs # Parsing Sysmon XML (toujours compilé, partagé par tail + eBPF)
│ ├── ebpf.rs # Loader eBPF + dispatch (feature `ebpf`, privileges requis)
│ ├── ebpf_event.rs # Synthèse XML eBPF → format Sysmon + tests
│ └── cli.rs # Sous-commandes de diagnostic
├── regressiondata-check/ # Binaire standalone cross-platform : validation régression (--json, --ignore, --fix, --path)
└── crates/
├── sigmacatch-ebpf/ # eBPF probes (exclue workspace, nightly, bpfel-unknown-none)
│ └── src/main.rs # 6 tracepoints : execve/exec/exit/connect/openat+exit/sendto+sendmsg
├── sigmacatch-ebpf-common/ # Types no_std partagés (ring buffer) : ExecEvent, NetEvent, ...
├── sigmacatch-runner/ # Pipeline partagé aux crates binaires :
│ └── src/runner.rs # run<C: CollectorKind> + trait CollectorKind
├── sigmacatch-config/ # Config YAML + parsing CLI + custom_channels.yaml
├── sigmacatch-logger/ # Abonnement tracing à deux couches (stderr error/info, fichier rolling)
├── sigmacatch-rule/ # SigmahqRules : chargement de règles, filtre, dédupe, remove_id
├── sigmacatch-detection/ # Wrapper DetectionEngine + pipelines par plateforme
├── sigmacatch-regression/ # SigmahqRegression, InfoYml, DataFormat (evtx/log)
├── sigmacatch-types/ # Types partagés : Event, Alert, RegressionHeader + parsing XML + logsource tables
├── sigmacatch-repo/ # wrapper grit-lib + SigmaRepo + opérations git + signing
├── sigmacatch-evtx-writer/ # Writer EVTX pur Rust
└── input-windows-evtx/ # Parser fichiers EVTX → Event
Collecteurs¶
Cinq binaires sont produits depuis trois crates (les binaires de collecte embarquent un
ensemble de collecteurs sélectionné par features cargo et required-features par binaire ;
regressiondata-check est un binaire standalone sans collecteur) :
| Binaire | Crate | Features | Description |
|---|---|---|---|
sigmacatch-channel |
sigmacatch-win |
winevt |
API Winevt native (EvtQueryW/EvtNext/EvtRender), multi-channel, rejouable |
sigmacatch-linux |
sigmacatch-lnx |
auditd + builtin |
auditd + syslog builtin uniquement (pas de sysmon, pas de root requis) |
sigmacatch-linux-sysmon |
sigmacatch-lnx |
auditd + builtin + sysmon |
+ tail Sysmon-for-Linux XML (feature sysmon) |
sigmacatch-linux-ebpf |
sigmacatch-lnx |
auditd + builtin + ebpf |
+ probes eBPF native (feature ebpf, root requis) |
regressiondata-check |
regressiondata-check |
— | Validation de régression cross-platform (EVTX + auditd + JSON), pas de collector |
Logsource Windows et catégories PowerShell¶
Les règles Windows sont contraintes par la pipeline 1_win_logsource.yml
(add_condition sur les EventID + change_logsource vers le service) : les catégories
PowerShell sont bornées à leurs EventID — ps_module (4103), ps_script (4104) vers
service: powershell ; ps_classic_start (400), ps_classic_provider_start (600) et
ps_classic_script (800) vers service: powershell-classic. Sans champ category injecté
sur l'event, le LogSourceExtractor d'rsigma évalue chaque event fail-open contre toutes
les règles.
Les events PowerShell classique (400/600/800 …) émettent des <Data> sans attribut
Name : le parseur les expose sous des clés positionnelles (Data0, Data1, …), et
inject_logsource_fields_for surface le contenu EventData sous le champ Sigma générique
Data pour que Data|contains fonctionne (rsigma n'a pas de mapping de champ dédié
powershell_classic).
Les collecteurs Linux¶
Chacun gardé par sa source ; aucune source disponible → bail :
- auditd — si
/var/log/audit/audit.logexiste : tail, parsing linux-audit-parser, groupement par event idtimestamp:sequence, logsourceproduct:linux, service:auditd. - syslog builtin — tail de chaque fichier existant parmi central (
/var/log/messages,/var/log/syslog), authpriv (/var/log/secure,/var/log/auth.log) et cron (/var/log/cron,/var/log/cron.log) : lignes RFC3164, service dérivé du program tag (fallback par groupe de fichier : authpriv →auth, cron →cron). Les lignes taggéessysmonsont exclues (prises en charge par le collecteur dédié).
Les deux binaires sysmon ajoutent un collecteur supplémentaire :
- Sysmon eBPF (feature
ebpf,sigmacatch-linux-ebpf) — probes Aya embarquées (crates/sigmacatch-ebpf, nightly+bpf-linker, exclue du workspace) couvrant EID 1 process_create, EID 3 network_connect, EID 5 process_terminate, EID 11 file_create et l'extension DNS (EID 22) : events rendus en XML Sysmon identique au chemin syslog puis injectés via le même pipeline (inject_logsource_fields_for). Prérequis runtime : root ou CAP_BPF+CAP_PERFMON (refus de démarrer sinon —entry.rsbail) + kernel avec BTF. Le hachage SHA256 des images est calculé userspace avec cache (chemin,mtime). Un échec de chargement des probes au runtime avertit (warn!) et continue sans source sysmon dans la saveur-ebpf; seul un build all-features (ebpf+sysmon) retombe sur le tail Sysmon-for-Linux. - Sysmon-for-Linux tail (feature
sysmon,sigmacatch-linux-sysmon) — lignes du syslog central taggéessysmondont le corps est XML winevt (parse_winevt_xml/_raw) → logsourceproduct:linux, service:sysmonvia le channelLinux-Sysmon/Operational. Lecture seule, pas de dépendance Aya.
Format de régression : DataFormat::Log.
Chaque binaire Windows définit son propre CollectorKind dans son main_*.rs
(name()/mode()/channels()/build()/regression_format()) ; les trois binaires Linux
partagent un unique LinuxCollector défini dans entry.rs. Le format de régression est
choisi par regression_format() : DataFormat::Evtx pour le binaire Windows
sigmacatch-channel, DataFormat::Log pour les trois binaires Linux.
Graphe de dépendances¶
sigmacatch-win ──┬── sigmacatch-runner (run<C: CollectorKind>, pipeline partagé)
sigmacatch-lnx ──┤ ├── sigmacatch-config (Config, CliArgs)
│ ├── sigmacatch-logger (init tracing)
│ ├── sigmacatch-rule (SigmahqRules : load/filter/remove_id)
│ ├── sigmacatch-detection (DetectionEngine : pipelines + bloom + LogSourceExtractor + resolve_channels)
│ ├── sigmacatch-regression (SigmahqRegression : skip set + génération données)
│ ├── sigmacatch-types (Event, Alert, RegressionHeader, Product, EventProducer, parsing XML)
│ └── sigmacatch-repo (SigmaRepo, wrapper grit-lib)
└── serde (sérialisation JSON des sorties diagnostics)
regressiondata-check ──┬── sigmacatch-detection (DetectionEngine)
├── sigmacatch-rule (SigmahqRules : load/filter)
├── sigmacatch-regression (SigmahqRegression)
├── sigmacatch-types (Event)
├── input-windows-evtx (parse EVTX → Event)
└── linux-audit-parser (parse records auditd → Event)
sigmacatch-detection dépend de sigmacatch-rule + sigmacatch-types + rsigma-eval.
Les collecteurs vivent dans leur crate binaire et ne dépendent que de sigmacatch-types
(types partagés + tables de mapping logsource). input-windows-evtx dépend de
sigmacatch-types + la crate evtx. regressiondata-check (validation de régression,
cross-platform) assemble detection + rule + regression + types avec
input-windows-evtx (EVTX) et linux-audit-parser (auditd) selon le LogType de chaque
entrée. Les sous-commandes de diagnostic (cli.rs) font un parsing manuel des arguments
et utilisent serde pour leurs sorties JSON (toujours compilées).
Pipeline (boucle continue)¶
1. parse_args() + Config::load_with_cli("config.yaml", cli)
2. setup_console() (Windows) ; init_logger(&config, verbose) → tracing (stderr `error` par défaut, `info` avec `-v`, fichier debug)
3. ensure_dirs() → dossier repo sigma + logs/
4. SigmaRepo init : set_info_user/set_info_http|ssh (+ ensure_ssh_host_config si ssh+réseau),
set_signing_key (si ssh_key_path), set_git_operations(offline, contrib),
set_remote_url(fork) → set_working_branch(sigmacatch/<date>) → check_remote_working_branch()
— no-op complet en offline (pas de `.git` requis, fichiers locaux tels quels)
5. SigmahqRegression::new() → set_author/max_failed_cycles/format(kind)/add_json_output
└── existing_rules = regression.get_sigma_id() ∪ sigma_repo.pending_regression_rule_ids()
(branches remote sigmacatch/* en attente ; scan sauté en offline) → HashSet<Uuid> (vide avec --all-rules)
6. SigmahqRules::new() → chargement + dédupe ; remove_id(existing_rules)
└── filter(SigmaFilterConfig { product, min_status, min_level, author, max_rule_size }) ; 0 règles → bail
7. custom_map = load_custom_channel_mapping("custom_channels.yaml")
8. DetectionEngine::new(&rules) (pipelines + bloom + LogSourceExtractor)
└── cycle_channels = kind.channels(&engine, &custom_map)
├── Some(vide) (winevt sans channel résolu) → warn + return
└── None (linux) → pas de résolution de channels
9. Handler Ctrl+C (watch channel) ; output_base = <sigma_repo_path>/regression_data ;
clean_partial_artifacts()
10. collector = kind.build(&cycle_channels) → tokio::spawn(collector.run(tx, stop))
├── sigmacatch-channel (winevt) → EventCollector::new(cycle_channels).run(tx, stop)
└── sigmacatch-linux (auditd + syslog + sysmon) → MultiCollector (les trois tails en parallèle, rotation détectée)
11. Boucle : tokio::select!
├── shutdown_rx (Ctrl+C ou --max-runs atteint) → break
├── event depuis rx → engine.put_events(vec![event])
└── generate_interval (30s) → spawn_blocking(process_and_generate) → upload_regression() si fichiers
12. Flush final : arrêt collector (timeout 10s, abort sinon) → drain des events restants (timeout 5s)
→ process_and_generate() → upload_regression() (commit par règle) → push unique si contrib
process_and_generate() :
engine.process_events() → get_alerts()
├── alerts vides → return (pas de log "evaluation complete")
├── regression.begin_cycle() ; log stats (events_processed, matches_found, alerts_count)
└── pour chaque alert : regression.add(&alert) → Option<Vec<String>>
├── None si règle déjà retirée / pas d'id valide / info.yml existant
└── Some(files) → écrit les fichiers + regression_tests_path + retire la règle
└── retired_ids += regression.take_blocked() (règles bloquées après N cycles d'échec)
└── règles retirées → rules.remove_id() → engine.reload_rules() (un seul reload batch)
↓
retourne (Pipeline restitué, batches: Vec<(Uuid, Vec<String>)>)
↓
upload_regression() → upload_rule_batches() (dans sigmacatch-repo)
├── un commit par règle : "🧪 test: add regression data for rule {rule_id}"
├── échec commit/push → rollback de la branche locale vers le tip pré-batch
└── UN SEUL push si git.contrib: true (sinon commits locaux) → message PR
Toute la génération tourne en spawn_blocking (état Pipeline déplacé puis restitué) —
les retries EvtExportLog ne gèlent jamais la collecte (les événements continuent à
s'accumuler dans le canal mpsc).
Notes de conception¶
- Skip set =
HashSet<Uuid>depuisSigmahqRegression::get_sigma_id()(info.yml existants + données valides) ∪SigmaRepo::pending_regression_rule_ids()(arbres des branches remotesigmacatch/*: PR en attente non mergés — une VM fraîche ne recapture pas leurs données), construit une seule fois au démarrage.--all-rulesle désactive. Après génération, une règle est retirée et le moteur est rechargé en un seul batch (engine.reload_rules). Les règles dont les données commitées sont invalides (EVTX cassé / texte vide) sont exclues du skip set → régénérées. - Output toujours dans le repo sigma :
<sigma_repo_path>/regression_data/<rule_rel_path>/(info.yml+ fichier de données.evtx/.log,.jsonoptionnel), commité sur le fork sicontrib(commits locaux sinon). Attention : le chemin de génération est codé sur le dépôt local./sigma— gardezgit.sigma_repo_path: "sigma"; toute autre valeur casse le miroir de chemins et le nettoyage des artefacts partiels. - Collecteur observable : le collecteur exclut une fois pour toutes les channels
inexistants dès
ERROR_EVT_CHANNEL_NOT_FOUND(un seulerror!) ; chaque channel vivant journalise « initial query OK » puis un heartbeat « still alive » (60s) ;warn!quand des events sont récupérés mais perdus au rendu/parsing. Les collecteurs Linux détectent la rotation du fichier tailé (changement d'inode) et rouvrent le fichier ; le collecteur syslog exclut les lignes taggéessysmonpour éviter les doubles événements (pris en charge par le collecteur sysmon dédié).