ThreatLens · ZEIT3118 Capstone

Dashboard / Web UI

The dashboard is a FastAPI application that serves a browser UI directly over the live SQLite database. It uses the same SQLAlchemy models the ingest pipeline writes to, so what it shows and edits is the live data, not a copy. It is where a reader reviews indicators, reports and quality flags, and also where reports are uploaded and ingested and where the per-machine settings in threatlens.conf are edited.

Running it

# with the virtual environment active; needs the dashboard extra:
#   pip install "threatlens-cti[dashboard]"     (or pip install -e ".[dashboard]" in a checkout)
threatlens-dashboard                                   # threatlens.conf [dashboard], default http://127.0.0.1:8000
threatlens-dashboard --host 0.0.0.0 --port 8000 --reload  # reachable from the local network

# or directly
uvicorn threatlens.dashboard.server:app --reload

# or in the background, from src/threatlens/dashboard/ (repo checkout only)
./start.sh [PORT]        # also start.bat / start.ps1; stop with stop.*

The address comes from threatlens.conf [dashboard] host / port (set by threatlens init), default 127.0.0.1:8000; --host / --port override it. With no threatlens.conf and a terminal, the dashboard first offers to run the setup wizard.

The start scripts run the server in the background, write its PID to .server.pid and its output to server.log, refuse to start a second copy, and use the repo's .venv when there is one. THREATLENS_DASHBOARD_HOST overrides the host. They are not part of the pip package; threatlens-dashboard is.

The pages, their JS/CSS and these documentation pages are package data, so a pip-installed copy serves the full UI and /docs/ with no repo checkout.

At startup the dashboard checks threatlens.conf (see setup check) and prints a SETUP: line for each step not yet done: the file is missing, is still the unedited template, or its [llm] settings have not been calibrated. These are warnings only; the dashboard starts on built-in defaults regardless.

No authentication. The default host, 127.0.0.1, keeps the dashboard to this machine. With 0.0.0.0 ([dashboard] host or --host) it is reachable from the local network, and anyone who can reach it can edit and delete database rows, upload files and change the configuration; the dashboard prints a warning at startup when it listens on all interfaces. The checkout's start.* scripts still default to 0.0.0.0 (THREATLENS_DASHBOARD_HOST changes it).

Which database it uses

At startup the dashboard opens the database folder (db/ in a checkout, <data folder>/db/ when pip-installed; see where files live) + threatlens.conf [database] default (else threatlens.db). THREATLENS_DB_URL or THREATLENS_DB_FILE override that. The header's Database dropdown switches to any other .db file in that folder at runtime, without a restart. Every page, the API, the database browser and newly queued ingest jobs follow the switch.

Pages

Main page (/)

A header with links to the other pages, the database dropdown and four stat cards: indicator count, report count, open quality flags and mean quality score. Below it, three tabs:

TabShowsActions
IndicatorsEvery stored indicator: type, value, confidence, quality score with its grade badge, sighting count, extraction engine, last seen. Paged (100 per page), sorted on the server so a sort spans every page.Search by value; filter by type, quality-score range and engine (llm, python, unknown); sort by any column. Edit any field in a form; Delete (with its sightings and flags). Click a row for the indicator page.
ReportsEvery ingested report: title (from its metadata, not the filename), mean quality score, filename, type, reliability, engine, indicator count, ingest timeOpen the report page; Delete the report
Quality FlagsFlags raised by the quality checker: indicator, flag type, dimension, severity, detail, status. Newest first; defaults to open flags.Filter by status (open, resolved, dismissed); Resolve or Dismiss a flag

An indicator's engine is not stored on the indicator: it is the combination of the engines of the reports it was seen in, so a value seen by both reads llm+python.

Report page (/reports/{id})

The report's metadata and quality summary, then a table of its indicators with the four dimension scores, quality score, confidence and flags. Under it is the original PDF, rendered page by page on the server with PyMuPDF, with every place an indicator appears highlighted. Rows and highlights link both ways: click a row to jump to the value in the PDF. The In PDF column counts how often each value was found; buttons step between pages with hits, and the view fits to page or width.

The matching is deliberately looser than an exact search, because the stored value often differs from the printed one (wrapped URLs and hashes, defanged forms). The PDF itself is not in the database: raw_documents.file_path points at it in docs/database/CTI/, so if the file is moved or deleted the view is unavailable but the data is not affected.

Indicator page (/indicators/{id})

Everything known about one indicator, with each number explained:

Upload Reports (/uploads)

Lists the PDFs in the report folder, the folder ingest reads from (docs/database/CTI/ in a checkout, gitignored; <data folder>/reports/ when pip-installed; created on the first upload or download), with size, date and whether each is already in the active database (matched by path, or by content hash for a renamed copy). Files can be uploaded (several at once, optionally overwriting), opened in the browser, or removed.

This page only changes files on disk. Uploading does not ingest, and removing a file does not touch the database; that is the Reports tab's delete. Uploads are accepted only if the content starts with %PDF-, are written under a temporary name and renamed when complete, and must be plain filenames (no paths).

Ingest (/ingest)

Runs the ingest pipeline (extract, quality checks, persist, rescore) on chosen reports:

How the queue works:

URL downloads are locked down because the dashboard may be reachable from the network: http(s) on ports 80/443 only, no credentials in the URL, no proxies, and every connection, including each redirect hop, must go to an address the server has itself resolved and checked to be publicly routable. A response is accepted only if it starts with %PDF-.

Configuration (/config)

Views and edits threatlens.conf, the same file the CLI and the ingest worker read:

