Skip to content

Repository files navigation

GMACH

GMACH - cookie counter-intelligence

Local cookie-store counter-intelligence. Parses browser cookie databases, reconstructs what the ad-tech ecosystem can infer about you, and flags concrete threats - all fully offline, on your machine.

Named after the Building from Stanisław Lem's Memoirs Found in a Bathtub: an environment of total surveillance where every artifact may be a message and every message may be a cipher. The tool is named after the adversary it maps.

python gmach.py                        # scan Chrome / Edge / Brave / Firefox profiles
                                       # -> gmach_report.json, gmach_purge.json,
                                       #    gmach_snapshots/<run>.json
python gmach.py --install-timer launchd  # weekly snapshot job (written, not enabled)
open dashboard.html                    # drag & drop the report, or serve both via http.server

Requires Python 3.10+, standard library only. No report yet? The dashboard ships with a seeded synthetic-data preview.

A single scan tells you what is in the cookie store right now. The questions that matter for a tracking audit are longitudinal: has this tracker held the same identifier for months, did a cookie you deleted come back unchanged, is the footprint growing. Those need repeated runs, which is what the snapshot store is for - see Continuity below.

Usage, step by step

Prerequisites

  • Python 3.10 or newer, standard library only (python3 -V to check).
  • Optional, only if you want them: publicsuffix2 for exact eTLD+1, cryptography for Chromium value decryption on some platforms. Everything runs without them, with the reduced-capability paths noted in the output.
  • Close nothing. The tool copies the cookie databases before reading, so it works while browsers are open.

Step 1 - a first, safe scan

python3 gmach.py

This scans the default profile locations for Chrome, Edge, Brave and Firefox and writes three things next to the script:

  • gmach_report.json - the full report, drag it into dashboard.html.
  • gmach_purge.json - cleanup candidates (stale and tracker cookies).
  • gmach_snapshots/<timestamp>.json - the first snapshot (0600), the baseline for continuity.

No raw cookie values are written. Firefox values are read in memory to compute derived signals and then dropped; Chromium values are not read at all in this mode. This run is safe to do on any machine.

Step 2 - open the dashboard

open dashboard.html          # macOS; on Linux: xdg-open, or just open the file

Drag gmach_report.json onto the drop zone. Everything is local, no network call is made. If you prefer a served context so the report auto-loads:

python3 -m http.server 8000  # then visit http://localhost:8000/dashboard.html

Navigate with the tabs or Alt+1..6; / jumps to domain search.

Step 3 - decide whether you need cookie values

The default scan classifies Chromium cookies by name only, because it never reads their values. That is deliberately conservative, and it is why some findings come back marked name-only with low confidence: the tool is telling you it is guessing from the name. To turn those guesses into measured verdicts:

python3 gmach.py --decrypt

--decrypt reads Chromium values through your own login keychain (Keychain / DPAPI / libsecret) - it only works as you, on your machine, and asks the OS credential store for permission. This unlocks: JWT decoding, real token classification by value shape, PII scanning, entropy, and identifier continuity for Chromium. Firefox is plaintext and always fully analysed regardless.

On Chrome 127+ with App-Bound Encryption the key is unavailable by design; those values are skipped and reported as such rather than failing.

Step 4 - build continuity (the part that needs repetition)

One scan is a snapshot. Continuity - durable identifiers, respawns, footprint growth - needs at least two. Either run gmach.py again yourself over the coming weeks, or install a periodic job:

python3 gmach.py --install-timer app       # macOS, recommended: signed wrapper
python3 gmach.py --install-timer launchd    # macOS, bare interpreter
python3 gmach.py --install-timer systemd    # Linux user timer
python3 gmach.py --install-timer cron       # portable crontab line

Each writes the job definition to disk and prints how to enable it. Nothing is enabled automatically - turning the job on stays an explicit command you run. On macOS prefer app: it builds a signed GmachSnapshot.app so the scheduled process has its own identity instead of appearing as a bare python3 (see Scheduled runs and process identity).

Step 5 - read the pentest view

