Skip to content

Use detection rules and the explainer

Use this guide when you have ingested Sigma or YARA rules into ZettelForge and you want an LLM-generated, structured explanation of what each rule detects, how it works, and its false-positive patterns.

This guide covers ZettelForge 2.8.0. The explainer ships in this release. The match-consumer protocol exists, but the built-in consumer registry is empty (see Limitations in 2.8.0).

Prerequisites

  • ZettelForge 2.8.0 installed (python -m pip install zettelforge==2.8.0)
  • A configured LLM provider. The explainer calls whatever provider zettelforge.llm_client.get_llm_provider() resolves to. The mock provider returns a deterministic placeholder and makes no network calls, which is useful for testing the wiring.
  • One or more detection rules in Sigma or YARA format

To test the call path without a live model, use the mock provider for the current shell:

export ZETTELFORGE_LLM_PROVIDER=mock

How the explainer fits

The explainer is a standalone function. ZettelForge 2.8.0 does not invoke it during ingest: parsing or ingesting a Sigma/YARA rule does not automatically generate an explanation. Call explain() after you have a parsed rule.

The pieces:

  • DetectionRule (zettelforge.detection.base) is the writeable supertype shared by all formats. SigmaRule and YaraRule are subtypes that share its field contract (rule_id, title, source_format, content_sha256, level, status, tags, and others).
  • explain(rule, *, rule_body, provider=None) (zettelforge.detection.explainer) sends the rule to the LLM and returns a RuleExplanation.
  • RuleExplanation is the structured result.

Explain a Sigma rule

The end-to-end path is: parse the Sigma YAML, validate it, turn it into a SigmaRule, then explain it.

from zettelforge.sigma import parse_yaml, validate, from_rule_dict
from zettelforge.detection import explain

rule_yaml = """
title: Suspicious PowerShell Download Cradle
id: 11111111-2222-3333-4444-555555555555
status: experimental
description: Detects PowerShell download cradle via Net.WebClient
author: Example
level: high
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    CommandLine|contains: 'Net.WebClient'
  condition: selection
tags:
  - attack.execution
  - attack.t1059.001
"""

rule_dict = parse_yaml(rule_yaml)

result = validate(rule_dict)
if not result.valid:
    raise SystemExit(f"invalid Sigma rule: {result.errors}")

rule, _relations = from_rule_dict(rule_dict)
explanation = explain(rule, rule_body=rule_yaml)

print(explanation.summary)
print("confidence:", explanation.confidence)

validate() returns a ValidationResult with a valid boolean and an errors list. from_rule_dict() returns a (SigmaRule, relations) tuple; the relations describe knowledge-graph edges and are not needed for explanation.

Pass the raw rule text as rule_body. The explainer sends that body, not the parsed object, to the LLM.

What you get back

RuleExplanation has these fields:

Field Meaning
summary One-line description of what the rule detects
mechanism How the rule matches (fields, strings, conditions)
threat_model The behavior or threat the rule targets
false_positive_patterns List of known benign triggers
related_techniques List of related technique identifiers
confidence Float clamped to [0.0, 1.0]
model Provider name that produced the explanation
generated_at ISO 8601 timestamp
schema_version RuleExplanation schema version (1.0)

Failure handling

The explainer never raises for recoverable conditions. If the LLM is offline, returns invalid JSON, returns an empty response, or rate-limits the call, explain() returns a RuleExplanation with confidence=0.0 and a diagnostic summary (for example explanation unavailable: rate limited or explanation unavailable: llm error (...)). Always check confidence before trusting an explanation:

explanation = explain(rule, rule_body=rule_yaml)
if explanation.confidence == 0.0:
    # degraded result — render the diagnostic summary, do not treat as authoritative
    print("explainer degraded:", explanation.summary)

With the mock provider, explain() returns a canned result: summary="mock provider — no real explanation", confidence=0.0, model="mock:mock". Use it to confirm your call site works without spending tokens.

Rate limiting and cost

The explainer enforces an in-process, per-minute cap: 60 calls per minute per process by default. Override it with the ZETTELFORGE_EXPLAIN_RPM environment variable:

export ZETTELFORGE_EXPLAIN_RPM=120

When bulk-explaining many rules, gate enqueue with zettelforge.detection.explainer.rate_limit_ok() before each call; it reports whether the next call fits under the cap without consuming a token. The explainer also re-checks the limit internally, so a caller that bypasses the gate still gets rate-limited rather than overspending.

Security model

Rule bodies are untrusted input. The explainer applies several controls before the body reaches the LLM:

  • The explainer wraps the body in a <rule_source untrusted="true"> delimiter, and the system prompt tells the model to treat everything inside as data, not instructions.
  • The explainer neutralizes every </rule_source> sequence so a crafted rule cannot close the delimiter and inject its own instructions.
  • The explainer caps the body at 8192 characters before the call, limiting prompt-injection blast radius and token cost. It marks a longer body with ... [truncated].
  • The explainer discards the verbatim LLM output after coercing its typed fields into RuleExplanation.

Limitations in 2.8.0

Match consumers do not ship in 2.8.0

ZettelForge defines a DetectionMatchConsumer protocol and a RuleMatchEvent type for adapting external rule-match events (from a SIEM or detection pipeline) into note writes. ZettelForge 2.8.0 ships the protocol but no concrete consumers — zettelforge.detection.ALL_CONSUMERS is empty. To react to live rule matches, write your own consumer against the protocol.

The explainer is not auto-invoked on ingest

Ingesting Sigma or YARA rules does not enqueue explanation jobs in ZettelForge 2.8.0. Call explain() yourself after ingest completes.