Skip to main content

Azimut

The OSINT workspace that runs on your machine.

PyPI Python versions CI License Platforms

A tour of Azimut: a clip downloaded into a case, examined and stitched, placed on satellite imagery, compared across dates, swept by Detect, then turned into a proof, a report and a note

Install & run

pipx install azimut   # isolated app install; plain `pip install azimut` also works
azimut                # starts on http://127.0.0.1:8477 and opens a browser tab

No Python? Every release attaches a self-contained binary for Windows, macOS (Apple Silicon) and Linux on the Releases page. Download it, run it, and it opens in your browser.

Your cases and settings live under ~/Azimut, outside the app: upgrading or removing Azimut leaves them alone. Install in detail covers the binaries' first-run warnings, building from source and the development loop.

What Azimut is

A local OSINT workspace for reviewing media, building geolocation proofs and keeping case notes together. It is built for open-source investigators, journalists and researchers. Each case is a plain folder that can be reopened, archived or shared.

The name is the French word for azimuth, the compass bearing you sight along to fix a point on the map.

Tool What it does
Board The whole case as one sortable table: typed identity fields per entity, one filter bar whose values are all taken from the case, shared Details, and a ticked selection deleted as one recoverable act.
Graph The same case drawn. Lenses pick what a reading is about, nodes cluster, edges carry their verb, and a click opens what a node connects to without losing the picture you were reading. Graph and Timeline both export a reading as an SVG or PNG plate carrying its question, period and legend.
Timeline The same case on a time axis. Dated statements and media stack in coloured tracks, the ruler reads in any zone or at a saved place, date quality is drawn instead of flattened, and the window can be handed to Board, Graph or the map.
Sheet The case's own CSVs in a plain grid: a comparison table, a worklist with its own state, or the half-facts too soft to be entities. Columns can be typed, a workbook arrives one sheet per tab, and a declared sheet promotes into entities, places and dated statements. A worklist can also be built back out of what the case holds.
Media Import local files or download by URL (X, Telegram, TikTok, YouTube, Instagram and more via yt-dlp, with a gallery-dl fallback for image-only posts). Public media is fetched cookie-less; a login-walled post prompts once for a browser session or an exported cookies.txt. Each item gets a clean local file, metadata and a SHA-256. Multi-photo posts open a picker.
Files Every saved artifact in one Finder-style view of your folders, not just media: select several, drag them into a folder, search across the lot.
Reverse Search Prepare an image or a video frame for keyless reverse-image services. With the capture extension, pressing an engine opens it with the picture already in it. Nothing leaves the machine until you press one.
Inspect Reads any photo or video closely: sharpest-frame capture onto a strip, frame adjustments, editable crop and ELA hints. Frames and edits are kept with the file as you make them; a frame or an adjusted video enters the Media Library only when you save it to the case.
Collage Lays out frames and images from any number of files on one canvas, with per-piece warp, scale, rotate and crop, and auto-stitch to solve a panorama's layout. Saved as you go; the picture it exports is case media.
Satellite Coordinates or a place name become an imagery crop. The search bar proposes matches as you type: saved work, coordinates and a bundled city list answer offline, and the geocoder fills in the rest once you pause. Select-area capture with an optional scale bar and north arrow, map rotation, measurement tools, a right-click menu on any point, stacked overlays (borders, roads, railways, power lines, fires, night lights), your own KML, KMZ, GeoJSON, GPX or My Maps layers, GeoConfirmed conflicts narrowed to a period, and editable AOI grids for area review. Esri/OSM by default, plus the Esri Wayback archive, Sentinel-2 with a date calendar and a cloud-ceiling slider, and Sentinel-1 radar passes; add a Mapbox or Google key for more basemaps. Open it in several tabs and they stay in sync.
Compare Links two dated map views for side-by-side, swipe, fade or blink, with Difference highlights laid over any of them. All dates lists every picture of a point, oldest first, to set on either side. Ground-anchored notes, shapes, numbered markers and measurements follow both maps. Editable sessions and rendered previews stay in the case; attributed PNG/GIF copies go to exports.
Detect Sweeps drawn areas of Sentinel-2 for vessels, fires, construction, burn scars and other changes, and of Sentinel-1 radar for vessels, razed buildings, new structures and floods through cloud, once or as a saved routine. Only a candidate you keep becomes a case pin, dated by the passes that found it.
Coords & Sky Convert common coordinate formats, copy the result, open map or geocoding links, and read the sun and moon at that point on a date: rise, set, azimuth, altitude, twilights, moon phase and bright-limb angle, in local time and UTC, computed offline.
Geo Proof Start a named proof from a reusable house style, or from a published post whose pictures become its panels. Compose case panels in a grid or free layout, turn or crop them, annotate with colored shapes, fills, symbols, numbered markers, blur boxes, freehand and text, state every place and every source the proof argues, and export proof.png plus a re-editable spec.
Geo Report Turn a proof into a prepared thread for X or Bluesky: coordinates, plus code, attribution, target-specific character counts, media, and a structured Markdown case note with linked evidence.
Notebook Tabbed Markdown notes with local media, Mermaid diagrams, linked case evidence, broken-reference markers, and PDF export of one note or a whole selection.