With --decrypt data present, the Pentest tab splits the analysis into two perspectives that read the same facts differently:

  • Attacker - what could be done with each token and the prerequisite for it (an XSS in the domain, a network position, a lured victim). These are attack surface, not confirmed vulnerabilities; the prerequisites are yours to verify.
  • Defender - what to change in the session configuration, grouped by fix.

The needs-manual-review list is the honest residue: names that look like tokens but whose values do not confirm it, usually because the value was unavailable. Re-run with --decrypt to shrink it.

Command reference

Every flag, what it does, and when to reach for it:

Flag Purpose
(no flags) Safe scan of default profiles. No values read from Chromium.
--path FILE Add a specific SQLite cookie file (repeatable). For a copied profile, a different browser install, or a forensic image. Auto-detects Chromium vs Firefox schema.
--out FILE Report path (default gmach_report.json). The snapshot dir, purge list and history are placed next to it.
--decrypt Read Chromium values via your login keychain to enable JWT decode, value-based classification, PII scan and Chromium continuity. Firefox is always analysed.
--reveal Opt-in, sensitive. Also write masked-but-recoverable raw values to a separate gmach_confidential.json (0600, gitignored). Live secrets - treat as a password file, delete after use. Never enters the main report or history.
--tracker-list FILE Use an external Disconnect services.json for domain categorisation instead of the built-in heuristic list.
--snapshot-dir DIR Where snapshots live (default gmach_snapshots/ beside the report). Point separate scan scopes at separate dirs to keep their timelines clean.
--no-snapshot Skip writing a snapshot this run. For a one-off scan you do not want folded into the timeline.
--keep N How many snapshots to retain (default 120, 0 = unlimited). Oldest are pruned first.
--durable-days N Threshold in days above which an unchanged identifier counts as durable (default 60). Lower it to flag faster, raise it to reduce noise.
--timeline-out FILE Also write just the cross-run aggregation to its own JSON, without re-scanning.
--install-timer {app,launchd,systemd,cron} Write a periodic-snapshot job definition (and, for app, build the signed wrapper). Enables nothing.
--history-keep N Runs retained in gmach_history.jsonl (default 500). Symmetric to --keep for snapshots; compaction is atomic and records that it happened.
--no-prune Skip history compaction on this run.
--reset-continuity Start a new continuity epoch: new salt, old salt destroyed, cross-epoch comparison refused. Asks you to type RESET.
--yes Skip the interactive confirmation. For scheduled jobs only.
--quiet Errors only on stderr. For the scheduled job, so its log stays clean.

Common recipes

# Full local audit with values, then read the pentest tab
python3 gmach.py --decrypt

# Investigate a copied profile without touching your timeline
python3 gmach.py --path /evidence/Cookies --no-snapshot --out case01.json

# Tighter durability threshold and a capped snapshot history
python3 gmach.py --decrypt --durable-days 30 --keep 52

# Extract live secrets for a one-off token inspection, then delete the file
python3 gmach.py --decrypt --reveal
#   ... inspect gmach_confidential.json ...
rm gmach_confidential.json

# Regenerate only the timeline JSON from existing snapshots
python3 gmach.py --no-snapshot --timeline-out timeline.json

How it works

The privacy boundary is the point of the design: raw values are read into memory, reduced to derived signals, and dropped. Nothing crosses the dashed line except with an explicit opt-in flag.

flowchart LR
  subgraph SRC["Cookie stores (read-only)"]
    CH["Chromium family<br/>Chrome / Edge / Brave"]
    FF["Firefox<br/>cookies.sqlite"]
  end

  SNAP["snapshot-copy<br/>0600 temp, incl. WAL"]
  CH --> SNAP
  FF --> SNAP

  SNAP --> META["Metadata<br/>host, flags, timestamps"]
  SNAP --> VAL["Values in memory only<br/>Firefox always<br/>Chromium with --decrypt"]

  META --> ENG["Findings engine<br/>measured / heuristic / inference"]
  VAL --> DERIV["Derived, masked<br/>token type, JWT claims,<br/>PII scan, entropy"]
  VAL --> HASH["Keyed hash<br/>HMAC-SHA256 local salt"]
  DERIV --> ENG

  ENG --> REP["gmach_report.json<br/>value-free"]
  ENG --> PURGE["gmach_purge.json"]
  HASH --> STORE[("gmach_snapshots/<br/>0600, per run")]
  STORE --> AGG["Cross-run aggregation"]
  AGG --> ENG

  VAL -.->|"--reveal only"| CONF["gmach_confidential.json<br/>0600, gitignored<br/>live secrets"]

  REP --> DASH["dashboard.html<br/>offline, zero CDN"]

  classDef secret stroke-dasharray:4 3
  class CONF secret
