A finding is a floor, not a verdict
The same problem query answered 0 and then 127 on one capture, thirty seconds apart. Four questions to ask of any finding before you act on it, and the fields that answer each one.
find_problems hands back a list. An agent reads the list, writes a
conclusion, and the conclusion is only as good as four things the list does not
say out loud. Every one of them has a field, and every one of them has a
failure mode where the honest answer and the alarming answer look identical.
Question 1 — was that the whole answer?
Here are two responses to the same find_problems call, on the same capture,
from the same binary. Nothing changed except how long the server had been
reading:
// asked immediately after the handshake
{ "returned": 0, "total_matched": 0, "source_exhausted": false,
"capture_identity": { "dialog_generation": 37 } }
// asked again once the file had been read to its end
{ "returned": 2, "total_matched": 127, "truncated": true,
"source_exhausted": true,
"capture_identity": { "dialog_generation": 9015 } }
Zero problems became 127. An agent that asked once and reported “no problems
found” would have been wrong about a capture holding 127 of them, and nothing
in the first response says “wrong” — it says source_exhausted: false, which
means the same thing and takes a reader who knows to look.
Three fields carry this, and they answer different questions:
source_exhausted— has sipnab read the source to its end? Until this goes true, every count is a floor. On a live interface it never goes true, by design.total_matched— how many matched across the WHOLE store, whateverlimitandcursorsay. This is the number that answers “how many”. Counting the returned array answers a different question.truncated— matches remain after this page. Note what it does in the first response above: it is notfalse, it is ABSENT. sipnab omits the field rather than claiming completeness it cannot support, so a client reading a missing key asfalseinvents the reassurance.
capture_identity.dialog_generation moved from 37 to 9015 between those two
answers, which is the same fact in a different currency. And a changed
instance voids every cursor you hold, so a paging client that ignores it pages
through a capture that no longer exists.
Question 2 — what did the run decline to read?
A finding counts what sipnab understood. Anything it threw away first makes
every count below it a floor, and --analyze puts that at the top of its list
under its own severity:
1. [BLIND] SIP discarded by --portrange — 10 message(s)
sipnab recognized these as SIP and then threw them away because neither port
was inside --portrange. They are in no dialog and no count.
BLIND sits above CRITICAL on purpose. Over MCP the same facts arrive on
capture_status as unanalysed_sip_messages, unanalysed_busiest_ports and
the capture_quality block, which carries kernel_dropped_packets,
undecodable_frames and snapped_frames beside the NAT and relay counters.
Read those before the findings, not after. A capture where
unanalysed_sip_messages reads 10 and dialog_count reads 0 is not a quiet
network.
Question 3 — was anything watching?
An empty list has three readings and only one of them is “clean”. sipnab keeps them apart with a flag rather than leaving you to guess:
// security_findings {}
{ "detection_armed": false, "armed_kinds": [], "findings": [], "total_matched": 0,
"note": "No detection rule is armed on this server, so no finding could have been recorded. An empty findings list here means nothing was watching, NOT that the traffic was clean. Arm a detector with --kill-scanner, --fraud-detect, --digest-leak or --reg-flood and re-run the capture." }
describe_endpoint carries the same distinction one level further in.
findings.selectable: false means nobody COULD ask — the alert engine files
every finding against a source address, so a query keyed on a SIP user has no
key to select with. An empty armed_kinds means nothing was watching. Only on a
server with a detector armed does an empty list mean the endpoint stayed clean.
The same instinct guards the arithmetic. failure_rate_pct comes back null
rather than 0.0 when nothing reached a final status, because a zero there
hands a clean bill of health to an endpoint nobody measured.
registration.applicable: false does the same job for REGISTER.
Question 4 — is this a fault, or a thing to rule out?
Severity in sipnab describes how much the finding constrains the answer, not how upset to be. Two examples where the text tells you to stand down:
[MINOR] Call failed 4xx
Many 4xx codes are ordinary call outcomes — 486 Busy Here, 404 for a
misdialled number, 480 for a phone that is off — so this is listed to be ruled
out rather than acted on. The codes are named below; a run of 403 or 408 is not
an ordinary outcome.
[MAJOR] RTP source no SDP advertised
Frequently benign on its own (symmetric RTP through a NAT looks exactly like
this); read it together with any one-way audio on the same call.
Both of those are true findings about real packets, and acting on either alone wastes a morning. The second one is the sharper lesson: a NAT-mismatch finding means something rewrote the media source, which is what a NAT is FOR. It earns attention only when a one-way finding names the same call.
Meanwhile a [CRITICAL] One-way audio on a session whose SDP negotiated
a=recvonly describes the packets perfectly and describes no fault at all.
Ruling that out is its own note.
Then go and check the packet
Every finding names evidence a reader can go back to. A dialog summary carries
a frame pointer, and that pointer is checkable:
sipnab -N -I capture.pcap --show-frame 'capture.pcap#0@db88659b94678546'
With the digest attached, sipnab verifies the bytes and prints VERIFIED
before the hex. Against a file that has rotated, truncated or changed since you
made the pointer, it refuses:
refusing: capture.pcap frame 0 is not the frame this pointer was made against.
The capture was rotated, truncated or rewritten since then. Showing you what is
there now would look like an answer and be the wrong one.
A pointer typed by hand, without the digest, still prints the frame and marks
it UNVERIFIED, because nothing exists to check it against. That word is the
whole difference between quoting evidence and quoting whatever now sits at that
offset.
The short version
Before acting on any finding: check source_exhausted, check the blind
findings and unanalysed_sip_messages, check that you armed a detector, and
read the finding’s own text for the sentence telling you it is a thing to rule
out. Then pull the frame and look at it.