Hand it a capture. It scores it, tells you who talked to whom, what crypto they negotiated, what is weak or leaking, and how to fix it.
pcap-doctor is an offline doctor for .pcap and .pcapng files, built for IoT, OTA and
network-device traffic. One command gives a 0-100 health score, findings grouped by category, and
six report files you can paste into a ticket. Every claim points at a frame number you can open in
Wireshark. It gates CI on new findings only, and hands a finding to Claude Code, Codex or Cursor
with a fix prompt when you want help.
No network access during analysis. No telemetry. Secrets it finds are reported, never repeated.
(pf still works as a short alias of pcap-doctor.)
Get started
1. Install
You need tshark on PATH (and, optionally,
openssl to read certificates exactly):
brew install wireshark # macOS
sudo apt-get install -y tshark # Debian / Ubuntu
Then run pcap-doctor without installing it, or install it:
uvx pcap-doctor analyze traffic.pcap # Python, via uv
npx pcap-doctor analyze traffic.pcap # Node launcher: runs the same version through uvx or pipx
pip install pcap-doctor # or pipx install pcap-doctor
pcap-doctor doctor checks that tshark works and knows the fields the detectors need.
2. Run your first audit
pcap-doctor analyze traffic.pcap
capture traffic.pcap: 5 packets, 1 flows, 2 hosts
Score 68/100 · Needs work
Crypto 4 finding(s) · worst high
high TLS_CIPHER_WEAK TLS_RSA_WITH_AES_128_CBC_SHA negotiated on tcp:10.0.0.10:40000<->10.0.0.20:443
high TLS_NO_FORWARD_SECRECY No forward secrecy on tcp:10.0.0.10:40000<->10.0.0.20:443
high TLS_VERSION_DEPRECATED TLS 1.0 negotiated on tcp:10.0.0.10:40000<->10.0.0.20:443
+1 more (--verbose shows all)
wrote traffic.pf-report/01-flows.md
...
The full report lands in traffic.pf-report/ (--out DIR to choose); start with index.md. No
capture to hand? make captures fetches the Wireshark project's public samples.
3. Understand a finding
pcap-doctor why 4 # every finding that cites frame 4
pcap-doctor why d1.tls_cipher.TLS_CIPHER_WEAK # or an id prefix: evidence, fix, references, rule text
pcap-doctor rules explain TLS_CIPHER_WEAK # what a code means, why it matters, how to fix and verify
pcap-doctor rules list --category Crypto # the whole catalog, by category
4. Gate CI on new findings
pcap-doctor ci install # writes .github/workflows/pcap-doctor.yml
On every pull request that touches a capture, the workflow compares each capture with its copy on
the base branch, comments one summary on the PR, uploads SARIF to code scanning, and fails only when
a new finding reaches --fail-on (default high). See CI.
5. Review it, or fix it with an AI agent
In a terminal, a scan ends on an interactive screen, for security testers and developers alike:
┌─────┐ 68 / 100 Needs work · device-boot.pcap
│ o o │ ██████████████████████████████████░░░░░░░░░░░░░░░░
│ ▭ │ pcap-doctor 0.3.0 · 1,204 packets, 18 flows, 6 hosts
└─────┘
Potential score 95 after priority fixes +27
❯ Review 7 finding(s)
› Add to GitHub Actions (Recommended)
› Hand off to an agent
↑/↓ move · enter select · q quit
Review lists the findings by category and shows the selected one's impact, evidence frames, fix,
references and rule guide; enter copies it as ticket-ready text, h hands it to an agent.
Hand off launches Claude Code, Codex or Cursor with a fix prompt, or copies or shows it. Read the
AI mode warning first: launched agents skip their approval prompts unless you pass
--safe. Piped output, files, CI, --json, -q and --score never show these screens.
pcap-doctor install # teach Claude Code, Cursor and Codex how to use pcap-doctor in this repo
6. Configure it
# pcap-doctor.toml (or [tool.pcap-doctor] in pyproject.toml)
profile = "ota" # a preset: keep Crypto/Credentials/Cleartext/DNS, fail on high
disable = ["DNS_EXTERNAL_RESOLVER"]
[severity]
TLS_CERT_EXPIRING = "low"
[[allow]]
code = "TLS_CIPHER_WEAK"
subject = "10.0.0.20*"
reason = "legacy appliance, replacement tracked in OPS-12"
See Configuration.
Table of contents
- What it catches
- The score
- The report
- Configuration
- CI
- AI mode
- Live capture
- Detectors
- Architecture
- How a finding is born
- Cipher policy
- Trust and honesty rules
- CLI reference
- Privacy and telemetry
- Testing
- Contributing
- Known limits
- License
What it catches
Findings are grouped into eight categories, the same ones --category, the summary and the
config use.
| Category | What it covers |
|---|---|
| Crypto | weak and deprecated TLS/DTLS: prohibited and legacy suites, deprecated versions, missing forward secrecy, weak suites a client still offers, expired or weak certificates, self-signed chains, alerts and truncated handshakes, JA3 fleet clustering |
| Credentials | HTTP Basic/Digest, SNMP communities, FTP PASS, LDAP simple binds, MySQL queries and Telnet logins (always redacted to <redacted N chars>), cookies without Secure |
| Cleartext | plain HTTP (high when the request path looks like a firmware or package download), cleartext FTP, Telnet, TFTP, NTP, SNMP, LDAP, SMTP, POP and MySQL |
| DNS | plaintext DNS leakage, external resolvers over IPv4 and IPv6, DNS tunnelling shapes |
| SSH & QUIC | weak SSH key exchange, cipher, MAC and host-key offers; Terrapin exposure (CVE-2023-48795); QUIC version inventory and payload opacity |
| Voice | cleartext SIP signalling, SDP offered without a=crypto, RTP media in the clear, media volume anomalies (the SIP call graph is drawn in 04-diagrams.md) |
| Network | unanswered-SYN scans, beaconing measured by regular gaps between bursts, services on non-standard ports |
| Other | anything a detector reports that the catalog does not know yet (a test keeps this empty) |
The distinction that matters is negotiated versus offered. A TLS 1.3 session is fine even if the same ClientHello still offers 3DES for TLS 1.2; that is reported as downgrade surface, not as a weakness of the session.
The score
The score is a local, deterministic function of the findings: 100 - Σ penalty, clamped to 0-100.
- Each distinct finding code counts once, at its worst severity, so one noisy flow cannot sink the score on its own.
- Penalty by severity: critical 20, high 10, medium 5, low 2, info 0.
- Multiplied by confidence: high 1.0, medium 0.75, low 0.5.
- Labels: Great ≥ 90, Good ≥ 75, Needs work ≥ 50, Critical below.
--score prints only the number, for scripts. With --baseline, --score and the console summary
cover the new findings only; the JSON envelope keeps the full report's score and lists new_findings.
The report
Every run writes six files, meant to be read in order and pasted into a ticket.
| Question | Where the answer lands |
|---|---|
| What communication is happening? | 01-flows.md: every conversation with bytes, duration, protocol and an encrypted/cleartext verdict |
| Which cipher is used between this sender and this recipient? | 02-ciphers.md: per-flow matrix of version, chosen suite, offered count, forward secrecy, SNI, certificate |
| Is that cipher weak? | 03-findings.md: every weak, prohibited or deprecated suite, with the frame it was seen in |
| What else is wrong? | 03-findings.md: credentials, cookies, SIP/RTP, DNS, SSH, scans, beaconing, certificates |
| What do I do about it? | every finding carries a remediation and, where one exists, an RFC, CWE or CVE reference |
graph LR
A["index.md<br/>navigation + summary"] --> B["01-flows.md<br/>who talks to whom"]
A --> C["02-ciphers.md<br/>negotiated crypto"]
A --> D["03-findings.md<br/>issues + evidence"]
A --> E["04-diagrams.md<br/>Mermaid views"]
A --> F["report.json<br/>machine readable"]
B --> D
C --> D
C --> E
style A fill:#1f2933,color:#fff
style D fill:#7f1d1d,color:#fff
style F fill:#14532d,color:#fff
| File | Read it for |
|---|---|
index.md |
capture identity (path, SHA-256, size, window), packet/flow/host counts, severity histogram, notes, cipher-policy provenance |
01-flows.md |
host table, every conversation with orientation, bytes, duration, crypto verdict and the evidence for that verdict, plus a cleartext-only section |
02-ciphers.md |
the matrix (sender → recipient), then per-session detail: version, record versions, chosen suite, forward secrecy, offered suites, SNI, ALPN, JA3/JA3S, certificate chain, alerts; also QUIC inventory and SSH algorithm offers |
03-findings.md |
triage table (worst first), then per-finding detail: severity, confidence, code, evidence table, remediation, references, tags |
04-diagrams.md |
topology graph, crypto matrix graph, severity pie, per-detector bar chart, RTP timeline, SIP sequence diagrams |
report.json |
the same data with a stable schema_version, for diffing between runs |
Machine-readable output, on top of the six files:
| Flag | Output |
|---|---|
--json |
only the JSON envelope on stdout: {tool, version, score, label, categories, report} (plus new_findings with --baseline) |
--json-out FILE |
the same envelope written to a file, with the normal console summary |
--sarif FILE |
SARIF 2.1.0 for code scanning: ruleId is the finding code, the fingerprint is the stable finding id |
--baseline OLD.json |
show and gate only findings whose id is not in an earlier report.json or envelope; the files on disk stay complete |
Sample finding
### 3. TLS_RSA_WITH_AES_128_CBC_SHA negotiated on tcp:10.0.0.1:41832<->10.0.0.243:443
- **Severity**: 🟠 high
- **Confidence**: high
- **Code**: `d1.tls_cipher.TLS_CIPHER_WEAK`
10.0.0.243:443 completed a handshake at TLS 1.2 with a static RSA key exchange
(RSA); no forward secrecy: a later compromise of the server private key decrypts
all recorded past traffic on this session.
| Frame | Field | Value |
|-------|----------------------|-----------------------------------|
| 20 | `tls.handshake.ciphersuite` | `TLS_RSA_WITH_AES_128_CBC_SHA` |
| 19 | `tls.handshake.ciphersuite[offered]` | `1 suite(s) offered by the client` |
Configuration
pcap-doctor reads pcap-doctor.toml in the current directory, else the [tool.pcap-doctor] table
of pyproject.toml; --config FILE points at another file. Every key is optional.
| Key | Meaning |
|---|---|
profile |
start from a preset; ota (firmware and OTA-update traffic) keeps Crypto, Credentials, Cleartext and DNS and sets fail_on = "high" |
categories |
keep only these categories (same as --category) |
fail_on |
default gate severity (same as --fail-on) |
disable |
codes that are never reported |
[severity] |
re-rate a code, e.g. TLS_CERT_EXPIRING = "low"; the finding id does not change |
[[allow]] |
accept one known finding: code, subject and/or flow_key (fnmatch patterns, all must match) and a required reason |
- Precedence: command-line flags win over the config, and the config wins over its profile
(
--profile NAMEreplaces the config's ownprofile). - Nothing disappears silently: every re-rated, disabled or allowed finding is counted in the report notes with the file and the reason.
- No silent typos: an unknown key, code, category, profile or severity exits
2before tshark runs, and lists the valid values.
CI
pcap-doctor ci install writes .github/workflows/pcap-doctor.yml:
on:
pull_request:
paths: ["**/*.pcap", "**/*.pcapng", ".github/workflows/pcap-doctor.yml"]
permissions:
contents: read
pull-requests: write # one summary comment, updated in place
security-events: write # SARIF upload to code scanning
jobs:
pcap-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Vaibhav91one/pcap-forensics@v0.3.0
with:
captures: "**/*.pcap **/*.pcapng"
fail-on: high
Options: --captures (bash globstar patterns), --fail-on, --ref (the action ref to pin),
--force (overwrite a workflow you edited; without it an edited file is kept and the exit code is
1).
The action (action.yml in this repository) installs tshark and pcap-doctor from its own ref, then,
for each matching capture:
- analyzes the base branch's copy of the capture, when there is one;
- analyzes the pull request's copy with
--baseline, so only new findings are shown and gated (a capture added by the PR counts all its findings as new); - writes a summary table to the job summary and to one PR comment that it updates on each push;
- uploads one merged SARIF run to code scanning (
upload-sarif: falseto skip; private repositories need GitHub Advanced Security); - fails the job when a new finding reaches
fail-on.
Anywhere else, the building blocks are plain flags:
pcap-doctor analyze main.pcap -o base -q
pcap-doctor analyze head.pcap -o head --baseline base/report.json --sarif head.sarif --fail-on high
AI mode
The interactive screens. They appear only after an interactive analyze: a terminal on both
ends, not in CI, not inside an agent, and not with --json, -q, --score or --no-handoff.
Everything else keeps its exact output.
- Review (↑/↓, enter, h, esc, q): findings grouped by category with
×Nper code and unread markers; the selected finding's impact, why, evidence frames, fix, references and rule guide. Enter copies it as ticket-ready text (pbcopy,wl-copy,xcliporclip). - Hand off to an agent, for the whole report or, from Review, one finding: pick an agent found on
PATH, or copy or show the prompt. The prompt carries tool and capture identity, the rule text, the fenced evidence and the task (find the configuration or code that produces this traffic, fix it at the source, verify with--baseline). It is always shown before a launch, with the skip-approvals warning. - Add to GitHub Actions writes the same workflow as
pcap-doctor ci install, at the repository root, after asking.
Inside an agent. When pcap-doctor runs inside Claude Code (CLAUDECODE), Codex
(CODEX_THREAD_ID, CODEX_SANDBOX), Cursor's agent (CURSOR_SANDBOX), or with
PCAP_DOCTOR_AGENT=1, it prints an Agent guidance block with pcap-doctor why <id> --prompt
commands instead of a menu, and never launches anything.
Agent guides. pcap-doctor install writes .claude/skills/pcap-doctor/SKILL.md,
.cursor/rules/pcap-doctor.mdc and a managed block in AGENTS.md (Codex), so agents in your repo
know how to run pcap-doctor and read its output safely. Re-running it is idempotent; --agent
picks one, --force overwrites a file you edited.
Live capture
pcap-doctor watch -i en0 # see interfaces with `dumpcap -D`
pcap-doctor watch -i eth0 --seconds 30 --files 20 --dir ./ring
watch runs dumpcap (or tshark) as a ring buffer, analyzes each file as soon as it is closed,
and prints every finding the first time its id appears, with the pcap-doctor why command to
explain it. Ctrl-C stops the capture, analyzes the last file and keeps the ring files and their
reports. Capturing needs the rights to open the interface (the access_bpf group from Wireshark's
ChmodBPF on macOS, the wireshark group on Linux, or root); without them watch exits 2 with
dumpcap's own error.
Detectors
One file, one detector, discovered automatically: pcapforensics/registry.py loads every
non-underscore module in detectors/, so adding one needs no registration.
| Id | Module | Owns |
|---|---|---|
d1.tls_cipher |
detectors/tls_cipher.py |
negotiated version, chosen suite, offered-but-unused weak suites, forward secrecy, certificate expiry/key/signature/self-signed, alerts, truncated handshakes, JA3 fleet clustering |
d2.transport_exposure |
detectors/transport_exposure.py |
plain HTTP (firmware-like downloads rated high; query strings never reported), HTTP Basic/Digest over cleartext, Basic inside TLS, cookies without Secure, cleartext FTP/Telnet/NTP/SNMP/LDAP/SMTP/POP/MySQL/TFTP, leaked credentials (SNMP community, FTP PASS, LDAP simple bind, MySQL queries, Telnet logins; always redacted), services on odd ports (lower-port side, low confidence), unanswered-SYN scan shapes, beaconing (regular gaps between bursts) |
d3.sip_rtp |
detectors/sip_rtp.py |
SIP call graph, cleartext REGISTER/INVITE with auth headers, SDP offered without a=crypto (no SRTP possible), RTP media in the clear, media volume anomalies |
d4.dns_quic_ssh |
detectors/dns_quic_ssh.py |
plaintext DNS leakage, DNS tunnelling heuristics (label depth + entropy + TXT/NULL volume), external resolvers (IPv4 and IPv6), QUIC version inventory and payload opacity, weak SSH kex/cipher/MAC/host-key offers, Terrapin exposure (CVE-2023-48795) |
Every code a detector can emit is listed in the rule catalog (rules.py, pcap-doctor rules list)
with its category and title, and explained in rule_docs/<CODE>.md.
graph TB
subgraph index["CaptureIndex (read-only, frozen after Phase 0)"]
flows[Flows]
tls[TLS/DTLS sessions]
http[HTTP exchanges]
dns[DNS queries]
sip[SIP messages + SDP]
rtp[RTP streams]
ssh[SSH sessions]
quic[QUIC sessions]
svc[Services + Telnet logins]
end
d1["D1 tls_cipher<br/>version, suite, PFS, certs, JA3"]
d2["D2 transport_exposure<br/>credentials, cookies, ports, scans, beacons"]
d3["D3 sip_rtp<br/>call graph, SIP auth, SRTP verdict"]
d4["D4 dns_quic_ssh<br/>DNS leak/tunnel, QUIC, SSH, Terrapin"]
flows & tls & http & dns & sip & rtp & ssh & quic --> d1
flows & tls & http & dns & svc --> d2
flows & sip & rtp --> d3
flows & dns & quic & ssh --> d4
d1 & d2 & d3 & d4 --> rep["Report + artifacts"]
style d1 fill:#1e3a5f,color:#fff
style d2 fill:#1e3a5f,color:#fff
style d3 fill:#1e3a5f,color:#fff
style d4 fill:#1e3a5f,color:#fff
style rep fill:#14532d,color:#fff
Architecture
flowchart TD
cap["capture.pcap"] --> runner["TsharkRunner<br/>subprocess tshark -T fields<br/>one pass per protocol family<br/>self-heals on field drift<br/>caches by sha256 + version"]
runner --> idx["IndexBuilder<br/>CaptureIndex"]
idx --> core["models.py (frozen)<br/>index.py (frozen)<br/>data_ciphers.py (frozen)"]
core --> dets["detectors/*"]
dets --> pol["config.py + policy.py<br/>re-rate, disable, allow,<br/>category and severity filters"]
pol --> rep["Report"]
reg["data/cipher_suites.json<br/>424 suites, names from tshark,<br/>weakness tags derived"] --> dets
cert["certificates.py<br/>openssl x509 enrichment"] --> core
rep --> md["render/markdown.py"]
rep --> mm["render/mermaid.py"]
md & mm --> files["6 artifacts"]
rep --> out["scoring.py, baseline.py, output.py<br/>score, new findings, JSON envelope, SARIF"]
out --> cli["cli/*: summary, why, handoff, CI"]
style core fill:#422006,color:#fff
style reg fill:#422006,color:#fff
style files fill:#14532d,color:#fff
Score, categories, the JSON envelope, SARIF and baselines are all computed around the report
from Finding.code and the stable Finding.id, so report.json keeps its own schema
(schema_version 1.4.0). The CLI is a package of self-registering command modules
(cli/<command>.py with a register(app)), so a new command is a new file.
Why tshark and not pyshark
pyshark was on the table. It is not used, for three reasons that cost real bugs to learn:
- Field names stay visible. Every field the analyzer uses is pinned against
tshark -G fields, and drift is recorded inreport.notes. When tshark renames something, the run degrades into a note instead of into a wrong answer. - Determinism. One process per pass,
-T fields, tab-separated, a documented aggregator character. Output is snapshot-testable. - Python 3.13+ support is not a coin flip. pyshark's last release predates it.
If you want pyshark anyway, it belongs behind the same source interface as a notebook adapter. It must never be on the critical path.
Self-healing tshark layer
Field names move between tshark releases, and one release may not know a protocol at all
(dtls.desegment_dtls_records does not exist in 4.6.6). The runner therefore:
- validates every requested field against
tshark -G fieldsand drops unknown ones, - validates every bare protocol token in a display filter against
tshark -G protocols, - only passes preferences the build actually has (
tshark -G defaultprefs), - retries with a fix-up if tshark still rejects a
-oflag, and records every drop.
tests/test_tshark_layer.py pins the fields the project cannot work without, including every
service field the index reads, so a tshark upgrade that breaks us fails CI instead of silently
returning fewer findings.
Certificates
tshark flattens X.509 into a bag of RDN strings with no subject/issuer split and no key size.
certificates.py takes the real route: tls.handshake.certificate (raw DER, one occurrence per
certificate) → PEM → openssl x509 -noout -text. Subject, issuer, validity, key size, signature
algorithm, SANs and self-signedness are exact. If openssl is missing, the tool falls back to the
flattened tshark values, marks name_source, and adds a note.
How a finding is born
sequenceDiagram
participant U as analyst
participant P as pipeline
participant I as CaptureIndex
participant D as detector
participant R as Report
U->>P: pcap-doctor analyze capture.pcap
P->>I: build (one tshark pass per protocol family, cached)
I-->>D: frozen read-only view
D->>D: decide, attach frame-numbered evidence
D-->>P: list[Finding]
Note over P: one detector raising is caught,<br/>the other detectors still run
P->>P: config (re-rate, disable, allow), then category and severity filters, all noted
P->>R: sort by severity, then confidence
R-->>U: score, grouped summary, 6 artifacts, exit code
A finding is only valid if it carries a stable id, a real frame number and a remediation. Tests enforce the mechanical parts: every finding on every fixture and corpus capture must cite a frame above 0, and every reference must be something a reader can look up.
Finding.id is detector.code.sha256(scope)[:16], so re-running the same capture produces the
same ids, two reports can be diffed, and --baseline knows what is new.
Cipher policy
The registry holds 424 suites. Names and ids come from tshark's own value table
(tshark -G values), vendored in data/cipher_names.tsv; everything else is derived from the IANA
name, which encodes key exchange, cipher, mode and MAC structurally.
This is deliberate. The first version hand-typed the common suites and 22 of the ids were wrong
(0x0008 was labelled with the suite that lives at 0x0009, and so on). A registry that lies
about which id is which is worse than no registry, so scripts/gen_cipher_suites.py derives
everything and tests/test_cipher_registry.py proves the registry agrees with tshark for all 424
entries.
| Tier | Meaning | Typical severity |
|---|---|---|
prohibited |
NULL, export-grade, RC4, RC2, DES, 3DES, IDEA, SEED, MD5, anonymous, CRC32, GOST | critical / high |
deprecated |
no forward secrecy (static RSA, bare PSK) | high |
legacy |
CBC mode or SHA-1 MAC, AEAD otherwise | medium |
acceptable |
AEAD with forward secrecy, TLS 1.2 | info |
recommended |
TLS 1.3 suites | info |
signalling |
SCSV and other non-cipher values | never reported |
The reasoning lives in docs/cipher-policy.md. Regenerate with
make regenerate (add --refresh to re-extract the name table from tshark).
Trust and honesty rules
These are the project's rules for itself, and tests enforce the mechanical ones:
- No silent passes. An unknown cipher id raises
TLS_CIPHER_UNKNOWNrather than being ignored. An unparseable certificate, or a field tshark does not have, produces a note. An unknown option, config key or code exits2with the valid values. - No invented facts. If subject/issuer cannot be separated, the field is
Noneandname_sourcesays where the data came from. If forward secrecy cannot be decided, the answer isNoneplus a reason, notFalse. - No secrets in output. Credentials are redacted before they reach a report or a prompt. Every fixture that carries a secret has a test that greps every artifact for it.
- Confidence is separate from severity. A high-severity, low-confidence item is a lead to investigate, and the report says so.
- Absence of findings is not safety.
03-findings.mdends with that reminder in every run. - One broken detector cannot sink a run. Detector exceptions are caught per detector and
surfaced in
report.notes. - Suppression is visible. Every finding hidden by a config, a profile or a filter is counted
in
report.notes.
CLI reference
pcap-doctor analyze CAPTURE [--out DIR] [--only ID]... [--category NAME]... [--min-severity LEVEL]
[--fail-on LEVEL] [--profile NAME] [--config FILE] [--baseline OLD.json]
[--json] [--json-out FILE] [--sarif FILE] [--score] [-v] [-q]
[--no-handoff] [--safe] [--no-cache]
pcap-doctor why QUERY [--report report.json] [--prompt] # QUERY: a frame number or an id prefix
pcap-doctor rules list [--category NAME]
pcap-doctor rules explain CODE
pcap-doctor install [--agent claude|cursor|codex]... [--force] [--dir DIR]
pcap-doctor ci install [--captures GLOBS] [--fail-on LEVEL] [--ref REF] [--force] [--dir DIR]
pcap-doctor watch -i IFACE [--seconds N] [--files K] [--dir DIR]
pcap-doctor flows CAPTURE [--top N]
pcap-doctor ciphers CAPTURE
pcap-doctor detectors | suites | doctor | schema
pcap-doctor --version
| Exit code | Meaning |
|---|---|
0 |
finished; nothing tripped the --fail-on gate |
1 |
a shown finding (new, with --baseline) is at or above the --fail-on severity; install / ci install kept a file you edited |
2 |
bad input or environment: an unknown option value, config key or code, an unreadable baseline, tshark missing, or watch could not capture |
--fail-on {none,critical,high,medium,low,info} defaults to the config's fail_on, else none:
report, do not fail. The tshark pass cache lives in PCAP_FORENSICS_CACHE (default
~/.cache/pcap-forensics) and is keyed by capture SHA-256, tshark version and pass arguments, so
editing a detector never re-runs tshark.
Privacy and telemetry
pcap-doctor collects nothing and makes no network calls during analysis. uvx and npx
download the package once, like any installer; make captures fetches public sample captures on
demand; the GitHub Action talks to GitHub only to comment and upload SARIF.
Your captures stay on your machine, but treat the reports as sensitive: they contain IP addresses,
hostnames, SNI names, usernames and certificate subjects. Passwords, community strings, tokens and
Authorization headers are replaced with <redacted N chars> and never written to any artifact or
prompt. When you hand a finding to an AI agent, that agent receives the prompt you previewed, and
from there your agent's own privacy terms apply.
Testing
make test # the full pytest suite
make verify # ruff + mypy --strict + pytest: exactly what CI runs
| File | What it protects |
|---|---|
tests/test_cipher_registry.py |
all 424 registry entries agree with tshark; tier and forward-secrecy expectations; unknown ids fail loudly |
tests/test_tshark_layer.py |
every field and protocol the project needs exists (including every service field the index reads); TLS/DTLS pass field sets stay in sync; cache behaviour; self-healing paths |
tests/test_detectors.py |
each synthetic fixture produces exactly the codes it is meant to, with frame-numbered evidence; secrets never leak; every finding on every capture cites a real frame and a well-formed reference |
tests/test_corpus.py |
real captures: assertions written against tshark -V output for the same file, e.g. that tls12-aes128ccm.pcap negotiates TLS 1.2 with 0xC0A4 and must not be reported as TLS 1.0 |
tests/test_rules_catalog.py, tests/test_prompts.py |
every code a detector can emit has a catalog entry and a rule doc; hostile evidence cannot break out of the prompt fence |
tests/test_baseline.py, tests/test_sarif.py, tests/test_config.py |
baselines by stable id, SARIF levels and fingerprints, config validation and visible suppression |
tests/test_handoff.py, tests/test_ci_install.py, tests/test_watch.py |
exact agent launch commands and safe mode, the CI action end to end in a temporary git repo, the ring-buffer watcher |
tests/test_docs.py |
every link and anchor in the docs resolves, and every documented command exists |
Fixtures are generated, not captured by hand. scripts/make_fixtures.py builds byte-exact packets
for the cleartext protocols and performs genuine OpenSSL handshakes through a logging proxy for TLS
(weak_tls.pcap is a real TLS 1.0 session with a real certificate). Hand-rolled handshake bytes
were tried first, and a single wrong length field made tshark report [Client Hello Fragment] and
the whole session vanish: exactly the kind of bug this suite exists to catch.
The regression corpus in captures/ is the Wireshark project's own test captures
(scripts/fetch_captures.sh). CI runs everything on every pull request, and runs this repository's
own GitHub Action against the fixtures whenever the action or the package changes.
Contributing
Issues and pull requests are welcome. Work is organised as one issue, one branch, one pull request, and every change ships with a fixture and a test that fails without it.
CONTRIBUTING.md: setup, ground rules and how to submit a pull requestAGENTS.md: the contract for automated agents working in isolationdocs/detector-authoring.md: writing a detectordocs/subagent-playbook.md: running a fleet of agents safelyCHANGELOG.md: what changed and when
From a clone: make setup (uv venv and an editable install with dev extras), then make verify.
The core (models.py, index.py, data_ciphers.py, tshark.py) is frozen: a detector that needs
a new field opens an issue instead of editing the model. A new finding code needs a RULES entry
and a rule_docs/<CODE>.md.
Known limits
Stated plainly, because a triage tool that hides its limits is worse than useless:
- Encrypted payloads are opaque. QUIC and TLS 1.3 application data cannot be read without a keylog. The tool says so rather than guessing.
- Certificate subject/issuer need openssl. Without it, only the flattened CN and validity dates are available, and the report says which source was used.
- Some heuristics are heuristics. DNS tunnelling and beaconing are scored on label depth,
entropy and inter-arrival regularity, and reported with
confidence: lowunless the shape is strong. They are leads, not verdicts. - No ground truth for "who is the server". Orientation uses port heuristics; a service on an odd port is reported as such instead of being silently trusted.
- Capture point matters. The tool sees the traffic it is given, and cannot know what the capture point did not record.
- Baselines match by finding id. If a detector changes a finding's title or scope, its old findings look new; pcap-doctor warns when a detector's version differs from the baseline's.
- The CI action splits
captureson spaces. Paths with spaces are not supported there. - Telnet logins are found by prompt matching. Typed keystrokes are rebuilt from the client
stream and a password is recognised after a
Password:prompt; logins without such a prompt are missed. The password itself is never stored, only its length and frame. - Redis is classified by port only. tshark has no RESP dissector, so Redis commands are not parsed and cannot be checked for secrets.
- JA3 clustering is v1. It groups by fingerprint, not by a maintained JA3 database.
License
MIT. The captures under captures/ belong to the Wireshark project (BSD-2-Clause) and
are fetched, not authored.
Metadata
Release files for pcap-doctor 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pcap_doctor-0.3.0.tar.gz | 429.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pcap_doctor-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 582.9 kB
Release files / pcap_doctor-0.3.0.tar.gz
| Download URL | pcap_doctor-0.3.0.tar.gz |
|---|---|
| Size | 429.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ae836689b0859f8d0480c90d0e1a8f0895ea8d892e40f26e4dfb47d821465165
|
|
BLAKE2b-256 checksum How to use checksums |
25fcacc3260ef7234f7b445630ed9267a8685c858f0f434c351e22993c57daac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logRelease files / pcap_doctor-0.3.0-py3-none-any.whl
| Download URL | pcap_doctor-0.3.0-py3-none-any.whl |
|---|---|
| Size | 153.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6a6247668cc227eb5dbda01fa29d68891bdeba30ee88fa8ade23a356171fa12f
|
|
BLAKE2b-256 checksum How to use checksums |
0e2ce2a556f021d4de9b9d94aff36ef53064475c85ca28b344a54a35f2285cb0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency log