Skip to content
hellstationPublic

About

Offline email triage + passive infrastructure OSINT in one local web UI. No link fetching on mail analysis.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Tracework

Tracework is a local investigation workbench that runs MailTrace and InfraChronicle in one process:

Module Role
MailTrace Offline triage of suspicious .eml files (headers, URLs, attachments, IOC export).
InfraChronicle Passive infrastructure OSINT (DNS, Certificate Transparency, RDAP), SQLite observation store, graph, and timeline.

The Python package is still named mailtrace on PyPI/install paths; the product UI is Tracework.

mailtrace serve

Opens http://127.0.0.1:8000/ — hub with MailTrace (/email/) and InfraChronicle (/infra). Inside each module, Tracework in the corner returns to the hub. MailTrace CLI commands analyze, ioc, and dns-policy work without the web UI.

MailTrace does not open links, execute attachments, unpack archives, or upload the message during analysis. Missing SPF, DKIM, or DMARC headers are recorded as missing context, not proof of phishing. InfraChronicle uses allowlisted providers only (no port scanning).

Install

Python 3.12 or newer:

python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

mailtrace is then on PATH. The dev extra installs pytest, Ruff, and mypy.

Web UI

Docker

docker compose up -d --build
docker compose logs -f tracework

Open http://127.0.0.1:8000/. Investigation data lives in the tracework_data volume (DATABASE_URL and DATA_DIR in docker-compose.yml).

Local

source .venv/bin/activate
mailtrace serve
mailtrace serve --host 0.0.0.0 --port 8000   # LAN only; trusted networks

Use TLS, authentication, and network isolation before exposing the UI beyond a lab.

Optional settings: copy .env.example to .env (DATABASE_URL, DATA_DIR, rate limits, optional InfraChronicle API keys).

InfraChronicle in the browser

From the hub, open InfraChronicle to run investigations, browse the relationship graph, timeline, observations, and HTML reports. Data is stored in SQLite (migrations via Alembic on startup). Demo data is available from the overview when the database is empty.

Analyze a message (CLI)

mailtrace analyze suspicious.eml
mailtrace analyze suspicious.eml --format json
mailtrace analyze suspicious.eml --output report.md
mailtrace analyze suspicious.eml --extract-attachments ./attachments
mailtrace ioc suspicious.eml --format json

analyze prints a terminal summary. --format accepts text, json, markdown, or html. If you pass --output and omit --format, the suffix chooses the format (.md, .json, .html, .txt). An explicit --format wins. JSON is written to stdout only when you do not pass --output.

--fail-on info|low|medium|high exits with code 4 when any observation is at that severity or higher. The analysis itself still completes.

ioc prints deduplicated indicators. The default format is soc: a tab-separated list for handoff. The value, and any URL in the context column, are defanged (hxxps, dots as [.], IPv6 colons as [:]) so the lines are not links. json and csv keep the original indicator values. Attachment hashes are not rewritten.

Exit codes:

Code Meaning
0 The command finished.
2 The file, format, or config could not be used, or dns-policy was started without --confirm-network.
4 --fail-on matched an observation.
1 An unexpected error.

Use - as the path to read a message from stdin. mailtrace --help and mailtrace analyze --help describe the flags.

Optional limits live in a TOML file. See examples/mailtrace.toml. The file can lower or raise limits inside hard ceilings. It cannot enable network access.

mailtrace analyze suspicious.eml --config examples/mailtrace.toml

Demo report

This report was produced locally with:

mailtrace analyze tests/fixtures/suspicious_reply_to.eml --format markdown

The same text is in examples/demo-report.md. The Generated timestamp is the time of that run. Re-running the command refreshes it. The attention score is 20 because the display-name mismatch is weighted higher than the Reply-To and Return-Path differences. The missing authentication header adds no points.

The fixture is a synthetic message: the visible From name contains security@paypal.com, the address is attacker@evil.example, Reply-To is phish@collector.example, and Return-Path is bounces@other.example.

# MailTrace report

Local triage only. MailTrace did not contact remote hosts or open attachments. This is not a verdict.

