Architecture Reference¶
Complete reference document — no need to read the source code.
1. Overview¶
Headless tool that captures real Windows events via Windows Event Log API (winevt), matches them against SigmaHQ rules, and outputs structured regression data.
Continuous run (one process until Ctrl+C):
- Load config + init logger
- Acquire SigmaHQ rules (grit-lib clone/fetch) + create branch
- Build the skip set from existing regression data
- Load the Sigma engine (rsigma-eval) with bloom pre-filter + LogSourceExtractor
- Resolve channels from the loaded rules
- Spawn a continuous collector (winevt, one task per channel)
- Evaluate every event against all loaded rules (FIFO API)
- Every 30s: generate regression output for matched rules, commit (per rule) + push (if contrib) to the fork
- On Ctrl+C: final flush → commit → push branch to fork (only when
git.contrib: true)
Platform: Windows (winevt + Sysmon required for rich events). Linux/macOS: collector is a no-op stub — the pipeline still runs end-to-end for testing.
2. Source tree¶
sigmacatch/
├── Cargo.toml # Workspace root (11 packages)
├── sigmacatch/ # Binary crate
│ └── src/
│ └── main.rs # Orchestration: continuous loop + process_and_generate + commit/push
├── tools/ # Dev tools (check_dry_run, check_channels, list_rules, check_filter, check_evtx, get_atomic, coverage)
└── crates/
├── sigmacatch-config/ # Config YAML, CLI parsing, custom_channels.yaml, dry-run git diagnostics (check_dry_run)
├── sigmacatch-logger/ # Two-layer tracing subscriber (stderr info + daily rolling file debug)
├── sigmacatch-rule/ # SigmahqRules: load (parse_sigma_yaml), filter, dedupe, remove_id
├── sigmacatch-detection/ # DetectionEngine wrapper + embedded pipelines (windows.yml, flatten_winevt.yml) + channel_resolver
├── input-windows-channels/ # Multi-channel Winevt collector (EventProducer)
├── sigmacatch-regression/ # SigmahqRegression, InfoYml, RegressionData, triplet validation
├── sigmacatch-types/ # Shared types: Event, Alert, RegressionHeader, Product + XML parsing + logsource mapping tables
├── sigmacatch-repo/ # grit-lib wrapper: SigmaRepo, git operations
└── input-evtx/ # EVTX file parser → Event (used by tools)
3. Configuration¶
config.yaml (auto-created with defaults on first run; the program exits after creation until you edit it — serde(default)):
git:
author: "sigmacatch" # GitHub username for the contrib workflow (must be set)
email: "you@example.com" # required for git commits (must contain '@')
github_token: "" # GitHub token (or GITHUB_TOKEN env var) — required for HTTP transport
transport: http # http (default, token) or ssh (private key)
ssh_key_path: "" # path to SSH private key (optional, only needed for SSH)
sigma_repo_url: "https://github.com/SigmaHQ/sigma.git"
sigma_repo_path: "sigma" # local path to the sigma repo (relative, no '..' traversal, not absolute)
offline: false # true = skip pull at startup (existing repo required). false (default) = pull
contrib: false # true = push to the remote fork. false (default) = local commits only
log:
level_file: "debug"
filter:
product: windows # windows, linux, or macos
min_status: "stable" # minimum rule status (inclusive): unsupported < deprecated < experimental < test < stable
min_level: "critical" # minimum rule level (inclusive): informational < low < medium < high < critical
author: "" # filter rules by author (optional, empty = no filter)
max_rule_size: 1048576 # bytes (1MB default, min 1024, max 10MB)
Rule filtering: product, min_status, min_level and author are applied by SigmahqRules::filter().
Rules whose status/level is below the threshold are excluded (only if the field is present);
rules without status/level are always accepted. If 0 rules remain, the program bails out.
Validation: git.author must be a valid GitHub username (alphanumeric + hyphens), git.email
is required, HTTP transport requires a token (config or GITHUB_TOKEN env) when needs_network()
is true — i.e. offline: false or contrib: true; a fully offline run (offline: true +
contrib: false) needs no token. sigma_repo_path is validated against traversal/absolute paths.
Offline / contrib: offline: true uses the existing repo as-is (no pull, complete repo required).
contrib: true enables the push to the fork at the end; by default (false) commits stay local.
The CLI flags --offline / --contrib force these values to true.
SSH transport: git.transport: ssh clones/fetches/pushes via the ssh_key_path private key. At
startup, ensure_ssh_host_config() (transport.rs) writes the IdentityFile/UserKnownHostsFile
directives into ~/.ssh/config (idempotent, atomic write tmp + rename); on Windows the ssh
executable is resolved via standard paths (Windows OpenSSH, Git for Windows) and used as a direct
exec (SshCommand::Program, no shell). A failed SSH pull is final (no HTTPS fallback): the error
message categorizes the cause (missing ssh binary or invalid key) and points to transport: http —
an HTTP retry only happens with the config switched to HTTP.
When ssh_key_path is set, every regression commit is signed with pure-Rust ed25519
(ssh-key): the gpgsig header is inserted between the committer line and the message, like
git commit -S with gpg.format = ssh, so GitHub shows the commit as "Verified".
CLI flags: --author <name>, -a/--all-rules, -o/--offline, -c/--contrib, -v/--verbose, --help / -h. The diagnostics (git dry-run, channels, rule list) are tools tools: check_dry_run, check_channels, list_rules.
4. Pipeline detailed¶
Step 1 — Init¶
parse_args() → CliArgs
↓
Config::load_with_cli("config.yaml", cli)
├── missing → write defaults → exit(1) with instructions
└── --author <name> overrides git.author before validation
↓
[windows] setup_console() (UTF-8 codepage + VT processing)
↓
init_logger(&config) → tracing (stderr info + daily rolling file debug)
Step 2 — Repo acquisition¶
ensure_dirs() → create <sigma_repo_path>/ and logs/
↓
fork_url = "https://github.com/{author}/sigma"
↓
SigmaRepo::new()
├── set_info_user(author, email)
├── set_info_http(token) | set_info_ssh(key_path)
├── [ssh] ensure_ssh_host_config(key_path) # writes IdentityFile into ~/.ssh/config (warn on failure)
├── [ssh_key_path set] set_signing_key(key_path) # signs every commit (ed25519, gpgsig)
├── set_git_operations(offline, contrib) # controls pull at startup + final push
├── set_remote_url(fork_url) → init() [async]
│ ├── incomplete/missing repo + offline → actionable bail
│ ├── existing repo → narrow pull of current branch (skipped if offline)
│ │ └── HEAD already on working branch (same-day re-run) → skip the master switch
│ └── otherwise → full clone (full-history, protocol v2) + pack
├── set_working_branch(branch_name) # fetch target branch (skipped if offline) + create_branch
│ └── switch_to_working_branch() # materializes the branch tree (exact commit mirror)
└── check_remote_working_branch() # guard: rejects orphan/amputated same-day branch
Post-processing: loose-object pack¶
The clone/fetch through grit-lib (http_fetch) writes every received object as a loose
file in .git/objects/xx/ (~131K files, ~650 MB for the Sigma repo) — grit has no
git gc --auto equivalent. After every clone/fetch (HTTP clone, SSH clone, HTTP pull,
SSH pull), pack_loose_objects() (crates/sigmacatch-repo/src/plumbing/pack.rs)
consolidates:
- collect loose objects (sorted by OID);
- zlib compression (default level, no delta) parallelized (rayon, 16K chunks),
then serialize a V2 pack + index
.idx(magic\xfftOc, fanout, sorted OIDs, CRC32 table, offset table, SHA-1 checksums); - delete loose files + empty
xx/directories; - observability: server progress messages (
remote: Enumerating objects…) are relayed to the log during download.
Result: .git/ drops from ~650 MB to ~218 MB (3x), git fsck --full --strict is clean,
objects remain readable through the ODB (loose or pack).
Benchmark vs native git clone (fork frack113/sigma, master branch, Linux 24 cores):
native git clone |
sigmacatch (grit + pack) | |
|---|---|---|
| Fresh clone time | ~3s | ~70s (fetch ~50s + pack ~17s) |
.git/ |
52 MB | 218 MB |
| Pack | 47 MB (deltas) | 215 MB (no delta) |
Why: native git writes the server's delta-compressed pack directly; grit unpacks everything to loose files, then we re-compress without deltas. The download itself is identical (~47 MB). The cost is paid once at first clone — subsequent pulls only transfer deltas (sub-second when nothing changed). On a slow VM the first clone can take several minutes.
Step 3 — Skip set (existing regression)¶
SigmahqRegression::new() # loads ./sigma/regression_data
└── scans all info.yml (walk, depth 64, skips symlinks)
└── lenient: missing dir → empty, not an error
↓
existing_rules: HashSet<Uuid> = regression.get_sigma_id().collect()
└── empty when --all-rules
↓
SigmahqRules::new() # loads ./sigma
├── find_rules_dirs() → rules, rules-* (excludes rules-compliance, index.yml)
├── sequential walk, parse_sigma_yaml() per file
├── cross-file dedupe by rule id (first occurrence wins)
└── for each id in existing_rules → rules.remove_id(&id)
↓
rules = rules.filter(SigmaFilterConfig { product, min_status, min_level, author, max_rule_size })
├── stats() → rules_loaded, filtered_product/status/level/author
└── 0 rules loaded → bail with a clear error message
Rules with existing regression data are excluded from the Sigma engine — this skip-at-load is the only load-time optimization. After generation, a rule is removed and the engine is reloaded in one batch (see Step 7). The skip set is built from the worktree ∪ the remote
sigmacatch/*branches (pending PRs, see git.md);<uuid>.evtxblobs are validated (parse ≥ 1 record, and ≤ 64 MiB) so empty/corrupt/oversized data does not skip the rule (re-captured).
Step 4 — Channel resolution¶
custom_map = load_custom_channel_mapping("custom_channels.yaml") # missing/empty → {}
↓
DetectionEngine::new(&rules)
├── loads embedded pipelines (flatten_winevt.yml, windows.yml) once
├── enables bloom pre-filter + LogSourceExtractor
↓
cycle_channels = engine.resolve_channels(&custom_map)
├── reads post-pipeline CompiledRule.logsource → service:category → channel list (deduped, sorted)
└── 0 channels → warn + return Ok (nothing to collect)
Step 5 — Continuous collection¶
output_base = <sigma_repo_path>/regression_data
clean_partial_artifacts(&output_base) # removes dirs with json/evtx but no info.yml
↓
let (tx, rx) = mpsc::channel::<Event>(100_000)
↓
EventCollector::new(cycle_channels).run(tx, stop) # tokio task, one task per channel
Per-channel loop (collect_continuous, spawned with spawn_blocking):
loop (until stop):
query = "*" if last_record_id == 0
else "*[System[EventRecordID > {last_record_id}]]"
EvtQuery(channel, query)
├── ERROR_EVT_CHANNEL_NOT_FOUND → error! once → exclude permanently (return)
└── other error → warn! + sleep 5s → retry
loop:
EvtNext(batch of 32, 5s timeout)
├── idle timeout / no more items → break (re-query)
└── error → warn! + sleep 5s → break
for each handle: EvtRender(EventXml) → Event::from_xml → inject_logsource_fields()
└── tx.blocking_send(event)
MAX_EVENTS (100k) reached → stop initial drain
if 0 sent:
├── cycle_fetched > 0 → warn! "fetched N but 0 sent — dropped during render/parse"
├── first cycle → info! "initial query OK — 0 events"
├── else heartbeat → info! "still alive" (every 60s)
└── record-id rollover probe (every 30 empty cycles) → reset last_record_id if needed
else:
├── first drain → info! "initial drain collected N events"
└── else progress → info! (every 10s)
The collector stops when stop is set (Ctrl+C) or the receiver is dropped. On non-Windows, each
channel task is a no-op stub.
Step 6 — Continuous event loop¶
generate_interval = 30s (first tick skipped immediately)
↓
loop:
tokio::select! {
shutdown_rx.changed() → info "Shutting down" → break
Some(event) = rx.recv() → engine.put_events(vec![event])
_ = generate_interval.tick() → process_and_generate()
→ upload_regression() if files created
}
Step 7 — process_and_generate¶
engine.process_events() → engine.get_alerts()
├── alerts empty → return (no "evaluation complete" log)
├── log stats: events_processed, matches_found (unique rules), alerts_count
└── for each alert:
regression.add(&alert) → Option<Vec<String>>
├── None if rule already retired / Uuid::nil() / valid info.yml exists
├── EVTX export failure → None too: rule stays loaded, re-captured later
└── Some(files):
├── RegressionData::for_rule(header, output_path, rule_rel_path, author, description)
├── write <rule_id>.json (event_json_raw of first matching event, pretty JSON)
├── write <rule_id>.evtx via EvtExportLog (validated ≥ 1 record + retry)
├── write info.yml
├── append "regression_tests_path" to the source rule YAML
└── retire the rule (regression.retired + rules.remove_id)
└── retired rules → engine.reload_rules(rules) # ONE batch reload
↓
returns batches: Vec<(Uuid, Vec<String>)> # (rule_id, written files) — empty if no alerts
↓
upload_regression() → upload_rule_batches() # in sigmacatch-repo
├── one commit per rule: "🧪 test: add regression data for rule {rule_id}"
├── commit/push failure → rollback local branch to pre-batch tip
└── SINGLE push if git.contrib: true (otherwise local commits only)
└── success → "Next step: create PR at https://github.com/SigmaHQ/sigma/pulls"
Output:
<sigma_repo_path>/regression_data/<rule_rel_path>/
├── <rule_id>.json # first matching event (raw Winevt JSON, original EventData key names)
├── <rule_id>.evtx # valid EVTX via EvtExportLog (non-Windows: no data generated)
└── info.yml # SigmaHQ-compatible metadata
<rule_rel_path> mirrors the rule path under sigma/rules/ (e.g.
rules/windows/builtin/security/win_security_foo/). The output always lives inside the sigma
repo and is committed to the fork when git.contrib: true (local commits otherwise).
Step 8 — Shutdown / commit / push¶
Ctrl+C → shutdown_rx.set(true)
↓
Final flush:
await collector task (30s timeout) → drain remaining rx → engine.put_events
↓
process_and_generate() → upload_regression() if files
├── per-rule commit ("🧪 test: add regression data for rule {id}")
└── push() to fork if git.contrib: true
└── success → "Next step: create PR at https://github.com/SigmaHQ/sigma/pulls"
5. Key data structures¶
Event (sigmacatch-types)¶
Event {
event_json_raw: serde_json::Value, // raw Winevt JSON (original EventData key names, spaces kept) — used for regression output
event_json: serde_json::Value, // transformed JSON for Sigma detection (EventData spaces stripped)
event_raw: Vec<u8>, // raw source bytes (XML)
}
Methods: from_xml(), new(), record_id(), inject_logsource_fields() (channel(),
provider() and event_id() are private). The collector calls inject_logsource_fields() which
injects product, service, category into event_json; the engine's LogSourceExtractor reads
these fields to prune incompatible rules.
Alert (sigmacatch-types)¶
Alert {
rule_id: Uuid, // parsed from the Sigma rule id
rule_title: String,
description: Option<String>,
rule_path: Option<PathBuf>, // source rule YAML path (relative to sigma repo)
severity: String,
event_json_raw: serde_json::Value, // raw Winevt JSON (original key names) — written to <rule_id>.json
event_json: serde_json::Value, // transformed JSON for Sigma detection
event_raw: Vec<u8>,
}
SigmahqRegression (sigmacatch-regression)¶
struct SigmahqRegression {
entries: Vec<(PathBuf, InfoYml, RegressionEntry)>,
author: String,
output_path: Option<PathBuf>, // default ./sigma/regression_data
retired: HashSet<Uuid>,
}
API: new() / new_from_path() (lenient), set_author() / author(), len() / is_empty(),
iter() / infos() / entries() / get_entry(), get_sigma_id() -> Vec<Uuid>,
get_raw_data(index), add(&Alert) -> Option<Vec<String>>.
InfoYml¶
id: <uuid v4>
description: "N/A"
date: YYYY-MM-DD
author: <config.author>
rule_metadata:
- id: <rule_id>
title: <rule_title>
regression_tests_info:
- name: "Positive Detection Test"
type: evtx
provider: "Microsoft-Windows-Sysmon"
match_count: 1
path: <rule_rel_path>/<rule_id>.evtx
6. Key modules¶
DetectionEngine (crates/sigmacatch-detection/src/lib.rs)¶
- Loads embedded pipelines (
flatten_winevt.yml+windows.yml) and rules via rsigma-eval - Enables bloom pre-filter + LogSourceExtractor in
new()for evaluation optimization - FIFO cycle:
put_events()/process_events()/get_alerts() reload_rules(&SigmahqRules)— batch reload after retiring rulesrule_count(),stats()(EngineStats),explain_rule(rule_id, event),save_hir/load_hir- Depends on
sigmacatch-rule+sigmacatch-types+rsigma-eval
SigmahqRules (crates/sigmacatch-rule/src/lib.rs)¶
new()(hardcoded./sigma) /new_from_path()— walk + parse + dedupefilter(SigmaFilterConfig { product, min_status, min_level, author, max_rule_size })→ LoadStatsremove_id(&Uuid),get(&Uuid),rules()/iter(),to_collection(),rule_paths()- Channel resolution no longer lives here — it moved to
DetectionEngine::resolve_channels(§10)
EventCollector (crates/input-windows-channels/src/lib.rs)¶
- Multi-channel Windows Event Log collector, implements
EventProducer(single module, no morecollector.rs) new(channels)→run(self, tx, stop)async; one blocking task per channel- Windows: EvtQuery → EvtNext (batch of 32, 5s timeout) → EvtRender →
Event::from_xml→inject_logsource_fields - Non-Windows: no-op stub
- Observability: permanent exclusion on
ERROR_EVT_CHANNEL_NOT_FOUND(singleerror!), liveness logs ("initial query OK", "still alive" every 60s, progress every 10s),warn!when events are fetched but dropped at render/parse, record-id rollover detection
EVTX Writer (sigmacatch-regression/src/evtx.rs)¶
- Windows:
EvtExportLogAPI (winevt) — re-queries the event by RecordID and exports a valid binary.evtx EvtExportLog(None, channel, query, path, EvtExportLogChannelPath | EvtExportLogOverwrite)- Validation: the exported file is re-parsed (
input_evtx::parse_evtx_file) and must contain ≥ 1 record.EvtExportLogreports success even when the query matched 0 events (header-only file) — an empty or corrupt file is a failure, not a success. - Retry: 4 attempts total (1 initial + 3 retries) with short backoff (2s/5s/10s) — the retention race is often transient.
- On failure the partial
.jsonis deleted, an error is returned, the rule is skipped this cycle (no commit) and re-captured on a later cycle. - Known limitation: race condition with log retention — if the event has been purged between collection
and export, the call fails silently (
ERROR_EVT_QUERY_RESULT_STALE) - Self-healing: rules whose committed data is invalid (empty EVTX) are excluded from the skip set
(
get_sigma_idviadata_file_is_valid, andpending_regression_rule_idsvia.evtxblob validation) → regenerated on the next run. - Non-Windows: no data is generated (the Winevt collector is a stub) and
write_evtxerrors.
Logger (crates/sigmacatch-logger/src/lib.rs)¶
- stderr layer:
errorlevel by default,infowith-v/--verbose, ANSI colors, filterable viaRUST_LOG - file layer:
debuglevel (configurable), daily rotation logs/sigmacatch.YYYY-MM-DD.log
7. Dependencies¶
| Dependency | Usage |
|---|---|
grit-lib |
all git operations (clone, fetch, push, branch, commit, checkout) via HTTP (token) and SSH (key), pure Rust |
reqwest (blocking + async) |
HTTP client for git transport |
ssh-key |
ed25519 commit signing (gpgsig header, pure Rust) |
zeroize |
zeroes secrets in memory (GitHub token) |
rsigma-eval + rsigma-parser |
Sigma rule loading/evaluation |
tokio |
async runtime |
tracing + tracing-subscriber |
logging |
serde / serde_json / serde_yaml |
config + event + regression serialization |
anyhow |
error handling |
chrono |
dates |
uuid |
UUID v4 for info.yml + rule IDs |
phf |
static hash maps for taxonomy tables (in sigmacatch-types) + channel resolution (in sigmacatch-detection/src/channel_resolver.rs) |
evtx |
EVTX file parsing (input-evtx crate, used by tools/check_evtx) |
roxmltree |
XML parsing for Winevt events (in sigmacatch-types) |
windows |
Winevt API (cfg-gated: windows only, features: Foundation, System, Security, Com, Console, Threading) |
tempfile (dev) |
integration tests |
Removed: ratatui, crossterm, quick-xml, winevt-writer, tdh, ntapi, ferrisetw
8. Build & Lint¶
cargo fmt --check
cargo clippy -- -W warnings
cargo test --workspace
cargo build --release
cargo xwin build --release --target x86_64-pc-windows-msvc # cross-compile Windows
9. CLI¶
sigmacatch
[-a], [--all-rules] # disables the skip set (loads all rules)
[-c], [--contrib] # enables push to the remote fork
[-o], [--offline] # skips pull at startup (forces offline)
[-v], [--verbose] # shows info-level logs on stderr
[--author <name>] # overrides git.author from config
[--help], [-h] # shows help and exits
Diagnostics moved to tools:
check_dry_run # git diagnostics (token, fork, API, info/refs, repo state)
check_channels # print the resolved channels
list_rules # print the loaded rules (id, title, status, level, techniques, path, ART link)
check_filter # validate SigmaFilterConfig against the rules (ground truth)
check_evtx # validate the regression data (evtx + json + engine match)
get_atomic # generate run_atomic.ps (Invoke-AtomicTest) for rules without regression data
coverage # rule coverage stats (local + pending remote branches)
Config is auto-created on first run with defaults. Edit config.yaml before running.
10. Embedded pipelines & channel resolution¶
windows.yml (crates/sigmacatch-detection/pipelines/)¶
Embedded transformation pipeline (loaded via include_str! in sigmacatch-detection), applied to every rule before compilation:
- Maps
logsource.category→ Sysmon EventID conditions viaadd_condition, gated byrule_conditions(type: logsourcewithcategory,product,servicefilters; all conditions combined with AND). - rsigma-eval v0.21+ :
add_conditionaccepts YAML sequences (conditions: {EventID: [17, 18]}) whose values are OR-linked, matching the pySigmaAddConditionTransformation(breaking API :AddCondition.conditionsisHashMap<String, Vec<SigmaValue>>). A multi-EventID category is a single transformation entry (e.g.wmi_event→[19, 20, 21]). - EventType registry filters :
registry_add= EventID 12 +EventType: CreateKey,registry_set= EventID 13 +EventType: SetValue,registry_rename= EventID 14 +EventType: RenameKey.registry_deletehas NO EventType filter — EventID 12 carries bothDeleteKeyandDeleteValue(rsigma-eval constraint), so it matches on EventID 12 alone. change_logsourcefinal (post-add_condition) : oneservice: sysmonblock per routed category, same logsource gate(category, product: windows)as itsadd_condition→ makes the post-pipeline logsource usable bychannel_resolver(zero duplicated category → service mapping).prepend: adds the condition before the existing detection (new AND existing) for short-circuit optimization.- Supported transformations :
field_name_mapping,field_name_prefix_mapping,field_name_prefix,field_name_suffix,drop_detection_item,add_condition,change_logsource,replace_string,value_placeholders,wildcard_placeholders,query_expression_placeholders,set_state,rule_failure,detection_item_failure,field_name_transform,hashes_fields,map_string,set_value,convert_type,regex,add_field,remove_field,set_field,set_custom_attribute,case_transformation,nest,include.
flatten_winevt.yml : flattens the nested Winevt XML structure for Sigma evaluation. Pipeline loaded once at engine init, applied to every rule before compilation.
Channel resolution (crates/sigmacatch-detection/src/channel_resolver.rs)¶
- Post-pipeline logsource :
resolve_channelsreadsCompiledRule.logsource(post-pipeline, publicly exposed by rsigma-eval 0.21) viaDetectionEngine::resolve_channels(&custom_map)inmain.rs— resolved at engine creation time, no extra cost (no re-transform). SERVICE_CHANNELS: staticphf::Map<service, &[channel]>— service → Windows Event Log channels mapping (runtime, not a generated table).CATEGORY_CHANNELS: categories the pipeline does NOT route (ps_classic_*,ps_module,ps_script).- Lookup :
servicepresent →SERVICE_CHANNELS[service]+custom_map(fromcustom_channels.yaml,channel → service) ; elsecategory→CATEGORY_CHANNELS[category]. - Sysmon categories are not in the table — the pipeline rewrites them to
service: sysmon(single source of truth inwindows.yml). - Unmapped logsource →
warn!(per logsource), no channels ; non-Windows rules are ignored. Result : deduped, sorted channel list. sigmacatch-typesremains owner of the inverse mapping tables (CHANNEL_TO_SERVICE,PROVIDER_TO_SERVICE,CHANNEL_EVENT_TO_CATEGORY,CHANNEL_EVENT_TO_SUBCATEGORY) used byinject_logsource_fields()(channel/provider → logsource enrichment).