Loading

What it shows

  • Exposure profile - interest sectors inferred from cookie domains (explicitly labeled as inference, not fact)
  • Activity rhythm - 7×24 heatmap from last_access timestamps: the same behavioral pattern every tracker on your list can reconstruct server-side
  • Sync bursts / tracking seed events - clusters of tracker cookies created within a 10-second window: one page visit seeding a wave of tracking, pointing at seeder sites
  • Cross-browser correlation - trackers present in ≥2 browsers, i.e. domains for which browser separation provides no isolation
  • Continuity - per-run snapshots aggregated into lanes: which domains were present in which run, whether the identifier stayed the same, and which cookies came back after deletion
  • Trend & diff - append-only run history (gmach_history.jsonl), dual-axis trend across runs, new/removed domains with new trackers highlighted, scan-scope changes marked on the chart
  • Findings engine - severity-ranked, each finding tagged with provenance: measured / heuristic / inference
  • Purge list - actionable gmach_purge.json (stale + ad-tech candidates); the tool never writes to live browser databases

Findings

id severity trigger
auth-no-secure-critical high value or name classifies as session/auth/JWT/OAuth, no Secure
auth-no-secure-review medium auth-shaped name, no clear token signal - manual review
cookie-no-secure-benign low CSRF / anti-forgery / known benign UI state
durable-identifier high/medium same value held across snapshots past the threshold (60d default)
identifier-respawn high cookie absent for a run, returned with the same value
tracker-reappearance medium tracker slot returned with a different value
tracker-footprint-growth medium tracker domain count up >=15% across the window
persistent-id-cookies high entropy >3.5 bit/char, len >=16, remaining >30d (values only)
pii-in-cookies high email/phone/uuid/ip found inside cookie values
session-hijack-surface high/medium auth/session cookies stealable via XSS/network
tracking-seed-events medium/high >=3 tracker domains created within 10s
long-held-handle medium tracker slot held for over a year (now - created)
samesite-none-insecure medium SameSite=None without Secure
lifetime-gt-400d medium remaining time beyond the Chrome cap
adtech-footprint medium/high ad-tech domain density
fingerprinting-adjacent medium device-intelligence / anti-bot domains
new-tracker-domains medium tracker domains absent in the previous run
cross-browser-tracking medium same tracker in >=2 browsers
jwt-identity-exposure medium JWTs carrying decoded identity claims
stale-cookies low unused >180d, still valid
scan-scope-drift info the set of scanned sources changed between runs
identity-graph info cross-service login ecosystems

Findings whose classification is uncertain now carry a confidence band in their evidence lines (conf 90 potwierdzone, conf 40 niepewne), and contradicted name/value pairs are marked SPRZECZNE.

Token classification and confidence

Cookie names lie. X-Contour-Session-Affinity is routing, not a session; is_user_logged_in=true is a boolean flag, not a token; a UUID is an identifier even when the name says nothing. Earlier versions matched on the name and produced false HIGH findings for exactly these cases.

Classification now returns a confidence score, not just a label, by confronting the name with the value:

  • The value decides when it can: a JWT structure, a bearer-token prefix, a high-entropy random string, a UUID. These score high regardless of the name.
  • The name is a weak signal, checked against the value. Name says session, value is web-07 (short, low entropy)? That is a contradiction - the verdict is demoted and marked, not reported as a session.
  • No value available (Chromium without --decrypt)? The name-only verdict is capped at low confidence and labelled name-only, so you can see which findings are guesses.