- Source: `suspicious_reply_to.eml`
- Generated: `2026-10-20T11:06:57.532744+00:00`
- Attention score: 20/100
- The attention score ranks how much review the recorded observations suggest. It is not a probability and not a verdict.

## Message

- Subject: `Mailbox storage`
- From: `PayPal <security@paypal.com> <attacker@evil.example>`
- Reply-To: `phish@collector.example`
- Return-Path: `bounces@other.example`
- To: `Bob Example <bob@example.com>`
- Date: `2024-10-15T11:00:00+00:00`
- Message-ID: `<rt-001@evil.example>`
- MIME parts analyzed: 1

## Observations

### low / low / inconsistency — Reply-To domain differs from From

`header.reply_to_domain`

A Reply-To domain is not the From domain or a subdomain of it. Mailing lists, shared mailboxes, and third-party senders do this often. It is a lead for the analyst, not evidence that the message is harmful.

### medium / medium / inconsistency — Display name contains a different address

`header.display_name_address`

The visible From name contains an email address that is not the actual address. Recipients often read the name and skip the address. This is a stronger lead than a Reply-To difference, and it is still not proof.

### info / high / absence — No recorded SPF, DKIM, or DMARC result

`auth.missing_results`

No Authentication-Results or Received-SPF header was found. Absence of these headers is not evidence of phishing.

## Limitations

MailTrace did not independently verify SPF, DKIM, or DMARC, did not connect to any URL, did not open attachments, and did not render HTML in a browser. Observations are heuristics, not a verdict.

The full export also lists Return-Path, indicators, score contributions, and analyst recommendations.

What the report separates

Each observation has:

  • Severity (info, low, medium, high): how much handling attention the artifact suggests.
  • Confidence (low, medium, high): how stable the signal is. Clock skew and forwarding stay at low confidence.
  • Kind: artifact (something is present), absence (a header was not present), or inconsistency (two fields disagree).
  • Evidence: the header, URL, or attachment field the rule used. Message bodies, script bodies, and archive members are not copied into the report.

The attention score sums weighted observations and stops at 100. Info observations add nothing. The score is not a probability and not a verdict.

HTML output escapes message text, sends Content-Security-Policy: default-src 'none', and does not turn extracted URLs into links.

Rules

Rules live in mailtrace.rules and are registered in one list. Pass another RuleSet to analyze_bytes to replace or extend them. Built-in rules:

Rule Usual severity What it records
header.reply_to_domain low, or info if the domain is a parent or subdomain Reply-To is not the From domain. Not proof of harm.
header.return_path_domain low, or info when related Return-Path domain differs from From. Common for bulk mail and forwarding.
header.display_name_address medium The From display name contains a different email address.
header.message_id_domain info Message-ID domain is unrelated to From. Often just the sending host.
header.received_time_inversion low A nearer Received hop is dated more than 24 hours earlier than the hop below it.
header.invisible_characters medium for bidi controls, otherwise low Bidi or zero-width characters were removed from displayed text.
auth.recorded_failure medium for fail, low for softfail and errors A header records a non-pass SPF, DKIM, or DMARC result. MailTrace did not repeat the check.
auth.missing_results info No Authentication-Results or Received-SPF header. Not evidence of phishing.
auth.conflicting_results medium The same method has more than one recorded result. Compare authserv-id with the gateway you trust.
auth.alignment low A recorded DKIM or SPF domain does not match From, and that scope has no recorded dmarc=pass. Not an independent DMARC evaluation.
auth.signature_unverified info A DKIM-Signature header is present and no recorded DKIM result was found. The signature was not checked.
url.ip_literal low A URL host is an IP address.
url.obfuscation medium or low Userinfo, an encoded IP, a backslash, odd percent-encoding, or whitespace. A scheme-relative URL alone does not match.
url.idn medium when scripts mix or characters are confusable, otherwise info Punycode or mixed-script host. There is no brand list.
url.href_mismatch medium Visible link text looks like a URL whose host is not the href host.
url.redirect_parameter low A query parameter looks like a redirect target. It is decoded locally and not fetched.
url.dangerous_scheme medium javascript:, data:, or vbscript:.
url.suspicious_host low or info Many labels, or a keyword such as login under the registrable-looking tail. No public-suffix list.
attachment.mime_mismatch medium Extension and a specific MIME type disagree. application/octet-stream is not a mismatch.
attachment.dangerous_type medium, or low for HTML-only Executable, macro, disk-image, or HTML attachment type. The bytes were not run.
attachment.archive_not_inspected low Name, type, or file header looks like an archive. It was not unpacked.
attachment.double_extension high when the final extension can carry active content, otherwise medium A document-style extension is followed by another extension.
attachment.content_mismatch high when the header looks executable, otherwise medium The first bytes do not match the extension. Only the prefix is inspected.
html.script medium A script tag is present. The script body is not copied.
html.event_handler low An event-handler attribute is present. Its value is not copied.
html.active_content low A form, iframe, object, or meta refresh is present.
html.hidden_content info CSS hides content. Weak context; marketing mail does this often.
structure.parse_defects low The parser recorded MIME or header defects.
structure.nested_message info A nested message was parsed, within the depth limit.