Under the hood: reusable proof and thread templates, per-case SQLite with a bounded catalog, a durable one-worker thumbnail queue, the map-capture browser extension, and cross-platform binaries with a bundled ffmpeg.

Every tool works one-shot (a scratch session, no setup) or inside a case, a plain directory holding the whole investigation.

New in v0.3.1

Detect sweeps an area for what appeared, and the map takes layers made elsewhere.

  • Detect, a tab of its own, sweeps drawn areas of Sentinel-2 for vessels, fires and flares, construction, small spots, surface change, burn scars, vegetation loss, new water or burned buildings. Candidates come strongest first, and only one you keep becomes a case pin.
  • Difference and Detect share one Sentinel-2 cloud and shadow mask.
  • Radar: five Sentinel-1 analyzers (vessels, any change, razed buildings, new structures, floods) see through cloud and at night, and Sentinel-1 passes are a basemap in Satellite, Compare and Detect.
  • Build your own analyzer from rules on bands, indices, ground classes or radar, with checks that turn red when a rule stops holding. Five examples ship with theirs.
  • A candidate opens in Compare with one press, and All dates in Compare lists every picture of a point to set on either side.
  • Satellite, Compare and Detect share one camera, and a middle-drag turns any map.
  • Inspect keeps one work per file, saved as you go, and Collage is a tab of its own.
  • Open KML, KMZ, GeoJSON and GPX files or a public My Maps as map layers, and add a GeoConfirmed conflict. A dated layer narrows to a period.
  • Proofs turn and crop their images, stamp numbered markers and blur boxes, and carry a sentence and the date their material was taken.
  • To-do lists on Home, one date field across the app, a claim filed from where you are, and Reverse Search from a Media Library row or an Inspect frame.
  • Existing cases open as they are, with 0.3.0 Inspect sessions merged into one work per file, and older bundles still import.

Cases on disk

Inside a case, Azimut owns only the azimut/ directory. README.txt explains the boundary; anything else at the case root is yours and travels with the case bundle.

The workspace root stays equally readable: permanent case folders sit directly under ~/Azimut. Azimut keeps scratch sessions, bundles, settings, runtime tools and tile caches under the hidden ~/Azimut/.azimut/ directory. Settings → Storage moves the workspace anywhere you like, including an external drive, or adopts one you moved yourself. The old copy is kept until you delete it.

The Case Doctor checks case integrity, including the derived Timeline index. It only changes a case after you choose a repair, and states what a database rebuild cannot recover before it starts.

Install in detail

Azimut runs in a normal browser tab (Firefox/Chrome); there is no separate window. Closing the terminal it prints its URL into stops the app.

