Add fm_analyze_pcap and fm_list_pcaps: read-only SIP capture decoder with evidence-scoped analysis - #35
Add fm_analyze_pcap and fm_list_pcaps: read-only SIP capture decoder with evidence-scoped analysis#35kierknoby wants to merge 64 commits into
Conversation
…ogman FreePBX 17 module, plus a companion fm_list_pcaps picker. PcapAnalysis.php is a dependency-free, pure-PHP libpcap decoder: it walks the classic pcap framing (little- and big-endian, microsecond and nanosecond), handles Ethernet, Linux SLL and SLL2, and raw-IP link types, decodes IPv4 and IPv6 with UDP and reassembled-TCP transport, lifts the SIP text out of each packet, and groups messages by Call-ID into per-call ladders carrying timing, endpoints, methods, status codes, final and INVITE-final status, release reason, SDP media summary, retransmission counts, largest signalling gap, and a set of agent-facing observations, outcomes and diagnostic hints derived from the SIP headers alone; raw per-message data is preserved untouched so an MCP agent can reason from evidence rather than the tool's heuristics. Input is constrained for safety: paths must end in .pcap or .cap, are realpath-resolved, NUL-byte rejected, and confined to an allowlist of capture directories (/var/spool/asterisk/packetcapture, where Sysadmin Pro writes, plus an optional Frogman-owned drop directory), with file, per-packet and per-stream size caps, pcapng detected and rejected with an editcap conversion hint, and SIP authentication headers deliberately never surfaced. fm_list_pcaps lists available captures newest-first as clickable chips (three shown, with a show-more), chaining each into fm_analyze_pcap so users never type a path, and on an empty capture set it links the operator to Sysadmin's Packet Capture page rather than invoking the commercial module itself. The change is additive and surgical: two new tool files under Tools/, one new formatter case each in Frogman.class.php wired into the existing chat result switch with every interpolated field passed through sanitizeForChat() per GHSA-7qvv, and parser branches plus help-text lines in ChatParser.php that accept both British and American spellings as input while dispatching the American fm_analyze_pcap name to match the repo's house style; no existing tool, dispatcher, loader or logic is modified, all tools auto-register via the existing glob loader, and reverting the additions returns the module byte-for-byte to v2.2.1.
…t: `fm_list_pcaps` lists allowlisted `.pcap/.cap` captures as clickable analyze chips with a Sysadmin Packet Capture empty-state link, ChatParser now dispatches list/show pcaps commands and help/typeahead stays in sync, and `fm_analyze_pcap` now appends a concise “Reader summary” highlighting outcome, notable signalling clues, and the best Call-ID to inspect next.
… made to sound failed by unrelated OPTIONS/qualify failures: the analyzer now separates INVITE call outcomes from non-call SIP transactions in the reader summary, adds structured focus metadata for the most relevant ladder, the formatter renders a clickable focus chip, and ChatParser accepts `analyze pcap <path> call_id <id>` so admins can jump directly into a single Call-ID.
…over assumptions by improving retransmission detection, SIP header parsing, unparsed message reporting, focus-call selection, TCP reassembly provenance, and confidence handling. Added lightweight RTP/RTCP analysis with timing-aware call correlation, reciprocal media direction checks, sequence gap estimation, codec comparison, structured media summaries, RTP memory safety guards, and explicit capture-point caveats to avoid overclaiming. Introduced support summaries, confidence notes, likely next checks, traceable diagnostic hints, strict separation between decoded evidence and interpretation, and chat formatter enhancements that surface RTP status, structured hints, support summaries, likely next checks, confidence notes, and unparsed-message caveats so support engineers and AI agents receive deeper analysis while retaining clear visibility of uncertainty and evidential basis.
…Updates RTP classification so SDP-negotiated calls with no matched RTP are no longer labelled `rtp_not_negotiated`; cancelled calls now report `rtp_not_seen_before_cancellation`, other negotiated/no-RTP cases report `rtp_not_seen_at_capture_point`, and descriptions were updated without changing RTP correlation logic.
… output. Analysis findings now include structured alternative explanations and traceable evidence references, with formatter support for interactive post-summary exploration while preserving the original evidence-driven analysis.
…g fm_analyze_pcap as the single PCAP tool. Summary lines and diagnostic hints now expose deterministic Simplify, Re-Explain and Show Evidence modes through optional action parameters, with formatter chips routing back to the same analyser rather than pre-rendering alternate explanations inline. The action output re-runs the existing analysis, preserves the evidence-first model, avoids session caching or extra resolver tools, and keeps the new chat parsing limited to mapping PCAP action commands back to fm_analyze_pcap.
…vailability from summary_action results, rendering follow-up chips for Simplify, Re-Explain and Show Evidence using the existing explicit PCAP command parameters, preserving optional call scoping across clicks, and keeping Simplify/Re-Explain chat output free of raw refs while labelling Show Evidence refs as internal references.
…nse scrolling and PCAP evidence presentation: long bot replies now open at the start of the completed response after layout and Mermaid rendering settle, while short replies retain the normal bottom-scroll behaviour, and high-level PCAP confidence evidence now presents scoped methodology notes without unrelated SIP snippets or duplicated reference lines.
…bot replies now settle at the start of the completed assistant message after layout and Mermaid rendering, while user messages still scroll to the bottom, and high-level PCAP confidence evidence now shows scoped methodology notes instead of unrelated packet snippets or duplicated refs.
…evel actions: each diagnostic hint, support summary, likely next check, and confidence note block now gets a single `Simplify · Explain · Evidence` action set, with `fm_analyze_pcap` aggregating block content deterministically, preserving old action aliases, and returning sibling block actions after each follow-up.
… a single whole-response Simplify, Explain, and Evidence workflow. Kept all logic within fm_analyze_pcap and avoided additional tools, caching, session storage, or frontend changes.
…o an OPTIONS/403 qualify falsely asserted an INVITE failure; now gated on the call containing an INVITE, otherwise reads "Request ended in a failure response".
…thods inside deriveDiagnosticHints() so INVITE failures keep INVITE wording while non-INVITE failures use neutral “SIP transaction” wording, made the failed_final_response Simplify/Explain template method-neutral, and removed the INVITE assumption from its evidence selector. Left failed_invites correctly INVITE-scoped.
…uting section=response Simplify and Explain through aggregate-aware text builders that compose prose from the existing analysis fields (SIP/call counts, INVITE outcomes, final statuses, observations, RTP summary, next-check and confidence IDs) rather than stitching item text; the wording remains deliberately capture-scoped, always ends with a Caveat: sentence, makes no clean-bill-of-health claim on all-clean captures, and limits positive findings to signalling-level and capture-point RTP evidence, while non-response Simplify/Explain and Evidence are left unchanged, verified with php -l Tools/PcapAnalysis.php and smoke checks covering clean-with-RTP, clean-without-RTP, failed, and unparsed narratives.
…an scan for a named capture instead of date and size alone, rendering the basename (already in the tool payload, sanitised like the path) ahead of the existing timestamp and size in the same clickable analyze-pcap chip; purely a formatter change, no tool change, verified with php -l Frogman.class.php.
…kable filename with date/time and size on a separate indented metadata line, while keeping the existing analyze pcap <path> action unchanged. Validation passed with php -l Frogman.class.php; diff scope is only Frogman.class.php.
…P transactions while INVITE-bearing groups are reported separately as INVITE call flows. The chat header, reader summary, focus candidates, and response actions now show SIP transaction counts alongside INVITE call-flow counts, with focus candidates prioritising INVITE call flows and labelling non-INVITE groups by primary method. Existing decoding, grouping, RTP analysis, outcome classification, call_count, and action routing remain unchanged. Validated with php -l Tools/PcapAnalysis.php and php -l Frogman.class.php.
…by using a single aggregate salient sentence when it already covers the INVITE outcome and suppressing repeated cancellation or RTP findings. Preserve the deterministic aggregate-field composer, Caveat: behaviour, and existing analysis logic.
…ing Explain wording, preserving the displayed-subset versus whole-capture count distinction and leaving decoding, RTP, Evidence, and action UX untouched. php -l Tools/PcapAnalysis.php passes, and the only changed file is Tools/PcapAnalysis.php.
…ing next-check wording, removing “framing”, “derived observations”, and “separately inferred cause” while preserving the same caution against assuming a fault. php -l Tools/PcapAnalysis.php passes, and the final diff scope is only Tools/PcapAnalysis.php.
… call-flow labels with duration, message count, and final status on the primary line, move Call-ID to an indented metadata line, prioritise INVITE call flows ahead of non-INVITE SIP transactions, and preserve existing drill-down actions, analysis logic, RTP processing, and Call-ID routing unchanged.
…signalling-derived statement. Cancelled INVITE flows are now reported as "cancelled before answer" rather than "need review", improving evidence-first presentation without changing analysis, decoding, or action behaviour.
…didate's primary description line in the same analyze-pcap call_id chip the existing bottom focus link already uses so an engineer can click straight from a ranked candidate to its single-ladder view instead of copying the Call-ID by hand, with the path sanitised and the call id passed through pcapCommandValue exactly as the summary-action targets are, the Call-ID kept as plain reference text on the line below, a plain-text fallback when path or call id is missing, and the existing INVITE-first ordering, top-3 slice, and call_count>1 gate all unchanged; formatter-only, no tool change, verified with php -l Frogman.class.php.
…or INVITE call flows and an "Other SIP transactions" group for everything else so the INVITE-vs-OPTIONS split is visible at a glance and a reader can tell real call attempts from keepalives and qualifies, keeping each candidate row tappable via the existing analyze-pcap call_id chip with continuous numbering across both groups and the top-3 slice, INVITE-first ordering, and call_count>1 gate unchanged, and removing the trailing auto-focus chip that previously printed after the reader summary because it duplicated the picker on multi-call views and pointed at the already-shown ladder on focused single-call views, leaving the response-block Simplify/Explain/Evidence actions as the message ending; formatter-only, no tool change, verified with php -l Frogman.class.php.
…ters, including partial action sets, render with spacing and a divider before the action chips.
…NVITE SIP transactions are distinguished more clearly in chat output. Focus entries now use observation-driven labels, showing `Cancelled before answer` only when the transaction explicitly carries a `cancelled_before_answer` observation, while preserving final SIP reason text. INVITE entries drop redundant type suffixes, non-INVITE entries retain their primary method, and the resulting focus list highlights real call flows without changing decoding, grouping, RTP analysis, outcome classification, action routing, or underlying PCAP analysis data. Validated with `php -l Frogman.class.php` and `git diff --check`.
…s summary, section, and action views. Consolidates duplicated action-spacing logic into a single implementation without changing PCAP decoding, grouping, RTP analysis, outcome classification, evidence generation, or action routing.
…le as a drill-down selector for calls and SIP transactions. Presentation-only change with no impact on analysis, grouping, RTP logic, or action routing.
…ing tool-composed friendlyCallSentence() result into each top_calls entry and rendering any non-empty value as a sanitised bold line beneath the picker row’s Call-ID, while keeping non-INVITE SIP transaction rows silent, leaving composition in PcapAnalysis and preserving formatter-only rendering in Frogman; verified with php -l Tools/PcapAnalysis.php && php -l Frogman.class.php.
…rection by reusing `deriveRelatedInviteLegs()` in `summariseCapture`, mapping paired Call-IDs to their inbound/outbound direction, and prefixing only paired INVITE-friendly strings with `Inbound leg:` or `Outbound leg:` while leaving standalone calls unchanged and keeping all forwarding/bridging interpretation in the existing hedged related-legs support line; formatter unchanged, friendly output still sanitised there, verified with `php -l Tools/PcapAnalysis.php`.
…d INVITE legs now use leg-framed wording without implying who personally dialled whom, while standalone call wording stays unchanged; verified with `php -l Tools/PcapAnalysis.php`.
…tore lowercase inbound/outbound directions and render leg-framed sentences directly, avoiding double prefixes while keeping standalone focus-context wording unchanged; verified with php -l Tools/PcapAnalysis.php.
…l Simplify text for non-technical readers, leaving the technical text and Explain/Evidence untouched. The salient-finding ladder is extracted into a shared responseSalientFindingKey so the headline and the gloss derive from one decision and cannot drift; responseSalientFindingSentence consumes it with its output unchanged. Wording avoids overclaiming: answered implies no media, the gap line does not steer toward ringing, no-INVITE does not assert a call was absent, cancelled stays neutral on who ended it. No analysis logic, LLM, or generated text.
…, so a benign gap on a cleanly-completed call no longer becomes the headline or the gloss. The gap is just talk-time before BYE, not a fault. Only changes selection when answered and a gap coincide with no stronger outcome; all other sentences unchanged.
…dered lines after sanitisation, so intentional spacing such as the gap before “This means:” survives in follow-up responses; verified with php -l Frogman.class.php and php -l Tools/PcapAnalysis.php.
…d_status_counts and adds the requested capture-point RTP clause for both-way audio, one-way audio, or absent answered RTP, while leaving the selector and every other gloss case unchanged. Verified with php -l Tools/PcapAnalysis.php.
…reached from multiple allowlisted bases or collapsed symlinks are listed once, without changing sorting, limits, counts, or the return shape. Verified with php -l Tools/ListPcaps.php.
…casting, rejecting malformed inputs such as `10abc` and `1.5` while preserving existing limits, defaults, sorting, counts, and return structure. Also deduplicate captures by resolved path so files reachable through multiple allowlisted directories or symlinked paths are listed only once. Validated with `php -l Tools/ListPcaps.php`.
… from MAX_FILE_BYTES = 104857600 (100 MiB) to MAX_FILE_BYTES = 524288000 (500 MiB), and updated the matching rejection message to say 500 MiB. No other safety controls were changed. Verified with php -l Tools/PcapAnalysis.php.
…g an explicit Calls found empty state when a capture has no decoded INVITE call flows before listing other SIP transactions, and by changing the response-level large-signalling-gap plain-English gloss to refer to the next SIP event rather than the next event in a call, keeping the wording accurate for OPTIONS/auth/non-call captures; verified with php -l Tools/PcapAnalysis.php and php -l Frogman.class.php.
…ly when a response contains zero decoded INVITE call flows, so Simplify now explains that the recording only shows SIP keepalives or other non-call traffic instead of allowing a large signalling gap to read like the main takeaway; verified with `php -l Tools/PcapAnalysis.php`.
…ter real-world testing showed a ~306 MiB Sysadmin capture exhausted the configured PHP memory limit during analysis. Updated the limit comment to reflect the tested operating envelope and left all parsing, decoding, RTP analysis, TCP reassembly safeguards, diagnostics, and output behaviour unchanged. Verified with `php -l` on modified files.
…nd updated the rejection message to Capture file exceeds the 200 MiB safety limit. Verified with php -l Tools/PcapAnalysis.php.
…bedded in `sysadmin_YYYY_MM_DD_HH_MM_SS` filenames rather than filesystem modification time, with automatic fallback to `filemtime()` for non-Sysadmin or unparseable filenames. Aligned the displayed `when` field with the same derived timestamp used for sorting, keeping capture ordering and displayed dates consistent while preserving the existing API payload shape, validation, deduplication, limits, path handling, and fallback behaviour. Verified with `php -l Tools/ListPcaps.php`.
…AP summary views by passing focused `call_id` context into the response-level Simplify/Explain narrative and changing only that unparsed-payload sentence to say the malformed payload was elsewhere in the capture, while preserving whole-capture wording, parser behaviour, counts, Evidence, diagnostics, and action handling; verified with `php -l Tools/PcapAnalysis.php`.
…er-side 3-row cap, preserving the existing `analysis.top_calls` limit and Calls found / Other SIP transactions grouping, and adding a short note when decoded INVITE call flows exceed the rendered call rows so larger captures no longer look artificially limited; verified with `php -l Frogman.class.php`.
…d `Other SIP transactions` read as independent lists, preserving existing item order, wording, empty-state text, tappable actions, and output structure; verified with `php -l Frogman.class.php`.
…and Explain use focused-analysis wording for selected Call-ID views and explicitly labelling capture-wide warnings, including clearer TCP reassembly cap messaging for payloads skipped elsewhere in the capture, while preserving parsing, analysis, evidence, diagnostics, RTP logic, limits, API shape, and actions. Verified with php -l Tools/PcapAnalysis.php and php -l Frogman.class.php.
…use “This focused analysis contains...” while full-capture summaries continue to use “This capture contains...”. Passed the focused flag through summary generation without changing parsing, analysis, evidence, diagnostics, RTP logic, limits, API shape, or actions. Verified with php -l, full repo PHP lint, and a reflection smoke check for full-capture versus focused wording.
… appending UDP or TCP-parsed SIP messages, preventing mixed UDP/TCP captures from exceeding the output cap, and guarding PCAP clickable command chips so paths with command delimiters or control characters render as safe plain text instead of malformed actions. Preserved picker section numbering, parsing, grouping, summaries, RTP logic, ListPcaps behaviour, and chat scroll behaviour. Verified with php -l on affected PHP files and full repo PHP lint.
…ng so rejected payloads no longer increment the stored-byte counter and the warning is emitted once rather than once per skipped packet. The arrival-time cap, merge-time reassembly check, and global TCP cap remain unchanged. Verified with php -l and full repository PHP lint.
|
Kieran, Really nice work on this. The safety posture is exactly right for a binary parser touching an admin-permission surface: NUL check, extension-then-realpath-then-extension-again on the path, allowlist prefix with trailing separator, pcapng magic detected and refused cleanly. The command-chip guard degrading to plain text on hostile filenames is the same pattern as the GHSA-7qvv formatter fix, and it's in every relevant place. The evidence discipline shows in the output too. "No RTP seen at this capture point" over "no audio," leg-framed wording only in the picker where the sibling leg is visible, related-legs surfaced only when four independent gates agree. These are decisions I'd expect from someone who reads a lot of captures. It reads well. Two things to sort before merge: 1. 2. Split the One soft ask, take it or leave it: 3. Adversarial-capture pass. The test plan is thorough on real captures but doesn't mention deliberately malformed ones. A truncated header, a bad IP length field, an oversized SDP. The bounds discipline should hold, but this is where you'd want to confirm before shipping. Not a blocker if you're comfortable with the tail risk. Paste-ready version for a Claude Code session:
Once those are in, I'll verify on the dev box and ship as v2.9.0 (feature addition, tool count 255 → 257), crediting you in the tag annotation. Thanks for a solid contribution. |
…onsistent class and nested block formatting without changing behaviour.
…js`, restoring the original bottom-scroll flow while preserving the PCAP feature changes for PR mwtcmi#35.
|
Thanks, Mike. I have fixed the ListPcaps.php indentation and removed the unrelated chat.js scroll changes from PR #35. The scroll work has been preserved separately for a follow-up PR. |
|
Hi Mike, The two requested cleanup items are complete, and I also ran the optional adversarial-capture pass.
No crashes, PHP errors, runaway output or excessive memory use were observed, and no further code changes were required. The PR is ready for your final verification and merge. Thanks, |
What this is
Tools/PcapAnalysis.phpis a dependency-free, pure-PHP decoder for classic libpcap captures. It reads a.pcap, decodes the link, IP, and transport layers itself (no libpcap, no shell-out to tshark), lifts SIP messages, groups them by Call-ID, and produces a structured per-call analysis: outcome, final status, observations, confidence-scored diagnostic hints, and a lightweight RTP/RTCP view.Tools/ListPcaps.phpis a companion picker that lists available captures newest-first as clickable chips.fm_analyze_pcapis the primary capability introduced by this PR.fm_list_pcapsis a small companion discovery tool that lists captures from the allowlisted capture directories and emits analyse actions. It exists to make the analysis workflow usable from chat without requiring users to know capture paths.Both are read-only and
PERM_ADMIN. Neither starts, stops, nor configures a capture;fm_list_pcapsdirects users to the Sysadmin Packet Capture page for capture creation and management.From chat the workflow is
list pcapsto see available captures as clickable chips, thenanalyse pcap <path>(oranalyze, both parse) to decode one; from a result, the focus picker and the Simplify / Explain / Evidence actions take it from there. A capture path can also be passed directly without listing first.The recommended way to produce captures is the onboard Sysadmin Pro Packet Capture feature, which writes them into the allowlisted directory. Beyond that, the decoder reads classic libpcap files from the allowlisted directories regardless of how they got there, so a capture placed in
/var/spool/asterisk/packetcaptureby other means is analysed under the same path, extension, and size checks; this has been exercised with classic-pcap captures from FreePBX 16 as well as 17.Auth headers are deliberately never surfaced in output. Confirmed on real captures: the parser only retains Call-ID, CSeq, From, To, Reason, and content-type/length headers, so WWW-Authenticate and Authorization are never lifted, and captures containing 401 responses on both INVITE and OPTIONS rendered the status line only with no credential header content.
No LLM, no new database tables or schema change, no session state. A file goes in, structured analysis comes out.
Why
When a call goes wrong the capture is the ground truth, but reading a raw
.pcapby hand or round-tripping through Wireshark is slow and does not belong in a chat console. This decodes the capture in-process and presents the SIP ladder the way an engineer reads it: grouped by call, ordered by time, with the final status and any Reason header foregrounded.The principle throughout is that false confidence is worse than incomplete analysis. The tool states what the packets show as fact, offers interpretation as explicitly labelled and confidence-scored additions, and never asserts a cause the capture cannot support. Absence of RTP is reported as "no RTP seen at this capture point", never "no audio". One-way RTP is "media visibility asymmetry", never "one-way audio". It reasons like an engineer who is careful to separate what they saw from what they infer.
Architecture
Pure-PHP decode path: classic pcap magic (LE/BE, micro and nano timestamps); Ethernet, Linux SLL, SLL2, and raw link types; IPv4 and IPv6 including extension-header walking; UDP, and TCP with sequence-ordered reassembly. SIP is lifted from UDP payloads and reassembled TCP streams, compact header forms (
i,f,t,m,v,c,l) are canonicalised, and messages are grouped by Call-ID.Per call, the analysis derives the methods seen, the final and INVITE-final status, the outcome (answered, busy, cancelled, failed, auth_challenge, incomplete, and so on), observations, and confidence-scored diagnostic hints. Byte-identical non-provisional retransmissions are detected by fingerprint, deliberately excluding provisional 183 resends and auth re-INVITEs so normal flows are not flagged as loss.
RTP/RTCP is observed with streaming counters only, with no packet retention. Streams correlate to calls by SDP media port, then by IP compatibility, then by timing overlap with the signalling window; divergence lowers confidence rather than forcing a match. Per-direction packet counts, SSRC, sequence range, and a loss estimate are reported, but the loss estimate is suppressed to null when a 16-bit sequence wrap or reorder is seen, because the gap arithmetic is then unreliable. RTCP (PT 200 to 204) is detected separately and never mixed into RTP loss. Codec mismatch against SDP is low-confidence only, since dynamic payload types are SDP-scoped.
Follow-up views (
Simplify,Explain,Evidence) re-run the tool withsummary_action,section, anditem_idparameters rather than caching analysis in session state, so there is no stored state to go stale.In chat, a multi-transaction capture presents a focus picker: the top candidates are grouped into "Calls found" (INVITE call flows) and "Other SIP transactions" (OPTIONS qualifies, auth challenges, and the like), each row tappable to re-run the tool scoped to that one Call-ID. The grouping makes the INVITE-vs-OPTIONS split visible at a glance, so a reader can tell a real call attempt from a keepalive without reading every ladder, which matters because a busy box's capture is mostly qualifies. Scoping by Call-ID collapses the output to the single ladder of interest.
Where a capture contains two cancelled INVITE legs that look like the same propagated cancellation (overlapping time windows, opposite PBX-side direction sharing a common endpoint, both showing a 183, and cancellation evidence within a tight 250ms window), the support summary surfaces a "possible related call legs" line. This is offered at medium confidence and explicitly worded as consistent with the PBX propagating cancellation between bridged or forwarded legs, while stating the capture does not prove the application-level reason. It is gated by four independent checks so unrelated near-simultaneous cancels are not paired.
Both the focus picker rows and the call-scoped follow-up focus-context block carry a plain-English fact line, for example a cancelled INVITE reads as the From party trying to call the To party at a given time and being cancelled before answer. The verb tracks the decoded outcome, so a cancelled or failed attempt is never described as a connected call and talk time is claimed only for answered calls. Caller and callee are read from the From and To URIs and shown as the signalling carried them: a numeric user part is shown as the number, a non-numeric user part (an SBC username, for instance) degrades to the URI host, and a URI with neither degrades to a neutral label. The host is printed verbatim as a fact about the signalling, without asserting whether the endpoint is internal or external or ascribing it a PBX or trunk role. The line is composed in the tool (so the outcome-verb discipline lives in one place) and the formatter only renders it; non-INVITE transactions stay silent.
When a call is one leg of a detected related-legs pair, the picker line is framed by direction rather than by who dialled whom: it reads "Outbound leg from X to Y, cancelled..." or "Inbound leg from X to Y, cancelled...". This avoids a specific misread. On a PBX that forwards an inbound call back out, the outbound leg's From header carries the original caller's number, so the bare "X tried to call Y" wording implies that caller personally dialled the forwarded destination, which the capture does not show. The leg-framed wording states the leg's direction (proven from packet source and destination against the shared PBX endpoint) and its From and To as the headers carried them, without asserting that the call was forwarded; the inter-leg relationship stays only in the hedged related-legs line, which the capture cannot prove beyond consistency. The direction-framed wording applies only to calls in a detected pair; a standalone INVITE keeps the plain "X tried to call Y" form, which is correct for a genuine single origination. As a deliberate consequence, the call-scoped focus-context line (shown when one Call-ID is explicitly opened) keeps the plain wording even for a leg that the picker frames by direction, because in that view the leg is shown alone rather than beside its sibling, so "this leg as its headers read" is the honest framing. Plumbing direction into the focus context is a separate, larger change kept out of scope here.
The response-level
Simplifyaction carries a plain-English one-line gloss below its existing text, introduced as a separate sentence prefixed "This means:". The existing Simplify text is accurate but reads in engineer register; the gloss restates the single salient finding for a non-technical reader, for example "at least one call was answered, and we saw audio going both ways at this capture point" or "the call ended before it was answered". It is a fixed hand-written lookup, never generated, and is additive: the technical Simplify text and theExplainandEvidencenarratives are untouched, soExplainkeeps the fuller register and onlySimplifygains the gloss. The gloss derives from the same salient-finding selector that chooses the headline sentence, so the two cannot drift; for an answered call the gloss additionally reads the answered RTP status to say whether audio was seen both ways, one way, or not at the capture point, which enriches the explanation of the answered finding without changing which finding the selector picks. The wording holds the same evidence discipline as the rest of the tool: media is only mentioned as seen at the capture point and never as proof of what either party heard, a benign signalling gap is not surfaced as the headline on a completed call, a no-INVITE capture is described as background traffic rather than asserting no call existed, and cancellation stays neutral on who ended the call.Safety and evidence discipline
Path resolution is allowlisted to the two capture directories (
/var/spool/asterisk/frogman/capturesand/var/spool/asterisk/packetcapture),realpath-checked, extension-checked, size-capped at 200 MiB, with per-packet and total TCP-reassembly byte caps to bound memory on a malformed or hostile file. Failed non-INVITE transactions are described without assuming an INVITE flow (a 403 to an OPTIONS qualify reads "SIP transaction ended in a failure response", not "INVITE ended..."). Every interpolated user-controlled field in the chat formatter passes throughsanitizeForChat()and is backtick-wrapped, per the GHSA-7qvv formatter-breakout class.Capture paths feed clickable chat command chips, so a path containing a command delimiter or control character (
|,{{,}}, control bytes) is treated as unsafe to render as a chip: those rows fall back to showing the filename and path as plain backtick-wrapped text with no clickable command emitted, rather than risk a malformed or misleading chip. A normal Sysadmin capture path renders clickable as usual. This was exercised with a deliberately adversarial filename (test|evil.pcap): the file and its path stayed visible, no command chip was generated.Test plan
Verified on a FreePBX 17 box against real Sysadmin captures via the chat console.
sysadmin_2025_05_22_16_56_32.pcapdecoded to 17 SIP messages across 6 SIP transactions, of which 1 was an INVITE call flow answered with a clean INVITE, 100, 183, 200, ACK, BYE (cause=16) sequence, SDP lifted both ways, RTP seen in both directions, and the remaining transactions classified as OPTIONS qualifies.sysadmin_2025_05_22_14_52_43.pcapdecoded to 17 SIP messages across 5 SIP transactions with the same single answered INVITE plus a 404 OPTIONS, a 501 NOTIFY, and 1 unparsed SIP-like payload. Both confirmed that an answered INVITE reconstructs correctly with SDP and bidirectional RTP, that non-INVITE failures (404, 501) are described as SIP-transaction failures and explicitly de-contaminated from the call verdict in the reader summary, and that the unparsed-payload path renders its incomplete-totals caveat correctly.sysadmin_2026_06_01_11_20_42.pcap) decoded to 33 SIP messages across 8 SIP transactions, of which 2 were INVITE call flows, with both ~26-second cancel-before-answer flows reconstructed, and the OPTIONS/403 transaction correctly described as a SIP-transaction failure rather than an INVITE failure. The chat focus picker grouped the two cancelled INVITEs under "Calls found" and the OPTIONS failure under "Other SIP transactions"; tapping a row re-ran the tool scoped to that Call-ID and collapsed the output to the single ladder.sip_message_count(33) andcall_count(8) as before the compact-header change, confirming that Call-ID, From, and To parse identically through canonicalisation.Simplify,Explain, andEvidencewere exercised on the cancel-timer capture, both whole-capture and scoped to a single Call-ID. The de-duplicating narrative composer states the salient finding once, always ends on the capture-point caveat, and singularises correctly on the focused single-call view.Simplifywas witnessed on both 2025_05_22 answered captures, whole-capture and scoped to the answered Call-ID. The gloss renders on its own line below the existing Simplify text. Both captures are answered with RTP in both directions, and the gloss correctly read "at least one call was answered, and we saw audio going both ways at this capture point", confirming the RTP-aware answered branch that reads the answered RTP status to describe media visibility. Testing also exercised the salient-finding priority: an answered call whose talk-time shows up as a multi-second signalling gap initially glossed as "there was a long pause", because the gap outranked answered in the shared salient selector. The selector now ranks answered above the gap, so a benign gap on a cleanly-completed call no longer becomes either the headline sentence or the gloss; the headline reads as an answered 2xx and the gloss as the answered line, with every other Simplify sentence (totals, unparsed-payload line, caveat) byte-identical before and after. The cancelled gloss ("the call ended before it was answered") was witnessed on the cancel-timer capture across two boxes.Explainwas confirmed untouched, still surfacing the gap context in its fuller register. The remaining gloss variants (busy, one-way RTP and RTP-absent on answered calls, and no-INVITE) are reasoned and code-covered but not yet witnessed, as no available capture triggers them.php -l Tools/PcapAnalysis.phpandphp -l Frogman.class.phpclean.test|evil.pcapfilename: the file and path remained visible, no clickable command was emitted, and normal Sysadmin captures remained clickable.fm_list_pcapswas checked for newest-first ordering with Sysadmin captures sorted by the timestamp parsed from thesysadmin_YYYY_MM_DD_HH_MM_SSfilename (falling back tofilemtime()when the name does not parse), the displayed timestamp matching the one used for sorting, deduplication by resolved path, and strictlimitvalidation.php -l Tools/ListPcaps.phpclean.php -l Tools/PcapAnalysis.phpand full repository PHP lint clean. This path is confirmed by code review and uses the same deduplication pattern as the witnessed SIP message cap, but was not exercised against a live capture because the available SysAdmin Pro captures are UDP-only and therefore do not enter the SIP-over-TCP reassembly path.What's deliberately not in this PR
Also fixed
Fixed chat scroll positioning for long bot responses. New assistant replies now settle at the start of the response, with Mermaid rendering allowed to complete first and manual user scrolling cancelling the pending auto-scroll.
Files changed
Tools/PcapAnalysis.php(new)Tools/ListPcaps.php(new)Frogman.class.php(formatter cases forfm_analyze_pcapandfm_list_pcaps)Tools/ChatParser.php(routing patterns for analyse/analyze and the pcap-action chips)assets/js/chat.js(scroll-to-start behaviour for bot responses; see Also fixed)