High severity marks a handling fact, such as a double extension. It does not mean the bytes were shown to be harmful.

Indicators

Indicators are domains, email addresses, IP addresses, URLs, and SHA-256 hashes. Each one records where it was seen: a header, the body text, or an attachment. Values are deduplicated. URL indicators keep their scheme and are not lowercased as a group with the other types; domains and addresses are stored in lowercase. The SOC export defangs URL, domain, address, and IP values at export time, including URLs that appear in the context column. IPv6 colons are rewritten as [:]. JSON and CSV keep the original values.

Optional DNS

Published SPF, DMARC, and DKIM TXT records are a separate command and require an explicit flag:

mailtrace dns-policy suspicious.eml --confirm-network
mailtrace dns-policy suspicious.eml --confirm-network --format json

Without --confirm-network the command exits 2 and does not query. Lookups use the system resolver through dnspython, only for sender-authentication names, and they do not evaluate the policy. The names are disclosed to whatever resolver the host uses. URL hosts are not queried.

Limits

Defaults, which a config file may change up to a hard ceiling:

Limit Default
Message size 25 MB (ceiling 100 MB)
MIME parts 300
MIME depth 30
Nested messages 5
Attachment size 20 MB
URLs 1,000
Header value length 100,000 characters
Headers stored 500
Text per part 2,000,000 characters

Limitations

  • Recorded authentication results are not an independent check. A sender can prepend a fake Authentication-Results header. Compare the authserv-id with the gateway you trust.
  • URLs are not contacted, so redirects, URL shorteners, and live pages are not followed beyond one locally decoded redirect parameter.
  • Archives, disk images, and nested files inside them are not opened.
  • HTML is not rendered, so script that depends on a browser does not run, and some obfuscation that a browser would decode can be missed.
  • Host checks do not use a public-suffix list or a brand dictionary.
  • Content in images, encrypted parts, and unknown encodings may be invisible to the parser.
  • Bidi and zero-width characters are stripped from displayed fields after they are recorded.

Tests

Synthetic fixtures in tests/fixtures/ cover mail parsing and rules; tests/infrachronicle/ covers providers, API, and infra UI routes. Tests do not use the network except where explicitly mocked.

ruff check .
ruff format --check .
mypy src
pytest

GitHub Actions runs those commands on Python 3.12 and 3.13.

Repository layout

src/mailtrace/          CLI, parser, rules, reports, /email web UI
src/mailtrace/platform/ Tracework hub and FastAPI assembly
src/infrachronicle/     Providers, SQLite models, /api, /infra UI
alembic/                InfraChronicle schema migrations

Shared styling: src/infrachronicle/static/css/workbench.css (served as /static/css/workbench.css). Mail-specific assets: /static/mail/.

Network paths: optional mailtrace dns-policy (explicit flag) and InfraChronicle collectors (allowlisted HTTP/DNS only).

See SECURITY.md for MailTrace handling rules and web exposure notes.

About

Offline email triage + passive infrastructure OSINT in one local web UI. No link fetching on mail analysis.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages