You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(wazuh_decoder_rule_tool): let a retrieved <order> contribute fields, verified
ml_order was routed through select_requested_fields(), which intersects it
against what the local extractor already found — so a retrieved decoder's
<order> could only ever reorder fields, never contribute one. The field most
worth having was exactly the one dropped: for an sshd-shaped failed login the
retrieved decoder says srcuser,srcip and the extractor finds only srcip.
Output was byte-identical whether the suggestion was correct, absent, or
nonsense:
ml_order=None -> order=['srcip']
ml_order=['srcuser','srcip'] -> order=['srcip']
ml_order=['totally','made','up'] -> order=['srcip']
propose_ml_order_fields() locates a value for each retrieved field name by its
label, and a proposal survives only if the regex the generator would actually
emit captures, in *every* sample log, exactly the value located in that log. A
pattern that fits only the first sample is overfitting — the failure mode this
path has to avoid — so it is rejected.
Proposals join the pool via common_fields, deliberately NOT requested_fields:
adding a name there flips selection to "requested only" and drops every
heuristic fallback field, so proposing srcuser would have cost us srcip.
osregex_captures() is new because osregex_matches() escapes parens and so
reports no match for any pattern with a capture group — it could never have
verified what a <regex> extracts. osregex_to_python() gains keep_groups, off by
default so prematch verification keeps its existing meaning.
Verified against real Wazuh (temporary install, logtest, removed):
Phase 2: name: 'mlfixprobe' srcip: '192.168.1.50' srcuser: 'admin'
Phase 2: name: 'mlfixprobe' srcip: '10.20.30.40' srcuser: 'carol'
Measured by scripts/eval_ml_order_proposals.py against logtest's own Phase 2
output for 1546 verified samples: 131 fields recovered, 57 additions the
official decoder did not have, 79 samples improved (5.1%). The remaining
additions are dominated by name normalisation where the captured value is
correct — checkpoint dstip where the official decoder said dst, cisco user
where it said username — and that 57 overcounts, since srcuser scores as an
addition when the official decoder emitted user for the same token.
Three guards kill the genuinely-wrong class, each pinned by a test:
* bare-space matching only for labels where it is the convention. "Unescaped
URL path matches" yielded url="path"; "dst outside:116.6.127.120" yielded a
dstip still carrying its interface prefix.
* quoted values honoured after `:` as well as `=`, so action:"Key Install" is
not truncated to "Key".
* structural words (from, for, invalid, user…) rejected for space matches
only. `Failed password for user from 172.18.1.1` names no user at all and
was offering "from" as the srcuser; `status=unknown` stays a valid value.
Also, the fuzzy affix fallback was relabelling values when fed retrieved names:
`timezone` matched `time`, `dstname` matched `dst`, `srcmac` matched `src`, each
emitting a decoder that captures a real value under a field name the log never
supported. select_requested_fields() gains allow_affix, off for the ML paths and
on by default so a human typing `ip` still finds `srcip`. Genuine synonyms that
had been working only through that fallback (proto/protocol, act/action,
username/user) are now explicit in FIELD_ALIASES. score_ml_decoder_template()
scores strictly too — it was crediting templates for fields selection would
then refuse, so templates ranked on matches they never had.
Costs 1.31ms on a worst-case 21-name order, against the ~950ms the ML model
already spends per analyze.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0 commit comments