Update with pipx upgrade azimut, remove with pipx uninstall azimut. Your cases and settings live under ~/Azimut; upgrades and uninstalling the app do not remove them. Delete ~/Azimut manually if you also want to remove the data.

Ready-to-run binary (no Python)

Each release attaches a self-contained binary per OS. Download it from the Releases page and run it; it opens Azimut in your browser.

OS Asset
Windows azimut-windows-x86_64.exe
macOS (Apple Silicon) azimut-macos-arm64
macOS (Intel, 14+) No standalone binary; install with pipx or pip
Linux (glibc 2.38+) azimut-linux-x86_64

The Linux binary needs glibc 2.38 or newer: Ubuntu 24.04, Debian 13, Fedora 39 or later. On an older distribution (Ubuntu 22.04, Debian 12, RHEL 9), install with pipx instead.

First run, the binaries are unsigned, so the OS warns before letting them open:

  • macOS: a browser download is neither executable nor trusted yet. In Terminal, from the folder it landed in:
    chmod +x azimut-macos-arm64
    xattr -d com.apple.quarantine azimut-macos-arm64
    ./azimut-macos-arm64
    
    Instead of xattr, you can open it once and allow it in System Settings → Privacy & Security → Open Anyway.
  • Windows: SmartScreen shows "Windows protected your PC"; click More info → Run anyway.
  • Linux: mark it executable with chmod +x azimut-linux-x86_64.

On startup, Azimut asks GitHub for a newer release and PyPI for newer downloaders by default, and links the download. Settings can disable that check, and Settings → System → Check for updates runs it manually. Replace the old binary with the new one. To uninstall, delete the binary. Either way ~/Azimut stays put, so cases open unchanged.

The downloadable binaries bundle a static ffmpeg (and ffprobe), so video thumbnails, frame scans, video enhancement, and downloads that merge separate audio+video streams work out of the box. If you pip install azimut instead, put ffmpeg on your PATH for those features. Everything else works without it. The bundled ffmpeg is redistributed under its own license; see ffmpeg.org/legal.html.

From source

Requires Python 3.11+ and Node.js 20.19+ or 22.12+ for the frontend build.

macOS and Linux:

git clone https://github.com/OsintMeThat/azimut && cd azimut
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
cd frontend && npm ci && npm run build && cd ..
.venv/bin/azimut

Windows PowerShell:

git clone https://github.com/OsintMeThat/azimut
Set-Location azimut
py -3.11 -m venv .venv
.venv\Scripts\python -m pip install -e ".[dev]"
Set-Location frontend
npm ci
npm run build
Set-Location ..
.venv\Scripts\azimut.exe

Rebuild and relaunch the local app with the cross-platform helper:

python3 scripts/relaunch.py       # macOS / Linux
py scripts\relaunch.py            # Windows

The tool rebuilds the frontend, stops the previous Azimut instance started through the same tool, and launches the fresh build. It never kills unrelated processes by name. Use --no-browser to keep it from opening a new tab, and --ext to bring the capture extension along (see below).

Working on the extension:

python3 scripts/devext.py             # load it into a dev Chrome and Firefox
python3 scripts/devext.py --watch     # and reload it on every save
python3 scripts/relaunch.py --ext     # app + extension in one command

It opens a Chrome and a Firefox of its own, loads extension/ from disk and writes the pairing token into it, so an edit needs no reinstall and no paste. Each dev browser keeps its profile between runs, and the everyday browsers are left alone. Every pass reports when the extension's background restarted, which is how you see the new code was taken. --fresh starts from an empty profile, python3 scripts/devext.py chrome (or firefox) drives just one.

Frontend development (hot reload, proxied API):

.venv/bin/azimut --no-browser &     # backend on :8477
cd frontend && npm run dev          # UI on :5173

Checks (CI runs these on every push):

uv run ruff check src tests scripts packaging   # lint
uv run mypy                    # type-check the package, scripts and packaging
cd frontend && npm run check   # svelte-check (blocks on errors)