The three-way auth-no-secure split is driven by this confidence: HIGH only for confidence >=60 with a real token type, MEDIUM (review) for uncertain, LOW for contradicted or known-benign. The confidential summary reports name_only_pct - what fraction of verdicts rest on the name alone - so the reliability of the run is visible up front.

Pentest view

With value data present, the report includes a pentest layer that splits the same facts into two perspectives, because a missing HttpOnly reads differently depending on who is asking:

  • Attacker - what could be done with each token, each vector carrying a prerequisite the attacker must satisfy separately (an XSS in the domain, a network position, a lured victim). This is attack surface, not confirmed vulnerability - the prerequisites are not checked, that is the pentester's job.
  • Defender - what to change in the session configuration, grouped by fix, each with its rationale and its cost.

Priority targets rank auth tokens by theft impact and confidence together; the needs-manual-review list is the honest residue of uncertain verdicts. This is a local, offline starting point for a cookie-focused engagement, not a vulnerability scanner.

Continuity

Every run writes a snapshot to gmach_snapshots/ (0600). Aggregation across snapshots is what turns a scan into monitoring:

  • Identifier persistence - whether a domain holds the same value over time, and how often it rotates. Tracked through HMAC-SHA256(local_salt, value)[:16]. The salt is generated once per snapshot directory, stored 0600, never leaves the machine. The hashes are non-reversible and non-portable: they only answer "same as last time, on this machine". Raw values still never touch disk.
  • Respawn detection - a cookie that disappears and returns with the same value hash. Clearing it did not break the link; the value was restored. The equality is measured, the mechanism (other browser storage, server-side relink, profile sync) is not observable from the cookie store and is labelled inference.
  • Scan-scope fingerprint - scope_key hashes the set of scanned sources. Adding a browser changes every number without any behaviour changing, so runs across a scope change are marked as not comparable rather than plotted as a trend.
  • Coverage caveat - continuity only covers cookies whose value is readable: Firefox always, Chromium only with --decrypt. Lanes without value coverage are drawn with diagonal hatching rather than implying stability.

What the aggregation distinguishes, run over run:

flowchart TD
  A["Cookie slot seen in run N"] --> B{"Present in run N+1?"}
  B -->|no| C{"Returns in a later run?"}
  B -->|yes| D{"Same value hash?"}

  C -->|no| E["Gone"]
  C -->|yes| F{"Same hash as before the gap?"}

  F -->|yes| G["identifier-respawn HIGH<br/>clearing did not break the link"]
  F -->|no| H["tracker-reappearance MEDIUM<br/>re-seeded by normal traffic"]

  D -->|yes| I{"Held past threshold?"}
  D -->|no| J["Rotation counted<br/>not proof the link broke"]

  I -->|yes| K["durable-identifier<br/>HIGH on tracker domains"]
  I -->|no| L["Stable, below threshold"]

  M["No value coverage<br/>Chromium without --decrypt"] -.-> N["Presence only<br/>hatched lane cells"]
Loading

Set it up as a periodic job with --install-timer app|launchd|systemd|cron. The definition is written to disk; enabling it stays an explicit manual command. On macOS prefer app - see the next section for why.

Scheduled runs and process identity (macOS)

A LaunchAgent pointing straight at the interpreter produces a process called python3.x with no identity of its own. For an EDR sensor that is three weak signals at once: an unnamed interpreter, persistence via LaunchAgent, and access to browser credential stores - the pattern generic rules describe as credential access.

--install-timer app builds a signed wrapper so the job is attributable instead of anonymous:

flowchart TD
  L["launchd"] --> A["GmachSnapshot<br/>Mach-O, ad-hoc signed<br/>local.hbcc.gmach-snapshot<br/>stable cdhash"]
  A --> P["python3<br/>gmach.py --quiet"]
  P --> S[("gmach_snapshots/")]

  A -.-> LI["Login Items &amp; Extensions<br/>shows GmachSnapshot"]
