Repository navigation
AiSOC v11.2.0
Added
-
The detection engine runs 2,603 rules instead of 833, and every one of the
1,770 added rules has been watched to fire. The library has held roughly
7,000 ATT&CK-mapped rules for months while 833 executed, and the question
that prompted this — why 800 and not 5,000 — turned out to have a single
concrete answer rather than a missing feature.Windows telemetry was unreachable, and nothing said so. The engine
flattensraw_event's top level into the namespace the matcher reads, and
the matcher does a plainevent.get(field)with no path traversal. A Windows
event puts its payload one level below that:CommandLinelives at
raw_event.EventData.CommandLine,EventIDunderSystem. Those are the
two most-used fields in the public Sigma corpus —CommandLinein 2,173
rules andImagein 2,300 — so every Windows rule readNoneon its first
clause and could not fire however correctly it was written.
windows_event.normalize()now lifts both containers, which is where the
fix belongs:SystemandEventDataare names from the Windows event
schema, and the engine is shared by every connector. Nothing in the engine or
the matcher changed, so the blast radius on the existing 833 is nil — proven
by replaying all 1,756 committed fixtures and confirming every native rule
keeps its verdict, now a standing test rather than a one-off check.scripts/sigma_compiler.pytranslates imported Sigma intomatch_when, or
refuses. Refusal is the design. Sigma compares strings case-insensitively
and several matcher operators do not, so equality and affix tests compile to
anchoredpattern_match_anyrather than toendswith_any, which would have
silently stopped matching\SvcHost.exe. Sigma'snot filteris true when
the field is missing, and onlynot_inandnot_contains_anybehave that
way, so the other negations are refused rather than narrowed. Of 3,132
imported rules, 1,770 ship and 1,362 are refused with a recorded reason — the
largest being 556 whose log source no connector emits and 464 whose negation
would flip on a missing field. The full taxonomy is in
docs/detections/sigma-compilation.md.A rule ships only after it is seen to fire.
check_detection_fields.py
says in its own docstring that it over-approximates and that "a false pass is
a rule this gate should have caught", so passing it is not evidence.
scripts/compile_sigma_ruleset.pyreplays a vendor-shaped event through the
real connectornormalize()and the realDetectionEngine, and
keeps the rule only if it produces a hit and stays silent on an empty event
of the same shape. The proof is that the rule is reachable and fires on a
well-formed event of its log source — not that it detects an attack, which
no gate here claims.--prove-gatereverts the connector to its pre-fix
behaviour and requires all 1,687 Windows rules to stop firing, so the proof
is known to be capable of failing. -
Eight windowed aggregation rules, covering the detection family the
quarantine index had been telling contributors to skip. Windows now
contributes here too: failed-logon volume per host, password spray counted by
distinct account rather than by attempt, service-install and scheduled-task
bursts, remote-thread fan-out, and per-process DNS fan-out for tunnelling
and domain-generation algorithms. -
scripts/measure_triage_reliability.py— the measurement behind the
auto-triage reliability figures under Fixed below. It imports the
production prompt, envelope and parser rather than restating them, and uses a
different alert per attempt: production pinstemperature=0.0, so asking
one alert twenty times measures one reply twenty times, not a rate. Failed
attempts record the model's own words, since "Expecting value: line 4 column
16" cannot be diagnosed without the text it indexes into. With no model
reachable it prints SKIPPED and says that nothing was measured, because a
skip is not a pass.
Changed
-
Nothing an operator has to do, but the detection surface is 3.1x wider.
There is no configuration change, no migration and no API change in this
release — the connector fix lifts theSystemandEventDatacontainers
into the namespace the matcher already read, and a connector-normalized key
still wins on collision, so it only adds fields. What does change is that
2,603 rules evaluate where 833 did, and 1,687 of the additions are Windows
rules that could never fire before, so a deployment with Windows telemetry
should expect more alerts from the same stream. Every imported rule carries
itsupstream_statusonto the alert, so the 125experimentalones can be
filtered without disabling the rest. -
Upstream lifecycle status no longer decides whether an imported rule runs.
The importer quarantined on SigmaHQ status, which put 2,844testand 211
experimentalrules behind a flag. That conflated whether a rule can
execute here with how confident its authors are in its content; only the
first was ever the blocker. In SigmaHQ,testmeans reviewed and in
community use — the normal state for most of the corpus, not a warning.
Fireability now gates, status is carried onto the rule and the alert so it
can still be filtered, anddeprecated/unsupportedare refused outright. -
Published counts lead with the library and always name the executable
figure beside it — "6,991 rules on disk, 2,603 executable". The two travel
together because a library figure on its own reads as coverage. 6,991 rather
than the 7,016 files underdetections/, because 25 of those are playbooks.
Fixed
-
76 rules were marked enabled and the engine had never heard of them. They
counted as shipped coverage and detected nothing: 44 native rules authored as
YAML with no Python spec (the YAML is a generated projection, so the engine
never reads it), 31 imported Sigma rules, and 1 community rule. 46 of the
original 122 became executable through the compiler; the rest are now
enabled: falsewith aquarantine_reasonnaming what each would need. They
were deliberately not translated: their fields have no known emitter, so
making them load would have added rules that cannot fire, which is the defect
being removed rather than progress against it.
detection_truth_table.py --checknow fails on any such rule. -
readme_gates.pymisread a formatted figure. Its pattern stopped at the
thousands separator, so "2,603 executable" was compared as 603 — a gate
measuring a number the README never claimed. -
The quarantine index told translators to skip the largest untranslated
family in it. Step 3 said| statsaggregations belong in the windowed
engine, "which today has three hardcoded rules and no loader. Skip those
until it has one."load_window_rules()readswindowed_ruleset.jsonand
main.pywires it on by default, so the instruction had been stale for some
time while continuing to steer people away from roughly half the Splunk
quarantine. Corrected inscripts/build_quarantine_index.py, since the
README is generated output and editing the file would have regenerated the
error — the same shape as the detection-coverage generator that kept
restoring a dead link after the page was fixed. -
Auto-triage never asked the provider for JSON, so a third of the bundled
model's replies were thrown away.run_auto_triageparses the reply as a
JSON object, but nothing inservices/agents/app/llm/ever set
response_format, leaving the model free to emit prose shaped like JSON and
the parser to correct it afterwards. Measured over 50 alerts from the
committed synthetic corpus, through the LiteLLM gateway exactly as production
routes, against the bundledllama3.2:3b-instruct-q4_K_Mat the production
temperature=0.0/max_tokens=512: replies triage could use went from
44 of 50 to 50 of 50 once the reply was constrained to a JSON object.The failure shape is worth recording, because it was not the one expected.
Every one of the six failures carried a correctverdictandconfidence
and a malformedrationale—"rationale": The alert is...with the
opening quote simply missing, a raw newline before an unquoted value, or an
invalid\'escape.finish_reasonwasstopon all 50 calls, so
truncation was not involved andmax_tokensis exonerated. A model that had
reasoned correctly was being discarded over its punctuation, which is
precisely what constraining the grammar prevents and no amount of parsing
after the fact can.The gateway ships
drop_params: true, so a provider that does not support
the parameter has it dropped rather than erroring. Requested through
model_kwargsrather than as a top-level argument, because langchain warns
on every construction otherwise. -
The published "7 of 19" reliability figure does not reproduce. Re-measured
on the production call path at the sample sizes above, the pre-fix tree reads
44 of 50. The original number's conditions are not recoverable — it was taken
during a QA run whose host had exhausted its disk — so rather than restate it,
scripts/measure_triage_reliability.pynow produces the figure repeatably and
records why each failed attempt failed. Every surface that published the old
ratio now carries the new one and names the method. -
A valid verdict was discarded when one field had the wrong type.
float(data.get("confidence", 0.5))was unguarded, so"confidence": "high"
raisedValueError, which the caller converts into anAutoTriageErrorand a
deterministic fallback — losing a verdict and rationale that may have been
perfectly good._coerce_confidencenow reads numeric strings and
percentages and degrades to a neutral 0.5 otherwise. That is safe here and
only here: auto-close requiresconfidence >= AUTO_CLOSE_THRESHOLD(0.85 by
default), so an unread field escalates to a human instead of closing an
alert. The verdict itself still fails closed. Not a cause of the measured
failures, but reachable by any model on any alert. -
structured_output.pydescribed itself as the single fail-closed parser
while nothing in production imported it. Its docstring said each agent
"had its own ad-hoc_parse_llm_response", past tense; all five still did,
and its only callers were its own tests — a passing test on an uncalled
function is indistinguishable from a working feature.auto_triage_agentnow
shares its extraction step, and the docstring records thatcloud_agent,
identity_agent,insider_threat_agentandphishing_agentstill carry
their own, rather than describing the consolidation as finished.Extracting is also now brace-balanced and string-aware. It trimmed prose
before the opening{and nothing after the closing}, so a model that
answered correctly and added "Hope that helps!" was scored unparseable — and
a}inside a string value would have ended the scan early. An unclosed
object is still handed to the JSON parser to report rather than silently
trimmed to the last balanced point, because extraction may read what the
model said and must not guess at what it meant. -
Three counters still classified rules by their directory, and disagreed
with the engine by about 1,700 each._quarantine/stopped meaning "cannot
run" when the Sigma compiler began translating rules where they sat, and the
tools that had not been moved to the compiled ruleset kept reporting the old
world while printing OK.validate_detections.pypublished "Quarantined
(parsed-but-disabled): 5937" against a truth table saying 4,213 about the
same tree; it now reportsExecutable (loaded by the engine): 2603; not loaded: 4388, read from the same artefact, so the two cannot diverge.
curate_detections.pyskipped every_quarantine/file before scoring, so
the published coverage page selected from 1,054 candidates out of 2,603
executable rules and its "By category" list contained a category named
_quarantine; candidates are now the rules the engine loads, and a rule
that cannot fire is no longer eligible for a page that promises coverage. -
build_marketplace.pypublished 7,016 detections against a README and a
truth table that both say 6,991. The 25-rule gap wasdetections/playbooks/
— response playbooks withtrigger:andsteps:and nodetection:block —
walked as native detections becauseplaybookswas missing from the skip set
thatdetection_truth_table.pyhas always carried. They are indexed as
playbooks now, so the marketplace keeps all 7,155 items,stats.detections
reads 6,991, and the landing page's pack figure is unaffected:stats.playbooks
counts every playbook and a newstats.playbook_packscounts the v1 pack
alone, because 62 of the 87 are packs and calling all 87 packs would have
traded one wrong number for another. -
Five refusal reasons rendered as broken table rows. The Sigma compilation
report escapes the example it prints and not the reason, and five reasons
name a Sigma modifier —|re,|cidr,|all,|base64,|fieldref—
whose leading pipe is also a Markdown cell delimiter, so each opened an empty
first column and shifted its row. The rows that rendered wrong were exactly
the ones explaining the least obvious refusals. -
The DRL-1.1 attribution gap is now stated in the report rather than
discoverable only from the JSON.provenance.authoris empty on all 1,770
compiled rules because the Sigma importer never read the upstreamauthor:
field. Attribution travels as repository, upstream rule id, upstream path and
licence — enough to find the rule, not enough to name who wrote it. The engine
builds its attribution sentence from what the block holds, so an alert reads
short rather than crediting an author called"", but a licence obligation
that is partly met is not met. Closing it needs a re-import.