SectionSettings
OllamaServer URL. The model pickers query the server for its installed models.
ModelsVerifier model; profiler model (blank = same as verifier)
Dashboard address ([dashboard])Not edited here; set by threatlens init or in the file, used at the next start. A save keeps it.
Data folder ([paths])Not edited here; set data_dir in the file or THREATLENS_HOME, then restart. The page shows the database and report folders in use, and a save keeps the setting.
LLM tuning ([llm])Not edited here. Batch size, context window, timeouts and retries are written by threatlens-calibrate (or by hand); a save from this page keeps them, and keeps the [calibration] record.
DatabaseDefault database, chosen from the files in the database folder. Applies at the next dashboard start; the header dropdown switches the live one.
Quality weightsThe four dimension weights. Each 0–1, and they must sum to 1.00. These override the database's scoring_config weights.
Verifier agreementThe confidence multiplier per agreement grade. An ingest always uses single.

A Setup incomplete box at the top lists anything the setup check found: no file yet (defaults are shown, and saving creates it), the unedited template, or no calibration. Saving from this page counts as reviewing the template, even with its values unchanged; calibration needs threatlens-calibrate.

Values are validated before saving, and saved changes apply to the next ingest without a restart. Re-score existing indicators runs rescore_from_db() on the active database. Changing weights does not change stored scores until that runs or the next ingest does.

The weights here apply to every database the dashboard or ingest touches. Test weights left in place will be applied to the next evaluation run, which is what happened to ex3 and ex4 (see the evaluation reports).

Export (/export) and the tabs' Export buttons

Downloads data from the active database (the header's Database dropdown). The Export page in the header walks through four choices, with a live count and the first rows as a preview:

DataFiltersFormats
Indicators: value, refanged value, quality score and grade, confidence, the four dimension scores, reports, engine, open flags and their types, STIX type, first/last seen (plus context and the full score breakdown, off by default)value contains, type, minimum / maximum score, engine, seen in report, sort and orderCSV, JSON, STIX 2.1
Reports: title, file, URL, published date, engine, model, reliability, indicator count, mean quality score, ingested (publisher, authors, SHA-256 and file path optional)title / file / URL contains, engineCSV, JSON
Quality flags: indicator, flag type, dimension, severity, status, detail, raisedstatus, flag type, severity, dimension, indicator IDCSV, JSON
Sightings: indicator, report, seen, excerpt (offsets optional)value contains, type, report IDCSV, JSON

Each column can be switched on or off. Every tab also has an Export button with a format selector: it downloads exactly what that tab is showing, on every page rather than just the visible one. The Indicators button uses the tab's filters and sort order, so the file matches the table row for row (the two share one query, threatlens.db.queries).

Files are named after the database, dataset and time, e.g. ex4_threatlens_indicators_20261018-0930.stix.json. The same exports are available from Python: threatlens.exporters.export(session, "indicators", "csv", filters={...}).

Database browser (/db-browser/) and documentation (/docs/)

The header's Database link opens Datasette, embedded in the dashboard, over the active database. It shows every table's raw rows and columns, unlike the curated tabs, and is read-only. It follows the database dropdown. Documentation serves these pages from src/threatlens/docs/, which ships with the package.

Editing and deleting: what changes

ActionEffect
Edit an indicatorWrites the fields as entered (type, value, normalised, confidence, quality score, first/last seen, context JSON). This bypasses the ingest validation in conformance.py; only the database's CHECK constraints apply, so a bad score or invalid JSON is refused but any ioc_type is accepted. An edited quality score is overwritten by the next rescore; an edited confidence stays.
Delete an indicatorRemoves it with its sightings and quality flags
Delete a reportRemoves the source, its raw document and its sightings. Indicators seen only in that report stay, with no sightings. The PDF file stays on disk. Their first/last seen dates are re-derived from the reports that remain.
Resolve / dismiss a flagSets quality_flags.status. Nothing else changes; the score is not affected.

API

Every page is static HTML plus a JavaScript module calling a JSON API. FastAPI's interactive API docs are at /api/docs and /api/redoc.

RouterEndpoints
indicators.pyGET /api/indicators (filters: ioc_type, search, min_score, max_score, engine, limit ≤ 1000, offset, sort, order) · GET / PATCH / DELETE /api/indicators/{id}
sources.pyGET /api/sources · GET / DELETE /api/sources/{id} · GET …/{id}/pdf, …/pdf/highlights, …/pdf/pages/{n}.png?scale=
quality_flags.pyGET /api/quality-flags?status= · PATCH /api/quality-flags/{id} (any status text is accepted)
stats.pyGET /api/stats: counts, open flags, mean score, indicators by type
db_admin.pyGET /api/db/files · POST /api/db/select
report_files.pyGET / POST /api/report-files · GET / DELETE /api/report-files/{filename}
ingest.pyGET /api/ingest/pending, /engines, /jobs, /jobs/{id}/log?offset=, /downloads · POST /api/ingest/queue, /url, /jobs/{id}/cancel, /cancel-all, /clear, /downloads/{id}/cancel, /downloads/clear
export.pyGET /api/export (datasets, columns, filters, formats) · /api/export/{dataset}/preview · /api/export/{dataset}?format=csv|json|stix&columns=…&filters
config.pyGET / PUT /api/config · POST /api/config/ollama-models, /api/config/rescore

GET /api/config includes a setup object (exists, edited, calibrated, ready, warnings), and GET /api/ingest/engines includes setup_warnings; the Configuration and Ingest pages show them.

A middleware makes browsers revalidate the pages and their JS/CSS on every load, so a code change shows up without a hard refresh.

Source