Loading

The main bundle executable is a real Mach-O, not a script: a shebang script would be replaced by the interpreter image at execve, the process would be called python3 again, and the bundle signature would have no relationship to what actually runs. The launcher does one thing - posix_spawn the interpreter and pass the exit code back.

What this does not do: it does not hide that the tool reads cookie stores. That is its function and any sensor will see it. The goal is attribution, so the job can be allowlisted as a deliberate decision rather than reappearing as something new after every interpreter upgrade.

Known limits, stated plainly:

  • An ad-hoc signature carries no Team ID. Against a "block unsigned" rule this is still untrusted code. What it does give is a deterministic cdhash.
  • The cdhash will not change when Python is upgraded - the wrapper calls the interpreter internally. It will change on any rebuild of the launcher, so a hash-based allowlist entry needs updating after a rebuild.
  • Interpreter and script paths are compiled in. Moving the repository requires rebuilding the bundle.
  • Under launchd the job does not inherit Terminal's TCC grants. If browser profile directories come back as Operation not permitted in gmach_timer.log, grant Full Disk Access to the bundle.

Confidential layer

By default the tool reads only metadata. The confidential layer goes deeper into what cookies actually carry - while keeping raw secrets out of any shareable artifact:

  • JWT decode - header + payload of your own tokens (no signature verification): issuer, subject, email, scope, expiry. Shows what services store about you and whom they trust. PII masked.
  • Token classifier - session / auth / jwt / oauth_bearer / csrf / opaque_id.
  • In-value PII scan - emails, phones, UUIDs, embedded IPs. Masked in the report.
  • Theft-impact score (0-100) - per cookie: what an attacker gains if it leaks, weighted by missing HttpOnly (XSS-stealable), missing Secure (network-interceptable), weak SameSite, lifetime.
  • Session-hijack surface - auth/session cookies ranked by theft impact.
  • Re-identification index - relative signal (inference) of how uniquely identifiable you are across the tracker set + PII/JWT presence.

Privacy architecture - deliberate, and the point:

  • The main gmach_report.json, the run history and the snapshot store never contain raw values. Snapshots hold a keyed hash for continuity only. Confidential data is derived and masked (prefix + length, redacted claims).
  • Firefox values are plaintext, so the confidential layer runs by default (masked output only).
  • --decrypt (opt-in) decrypts Chromium values using the current user's own OS credentials - self-scoped, the same operation the browser performs; App-Bound-Encrypted stores return no key and are skipped.
  • --reveal (opt-in) is the only way raw values leave memory: they go to a separate gmach_confidential.json, chmod 600, warning-stamped, gitignored. Treat it like a password file: analyze, then delete. A file of live session tokens is a liability, not a feature - the tool is built so you have to ask for it explicitly.

Design decisions (deliberate)

  • Chromium decryption is opt-in, not default (--decrypt). Default analysis runs on metadata. When enabled, it uses the current user's own OS key material and is self-scoped by the OS crypto.
  • Raw values never enter the main report or history. They leave memory only with --reveal, into a separate gitignored 0600 file. The shareable report is always value-free.
  • Snapshot-copy before read - locked SQLite files from a running browser are handled safely; source stores are opened read-only, never modified.
  • Provenance on every claim. Tracker classification and naive eTLD+1 are explicitly labeled heuristics; the method in use is stamped in report meta and the dashboard footer.
  • Fully offline dashboard. Zero CDN, zero external fonts, zero network calls beyond an optional local report fetch. Works air-gapped.

App-Bound Encryption and why Chromium values may be unavailable

If --decrypt reports full coverage on Firefox but zero on Chrome, the tool is not broken - it has hit a deliberate security boundary, and it now says so explicitly rather than returning a silent empty confidential layer.

What changed

Before Chrome 127 (July 2024), the key that encrypts cookie values was bound to the OS user. On macOS it lived in the login Keychain under "Chrome Safe Storage"; any process running as that user could ask for it and receive it. That is the path --decrypt uses. The problem: an infostealer running as the same user could do exactly the same thing - no cryptography to break, just ask the system for a key you already have access to. That is precisely how mass session-theft campaigns worked.

