██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗███████╗██████╗
██╔══██╗██╔══██╗██╔═══██╗██║ ██║██╔════╝██╔════╝██╔══██╗
██████╔╝██████╔╝██║ ██║██║ █╗ ██║███████╗█████╗ ██████╔╝
██╔══██╗██╔══██╗██║ ██║██║███╗██║╚════██║██╔══╝ ██╔══██╗
██████╔╝██║ ██║╚██████╔╝╚███╔███╔╝███████║███████╗██║ ██║
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝ ╚══════╝╚══════╝╚═╝ ╚═╝
█████╗ ██╗ ██╗████████╗ ██████╗ ██████╗ ███████╗██╗ ██╗
██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝╚██╗ ██╔╝
███████║██║ ██║ ██║ ██║ ██║██████╔╝███████╗ ╚████╔╝
██╔══██║██║ ██║ ██║ ██║ ██║██╔═══╝ ╚════██║ ╚██╔╝
██║ ██║╚██████╔╝ ██║ ╚██████╔╝██║ ███████║ ██║
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═╝
Offline forensic analyzer for Chromium and Gecko browser artifacts, built for DFIR work. It reads copied browser profiles, builds one timeline, recovers deleted history from the SQLite WAL, flags suspicious activity against an IOC list, and writes HTML, CSV and JSON reports.
The tool makes no network calls. It uses no external API and no live capture.
- What it does
- The report
- Install
- Quick start
- Demo with sample data
- Usage
- IOC configuration
- Deleted history recovery
- Forensic soundness
- Limitations
- Project structure
- Testing
The pipeline has five steps:
- Extract. Read seven artifact types from each browser profile.
- Unify. Convert every record to one event format with a UTC timestamp.
- Recover. Compare each history database with its WAL to find deleted visits (optional).
- Detect. Match events against a configurable IOC list.
- Report. Write a self-contained HTML report, two CSV files and one JSON document.
| Engine | Browsers |
|---|---|
| Chromium | Chrome, Edge, Brave, Opera, Vivaldi, Chromium, Yandex |
| Gecko | Firefox, Tor Browser, LibreWolf, Waterfox |
| Artifact | Chromium source | Gecko source | Event type |
|---|---|---|---|
| History | History |
places.sqlite |
*_visit |
| Downloads | History |
places.sqlite (annotations) |
*_download |
| Searches | URLs in History |
URLs in places.sqlite |
*_search |
| Cookies | Network/Cookies or Cookies |
cookies.sqlite |
*_cookie |
| Bookmarks | Bookmarks |
places.sqlite |
*_bookmark |
| Autofill | Web Data |
formhistory.sqlite |
*_form |
| Extensions | Preferences, Secure Preferences |
extensions.json |
*_extension |
The * stands for the browser label, for example chrome_visit or tor_download.
Notes on the artifacts:
- History gives one event for each visit, not one for each URL.
- Downloads show the final URL of the redirect chain and the local target path. The summary marks a download that did not finish, for example
[CANCELLED]. State, size and end time are indetails_jsonand inreport.json. - Search queries come from visited URLs of nine engines: Google, Bing, DuckDuckGo, Yahoo, YouTube, Ecosia, Brave, Startpage and Yandex.
- Cookies give metadata only. The tool does not read cookie values.
- Only
History(Chromium) orplaces.sqlite(Gecko) is mandatory. The tool skips other missing files with a warning.
| Rule | Severity | Trigger |
|---|---|---|
SUSPICIOUS_DOMAIN |
high | The URL of an event points to a domain on the IOC list |
DOUBLE_EXTENSION |
high | A download such as invoice.pdf.exe hides a risky extension |
SUSPICIOUS_EXTENSION |
medium | The extension of a downloaded file is on the IOC list |
SUSPICIOUS_KEYWORD |
medium | A search query contains a keyword from the IOC list |
The HTML report is one file with inline CSS and no JavaScript. It opens in any browser and prints to a clean A4 PDF.
The summary section shows a timeline strip for each browser and the activity for each hour of the day. Red dots mark events with an anomaly.
The anomaly table lists every hit with its rule, reason and the IOC entry that matched.
The full timeline highlights flagged rows. A visit that the user deleted, but
that is still in the main database image, has the tag DELETED_PENDING.
The screenshots show the demo case from this repository. The file
report_demo.html in the repository root contains the same report.
The --report flag selects the files that land in --output-dir:
| File | Content |
|---|---|
report.html |
Source files with SHA-256, summary, charts, anomaly table and full timeline |
timeline.csv |
One row for each event, with flat url, query and filename columns and a details_json column |
anomalies.csv |
One row for each anomaly, with the context of its event |
report.json |
Case metadata, source files, full timeline and anomalies in one document |
The CSV files use UTF-8 with BOM. Excel on Windows then reads non-ASCII text correctly.
A cell that starts with =, +, - or @ gets a leading apostrophe. A spreadsheet
then shows the cell as text and does not run it as a formula.
The tool needs Python 3.10 or newer. CI runs Python 3.10, 3.11, 3.12 and 3.13 on Linux, Windows and macOS.
With uv:
uv sync --extra devWith pip:
pip install -r requirements.txtpython main.py --chrome-profile /path/to/chrome/Default --case-id INC-2024-001The reports land in ./output/. Open output/report.html in a browser.
The repository contains synthetic artifacts for the case INC-2026-03-14. An
employee researches offensive tools, visits flagged domains, downloads
suspicious files and then deletes one history entry. That entry is still
recoverable from the WAL.
The samples hold three profiles: Chrome, Firefox and Tor Browser. The Tor profile has no history, because Tor Browser does not store it. It has two bookmarks to onion sites and one extension.
-
Run the tool on the sample directory:
python main.py \ --input samples \ --wal-recover \ --case-id INC-2026-03-14 \ --report all \ --output-dir output/demo -
Open
output/demo/report.html.
The run finds the three profiles and gives 58 events and 13 anomalies. It
prints two warnings, because the Tor profile has no cookie and no form
history file. To build the sample artifacts again, run
python samples/generate.py.
Each run of the generator writes a new History-wal, because SQLite puts
random salt values in the WAL header. The hash of that file then changes, so
build report_demo.html again. The other sample files stay identical.
python main.py \
--input /evidence/seized_laptop \
--output-dir /cases/INC-2024-001/reports \
--case-id INC-2024-001 \
--ioc-file /cases/INC-2024-001/custom_iocs.yaml \
--wal-recover \
--start 2024-01-15T22:00:00 \
--end 2024-01-16T06:00:00 \
--report all| Flag | Default | Description |
|---|---|---|
--input |
none | Evidence directory. The tool finds every Chromium and Gecko profile below it |
--chrome-profile |
none | Path to one Chromium profile directory |
--chrome-browser |
chrome |
Label for that profile: chrome, edge, brave, opera, vivaldi, chromium, yandex |
--firefox-profile |
none | Path to one Gecko profile directory |
--firefox-browser |
firefox |
Label for that profile: firefox, tor, librewolf, waterfox |
--wal-recover |
off | Recover deleted history visits from the SQLite WAL |
--output-dir |
./output |
Directory for the reports |
--report |
both |
html, csv, json, both (HTML and CSV) or all |
--case-id |
UNSPECIFIED |
Case identifier in the report header |
--ioc-file |
config/iocs.yaml |
Path to the IOC file |
--start |
none | Earliest event to include, in ISO 8601 |
--end |
none | Latest event to include, in ISO 8601 |
At least one of --input, --chrome-profile and --firefox-profile is mandatory.
You can combine them.
Notes on the time filter:
- The tool reads a value without a time zone as UTC.
- Both limits are inclusive.
- The filter removes events that have no timestamp.
- A value with a date only means 00:00:00 of that day. To include the full last day, write
--end 2024-01-16T23:59:59.
With --input, the tool scans the directory at any depth. A directory with a
History file is a Chromium profile. A directory with places.sqlite is a
Gecko profile.
The tool reads the browser label from the path of the profile. For example,
Microsoft/Edge/User Data/Default gives edge. A path with no known browser
name gives chrome or firefox.
The tool skips a profile that it cannot read and prints a warning to stderr. The analysis of the other profiles continues.
| OS | Chrome | Firefox |
|---|---|---|
| Windows | %LOCALAPPDATA%\Google\Chrome\User Data\Default |
%APPDATA%\Mozilla\Firefox\Profiles\<id>.default-release |
| macOS | ~/Library/Application Support/Google/Chrome/Default |
~/Library/Application Support/Firefox/Profiles/<id>.default |
| Linux | ~/.config/google-chrome/Default |
~/.mozilla/firefox/<id>.default-release |
Work on a copy of the profile. On Windows, a browser that is open locks its databases, so close it before you copy the files.
| Code | Meaning |
|---|---|
0 |
Success. This includes a run that skipped some profiles found by --input |
1 |
Runtime failure: unreadable profile given by flag, no readable profile, missing or invalid IOC file |
2 |
Argument error: no profile, invalid date, --start later than --end |
The default file config/iocs.yaml contains starter content: Tor hidden
services, paste sites, anonymous file sharing, risky file extensions and
offensive security keywords. Use --ioc-file to give a different file for an
investigation:
suspicious_domains:
- "*.onion" # wildcard: any domain that ends with .onion
- pastebin.com # exact match: sub.pastebin.com does not match
- cdn.discordapp.com
suspicious_extensions:
- .exe
- .ps1
- .hta
suspicious_keywords:
- mimikatz
- "bypass uac"
- "disable defender"Matching rules:
- All matching ignores letter case.
- A domain entry matches the exact domain. An entry that starts with
*.matches every domain with that suffix. - The tool removes a leading
www.and a trailing dot from the URL and from the IOC entry before it compares domains. - The leading dot of an extension is optional:
exeand.exeare equal. - A keyword matches when the search query contains it.
A SQLite database in WAL mode consists of two files. The main file holds the
state at the last checkpoint. The -wal file holds later changes that SQLite
has not yet merged.
A copied profile freezes both files. With --wal-recover, the tool reads the
history twice, with the WAL and without it, and compares the two results:
| Tag | Meaning |
|---|---|
DELETED_PENDING |
The visit is in the main file, but the WAL deletes it. The user deleted it after the last checkpoint |
A visit that is only in the WAL is a normal visit. The timeline lists it once,
as a *_visit event.
The tool identifies a visit by its URL and timestamp. A change of the page title alone gives no result.
A deleted visit to a search URL also gives a *_search_recovered event. The
keyword rule checks this event.
This method compares two valid database states. It does not carve pages. It cannot recover a record after SQLite merges the WAL and reuses the pages.
- Read-only access. The tool copies each database to a temporary directory and opens the copy. It does not open the original for writing.
- Chain of custody. The tool calculates the SHA-256 of each source file before it reads the file. Every event carries the hash of its source. The report also lists the
-walfile of a database with its own hash. - Offline operation. The tool makes no network calls.
- UTC timestamps. The tool converts every browser timestamp to UTC.
- Escaped output. The HTML report escapes all text from the artifacts.
- The tool does not read cookie values. Chromium encrypts them with a key from the operating system.
- The tool calculates the hash of a
-walfile after it reads the database, not before. - The WAL recovery finds only records that SQLite has not yet merged into the main file.
- The browser label from
--inputcomes from the directory path. Use--chrome-browseror--firefox-browserwith an explicit profile to set it. - A domain entry without a wildcard does not match subdomains.
- The report does not list the profiles that the tool skipped. Read stderr for that.
- The report lists a source file only when the file gives at least one event. A time filter does not remove a file from the list.
- With
--input, the tool does not follow directory links. It prints a warning for each link.
Browser-Autopsy/
├── main.py CLI entry point
├── pyproject.toml
├── requirements.txt
├── report_demo.html rendered report of the demo case
│
├── config/
│ └── iocs.yaml default IOC file
│
├── extractors/ read raw browser artifacts
│ ├── base.py shared helpers and dataclasses
│ ├── chrome.py Chromium parser
│ ├── firefox.py Gecko parser
│ └── discovery.py profile discovery for --input
│
├── analyzers/ process the extracted data
│ ├── timeline.py one chronological timeline
│ ├── anomaly.py IOC detection
│ └── wal.py deleted history recovery from the WAL
│
├── reporters/ write the output
│ ├── html.py HTML report
│ ├── csv.py CSV export
│ ├── json.py JSON export
│ └── templates/
│ └── report.html report template
│
├── samples/ synthetic artifacts for the demo case
├── docs/img/ screenshots for this README
└── tests/ pytest suite
The pytest suite contains more than 500 tests. Most extractor tests build a real SQLite database with the schema of the browser and read it back.
| Area | Test modules |
|---|---|
| Extractors | test_base, test_open_db, test_chrome_*, test_firefox_* |
| Discovery | test_discovery |
| Analyzers | test_timeline, test_timeline_artifacts, test_anomaly, test_wal_recovery |
| Reporters | test_html_reporter, test_csv_reporter, test_json_reporter |
| CLI | test_main |
pytest # full suite
pytest tests/test_timeline.py # one module
pytest -v -k chrome # tests with "chrome" in the nameWith uv, put uv run before each command.



