Added
-
The DNS suspicion score's measured catch rates are now written down. The FAQ and the
known-issues list state what the score actually catches, from a seeded measurement against
the live scorer (1,000 samples per label length, eleven lengths from 6 to 63): the
vowel-free digit-and-consonant shape the score is tuned toward clears the candidate bar
about 19-36% of the time at typical DGA lengths (10-16 characters); a uniformly random
letter-digit label, 6.6-14.8% at the same lengths; and random all-letter labels never clear
it at any length - zero of 11,000 samples, with the best possible all-letter label below
the bar by arithmetic. Boosting the letter-only class lexically was measured and rejected:
every boost rule tested either flagged real benign names - the strictest still crossed 57
in a benign reference week - or caught under half of its target, because the benign score
curve decays smoothly through the region with no gap to put a threshold in. The docs pair
the numbers with what one name's score does and does not decide: a score over the bar makes
a candidate, families group by parent, and the below-gate family check - five or more
low-scoring subdomains under one parent on Zeek data, nearly all of whose lookups fail to
resolve - surfaces as one INFO finding regardless of score, while a family spread across
many parents, or one whose names resolve, is outside it. The hex-tunneling entry is
corrected by the same measurement: a long hex label straddles the bar - roughly half of
samples clear it - rather than sitting just under it; the conclusion that a high-volume hex
tunnel can slip the dense-cluster scan is unchanged, since half is well short of the member
share the scan requires. -
Every expandable system-log finding now shows its vocabulary above the fold. The short
tokens:line that previews what a capsule contains was reaching grouped families and
bursts but not recognized administrative sessions or update runs - so the most structured
finding sigwood produces was the one row on the page with nothing under it until you opened
it. Recognized sessions now carry the same preview, distilled from the lines they group,
and it appears on both the plain-text and HTML reports. A capsule whose content is entirely
opaque identifiers still shows no preview rather than an empty one. -
A field-validation kit for independent testers.
tools/fieldkit.pyis one standalone
script a collaborator can download and run wherever thesigwoodcommand is installed: it
proves the install with a small synthetic canary, runs exactly one ordinary default hunt,
offers an optional local triage pass, and writes one reviewable Markdown report. The
automated projection never copies a log-derived string - it keeps enumerated numeric
measurements, counts, and fixed tokens, and groups anything unexpected underother; the
three typed answers are the sole free-text exception, and the file is created privately and
reviewed before the collaborator chooses to email it.docs/FIELDKIT.mdstates the complete
field list and the exact privacy boundary. The kit is a repository tool, not a sigwood
command, and nothing about the product changes. A quiet network now reads as a quiet
network: a hunt that completes and reports nothing says so in its own words, distinct from
a run where sigwood produced no readable report at all. The collaborator page also names
pipx as a precondition, since several distributions do not ship it and block the
pip-install alternative. -
Families of low-scoring names whose lookups nearly all fail to resolve are now visible. A single
odd-looking domain that scores below the detector's bar stays quiet, as before - but when
one parent domain carries several such names and nearly all of their lookups come back
NXDOMAIN, sigwood now reports the family as one INFO finding. That is the shape of a
rotating rendezvous or a subdomain generator whose infrastructure is dead, and it was
previously invisible no matter how many names were involved. It reads the whole log rather
than the clustered subset, so a busy family cannot hide by being big. Zeek only - Pi-hole
logs carry no response code, so the check stays silent there rather than guessing. Tunable
under[detectors.dns](promote_below_gate,promote_min_subdomains,
promote_min_nxdomain_fraction), and off by one setting if you would rather not see them. -
The known-issues list now records what a very thin allowlist costs the DNS path. Turning
suppression off, or running with an unusually short allowlist over a large window, makes DNS
clustering markedly more expensive - on one seven-day corpus of about 2.2 million rows the DNS
detector on its own took about eight minutes unsuppressed and sat near 8 GiB of memory, and
running the whole default hunt over that same window unsuppressed did not finish inside a
nine-minute bound. The entry gives the measured figures, says plainly that they come from one
corpus on one machine, and offers the three things that reliably help: narrow the window, run
the detector on its own, or leave suppression on. Nothing about detection changed - this is a
limitation that was always there and is now written down. -
The known-issues list now records how a recognized admin session or update run groups
lines. Grouping claims the rare lines that fall inside the session's window - that is what
turns one administrator's work into a single row instead of nineteen - and it means a line
that merely happened at the same time is grouped too, and can set the unit's severity if it
came from a privileged program. Every grouped line is still listed under the unit with its own
time and program, so the entry tells you to read a unit as "these things happened together"
rather than "these things are the same event". Nothing about detection changed; narrowing the
grouping by program was measured and rejected because it dissolved genuine sessions. -
Beacon findings carry their own event times. Each finding now records when the
periodic flow was first and last seen, how long it ran, and roughly how many cycles
that span covers - so a ten-minute cadence that ran for an hour reads differently
from one that ran all week, without leaving the finding. The first-seen time and the
span show at--verboseand in the CSV worklist; the full set is in-vvand the
JSON feed. Detection is unchanged. -
Every grouped syslog finding now says what it is a fraction of. Bursts, reboots,
and recognized transactions carry the host's full analyzed line count beside their own,
so "12 rare lines" can be read against the 40 lines that host produced or the 400,000
it produced. Rare-line and per-program findings already carried the equivalent figure.
Visible with-vand in the CSV worklist; the default one-line rows are unchanged.
Changed
- DNS clustering now treats response outcomes as evidence, not geometry. Response
codes no longer act like ordered numbers in the clustering matrix, suffix categories
keep stable identities when counts tie, and a missing round-trip time stays
distinguishable from a median observation without overpowering the other features.
Cluster membership near the candidate bar can shift slightly as a result: on a benign
reference week this meant four fewer barely-over-the-bar findings, with every
corroborated and grouped finding unchanged. - DNS severity is earned by behaviour now, not by the label score alone. A
high-scoring domain name on its own is a lead, not a verdict, so it reports
MEDIUM. HIGH additionally requires corroboration from what the queries actually
did: most lookups under the name failing to resolve (a majority of responses,
and at least two failures), or the name belonging to a dense high-entropy
cluster - the shape of tunneling. One failed lookup is an event, not a pattern,
so a single dead or mistyped hostname no longer reaches HIGH. Every DNS finding
now records which corroboration applied, and says plainly when none did. - DNS findings carry their resolution evidence. Where it can be measured,
findings report the share of lookups that failed to resolve and how many failed.
Pi-hole logs carry no response code, so on a Pi-hole-only run that corroboration
is unavailable and DNS findings top out at MEDIUM - add Zeek DNS logs for the
fuller picture. Whether a domain was blocked remains evidence you can read; it
never decides severity. - Beacon severity follows the same rule: timing alone tops out at MEDIUM. A
periodic connection train is one category of evidence - a shape worth review,
not a corroborated verdict - so a beacon finding on its own no longer reaches
HIGH, whatever its score. The finding's prose now says what was measured (the
regular cadence of an automated check-in) instead of asserting a C2 beacon,
and its suggested next steps lead with local evidence - the process, dns.log,
and conn.log history - before any external reputation lookup. Detection is
unchanged: the same flows surface with the same scores.
Fixed
-
Imitating a service role name no longer excludes a CloudTrail event from analysis.
Events are treated as automated service activity - and so left out of the per-principal
scoring - when a role name in the record carries theAWSServiceRoleFormarker AWS uses
for its own service roles. That marker was matched anywhere in the name, so a role called
MyAWSServiceRoleForSomethingqualified and its events were set aside with no note and no
count. The marker must now begin a path segment of the role's identifier, and the
comparison is case-sensitive; anything else is treated as interactive and is analyzed.
Genuine service roles are unaffected. A role named exactly like a service role still
qualifies, which is recorded in the known-issues list with the reason it is not closed. -
A CloudTrail record can no longer exclude itself from analysis by imitating an AWS
service name. An event is treated as automated service activity - and so left out of
the per-principal scoring entirely - when the field naming what invoked it ends in
amazonaws.com. That test had no boundary, so a value such asnot-a-host/amazonaws.com
satisfied it and the event was set aside with no note and no count. The field must now be
exactlyamazonaws.comor end with.amazonaws.com, and the comparison is
case-sensitive; anything else is treated as interactive and is analyzed. Genuine service
activity is unaffected, because every value AWS writes there has the form
<service>.amazonaws.com. -
A malformed log file can no longer stop a run in two specific ways. A connection log
whose timestamps span an implausible range - ten lines dated years apart - made the beacon
detector ask for billions of memory slots before it scored anything, which ended the
process. It now declines to score that flow and carries on, which is what it already did
for every other thing it cannot measure. Separately, a CloudTrail file containing a number
with thousands of digits, or JSON nested thousands of levels deep, produced a raw Python
error instead of a warning; both are now reported as an unreadable file and skipped, and a
single bad line in a newline-delimited file no longer discards the good lines around it.
A Zeek TSV log whose column-type header declares a container nested thousands of levels
deep did the same thing; that line is now skipped and counted with the file's other
malformed lines, and the rest of the file still loads. A column with an unusual type that
never carries a value keeps parsing exactly as before - the limit applies to values, not to
the header. None of these change what sigwood reports on ordinary logs. -
A system-log capsule that shows a sample now says so. Opening a finding that reported
97 rare lines showed twenty of them and gave no sign the rest existed, which reads as a
miscount or a broken control. The capsule now closes withshowing 20 of 97 rare lines
whenever the lines on screen are fewer than the lines counted, and says nothing at all when
the capsule is complete. A grouped administrative session or update run discloses per
grouped entry rather than once at the top, using the same count that entry already prints.
The twenty-line sample itself is unchanged: those are the lines sigwood carries, so the
disclosure states the shortfall rather than offering a switch that cannot recover it. -
A Zeek
syslog.logline with no message no longer becomes a finding about text
that never existed. A row whosemessagefield is empty of content - JSONnull,
or-in TSV - was turned into the literal wordsNoneornan, which then read as
a program name, templated like any other line, and could surface as a rare finding.
Those rows are now dropped before that can happen, and the count is reported:
syslog.log: skipped 3 rows with a missing or non-text message. A genuinely empty
message is kept, and so is a real log line whose text happens to readNone- the
check is on the field's type, not on how it prints. -
A mistyped syslog setting now says which setting is wrong, instead of failing
strangely later.[detectors.syslog]values are checked before the detector
runs: an out-of-rangerarity_pctused to crash mid-run with an unhelpful
message,max_count = 0silently switched rare-line detection off entirely, and
writingprivileged_programs = "useradd"instead of a list quietly emptied the
privileged tier - a string is a sequence of letters, so the roster becamea,
d,e,r,s,u. Each is now reported against the setting that caused it,
the other detectors still run, and no traceback reaches the terminal. This
repairs crash and silent-disable shapes; it does not change detection on a valid
configuration, andrarity_pctremains inert at the shippedmax_count = 1. -
Beacon says why it scored nothing, instead of just reporting nothing. When the
connection log is missing a column beacon needs, when every eligible connection
lacks byte counts, or when rows carry no source, destination, port or protocol,
the run summary now says so and names the cause - previously the detector
returned an empty result that read exactly like "nothing was periodic." The
counts describe the connections beacon actually received, after allowlist
suppression, so a note never blames missing bytes for rows your allowlist
removed. A configuredmin_connectionsbelow the scorer's own floor is now
disclosed as well, rather than quietly having no effect. -
A malformed
[detectors.beacon]setting fails with a message that names the
key. A zerobin_seconds, a non-numeric threshold, or a text value where a
number belongs used to surface as an opaque detector crash partway through a
run; it is now reported against the setting that caused it, the other detectors
still run, and no traceback reaches the terminal. -
A connection log missing an optional column no longer crashes beacon. Absent
bytes,conn_state,portorprotocolumns are a fidelity limitation of the
source, so beacon now abstains and says which columns were missing. -
The beacon run-time disclosure describes the excluded connections truthfully.
It now says unscored connections were outside the Zeek SF/S1 states beacon
analyzes, rather than calling them all "not established" - reset and
half-closed connections are established states, and the old wording misfiled
them. The known-issues entry carries the same correction, and the eligibility
it states now matches the code exactly. -
Beacon's suggested history pivot no longer prints a command that hangs. The
oldzeek-cutsuggestion piped nothing intozeek-cutand assumed a TSV file
in the current directory; the step is now a plain instruction to review the
destination's history inconn.log.