Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "openfoia",
"owner": {
"name": "Jordan Coin Jackson"
},
"metadata": {
"description": "Claude Code plugin for OpenFOIA — local-first FOIA automation and AI document analysis for journalists and researchers."
},
"plugins": [
{
"name": "openfoia",
"source": "./plugin",
"description": "Help journalists and researchers run FOIA investigations using the openfoia CLI. Ships a skill covering the full records-search → download → OCR → extract → crossref → graph loop plus slash commands for each phase."
}
]
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,6 @@ Thumbs.db
# Logs
*.log
logs/

# Generated benchmark artifact (tests/benchmark_extraction.py)
test_graph.html
191 changes: 191 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Changelog

All notable changes to OpenFOIA are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Entries before 4.0.0 are backfilled from git history and are summaries, not
exhaustive lists.

## [4.1.0] - 2026-08-11

Additive release on top of the 4.0.0 security hardening. No security
regressions: the 4.0.0 network egress choke point, graph escaping, CDN
removal, and fail-closed installer verification are all preserved.

### Added

- A Claude Code plugin (`plugin/`) — an `openfoia` skill plus four slash
commands (`/foia-install`, `/foia-search`, `/foia-investigate`,
`/foia-graph`). Install with
`/plugin marketplace add JordanCoin/openfoia` then
`/plugin install openfoia@openfoia`. Every documented `openfoia ...`
invocation is validated against the live 4.0.0 command tree, including the
new `egress-status` command.
- `openfoia --version` (also `-V`), sourced from `openfoia.__version__`.
- `openfoia analyze graph --no-text` — export the entity graph without
embedding document bodies. When text *is* included, the command now warns
that the export is plaintext and lives outside the encrypted database.
- `CHANGELOG.md` (this file) and a web-UI section in `docs/AIRGAP.md`.

### Fixed

- **Installer could mistake a checksum file for the binary.** `install.sh`
matched release assets by unanchored substring, so `pdf-extract-<platform>`
also matched `pdf-extract-<platform>.sha256`. With no guaranteed asset
ordering, the installer could have downloaded the checksum file and installed
it as the extractor. The match is now anchored to the exact asset name. This
sits on top of 4.0.0's fail-closed checksum verification.
- The benchmark's graph writer (`tests/benchmark_extraction.py`) spliced raw
JSON into an inline `<script>` via `JSON.parse('...')`, which both broke on
the apostrophes and quotes FOIA data is full of (`O'Brien`,
`Prince George's County`) and re-opened the same injection vector 4.0.0
closed in the main renderer. It now uses `escape_json_for_script`.

### Internal

- `pyyaml` added to the `dev` extra — `tests/test_plugin.py` parses plugin
frontmatter and imported it without it being declared.
- `tests/test_plugin.py` resolves every documented plugin invocation against
the real command tree, and fails if a top-level command is undocumented in
the skill.
- `tests/test_security.py` pins the graph escaping and the absence of external
resources in the web UI, complementing the broader 4.0.0 security suite.
- `test_graph.html`, a generated benchmark artifact, is no longer tracked.

## [4.0.0] - 2026-08-09

