Skip to content

Dashboards

lex-align-server ships four dashboards. They render server-side from templates baked into the Python package and call the read-only JSON API for their data, so they work against any deployment without needing a separate frontend build.

Path Audience Goal
/dashboard/registry Platform / dev experience Triage approval requests, edit the registry, export YAML.
/dashboard/legal Legal / OSS compliance License-compliance posture across all evaluations.
/dashboard/security AppSec / supply-chain Vulnerability posture across all evaluations.
/dashboard/agents Operators Which agent identities are doing what.

Every page accepts a ?project=… filter scoped to the X-LexAlign-Project value clients send.


Registry workshop — /dashboard/registry

The interactive page. It loads the live registry, surfaces what's waiting to be triaged, and routes any change through the configured proposer so the YAML on disk stays the source of truth. For most installs that's the local_file proposer — the click writes registry.yml and the file watcher reloads. The advanced PR-based flow is opt-in.

Key pieces:

  • Pending approval requests — packages someone explicitly filed a request-approval for.
  • Implicit candidates — packages the audit log has seen but no one filed an approval for, classified by why they're surfacing (provisional-no-rationale, repeatedly-denied, pre-screened).
  • Global policies editor — CVSS threshold, auto-approve / hard-ban license lists, unknown-license policy.
  • Add to registry… — opens the proposer flow (direct write for local_file, commit for local_git, PR for the opt-in github backend).
  • Export YAML — last-resort manual save. Useful when running with log_only.

descriptive alt text


The legal dashboard answers "what licenses are in our supply chain, and how is the policy classifying them?" It's framed for whoever owns OSS-licensing risk; the policy itself lives in the registry workshop.

It pulls from /api/v1/reports/legal, which returns:

Field What it shows
total_denials Count of DENIED audit rows whose denial category is license.
recent The 100 most recent license-driven denials.
license_breakdown Every audit row in scope grouped by normalised license, split into allowed / provisional / denied.
unknown_license The same shape, restricted to rows that normalised to UNKNOWN.
top_projects Projects ranked by number of license-driven denials.

Page sections:

  • KPI strip — total evaluations with a license, license-driven denials, unknown-license rows, and the count of distinct licenses ever seen.
  • License breakdown table — every license sorted by frequency, with a stacked bar showing the verdict mix at a glance. Copyleft licenses (GPL, AGPL, LGPL) get a red tag; permissive ones (MIT, BSD, Apache, ISC) get a green tag; UNKNOWN gets an amber tag.
  • Unknown-license policy panel — how the configured unknown_license_policy is performing in production. If you're seeing a lot of provisional rows that never get followed up on, that's a signal the policy is too lax.
  • Top projects — which repos are pulling the most non-compliant packages.
  • Recent denials — the existing 100-row tail with project, package, license, reason, and the agent identity that triggered it.

descriptive alt text

Reading the breakdown

  • A copyleft row that's mostly denied is the policy doing its job — investigate only if the count is climbing.
  • A copyleft row with allowed rows means it slipped past the policy. Most often that's because the license isn't on hard_ban_licenses and the package matched a registry preferred / approved rule. Either ban the license globally or downgrade the package's registry status.
  • High UNKNOWN count with provisional verdicts means PyPI metadata isn't giving us a normalisable license string for those packages. Either chase upstream metadata or tighten unknown_license_policy from pending_approval to block.

Security posture — /dashboard/security

The security dashboard answers "are we letting known vulnerabilities into the codebase, and are any already-approved packages turning hot?" It pulls from /api/v1/reports/security, which extends the base denial report with:

Field What it shows
total_denials Count of DENIED audit rows whose denial category is cve.
recent The 100 most recent CVE-driven denials.
severity_distribution CVE-denied rows bucketed by max CVSS (critical ≥ 9.0, high ≥ 7.0, medium ≥ 4.0, low > 0, unknown).
top_packages Packages with the most CVE-driven denials, carrying their highest-seen CVSS and up to five CVE ids.
top_cves The CVE identifiers showing up most often.
hot_registry_packages Registry-allowed packages whose audit rows in the last 30 days include a CVE-driven denial or provisional.
cve_alerts Most recent CVE_ALERT rows written by the background re-scan scheduler. One per (package, scan tick) where the registered package's max CVSS now crosses the configured threshold.

Page sections:

  • KPI strip — total CVE denials and per-severity counts, sized so a spike in critical is impossible to miss.
  • "Already-approved packages with new CVE activity" — the highest-signal cell. Anything in the registry as preferred, approved, or version-constrained whose audit rows in the last 30 days went through a CVE denial or provisional verdict shows up here. This is exactly the case the pre-commit hook in CLAUDE.md is documented to catch — except the dashboard finds it before someone tries to commit.
  • Top packages by CVE denials — sorted by max CVSS first, then by denial count, with their CVE ids inline.
  • Severity distribution panel — a simple counts-by-bucket table.
  • Top CVE identifiers panel — which CVE ids drive the most blocks, and which packages they hit.
  • Recent CVE denials — the chronological tail.

descriptive alt text

Background CVE re-scan

The dashboard's "hot packages" panel reacts to /evaluate traffic — a package only surfaces there if someone has actually checked it recently. To catch the case where a critical CVE drops on an already-approved package that nobody is currently checking, the server runs an asyncio scheduler in-process that walks every package in the live registry on a fixed cadence and re-queries OSV.

  • Cadence is set by LEXALIGN_CVE_SCAN_INTERVAL_HOURS (default 24, 0 disables the scheduler entirely).
  • For each registered package whose max CVSS now crosses global_policies.cve_threshold, the scanner writes a CVE_ALERT row to the audit log with the package name, CVE ids, max CVSS, and current registry status.
  • The scanner is alert-only. It never auto-flips a package to banned or otherwise mutates the registry — the policy decision stays with the operator, so green builds don't break overnight.
  • Alerts surface as the cve_alerts field of /api/v1/reports/security (and therefore in the security dashboard) and in the lex-align-client status summary, with no other endpoint changes.

The scheduler shares OSV's response cache with the on-demand /evaluate path, so a scan tick warms the cache for everyone else. With the default 24h cadence and 6h cache TTL, every alert is at most a scan-cycle behind the OSV publish.

Acting on a "hot" registry package

When something appears in already-approved packages with new CVE activity (or as a CVE_ALERT from the background scanner):

  1. Check the CVE ids and the affected version range in OSV.
  2. If you're on a safe version, pin it: open the registry workshop, set the package to version-constrained with a min_version, and propose the change.
  3. If no safe version exists yet, set it to banned (with a reason pointing at the CVE id) until upstream ships a fix.
  4. Either path goes through the same proposer flow as everything else in the registry workshop, so the rationale is captured in the audit trail.

Agent activity — /dashboard/agents

A simple read-only roll-up of /api/v1/reports/agents. Groups every audit row by (agent_model, agent_version) so operators can answer "which Claude version made that request?". The X-LexAlign-Agent-Model and X-LexAlign-Agent-Version headers the client sends propagate into every audit row; rows without them collapse into a single unknown bucket.


Direct API access

Every dashboard is a thin renderer over the JSON API; you can hit the endpoints directly with curl for scripting:

curl -s http://localhost:8000/api/v1/reports/legal | jq .
curl -s http://localhost:8000/api/v1/reports/security?project=demo | jq .
curl -s http://localhost:8000/api/v1/reports/agents | jq .

The schema additions on the legal and security endpoints are additive; older clients that only consumed total_denials and recent keep working.