Capture extension (optional)

A browser extension (Chrome/Edge and Firefox) captures external map sites straight into a case: Google Maps & Earth, Bing, Yandex, OSM, Apple Maps, Zoom Earth, Copernicus Browser and Satellites.pro, one screenshot per click with coordinates parsed from the URL. It also powers the Capture button on the Google (Maps JS) basemap, fills a composer with a thread Geo Report prepared, and opens a reverse-image engine with a picture from the case.

On a 2D map it can draw Azimut's own tools over the site: measure, the case's saved points, sun and moon, a search grid that opens in the app afterwards, and reference windows holding the case's own images and videos beside the imagery. The scale is measured off the map rather than assumed, so the tools switch themselves off on a view they cannot compute on, and say why.

On Chrome, Edge and Brave, install it from Settings → Capture extension: the app writes the extension into a folder it owns, you load that folder unpacked once and paste the pairing token. Because the app owns the folder, later updates are one button: it rewrites the files and the extension restarts itself.

Firefox refuses an unsigned extension and forgets an unpacked one on exit, so it installs the signed azimut-capture-<version>.xpi instead. Settings → Capture extension links that exact file, which rides on the release that last changed the extension rather than on the newest one. That copy is sealed, so the update button does not apply to it: Firefox reads Azimut's own update manifest and replaces the add-on itself. Full instructions in extension/README.md.

Building & releasing

The Svelte frontend builds into src/azimut/static/ (git-ignored) and is bundled into the Python wheel via hatchling artifacts. So npm run build must run before building the package, or the shipped UI is stale.

cd frontend && npm run build && cd ..    # refresh the bundled UI
uv sync --frozen --no-dev --group release --no-install-project
uv sync --frozen --no-dev --group release --no-build-isolation --no-editable
uv run --no-sync python -m build --no-isolation
uv run --no-sync pyinstaller packaging/azimut.spec

Versions

The app version lives in src/azimut/__init__.py alone; pyproject.toml reads it back, and the release tag must match it.

The capture extension keeps its own version: the app release that last changed a shipped file, so it lags whenever the extension is left alone. tests/test_updates.py digests what the extension ships and fails either way: a change without a bump, or a bump without a change. When you do change the extension, set extension/manifest.json to the current app version and record the digest the failing test prints.

Within a development cycle that version cannot move: it is already the app's own. So the update button compares the digest instead, which the installed folder records in its install.json, so any edit under extension/ shows up as an available update immediately, with no bump and no restart. That is also the loop for testing the updater: Settings → Capture extension → Install, load the extension from the folder it shows, edit a file under extension/, then reopen Settings. Update lights up, and pressing it rewrites the folder and reloads the extension.

Signing the extension for Firefox

Firefox Release enforces extension signing with no override, so the Firefox copy is an XPI signed by Mozilla and served from the release. Signing is decoupled from publishing: --channel unlisted runs automated validation, puts no listing on addons.mozilla.org, and hands the file back.

Run it after the release carrying that version exists, from a checkout of the tag, with credentials from the AMO developer hub kept in a file outside the repo. Never type the secret at a prompt or pass it as an argument, where the shell history keeps it.

source ~/.config/azimut/amo.env   # AMO_JWT_ISSUER + AMO_JWT_SECRET, 0600 file outside the repo
python3 scripts/sign_extension.py                    # signs, writes packaging/updates.json
gh release upload v0.3.1 dist-xpi/azimut-capture-0.3.1.xpi

Then commit packaging/updates.json. That file is what browser_specific_settings.gecko.update_url points at, served raw from main, and Firefox re-reads it about once a day, so a release that changed the extension is not delivered to Firefox users until it lands on the branch. After that manifest PR is merged, verify the public manifest, asset URL and signed bytes together:

python3 scripts/sign_extension.py --verify-release