Security hardening release (merged via PRs #64 and #65). A parallel
security/OPSEC pass; the notes below describe what it changed, factually.

### Security

- **Fail-closed network egress choke point.** A single egress layer
(`openfoia/net.py`) routes outbound requests and can force everything through
Tor's SOCKS5 proxy; if the requested policy cannot be honored the request
fails rather than silently going out direct. Crossref and web archiving were
routed through it, and the tool's outbound fingerprint was dropped.
- **`egress-status` command.** Reports honestly whether traffic goes DIRECT or
via Tor, whether the Tor proxy is actually reachable, and what is and is not
protected.
- **Graph HTML could execute code from document text.** Untrusted document text
and entity labels were spliced into the graph's `<script>` block; a FOIA
response containing a literal `</script>` broke out and ran arbitrary
JavaScript on `file://`. `escape_json_for_script` now escapes `<`, `>`, `&`,
and U+2028/U+2029. Regenerate any `graph.html` produced before this release.
- **The web UI loaded JavaScript from a CDN.** `openfoia serve` pulled Tailwind
from `cdn.tailwindcss.com` on every page load, disclosing your IP and the
timing of your sessions. Replaced with a stylesheet served from disk; the web
UI now makes zero external requests, sends a restrictive Content-Security-
Policy header, rejects non-loopback `Host` headers, and keeps the auth token
out of the URL and the `Referer`.
- **Metadata, resource caps, and at-rest hardening.** Metadata stripping was
broadened, downloads and extraction gained size/resource caps, and database
file permissions were tightened. The gateways (email, fax, mail) were
hardened.
- **Installer verification fails closed.** `install.sh` verifies the
pdf-extract binary against a published `.sha256` and refuses to proceed when
it cannot verify, rather than warning and continuing.

## [3.2.2] - 2026-04-10

### Added

- LLM validation now reports keep/remove counts and surfaces errors instead of
failing quietly.

### Changed

- The extraction warning distinguishes a local AI provider from a cloud one, so
"AI is running" no longer reads the same whether or not documents are leaving
the machine.

## [3.2.1] - 2026-04-10

### Added

- Cassette-compatible LLM routing and selectable PDF extraction profiles.
- Multi-backend extraction pipeline with mention merging and an `--ensemble`
mode.
- LLM used as a validator rather than an extractor, plus junk filtering and
OCR-aware fuzzy merging (477 → 333 entities on the benchmark).
- `--model` flag and Qwen3 support.

### Fixed

- Crossref rate limiting, deduplication, error handling, progress output, and a
ProPublica 404.
- Forced re-extraction (`--force`).

## [3.2.0] - 2026-03-24

### Added

- DocumentCloud adapter and an interactive document reader in the graph view.
- Multi-layer MuckRock search with cleaner table display.
- MSG email support and file-type display in search results.

### Fixed

- spaCy auto-download.
- Web upload text extraction. (The upload path was later routed through the safe
ingest API and CSP-hardened in 4.0.0.)
- Portable-mode config, request-send persistence, and agent draft handling.

## [3.1.1] - 2026-03-23

### Added

- Python CI, a pre-commit hook running ruff lint and format, and `CLAUDE.md`
with the project's mission and principles.

### Fixed

- ruff lint and format across the codebase.

## [3.1.0] - 2026-03-23

### Security

- Duress mode redesigned: no stored password hash, an encrypted decoy database,
and opaque filenames.
- Honest security messaging, a written threat model, and install checksums.
- Addressed seven findings from an adversarial review.

### Fixed

- LLM-extracted entities are validated against the source text.
- MuckRock search uses tags (the API has no full-text search).
- The install script searches all releases for the pdf-extract binary.

## [3.0.1] - 2026-03-22

### Changed

- Core install is lightweight; heavy packages are opt-in extras.
- Every missing-dependency error now names the `openfoia install-extras`
command that fixes it.

### Added

- Portable install — the entire app lives on the USB stick.

### Fixed

- Install uses an isolated venv rather than polluting the system Python.

## [3.0.0] - 2026-03-22

Baseline for this changelog. Earlier tags (`v0.0.1` through `v2.0.0`,
2026-02-19 to 2026-03-22) predate it; see the git history for details.

[4.1.0]: https://github.com/JordanCoin/openfoia/compare/v4.0.0...v4.1.0
[4.0.0]: https://github.com/JordanCoin/openfoia/compare/v3.2.2...v4.0.0
[3.2.2]: https://github.com/JordanCoin/openfoia/compare/v3.2.1...v3.2.2
[3.2.1]: https://github.com/JordanCoin/openfoia/compare/v3.2.0...v3.2.1
[3.2.0]: https://github.com/JordanCoin/openfoia/compare/v3.1.1...v3.2.0
[3.1.1]: https://github.com/JordanCoin/openfoia/compare/v3.1.0...v3.1.1
[3.1.0]: https://github.com/JordanCoin/openfoia/compare/v3.0.1...v3.1.0
[3.0.1]: https://github.com/JordanCoin/openfoia/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/JordanCoin/openfoia/releases/tag/v3.0.0
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ Your data never leaves your machine. Works offline. Works everywhere.

## Install

### With Claude Code (recommended)

If you use [Claude Code](https://claude.ai/code), install the OpenFOIA plugin — you get the CLI plus a built-in copilot that knows every command:

```
/plugin marketplace add JordanCoin/openfoia
/plugin install openfoia@openfoia
/foia-install
```

The plugin ships a skill that teaches Claude how to run FOIA investigations, plus slash commands for the core loop: `/foia-search`, `/foia-investigate`, `/foia-graph`, `/foia-install`. The last one bootstraps the CLI on your machine.

### Shell install (no Claude Code needed)

```bash
curl -fsSL https://raw.githubusercontent.com/JordanCoin/openfoia/main/install.sh | bash
```
Expand Down Expand Up @@ -50,7 +64,7 @@ openfoia crossref # check entities agai
| **Encrypted storage** | SQLCipher AES-256. Decoy profile mode |
| **Forensic purge** | 3-pass overwrite, shell history scrub, free space fill |
| **Portable mode** | `openfoia portable` — everything stays on the USB, nothing on the host |
| **Metadata stripping** | Auto-strips EXIF, PDF author, DOCX revision history on ingest |
| **Metadata stripping** | Strips EXIF, PDF author, DOCX revision history on file and web-UI ingest. Email attachments and archived web pages are stored as received — see [THREAT_MODEL.md](docs/THREAT_MODEL.md) |

### PDF Extraction Engine

Expand Down
21 changes: 21 additions & 0 deletions docs/AIRGAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ ollama serve &
openfoia config --init # Select "ollama" as the AI provider
```

## The Web UI Works Fully Offline

`openfoia serve` needs no internet. It binds to `127.0.0.1`, serves a single
self-contained HTML page, and fetches nothing from any external host -- the
stylesheet is hand-written and inlined, there are no web fonts, no CDN
scripts, and no analytics. The only external URL anywhere on the page is a
link to the project's GitHub repo in the footer, which does nothing unless
you click it.

This was not true before v4.0.0: the page loaded Tailwind CSS from
`cdn.tailwindcss.com`, so every page load made a DNS lookup and a TLS request
to a third party, and the UI was close to unreadable without one. If you are
running an older version on an air-gapped machine, expect a broken-looking
interface -- and on a networked machine, expect the request. Upgrade.

Entity graphs (`openfoia analyze graph --view`) are self-contained too: a
single HTML file with inline CSS and JS that opens from `file://` with no
network access at all.

## Security Checklist

- [ ] Air-gapped machine has no WiFi/Ethernet/Bluetooth enabled
Expand All @@ -165,4 +184,6 @@ openfoia config --init # Select "ollama" as the AI provider
- [ ] Duress mode configured (`--duress-password`)
- [ ] Swap disabled or encrypted on the air-gapped machine
- [ ] Ollama running locally for AI features (no cloud API keys)
- [ ] Running v4.0.0 or later (earlier versions load CSS from a CDN on every
`openfoia serve` page load)
- [ ] Physical security of the USB drive when not in use
13 changes: 11 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,18 @@ download_binary() {

local url
# Check all releases for binaries (they live on whichever release the
# glyph-api CI pushed them to — not necessarily the latest release)
# glyph-api CI pushed them to — not necessarily the latest release).
#
# Match the EXACT asset name: anchor on the leading '/' and the closing
# quote. Releases also carry a "<name>.sha256" asset, and an unanchored
# substring match hits both — with JSON asset order not guaranteed, the
# installer could download the checksum file, chmod +x it, and install
# that as the binary. The API lists releases newest-first, so head -1
# still picks the most recent release carrying this platform's binary.
local name_re
name_re=$(printf '%s' "$name" | sed 's/[][\.*^$/]/\\&/g')
url=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases" \
| grep "browser_download_url.*${name}" \
| grep -Eo "\"browser_download_url\"[[:space:]]*:[[:space:]]*\"[^\"]*/${name_re}\"" \
| head -1 \
| cut -d '"' -f 4) || true

Expand Down
2 changes: 1 addition & 1 deletion openfoia/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""OpenFOIA - Crowdsourced FOIA automation with AI-powered document analysis."""

__version__ = "0.1.0"
__version__ = "4.1.0"
42 changes: 41 additions & 1 deletion openfoia/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@
)


def _version_callback(value: bool) -> None:
if value:
from . import __version__

rprint(f"openfoia {__version__}")
raise typer.Exit()


@app.callback()
def _root(
version: bool = typer.Option(
False,
"--version",
"-V",
help="Show the installed OpenFOIA version and exit.",
callback=_version_callback,
is_eager=True,
),
) -> None:
"""Crowdsourced FOIA automation with AI-powered document analysis."""


# === Init Command ===


Expand Down Expand Up @@ -2571,16 +2593,27 @@ def analyze_graph(
view: bool = typer.Option(
False, "--view", "-v", help="Open interactive HTML visualization in browser"
),
no_text: bool = typer.Option(
False,
"--no-text",
help="Omit full document text from the export (keeps entities, links and context snippets)",
),
):
"""Build entity relationship graph from extracted entities.

Use --name to save graphs by investigation name. Each investigation
gets its own graph that you can revisit later.

The exported .json and .html files are plaintext and live outside the
encrypted database. By default they embed the full text of every document
behind the graph, which is what makes the reader view work. Pass --no-text
to export the structure without the document bodies.

Examples:
openfoia analyze graph --view # everything, open in browser
openfoia analyze graph --name defense-contracts --view # save + view
openfoia analyze graph --request REQ-001 --name epa # filter + save
openfoia analyze graph --no-text --name epa # structure only, no doc bodies
openfoia analyze graphs # list saved graphs
"""
from .db import get_db_path, get_session
Expand Down Expand Up @@ -2680,7 +2713,7 @@ def analyze_graph(
"id": doc.id,
"filename": doc.filename or "Unknown",
"page_count": doc.page_count,
"text": doc.extracted_text or "",
"text": "" if no_text else (doc.extracted_text or ""),
"request_id": doc.request_id,
"source_url": source_url,
}
Expand Down Expand Up @@ -2709,6 +2742,13 @@ def analyze_graph(
rprint(f" Relationships: {len(edges)}")
if name:
rprint(f" Saved as: [cyan]{name}[/cyan]")
if no_text:
rprint(" [dim]Document text omitted (--no-text). The reader view will be empty.[/dim]")
elif documents:
rprint(
"[yellow]Note:[/yellow] this export embeds full document text in plaintext, "
"outside the encrypted database — share deliberately, or re-run with --no-text."
)

if view:
_generate_graph_html(graph_data, html_path)
Expand Down
10 changes: 10 additions & 0 deletions plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "openfoia",
"description": "Help journalists and researchers run FOIA investigations using the openfoia CLI. Ships a skill covering the full records-search → download → OCR → extract → crossref → graph loop plus slash commands for each phase.",
"version": "0.1.0",
"author": {
"name": "Jordan Coin Jackson"
},
"repository": "https://github.com/JordanCoin/openfoia",
"license": "MIT"
}
Loading
Loading