App-Bound Encryption moves the trust boundary from the user to the application. The key is no longer handed to "any process of this user", only to a verified browser binary.

  • On Windows, a privileged COM service (IElevator) unwraps the key and checks that the caller is a signed Chrome at the expected install path. The key in Local State carries an APPB prefix.
  • On macOS, the Keychain entry is ACL-bound to Chrome's code-signing identity. An external process - even as the same user - is refused, silently. That silent refusal is the withval=0 you may see.

The cipher did not change (still AES-GCM). The access control to the key did: from "you are this user" to "you are this application". It is a shift in the trust model, not stronger math.

Why the bypasses are all malware territory

Every technique that gets the key anyway - injecting into the Chrome process to inherit its identity, spoofing Chrome to the key-issuing service, or elevating privileges past the application-identity check - is circumventing a security control, which is the definition of offensive activity. GMACH deliberately does not implement any of them. A cookie-audit tool that starts injecting into other processes stops being an audit tool and becomes a stealer with a nicer README. The line is not technical, it is about what the tool is: GMACH accesses values only through the legitimate OS API (ask the Keychain as the user, receive the key or don't), and stops where that path stops.

What the tool does about it

  • Detects the scheme up front. chromium_key_scheme() reads the key prefix in Local State before attempting anything: APPB -> App-Bound (undecryptable from outside), DPAPI / v10 / v11 -> OS-bound (decryptable as the user).
  • Says so, once, plainly. Instead of a silent zero, the run prints that the browser uses App-Bound Encryption, that this is by design and not a package or permission error, and that Firefox is the way to get value coverage.
  • Reports coverage. The report's value_coverage block and the dashboard overview banner state how many values were read and why the rest were not, so a reader can tell an App-Bound limitation apart from a missing cryptography package or a forgotten --decrypt.

The practical answer: Firefox is a first-class source

Firefox stores cookie values in the clear in cookies.sqlite, with no App-Bound barrier. It is not a fallback - it is where GMACH's confidential and pentest layers work fully and without cryptographic compromise:

python3 gmach.py --path ~/Library/Application\ Support/Firefox/Profiles/<profile>/cookies.sqlite --no-snapshot

Firefox needs neither --decrypt nor cryptography - the values are plaintext. On a Chrome-only machine running 127+, the metadata, tracking map, continuity and flag hygiene all still work; only the value-dependent layers (token classification by value, JWT decode, PII scan, token pentest) require a source whose values are actually readable.

eTLD+1 and the bundled Public Suffix List

Domain aggregation depends on collapsing hosts to their registrable domain, and a wrong answer is invisible: a.b.stooq.pl and stooq.pl either merge or do not, and every count, lane, and graph node inherits whichever happened. Up to 1.7 the default path was a label-count heuristic with six hardcoded two-level suffixes.

1.8 resolves eTLD+1 through three tiers, reported in meta.etld:

Source When Provenance
psl-lib publicsuffix2 is installed measured
psl-bundled default - psl_snapshot.dat in this repo measured
heuristic the snapshot file is missing heuristic

The bundled list is complete (10239 rules), not trimmed. A trimmed list would fall back to the heuristic for anything outside the subset, and that failure is indistinguishable from a correct answer. Nothing is fetched at runtime, which keeps the offline-first guarantee intact. The snapshot date and age travel in the report; past 180 days the result is flagged stale.

The ICANN and PRIVATE sections are both applied. For a tracking-analysis tool this is the right call - it keeps user.github.io separate from other users of the same host - but it means some cloud provider hostnames are public suffixes in their own right and therefore have no registrable domain below them.

Because this changes how hosts collapse into domains, the resolver is part of scope_key. Your first 1.8 run against an existing history will register one scope change. That is the mechanism working: the numbers moved because the measurement changed, not because your browsing did.

Continuity epochs

The snapshot salt is never rotated automatically, because rotating it breaks identifier continuity by design. Until 1.8 there was also no way to start over.

A bare rotation would be worse than no feature: with a new salt every value hash stops matching, and the engine cannot tell that from every provider rotating their identifiers at once. So the salt file now carries an epoch number, snapshots record the epoch they were hashed under, and the aggregator refuses to compare across epochs, reporting how many runs it set aside.

python gmach.py --reset-continuity        # type RESET to confirm
python gmach.py --reset-continuity --yes  # scheduled jobs

The old salt is destroyed rather than archived. Archiving it would preserve the ability to re-link exactly the identifiers the reset was meant to sever, which defeats the point. Snapshot files from earlier epochs are kept on disk, but they no longer participate in continuity analysis.

Language

The dashboard ships in Polish and English. Switch with the header pill, Alt+L, or ?lang=en in the URL; the choice is remembered in localStorage where available, and otherwise falls back to navigator.language.

One catalog is the source of truth for both the engine and the UI (gmach_i18n.py for findings, gmach_i18n_ui.py for dashboard chrome), injected into the single-file dashboard at build time:

python tools/build_i18n.py          # inject
python tools/build_i18n.py --check  # gate: fails if the injected block is stale
python tools/i18n_audit.py          # measure coverage on the rendered demo

Coverage is measured rather than claimed. The audit renders the synthetic demo twice, once forced to each language, and diffs the visible text. As of 1.8: 76.5% of visible Polish strings are translated, with zero remaining in the dashboard's own chrome. The remaining 62 are evidence rows, which the engine builds as complete Polish sentences in Python and which therefore cannot be translated client-side. Fixing that properly means giving evidence a structure instead of a rendered sentence; it is logged in TODO rather than papered over.

Optional dependencies

  • pip install playwright then playwright install chromium - required only by tools/i18n_audit.py, which measures translation coverage by diffing headless renders. Not needed to run GMACH.
  • pip install cryptography - required for --decrypt to read Chromium values on macOS/Linux (v10/v11 AES-GCM). Without it, --decrypt silently falls back to metadata-only and prints a one-line notice. Not needed for Firefox (plaintext) or for App-Bound Chrome (values unavailable regardless - see above).
  • pip install publicsuffix2 - upgrades eTLD+1 resolution from the bundled PSL snapshot to a live library (the three-tier fallback is automatic and labeled)
  • python gmach.py --tracker-list services.json - swap the built-in tracker snapshot for a Disconnect-format list

Methodology caveats

  • last_access is overwritten by browsers: the rhythm heatmap is a snapshot of last touches, not full visit history.
  • Diff compares domain sets between runs. Changing scan scope (different --path, added browser) reflects scope change, not behavior - keep scope constant for a meaningful trend. Natural usage pattern: a weekly scheduled task.
  • Third-party status is proxied by domain classification, not by request-context measurement.
  • No entropy-based ID detection for Chromium on the default path, because values are not read there; --decrypt enables it. Metadata analysis remains complete either way.
  • Built-in tracker list is a curated snapshot, not exhaustive - hence the Disconnect hook.
  • Continuity requires at least two runs; the first run only establishes a baseline.
  • total_span_days is not the server-declared max-age. Browsers preserve created across overwrites, so a rolling-refreshed cookie inflates it without the declaration changing. It answers "how long has this handle existed", which is a different question.
  • A rotating identifier is not proof that the link was broken - a provider can map the old identifier to the new one server-side. Labelled inference, deliberately left open.

Tests

python -m unittest discover -s tests -v

Standard library only.

Gates

Three checks run before a release tag. Each is run on its own, never chained and never piped into something whose exit code is then read.

python -m unittest discover -s tests
python tools/build_i18n.py --check   # injected i18n block is current
python tools/verify_diagrams.py      # README claims match the tree

tools/i18n_audit.py is a measurement, not a gate: it reports coverage rather than passing or failing, and it needs playwright. Every test tied to a fixed defect names it in the docstring, so a regression is recognisable without reading git history.

License

MIT

About

Local cookie-store counter-intelligence: audits browser cookies offline and maps what trackers can infer about you

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

Contributors

Languages