Two rules the script enforces rather than trusts:

  • It signs what the extension ships, not the extension/ directory (extinstall.shipped_files()), so the XPI, the .zip and the folder the app owns are the same bytes.
  • A version is signed once. AMO refuses a second copy of one it already has, and the extension's version deliberately stays put across releases that leave it alone, so the script stops with that reason instead of failing mid-upload.

The add-on id in extension/manifest.json is permanent: AMO reserves it at the first signature, and changing it later is a different add-on that loses every pairing. tests/test_extension_signing.py gates the id, the update URL and the manifest's shape.

Nothing about this touches development. scripts/devext.py still loads extension/ straight into a dev Firefox as a temporary add-on, which needs no signature.

Dependencies

pyproject.toml declares ranges (the contract for pip install azimut users); uv.lock pins the exact set, and is what CI and the release builds install. The wheel only declares its dependencies, but the binary contains them, so building it outside the lock ships whatever the resolver happened to pick that day.

uv lock --check                  # CI does this: is the lock in sync with pyproject?
uv lock --upgrade                # refresh everything, then run the suite
uv lock --upgrade-package yt-dlp # refresh one

Raising an upper bound is a deliberate act: bump it in pyproject.toml, run uv lock, and make sure the suite passes before it lands. The weekly "latest deps" CI job re-resolves past the lock, so upstream breakage shows up as a red run of ours rather than a broken install for someone else.

The map's offline city list is data, not a dependency: src/azimut/engine/data/cities.tsv.gz, about 770 KiB, trimmed from GeoNames (CC BY 4.0) by python scripts/build_cities.py. Rebuild it when it goes stale; nothing else reads it.

yt-dlp and gallery-dl are deliberately unbounded: they track sites that change, so pinning them just schedules a breakage. They can also be updated from inside the app (Settings → System → Downloaders), which is what keeps a months-old binary working.

Releases are automated: push a semver tag and GitHub Actions (.github/workflows/release.yml) builds the wheel + Windows/Linux/macOS binaries, attaches them to a GitHub release, and publishes to PyPI. Don't publish by hand.

git tag v0.3.1 && git push origin v0.3.1

One-time setup: register the repo as a PyPI Trusted Publisher for the azimut project (no API token to store).

Principles

  1. No account, telemetry or automatic upload. The server binds to 127.0.0.1, and Azimut never posts on your behalf.
  2. A case contains the investigation's files and SQLite graph. A closed case folder is complete and portable; bundle export carries both Azimut's files and anything kept beside them.
  3. One tab = one tool, useful in 30 seconds.
  4. Azimut integrates specialized services instead of recreating them.
  5. The analyst decides; tools do not produce automated verdicts.
  6. Every artifact records how it was produced.
  7. Free and open source. No paid key is ever required; bring your own for more basemaps.

Full spec: docs/SPEC.md.

License

AGPL-3.0-only: free and open source; hosted or modified versions must share their source.

Metadata

Release files for azimut 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for azimut 0.3.1
File Size Uploaded
azimut-0.3.1.tar.gz 19.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for azimut 0.3.1
File Interpreter ABI Platform
azimut-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 30.3 MB

Release files / azimut-0.3.1.tar.gz

Download URL azimut-0.3.1.tar.gz
Size 19.1 MB
Tags Source
SHA-256 checksum
How to use checksums
2ad18027039d3b7c969a4537d088dda45e461ebcb3967207dc8d1f908c6fe6b6
BLAKE2b-256 checksum
How to use checksums
3cd8759fa7699bfc25e15971ca6782b9157faedd4c34df59148f37283ea1c09b
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 26, 2026.

Transparency log

Release files / azimut-0.3.1-py3-none-any.whl

Download URL azimut-0.3.1-py3-none-any.whl
Size 11.2 MB
Tags Python 3
SHA-256 checksum
How to use checksums
e2622b04407cf76a5855902ae6c38ef622ddf68f40b943237689eab7edce5cdd
BLAKE2b-256 checksum
How to use checksums
3b22e542f3fdec650551779de6c0015c719c14d73aacc0b986b6e414128c8322
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 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page