AdvisoryHub — Requirements¶
This document is the top-down functional specification for AdvisoryHub. It
describes what the system does — the actors it serves, the domain
objects it manages, the workflows it supports, and the rules those
workflows must honour. It is paired with architecture.md,
which covers the how (technology stack, internal structure, operations).
Where a rule has a stable ID or a state-machine diagram, the deep-dive lives in the existing specification triad and this document cross-links to it:
invariant.md— load-bearing rules with stableINV-XYZ-NIDs.advisory-lifecycle.md— state diagrams, transition tables, the publication sequence diagram.permissions.md— actors, roles, capability matrix, enforcement surfaces.
The specification set under docs/specification/ is the single
source of truth: code must conform to it. If this document and the
code disagree, that is a defect — either the code drifted (fix the
code) or the behavior changed deliberately without a spec update
(fix the document in the same change). Deviating from the spec
requires explicit maintainer confirmation before implementation.
1. Purpose & scope¶
AdvisoryHub is a private Django application for authoring, reviewing, publishing, and auditing security advisories for Eclipse Foundation projects. It is the system of record for advisory content, advisory workflow, and advisory-related governance actions (access grants, review decisions, CVE requests, publication attempts, audit history).
Published advisories are exported as OSV and CSAF JSON and pushed to a separate publication Git repository; that repository has its own CI/CD that renders the public-facing website. AdvisoryHub does not host the public website, does not interact with end users who read the public advisories, and does not orchestrate the publication-repo's build. Publication is a one-way handoff: every advisory that has ever been pushed appears as a deterministic commit on the configured branch, and the public surface is whatever that branch becomes.
In scope¶
- Authoring of advisory content (native or GHSA-linked).
- A four-state lifecycle with three orthogonal status machines (review, CVE request, publication).
- A private public-form intake (triage) for incoming reports.
- Access control on every advisory, with per-advisory grants and invitations on top of project-derived ownership.
- Comments with markdown rendering, mentions, and per-comment visibility (internal vs. everyone with advisory access).
- An append-only audit log over every governance action.
- Generation, validation, and Git-push of OSV + CSAF documents.
- GHSA integration: linking advisories to GitHub Security Advisories, syncing metadata, pushing EF-assigned CVE IDs back to GitHub, ingesting webhooks, and mirroring the project↔repo map from the Eclipse Foundation PMI API.
- An admin console for the global security team to triage, review, assign CVEs, retry publications, and read the audit trail.
Out of scope¶
- The public anonymous read surface (lives in the publication Git repo's static site).
- A real MITRE CVE-services integration — the CVE request workflow is
an internal queue (
invariant.md§14). - IdP / OIDC group administration. Group membership is mirrored on login and never edited from AdvisoryHub (INV-OIDC-1).
- Email deliverability concerns and template content beyond the redaction guarantees.
2. Actors¶
| Actor | How identified | Default authority |
|---|---|---|
| Anonymous web client | No session | May POST a triage report to /report/. May follow the OIDC redirect to sign in. Nothing else. |
| Authenticated user | OIDC session | No role on any advisory by default. Sees only advisories they hold a grant for. |
| Triage reporter (authenticated) | OIDC session at submission time | Auto-granted viewer on the advisory they filed. |
| Triage reporter (anonymous) | None retained | No link is recorded; the report cannot later be claimed. |
| Project security-team member | Member of the Django Group referenced by Project.security_team |
Derived owner on every advisory under that project. |
| Global admin / reviewer | Member of OIDC_ADMIN_GROUP |
Derived owner everywhere; exclusive reviewer of submitted advisories; sole holder of CNA-side admin actions (unassign CVE, mark orphan rejected at cve.org, ban CVE requests). |
| Celery worker | Background process | No ambient authority — every task acts on behalf of a stored user and re-checks the relevant predicate at execution time. |
The full actor / authority detail is in
permissions.md §3.
3. Domain concepts¶
The objects below carry the bulk of the system's state. Most are modeled in their own Django app; cross-references point at the corresponding model module for the full field list.
Advisory¶
The central object — a security advisory about an Eclipse Foundation
project. Each advisory has a kind (native or ghsa_linked), one
of four lifecycle states (triage / draft / published /
dismissed), and an OSV-aligned content payload (summary, details,
aliases, references, affected, severity, cwe_ids, credits,
plus assigned_cve_id and withdrawn_reason). Each advisory carries
a public, immutable advisory_id matching ECL-…-…-… using a
confusion-resistant alphabet (INV-ID-1).
GHSA-linked advisories are a bridge over a GitHub-hosted Security Advisory: their OSV content fields are sourced from GHSA and read-only in AdvisoryHub; AdvisoryHub initiates CVE-ID allocation and pushes it back to GitHub, and publication is gated on the upstream GHSA having been published. Their owning project is derived from the source repository's PMI ownership and follows PMI automatically — it is never reassigned by hand (INV-GHSA-1).
Advisory.kind is set at creation and immutable. Advisory.delete()
is blocked at the model and DB layers
(INV-IMPL-1).
AdvisoryVersion¶
Append-only edit log of advisory content. Version 1 is seeded
automatically when the advisory is created; every payload-visible edit
appends v(n+1); non-payload saves (state flips, heartbeat metadata
sync) do not. AdvisoryVersion.save() on an existing row and
AdvisoryVersion.delete() both raise
(INV-IMPL-5). Workflow tasks
(ReviewTask, PublicationTask) PROTECT-FK into this table so a
version that was ever pinned by a workflow cannot be removed
(INV-VERSION-2).
AdvisoryIntakeMetadata¶
One-to-one sidecar on triage advisories. Stores the reporter's
authenticated identity (when present), the request IP and User-Agent,
the admin-routing flag, and the time fields used to scrub PII at
retention boundaries. Reporter email is never taken from form
input; only an OIDC-verified email may appear (via the linked
reporter_user) (INV-INTAKE-2,
INV-PRIVACY-3).
Project & ProjectGitHubRepository¶
Project is an Eclipse Foundation project. Carries a slug, a name, a
homepage URL, the security_team Django Group, and the
is_mature_publisher flag that controls whether the team may publish
without a top-level review (INV-PERM-1,
INV-PERM-2). A singleton unsorted
project owns triage filed without a known project; its security team is
the admin group by construction (INV-PROJECT-2).
ProjectGitHubRepository mirrors the (owner, name) pairs that PMI
declares for a project. The mirror is refreshed by a Celery beat task
and used by the GHSA discovery flow to know which repos to query.
SecurityTeamRosterEntry mirrors the PMI security-team roster for a
project (dormant unless PMI_ROSTER_SYNC_ENABLED). The roster sync
pre-provisions notification-only shadow users
(User.is_provisioned=True) so triage events and @team mentions
reach members who have never logged in; shadows belong to no group,
hold no authorization, and are linked to the real account on first
login (INV-OIDC-5,
INV-ROSTER-1).
AdvisoryAccessGrant & PendingInvitation¶
AdvisoryAccessGrant carries one grant per (advisory, principal_type,
principal_id) (INV-ACCESS-1).
principal_type is user or group; permission is viewer or
collaborator. The grant table never carries owner —
ownership is structural, not grantable
(INV-AUTH-3,
INV-ACCESS-4).
PendingInvitation is the email-address-keyed staging row used when an
owner invites someone who does not yet have a Django user. Redemption
matches the authenticated user's email case-insensitively
(INV-ACCESS-2) and refuses expired rows
(INV-ACCESS-3; default lifetime 14 days).
AdvisoryComment & CommentVersion¶
Comments on an advisory. Each comment has an author, a markdown
body, and an is_internal boolean fixed at creation
(INV-COMMENT-1). Markdown is rendered
on demand through a strict nh3 allowlist — no inline HTML, no
images, no scripts; rendered HTML is never stored. @mentions
resolve to users by full email or local-part.
CommentVersion is the append-only edit history; redaction sets
redacted_at and clears the visible body but preserves the row's
place in the timeline (INV-COMMENT-3,
INV-COMMENT-4).
AuditLogEntry & AccessLogEntry¶
One append-only row per governance action, recording the actor,
timestamp, action type, advisory (and optionally the comment id),
previous and new values, IP, User-Agent, and a JSON metadata blob.
Both the application layer and a Postgres trigger refuse UPDATE and
DELETE (INV-AUDIT-1). All
user/CI-supplied strings pass through audit.services.redact_secrets
before persistence (INV-AUDIT-2).
AccessLogEntry is the companion high-volume table for ephemeral
events that never appear on an advisory timeline — advisory views,
auth events (login, logout, failed login, step-up re-auth),
notification deliveries, and GHSA/PMI machine chatter. Unlike the
durable ledger it is range-partitioned by month and
retention-managed: partitions older than
AUDIT_ACCESS_LOG_RETENTION_DAYS (default 90) are dropped by a beat
task (INV-AUDIT-5).
NotificationPreference, AdvisoryNotificationPreference, Notification¶
NotificationPreference is one row per user holding the global
notification defaults; AdvisoryNotificationPreference is a sparse
per-advisory override (any field at its inherit sentinel uses the
global setting; rows with every field at the sentinel are deleted).
Notification is the per-recipient in-app inbox row created alongside
each delivered email; users mark entries read explicitly or implicitly
by visiting the page the notification points at. AdvisoryVisit (one
row per user/advisory) records the last visit and powers the
"changed since last visit" markers on advisory lists.
CveRequestTask, ReviewTask, OrphanCve, OrphanCveReassignmentTask¶
The workflow queues attached to an advisory. CveRequestTask
tracks the request lifecycle (queued → reserved | rejected |
cancelled) with at most one open task per advisory
(INV-CVE-1). ReviewTask pins the exact
AdvisoryVersion a reviewer is judging
(INV-REVIEW-2). OrphanCve records
admin-initiated CVE unassignments so admins remember to mark the CVE
rejected at cve.org. OrphanCveReassignmentTask queues admin
follow-up when an advisory is reopened after its orphaned CVE was
already marked rejected — the admin either reattaches the CVE or
replaces it with a fresh request (see
advisory-lifecycle.md §3.1).
PublicationTask, PublicationArtifact, PublicationRepositoryConfig¶
PublicationTask is one row per publish or re-publish attempt,
pinning the AdvisoryVersion that was (or will be) exported, with a
status of queued / running / succeeded / failed.
PublicationArtifact is one row per (task, kind ∈ {osv, csaf,
cve}) holding the validated JSON document that was pushed (the cve
kind exists only when the advisory carries an EF-assigned CVE); it is
also the data source for the admin-console preview screens.
PublicationRepositoryConfig carries the per-repository settings
(URL, branch, auth method, key/token, commit author, OSV / CSAF /
CVE-record path templates, and the CVE assigner identity).
GHSA integration objects¶
GitHubAppInstallation records the per-organization GitHub App
installation that AdvisoryHub authenticates as. WebhookDelivery
deduplicates inbound webhooks by delivery ID. GhsaCvePushTask is one
queued/running/finished row per attempt to push an EF-assigned CVE
back to a linked GHSA. GhsaSyncRun is one row per sync operation
(single advisory / one project / all projects / PMI mirror) with
counts and last-error redaction.
HoneypotSubmission¶
One row per honeypot trip on the public intake form. No Advisory is
created; the response page is identical to a real submission so bots
learn nothing (INV-INTAKE-1).
SimilarityCheck, SimilarityCandidate, AdvisoryFingerprint¶
The duplicate-detection rows (similarity app; dormant unless
SIMILARITY_CHECK_ENABLED — see §4.13). SimilarityCheck is one row
per LLM-assisted duplicate check, pinning the AdvisoryVersion
payload it judged (INV-SIM-4) with a
queued/running/succeeded/failed status and a redacted last_error
(INV-SIM-3). SimilarityCandidate stores
the judged matches (a 0–100 confidence plus a one-line rationale,
top five per check). AdvisoryFingerprint caches the per-advisory
LLM digest, keyed on a content hash so unchanged content is never
re-digested.
4. Functional requirements¶
4.1 Advisory content & validation¶
Advisory content fields are modeled after the OSV schema:
| Field | Type | Notes |
|---|---|---|
advisory_id |
string | Public id matching ECL-([23456789cfghjmpqrvwx]{4}-){2}[23456789cfghjmpqrvwx]{4}. Generated at creation, immutable. |
project |
FK | Eclipse Foundation Project. May be changed by an owner; appended to the version log and resets approval. |
summary |
string (≤300) | Short headline. |
details |
text | Markdown-flavoured long description. |
aliases |
list[string] | Other identifiers (CVE IDs, vendor IDs, GHSA IDs, …). |
references |
list[{type, url}] | type ∈ {ADVISORY, ARTICLE, DETECTION, DISCUSSION, REPORT, FIX, INTRODUCED, GIT, PACKAGE, EVIDENCE, WEB} (default WEB). |
affected |
list[{package, ranges?, versions?}] | OSV-style. Each entry needs package.name and at least one of ranges / versions. Each range needs a type, a list of events, and at least one introduced event; fixed and last_affected are mutually exclusive within a range. |
severity |
list[{type, score}] | type ∈ {CVSS_V2, CVSS_V3, CVSS_V4, Ubuntu}; Ubuntu scores are negligible/low/medium/high/critical. |
cwe_ids |
list[string] | Each CWE-N; validated against a vendored catalogue. |
credits |
list[{name, type?}] | type ∈ the OSV credit-type enum. |
published_at / modified_at |
datetime | modified_at is auto_now. published_at is set on first successful publish. |
withdrawn_reason / dismissed_reason |
text | The latter is required when the lifecycle state is dismissed. |
assigned_cve_id |
string | EF-assigned CVE id (write-once via the CVE workflow). Validated as CVE-YYYY-NNNN…. Effectively immutable after first assignment (INV-CVE-2). |
republish_required |
bool | Set when a published advisory is edited; cleared on next successful publish. |
access_review_required_at |
datetime | Set when the project is reassigned; surfaces an access-review banner until dismissed. |
severity_level / severity_score |
derived | Denormalised from severity at save time — the worst entry's qualitative level (critical/high/medium/low/none; Ubuntu negligible folds into low) and its numeric CVSS base score (null for Ubuntu/unscored). Indexed so the advisory list can filter, sort, and badge by severity without parsing CVSS vectors per row. Recomputed whenever severity is written; excluded from the version payload, so they are never versioned (INV-VERSION-1). |
GHSA-linked advisories additionally carry ghsa_id (unique-when-set
INV-ID-2), ghsa_owner, ghsa_repo,
ghsa_metadata (raw payload), ghsa_state, ghsa_metadata_synced_at,
ghsa_sync_error (redacted last-sync failure — surfaced on the GHSA panel
and cleared on the next successful sync; excluded from the version payload,
so it is never versioned), and the ghsa_cve_push_* fields that track our
outbound push of an EF-assigned CVE to GHSA. For these advisories the
OSV-content fields are populated from GHSA and read-only in the AdvisoryHub
edit form.
Field-level validators live in advisories.validators. The advisory
id format is also enforced at the URL converter and at the model layer.
4.2 Advisory lifecycle¶
Lifecycle states are exactly four (INV-LIFECYCLE-1):
| State | Meaning | Created by |
|---|---|---|
triage |
Untrusted incoming report awaiting promotion. | Public intake only (INV-LIFECYCLE-2). |
draft |
Curated content being prepared for publication. | advisory_create view, GHSA link, or promotion from triage. |
published |
Successfully pushed to the publication Git repo. | Only via the publication worker's success branch (INV-LIFECYCLE-3). |
dismissed |
Rejection (duplicate, not-a-vuln, out-of-scope) — reversible, not terminal (INV-LIFECYCLE-4). | dismiss_triage (from triage) or advisory_dismiss (from draft). |
Three orthogonal status machines ride alongside the lifecycle and are
documented in full in
advisory-lifecycle.md:
- Review (
Advisory.review_status):none→submitted→approved | changes_requested(a withdrawal returns it tonone;withdrawnexists only onReviewTask.status). Submission freezes the currentAdvisoryVersion; subsequent edits to the advisory append versions but do not move the pinned one. Editing anapproveddraft by a non-admin invalidates the approval (INV-REVIEW-4). Admins cannot submit or withdraw — they are the reviewers (INV-REVIEW-3). - CVE request (
CveRequestTask.status):queued→reserved | rejected | cancelled. Dismissing a draft auto-cancels the open task; the admin may also flipcve_requests_bannedon rejection (INV-CVE-3). - Publication (
PublicationTask.status):queued→running→succeeded | failed. The flip tostate=publishedhappens only inside the success branch, only aftergit pushreturns clean (INV-LIFECYCLE-3, INV-PUB-4).
Reopen. Dismissal is reversible: reopen_advisory (owner-gated)
returns the advisory to Advisory.dismissed_from_state (draft or
triage), audited as ADVISORY_REOPENED
(INV-LIFECYCLE-4). If the advisory
had a CVE that was orphaned on dismissal, reopening either reattaches
it directly or — when the orphan was already marked rejected at
cve.org — queues an OrphanCveReassignmentTask for admin resolution.
The full state diagrams, transition tables, and the publication
sequence diagram are in
advisory-lifecycle.md §3, §5, §6, §7, §8.
4.3 Public intake (triage)¶
A public form at /report/ accepts anonymous and authenticated
submissions. Successful submissions create an
Advisory(state=triage) + AdvisoryIntakeMetadata sidecar via
advisories.services.submit_triage_report. Required fields:
project_slug— a PMI project id or the literal__unsorted__sentinel ("I don't know"), which routes the advisory to theunsortedproject and setsneeds_admin_routing=True(INV-INTAKE-4).summaryanddetails(markdown).- Optional
reporter_display_namefor crediting on the resulting advisory. Display-only and never used for authorization (INV-PRIVACY-3).
The form has no reporter-email field. Email is derived only from the authenticated user's OIDC profile; anonymous submissions cannot be re-associated with a user later (INV-INTAKE-2).
Anti-abuse:
- Honeypot field rendered only to bots (anonymous form only). A trip
persists a
HoneypotSubmissionrow and renders the same thank-you page as a real submission (INV-INTAKE-1). - Optional self-hosted ALTCHA proof-of-work captcha for anonymous
reporters when
ALTCHA_HMAC_KEYis configured (widget assets vendored and served same-origin — no third-party calls). - Rate limits keyed per-IP for anonymous submitters and per-user for
authenticated ones, with
RATELIMIT_INTAKE_ANON/RATELIMIT_INTAKE_USERconfigurable.
Authenticated reporters are auto-granted viewer on the new advisory
(INV-INTAKE-3) so they can track it
from their dashboard. Triage advisories are owner-only for editing,
publishing, CVE requesting, and internal commenting
(INV-AUTH-5); the reporter's auto-grant
gives read and non-internal commenting only.
The triage queue and per-advisory detail pages live under the admin
console's Inbox section; admins can promote
(promote_triage_to_draft), dismiss (dismiss_triage), reassign the
project (reassign_triage_project), and flag /unflag for admin
routing (flag_for_admin_routing / clear_admin_routing_flag). Once
flagged, the advisory becomes admin-only for triage decisions
(INV-AUTH-6). The unsorted project's
security_team is the admin group by construction, so admin routing
falls out of normal permission resolution
(INV-PROJECT-2).
4.4 Comments¶
Comments are authored by the requesting user. The form accepts
markdown; the body is stored as raw markdown and re-rendered on every
read through a strict nh3 allowlist (p, br, strong, em,
u, code, pre, blockquote, hr, ul, ol, li, h1–h6,
a, tables, plus span carrying only class="mention" for the
@mention chips the markdown parser emits before sanitisation). Raw
inline HTML is not rendered — it is escaped to text. Anchor tags are
augmented with rel="nofollow noopener". Rendered HTML is never
persisted, so tightening the allowlist applies retroactively.
is_internal is a per-comment boolean fixed at creation
(INV-COMMENT-1). Internal comments
are visible only to collaborators and owners; visibility is
re-checked at read time so a revoked collaborator stops seeing
internal comments immediately
(INV-COMMENT-2). Comments are never
published or disclosed externally: at most a comment is visible to
people with viewer+ access to the advisory inside AdvisoryHub, and
internal comments are further restricted to collaborators and owners.
Mentions are written as @email or @local-part; the parser
resolves them against the user table and emits a mention notification
to the resolved user. A mention does not elevate visibility: if the
mentioned user lacks collaborator access on an internal comment, no
mention email is sent.
Action notes surface as comments. Many workflow actions collect a
free-text reason/note in their modal. Rather than burying that text on
a model field or in audit metadata, the originating service posts it to
the Activity pane as an author-attributed comment via
comments.services.record_action_note. The visibility follows a fixed
per-action policy:
- Public (viewer+): dismiss reason, withdraw-published reason, lock-comments reason, review approve / request-changes notes, and the CVE-request rejection reason.
- Internal (collaborator+): request-withdrawal note, cancel-withdrawal note, flag-for-admin-routing note, clear-routing-flag note, request-reassignment note, withdraw-reassignment note, unassign-CVE reason, and revoke-approval reason.
The post is silent (no notification — the action owns any of its own)
and uses add_comment(system=True), so it is never blocked by a comment
lock the same action may be setting nor by the actor's per-advisory
rank. It no-ops when an optional note is left blank, or for a
system-policy action with no human author (the GHSA auto-dismiss /
auto-withdraw, by=None). The companion audit event stays a terse
marker — the note text lives only in the comment and is not duplicated
in the event summary (the dismissed this advisory / locked comments
timeline rows no longer inline the reason). Ban-user / forget-user notes
are not advisory-scoped and are not posted; orphan-CVE registry
notes (mark-rejected, resolve-reassignment) stay in the Admin Console.
Edits append a CommentVersion row carrying the new body
(INV-COMMENT-3) and emit
COMMENT_EDITED to the audit log. Redaction stamps redacted_at and
redacted_by, clears the visible body, and is irreversible
(INV-COMMENT-4).
4.5 Audit log¶
Every governance action emits exactly one AuditLogEntry
(INV-AUDIT-3). The authoritative
catalogue of recordable actions is the Action enum in
audit/models.py; the event categories it covers are:
- Advisory lifecycle: created, viewed, edited, state changed, project changed, published, dismissed.
- Review: submitted, approved, changes requested, approval revoked, approval invalidated, withdrawn, task status changed.
- Access: granted, revoked; invitation created, redeemed, revoked.
- Comments: created, edited, redacted.
- CVE request: requested, task status changed, request banned, request cancelled, CVE unassigned, marked rejected at cve.org.
- Publication: export started / completed / failed, OSV generated, CSAF generated, Git commit, Git push, Git push failed.
- Notification preferences changed.
- GHSA: metadata fetched, linked advisory created, CVE push requested / succeeded / failed, CVE conflict detected, sync run started / finished, installation registered / suspended / removed, webhook received / rejected.
- PMI repo mirror synced.
- Triage: triage submitted, promoted, dismissed (legacy
report.*actions remain in the enum read-only for historical rows but are not emitted by new code). - Authentication: login, logout, failed login, step-up re-auth completed.
- Notification delivery: one
notification.sentrow per recipient. - User governance: account banned / unbanned.
Each row carries the actor (nullable for system actions), the action
type, the affected advisory (when applicable), an optional
comment_id, structured previous_value / new_value deltas, a
machine-readable metadata JSON blob, and the requesting IP and
User-Agent when the entry originated from an HTTP request
(INV-AUDIT-4).
All user/CI-supplied strings are funnelled through
audit.services.redact_secrets before persistence
(INV-AUDIT-2). The audit table is
append-only at both layers (model save/delete guards plus
Postgres triggers; INV-AUDIT-1,
INV-IMPL-2).
Ephemeral events — advisory views, the auth events, notification
deliveries, and GHSA/PMI machine chatter — are routed to the
partitioned AccessLogEntry table instead of the durable ledger;
they are retention-bounded (default 90 days) and browsable by admins
at /admin/access-log/
(INV-AUDIT-5).
4.6 Access management¶
Owners (project security team or global admins) may grant access on an advisory to:
- a Django user, by primary key (resolved via the existing user table); or
- a Django group; or
- an email address that has no user yet — this creates a
PendingInvitationand queues an invitation email.
Permission levels available to grants and invitations are viewer
and collaborator; owner is never grantable
(INV-ACCESS-4). A grant is unique per
(advisory, principal_type, principal_id); re-granting the same
principal updates the existing row in place.
Invitations carry an opaque token and an expiry (default 14 days). Redemption matches the authenticated user's email case-insensitively (INV-ACCESS-2) and rejects expired rows (INV-ACCESS-3). An invitation cannot be redeemed by a different email.
Every grant create / update / revoke and every invitation create / redeem / revoke emits an audit entry (INV-ACCESS-5).
A project change on an advisory stamps access_review_required_at,
surfacing a banner that prompts the owner to prune grants that no
longer apply.
4.7 Notifications¶
Each user has a single NotificationPreference row carrying global
defaults; per-advisory overrides live in
AdvisoryNotificationPreference (sparse — only fields the user has
explicitly customised). The events that can produce email are:
| Event | Per-user toggle | Per-advisory override |
|---|---|---|
| Advisory created in a project where you are on the security team | on_advisory_created |
— (global only) |
| Advisory you have access to is submitted for review | on_advisory_submitted_for_review |
yes |
| Advisory you have access to is published | on_advisory_published |
yes |
| Publication export status (success or failure) for an advisory you have access to | on_publication_export_status |
yes |
| New comment on an advisory you have access to | comments_level ∈ {all, mentioned} |
yes (incl. inherit sentinel) |
You are @-mentioned on a comment |
— (always delivered to comment-visible users) | — |
Triage-flow events (advisory_triage_submitted/promoted/dismissed/reassigned/flagged_for_routing/routing_flag_cleared) |
gated by project security-team membership; admins receive the flagged_for_routing events regardless of project |
— |
| You receive an invitation to an advisory | always delivered | — |
comments_level has no none option — mentions are always delivered
to users who can see the comment, and "no comments at all" would
let an owner suppress mentions on their own advisory.
Recipient lists are recomputed at send time via
notifications.recipients.filter_for_event; a user whose access was
revoked between enqueue and send drops from the queue
(INV-PRIVACY-2). For internal
comments, users who cannot see internal comments are dropped even
when mentioned. Body templates are deliberately sparse — recipients
see what changed and a link back into the authenticated app, never
private advisory content directly in the email.
When PMI_ROSTER_SYNC_ENABLED is on, the security-team roster sync
pre-provisions notification-only shadow users so triage events and
@team mentions also reach security-team members who have never
logged in (INV-OIDC-5,
INV-ROSTER-1).
Every delivered email also lands in the recipient's in-app inbox
(Notification rows, §3), where entries are marked read explicitly
or by visiting the linked page; each delivery is recorded in the
access log (notification.sent).
Changes to a user's NotificationPreference are audited
(NOTIFICATION_PREFS_CHANGED).
4.8 Advisory workflow actions¶
The three workflow actions available from an advisory's edit view (when allowed) are:
- Request a CVE. Opens a queued
CveRequestTaskfor the global admin team. Owner-only; refused when the advisory already has anassigned_cve_id, an open task, orcve_requests_banned=True. The admin team transitions it toreserved(carrying the chosencve_id),rejected(with non-empty notes and optionally settingcve_requests_banned), or implicitlycancelledon dismissal. - Submit for review. Pins the current latest
AdvisoryVersioninto a newReviewTask, setsreview_status=submitted, and freezes edits for non-admins. Owner-only — admins are the reviewers and cannot submit. Reviewers (admins) approve, request changes, or revoke an existing approval; the submitter may withdraw. - Publish. Owner-only; for projects where
is_mature_publisher=False, the security team may only publish whilereview_status=approved(or hand off to an admin). For mature publishers, drafts can be published directly. Publication is always refused whilereview_status=submitted(INV-PERM-3).
Dismissing a draft auto-cancels any open CVE request and, when an
assigned_cve_id is present (admin-only path), creates an
OrphanCve row so the admin team marks it rejected at cve.org.
4.9 Publication¶
Publication is the only path to state=published. The complete
sequence is documented in
advisory-lifecycle.md §8;
the requirements are:
- The publish action creates a
PublicationTaskpinning the current latestAdvisoryVersion. A second attempt while one is queued or running raisesPublicationInProgress(INV-CONCURRENCY-1). The Celery task is enqueued viatransaction.on_commitso a rolled-back caller never leaves a stray queued task (INV-PUB-5). - For GHSA-linked advisories the worker first refreshes metadata from GitHub; if the refresh appends a new version, the task is re-pinned to the latest version before generation.
- OSV and CSAF documents are built from
task.version.payload(INV-VERSION-3) and validated against the vendored JSON schemas inpublication/schemas/(INV-PUB-6). Each validated document is persisted to aPublicationArtifactrow (one per kind per task); the dashboard's preview screens read these rows. - The publication repository is cloned into a fresh
tempfile.TemporaryDirectory()per attempt (INV-PUB-1), shallow (INV-PUB-3). Files land at the configured templates (defaultosv/{year}/{advisory_id}.jsonandcsaf/{year}/{advisory_id}.json, bucketed by the advisory's first-publication year). - The commit is created with the configured author (no GPG signing — the deploy key/token is the trust signal) and pushed to the configured branch.
- Only on a clean push the worker, inside the same
transaction.atomicblock guarded byselect_for_update(INV-PUB-4): setsstate=published, stampspublished_atif previously null, clearsrepublish_required, finalises the task with the commit SHA, and recordsADVISORY_PUBLISHED,PUBLICATION_GIT_COMMIT,PUBLICATION_GIT_PUSH, andPUBLICATION_EXPORT_COMPLETED. - On any failure (schema validation, clone, write, commit, push)
the advisory's state is unchanged
(INV-LIFECYCLE-3), the task is
marked
failedwith a redactedlast_error, andPUBLICATION_EXPORT_FAILED(orPUBLICATION_GIT_PUSH_FAILEDfor push-time errors) is emitted.
Re-publication runs the same pipeline against the current latest
AdvisoryVersion; deterministic paths mean a re-publish appears in
the publication repo as a new commit on the same path, while every
prior AdvisoryVersion and PublicationArtifact remains immutable.
Two authentication modes are supported, mutually exclusive (INV-PUB-2):
ssh— git'sGIT_SSHhook is pointed at a per-call generated wrapper that execs ssh withIdentitiesOnly=yes,BatchMode=yes,StrictHostKeyChecking=accept-new; the wrapper lives in the call's scratch directory and vanishes with it (INV-SECRET-2).token— the HTTPS URL is rewritten withhttps://x-access-token:$PUB_REPO_TOKEN@…for the duration of the clone; the token never appears in repo state, audit metadata, task rows, or notification bodies (INV-SECRET-1, INV-SECRET-2, INV-SECRET-3).
4.10 GHSA integration¶
The integration's high-level shape:
- AdvisoryHub authenticates to GitHub as a registered GitHub App
(
repository_security_advisories: read & writeplus the defaultmetadata: read). The installation is recorded inGitHubAppInstallationand re-discovered via thediscover_github_installationsmanagement command or the inboundinstallation.createdwebhook. - The PMI repo mirror (
ProjectGitHubRepository) is refreshed by the Celery beat taskghsa.tasks.run_pmi_repo_sync, which runs everyPMI_SYNC_INTERVAL_HOURShours and is the authoritative source for which(owner, name)repos belong to which project. - GHSA discovery happens on-demand — admins can sync one project
or all projects from the admin console; project security-team
members can sync their own project — and on a slow beat schedule
(
run_scheduled_ghsa_discovery, everyGHSA_DISCOVERY_INTERVAL_HOURS) as a backstop forrepository_advisory.reportedwebhooks GitHub may not deliver. Discovery creates newAdvisory(kind=ghsa_linked)rows (or updates existing ones) whoseghsa_idis uniquely mapped (INV-ID-2). A new row'sstatemirrors GitHub'sghsa_state—triagewhen the GHSA is still in triage upstream, elsedraft(INV-GHSA-3). A GHSA-linkedtriagerow is a read-only mirror — it advances todraft/published/dismissedonly by mirroring GitHub, never a human triage decision. - Per-advisory sync refreshes metadata from GitHub for a single
advisory; updates that change payload-visible fields append a new
AdvisoryVersion, updates that are heartbeats (no change) only refreshghsa_metadata_synced_at. The sync also mirrors GitHub's pre-publication lifecycle: atriagerow whose GHSA was accepted into a draft upstream is promotedtriage→draft. - CVE push. When an admin reserves an EF-assigned CVE on a
GHSA-linked advisory, AdvisoryHub queues a
GhsaCvePushTaskthat writes the CVE back to the upstream GHSA. Push success / failure / conflict (when GitHub already has a different CVE for the same GHSA) is audited and surfaced. - Webhooks. Inbound webhook deliveries are HMAC-verified against
GITHUB_APP_WEBHOOK_SECRETand deduplicated by delivery ID. Suspended or removed installations have their access immediately revoked.
The GHSA feature is gated behind the GHSA_FEATURE_ENABLED flag.
4.11 Admin console¶
The admin console at /admin/ is the global security team's workspace
(Django's built-in admin is not mounted — see §4.11.1).
Sidebar sections:
- Inbox (
/admin/) — the unified action feed: triage queue, pending reviews, awaiting CVE assignment, publish required (theneeds_publishcategory — failed exports and advisories edited since their last successful publish, i.e.republish_required), recent audit activity. Chip-driven?category=<slug>filter. There is no dedicated Reviews section: openReviewTasks surface here as an Inbox category, and the approve / request-changes decision UI lives on the advisory page itself. - CVE Assignment (
/admin/cves/) — openCveRequestTasks and per-task transition actions; modal flow for the rejection note. Also surfacesOrphanCverows for "mark rejected at cve.org". - Publication (
/admin/publications/) — publication task history, including failed exports, with retry, the redactedlast_error, and OSV / CSAF previews from the storedPublicationArtifactcontent. An Awaiting re-publication section lists published advisories withrepublish_required=True(linking to the advisory page, where Re-publish lives). Failed exports and republish-required advisories are deduped — a failed re-publish appears only under failed exports — and GHSA-linked advisories are excluded from the republish listing (they auto-re-publish, INV-GHSA-3). - Projects (
/admin/projects/) — CRUD onProjectrows (security team, mature-publisher flag, PMI sync status). - Groups (
/admin/groups/) — read-only directory of the mirrored OIDC groups: members, projects secured, and per-advisory group grants. - Audit (
/admin/audit/) — filterable read of the audit log. - Access log (
/admin/access-log/) — filtered read of the ephemeral access-log events (advisory views, auth events, notification deliveries), retention-bounded (INV-AUDIT-5). - Users (
/admin/users/) — read-only directory of accounts (groups, per-advisory grants, notification settings) with admin ban / unban and theforgetretention action; a banned account is denied login and dropped mid-session (INV-AUTH-8). - Maintenance (
/admin/maintenance/) — single-button maintenance-mode toggle with an admin-configurable banner message. While on, only global admins may mutate state; every other user's writes are paused server-side bycommon.middleware.MaintenanceModeMiddleware(INV-MAINT-1).
Every admin-console view is wrapped with @admin_required, which is
can_review (global admin only) — see
permissions.md §9.
4.11.1 No Django admin¶
Django's built-in admin (django.contrib.admin) is not installed and
/django-admin/ is not routed. It was removed as defense-in-depth: its raw
model CRUD bypassed the service layer that enforces the lifecycle, versioning,
owner-derivation, and secret-redaction invariants, and its writes landed in
Django's separate django_admin_log table rather than the app's append-only
audit log (INV-AUDIT-1) — making admin mutations
invisible to the audit trail. The admin console above is the only admin surface;
manage.py shell / dbshell remain for genuine break-glass access. is_staff
/ is_superuser are still synced to admin-group membership on login as hygiene
(INV-OIDC-3).
4.12 Internal JSON API¶
The API at /api/ exposes the same actions as the HTMX views for
script / integration use. The surface is intentionally small and
covers:
- Advisories: list + detail (filtered by
can_view); patch detail (gated bycan_edit). - Comments: list + create for a given advisory.
- Access grants: list, create, update, revoke.
- Publication: read task status, start a publish, retry a failed task, preview the stored OSV or CSAF artifact.
- Dashboard transitions: CVE-task transition, review decision.
The complete enforcement map is in
permissions.md §9. Every
endpoint re-evaluates the predicate from advisories/permissions.py
that the corresponding HTML view does; list endpoints filter their
querysets through can_view.
4.13 Duplicate detection (similarity)¶
Off by default; enabling SIMILARITY_CHECK_ENABLED is the explicit
consent for advisory content to leave the deployment for the
configured LLM provider (INV-SIM-2).
When enabled:
- Every advisory-creation path (public intake, manual creation, GHSA
import) enqueues a background
SimilarityCheck; owners can re-run the check on demand from the advisory page. - A Postgres prefilter (trigram similarity over summary/details plus
exact alias / CVE / GHSA-id and affected-package overlap) selects
up to
SIMILARITY_CANDIDATE_LIMITsame-project candidates. The pipeline then spends at most two LLM calls per check regardless of corpus size: one to fingerprint the new report (cached inAdvisoryFingerprint, keyed on a content hash) and one to judge all candidates. - The top five matches scoring at least
SIMILARITY_MIN_CONFIDENCEare stored with a 0–100 confidence and a one-line rationale. - Results (including the in-progress state) are visible to owners only — global admins and the project security team — in a polling panel on the advisory page; collaborators and viewers never see the surface (INV-SIM-1).
- Checks judge the pinned
AdvisoryVersionpayload, never live form data (INV-SIM-4); provider errors are redacted before persistence (INV-SIM-3). - The provider client is a thin, SDK-free abstraction: the Anthropic Messages API or any OpenAI-compatible Chat Completions endpoint (including local servers — the on-prem option for embargoed content).
5. Authorization summary¶
Three roles only, ranked viewer < collaborator < owner
(INV-AUTH-2). Resolution order:
- Anonymous → no access.
- Global admin (member of
OIDC_ADMIN_GROUP) → owner everywhere. - Project security-team member → owner on that project's advisories.
- Highest matching
AdvisoryAccessGrant(direct user grant or via a group the user belongs to) →collaboratororviewer(INV-AUTH-4). - Otherwise → no access.
Owner is structural (admin or project membership) and never
grantable (INV-AUTH-3,
INV-ACCESS-4). The capability matrix
and per-state overrides (triage, review-submitted, published,
dismissed) are in permissions.md §5 and §6.
Step-up authentication. Publishing or withdrawing an advisory,
connecting/modifying the GitHub App, org-wide GHSA operations, retrying a
CVE push, and break-glass admin actions (forget user, ban/unban, toggling
maintenance mode) require a recent OIDC re-authentication
(step_up_auth_at within STEP_UP_MAX_AGE_SECONDS, default 300 s),
gated by accounts.step_up.require_step_up_or_redirect. The check is
session-scoped and is set only when the IdP returned from a
prompt=login&max_age=0 request; an ordinary sign-in does not
satisfy it. The mechanism is disabled by default in test settings
and can be turned off in dev via STEP_UP_REQUIRED=False. See
permissions.md §8.
Mature publisher. A boolean on the Project row, flippable by
admins from the admin console
(INV-PERM-2). When true, the project's
security team may publish drafts without review_status=approved,
subject only to the universal "no publish while review is submitted"
gate (INV-PERM-1,
INV-PERM-3).
6. Use cases¶
6.1 Anonymous triage submission¶
- An external researcher visits
/report/. - They pick a project from the autocomplete (or pick "I don't
know", which maps to the
unsortedsentinel) and fill summary + details in markdown. - They submit. The honeypot field is empty (real user); the form passes the per-IP rate limit; ALTCHA, if configured, validates.
submit_triage_reportcreates anAdvisory(state=triage)+ anAdvisoryIntakeMetadatarow carrying the submitting IP and User-Agent. Because the project isunsorted,needs_admin_routingis set automatically.- An audit row
ADVISORY_TRIAGE_SUBMITTEDis written; anadvisory_triage_submittednotification is queued (admin team forunsorted, project security team otherwise). - The reporter is redirected to the same thank-you page that a honeypot trip would have produced.
- An admin opens the Inbox in the admin console, reads the report,
either reassigns the project (
reassign_triage_project) to the correct project — at which point that project's security team becomes the owner — and then promotes it (promote_triage_to_draft), or dismisses it (dismiss_triage) with a non-empty reason. - On promotion the same advisory row continues forward as
state=draft; the PK, public id, and audit history are preserved (INV-LIFECYCLE-5).
6.2 Draft → review → publish (non-mature project)¶
- A project security-team member opens "New advisory" for their
project. The advisory is created in
state=draft; v1 is seeded automatically. - They fill in summary, details, affected packages and ranges,
severity, references, credits. Saving appends
AdvisoryVersionv2 with the payload snapshot. - They open "Submit for review". A
ReviewTaskis opened pinning v2;review_statusflips tosubmitted. The advisory becomes non-editable for non-admins until the review concludes. - An admin reviewer opens the review action on the advisory and
approves.
review_statusbecomesapproved,ADVISORY_REVIEW_APPROVEDis audited. - The team member clicks "Publish". The publish view checks
can_publish(owner, approved review, not dismissed), asks for a step-up re-authentication if stale, and requires the operator to re-enter the advisory'sECL-…ID (client-gated, server re-checked) to confirm they're publishing the right advisory. APublicationTaskis created pinning v2 (still latest); a Celery task is enqueued viatransaction.on_commit. - The worker builds OSV and CSAF from v2's payload, validates each
against the vendored schemas, persists
PublicationArtifactrows, clones the publication repo into a freshTemporaryDirectory, writes both files at the configured paths, commits, and pushes. - On a clean push, the worker (inside
transaction.atomic+select_for_update) flipsstatetopublished, stampspublished_at, clearsrepublish_required, finalises the task with the commit SHA, and emitsADVISORY_PUBLISHED,PUBLICATION_GIT_COMMIT,PUBLICATION_GIT_PUSH,PUBLICATION_EXPORT_COMPLETED. - Watchers receive the
advisory_publishednotification.
6.3 Mature publisher direct publish¶
Same as §6.2 but skipping steps 3–4: the project carries
is_mature_publisher=True, so the team member publishes the draft
directly. The "Publish" action remains gated by step-up, by the
typed-ID confirmation, and by "no publish while a review is submitted",
but no admin-side approval is required.
6.4 Re-publish after an edit¶
- A published advisory is opened by the project security team. Edit
appends an
AdvisoryVersion, setsrepublish_required=True, and if the team is non-admin and a previous review was approved, resetsreview_statustonone(INV-REVIEW-4). - The advisory page's "Publish" button now reads "Re-publish".
- Re-publishing runs the full pipeline against the new latest
version. A new
PublicationTaskrow is created; OSV + CSAF regenerated; a new commit appears at the same file path; priorAdvisoryVersionandPublicationArtifactrows remain immutable. stateis stillpublished; onlymodified_at,published_at(left as-is — it is "first published") and the task / artifact chain reflect the re-publish.
6.5 Dismiss a draft with an open CVE request¶
- A project security team owner opens the draft and clicks
"Dismiss" with a non-empty
dismissed_reason. The owner does not hold anassigned_cve_id, so the dismissal is allowed for them (admins may dismiss even with an assigned CVE). - Inside the dismiss service:
state=dismissed,ADVISORY_DISMISSEDaudited. cancel_open_cve_requestruns: the queuedCveRequestTaskflips tocancelled,CVE_REQUEST_CANCELLEDis audited.- If the advisory had ever carried an
assigned_cve_id(admin-only scenario),unassign_cvewould have created anOrphanCverow for the admin team to mark rejected at cve.org.
6.6 GHSA-linked advisory: link → sync → push EF CVE → publish¶
- An admin runs the org-wide GHSA sync. New GHSAs surface as
Advisory(kind=ghsa_linked)rows instate=draft, withghsa_id,ghsa_owner,ghsa_repopopulated, OSV-content fields read-only and sourced from GHSA's payload. v1 is seeded; the first sync may append v2 with refreshed content. - The admin (or project security team) requests a CVE for the
advisory through the normal flow; the admin team transitions the
CveRequestTasktoreservedwith the EF-assigned CVE id. assigned_cve_idis set; because the advisory is GHSA-linked AdvisoryHub queues aGhsaCvePushTaskthat writes the CVE back to the upstream GHSA. Success / failure / conflict is recorded (GHSA_CVE_PUSH_SUCCEEDED/_FAILED/_CONFLICT_DETECTED).- When the upstream GHSA is published and the team is ready, they
click "Publish". The publish service first calls
ghsa.services.refresh_for_publishto pull the latest GHSA metadata; if anything changed, a newAdvisoryVersionis appended. The publication task pins the (possibly new) latest version and runs the standard OSV + CSAF + Git pipeline.
6.7 Access grant via group + invitation redemption¶
- An owner opens the access page on an advisory.
- They grant
collaboratorto a Django group that mirrors an IdP group of external contributors. The grant row is unique on(advisory, group, group_id); an existing grant for the same group updates in place.ACCESS_GRANTEDis audited. - They invite a new external collaborator by email. An invitation
email is queued with a single-use token; the
PendingInvitationrow carries the targetpermission, the token, and a 14-day expiry.INVITATION_CREATEDis audited. - The invitee signs in via OIDC; their email matches the
invitation case-insensitively.
redeem_invitations_for_usercreates the grant, stampsredeemed_at/redeemed_by, and emitsINVITATION_REDEEMED. From this point the user holds the grantedcollaboratorrank on the advisory; group membership in the IdP can layer further grants on top.
6.8 Comment with @-mention¶
- A collaborator opens the advisory and posts a comment
mentioning
@aliceand@bob@example.org. - The markdown is rendered through the nh3 allowlist and saved
to
AdvisoryComment.body;CommentVersionv1 carries the same body;COMMENT_CREATEDis audited. notifications.tasks.send_comment_emailis queued. At send time recipients are recomputed: each mentioned user is checked against the per-advisory access list (and against comment visibility for internal comments); the per-advisory and per-usercomments_leveldecides whether unmentioned watchers also get an email.- Mentioned recipients receive a
comment_mentionemail; unmentioned recipients get acommentemail; each user receives at most one email per comment. - Alice edits the comment to fix a typo. A new
CommentVersionrow is written; the visiblebodyis updated;edited_atis stamped;COMMENT_EDITEDis audited.
7. Non-functional requirements¶
7.1 Security¶
- Authorization is enforced server-side on every view, every API endpoint, and every Celery task (INV-AUTH-1). Templates are render-only and never make access decisions.
- The IdP is the authority for group membership; AdvisoryHub mirrors it on every login and never trusts client-submitted group data (INV-OIDC-1, INV-OIDC-2).
- Owner is structural; the grant API rejects
permission="owner"(INV-AUTH-3, INV-ACCESS-4). - CSRF protection is enabled via
CsrfViewMiddlewareon every state-changing endpoint; the public intake/report/form inherits the same protection (the JSON project picker is GET-only and cache-controlled). - Markdown bodies (advisory
details, comments) are sanitised at render time through a strict nh3 allowlist; rendered HTML is never persisted. - Step-up authentication gates publication and GitHub App configuration.
- Cookies are secure in production (
SESSION_COOKIE_SECURE,CSRF_COOKIE_SECURE),X_FRAME_OPTIONS=DENY, content-type sniffing is disabled. - A nonce-based
script-src 'strict-dynamic'Content-Security-Policy (django-csp) is enforced by default;style-src 'self'additionally forbids inline styles, and a fixedPermissions-Policyis emitted alongside.CSP_REPORT_ONLY=Truefalls back to Report-Only while diagnosing a violation. - Secrets — Git tokens, SSH key paths, GitHub App private keys, the ALTCHA HMAC key, OIDC client secrets — are never persisted into audit metadata, task error strings, notification bodies, or artifact rows (INV-SECRET-1, INV-SECRET-2, INV-SECRET-3).
7.2 Privacy¶
- Inside AdvisoryHub, "published" grants no implicit read access: every advisory remains gated by the same explicit grants as a draft (INV-AUTH-7).
- List endpoints, filters, and search totals scope to advisories the caller can see — counts never leak the existence of inaccessible rows (INV-PRIVACY-1).
- Notification recipients are recomputed at send time so revoked grants drop from the queue (INV-PRIVACY-2).
- The triage form has no reporter-email field; anonymous reports cannot later be re-associated by claiming the email (INV-INTAKE-2).
- Intake-only PII (submitter IP, User-Agent, display name) lives on
the
AdvisoryIntakeMetadatasidecar and can be cleared via theforget_userretention command without removing the advisory. - Other users' email addresses are owner-only: collaborators and
viewers see display names, with the email rendered masked
(
a•••@example.org) on every surface — rendered pages, the@-mention autocomplete, and the JSON API. A user always sees their own email (INV-PRIVACY-4).
7.3 Auditability¶
- Every governance action recorded in
Actionis audited exactly once at the moment it succeeds (INV-AUDIT-3). - Audit history is append-only at both layers (model guard +
Postgres trigger); deleting it requires a follow-up migration that
itself appears in
git log(INV-AUDIT-1, INV-IMPL-2). - Web-originated entries capture IP and User-Agent (INV-AUDIT-4).
- Audit metadata is redacted at every entry point (INV-AUDIT-2).
7.4 Reliability & concurrency¶
- The publish service serialises concurrent publishers on the same
advisory with
select_for_updateand refuses a second attempt while one is in flight (INV-CONCURRENCY-1). - The state flip, task finalisation,
published_at, and audit emissions share a singletransaction.atomicblock (INV-PUB-4). - Celery enqueues happen on
transaction.on_commit, eliminating ghost queue entries from rolled-back callers (INV-PUB-5). - Each publish attempt clones into a fresh
TemporaryDirectory(INV-PUB-1) — no shared mutable checkout, no race between concurrent publishes.
7.5 Operability¶
/healthzanswers 200 whenever the Django process is up./readyzchecks the database, the cache, and (whenREADYZ_INCLUDE_PUB_REPO=True) the publication remote; returns 503 with the failing check names otherwise.- Logs are single-line JSON to stderr, with a per-request id from
common.middleware.RequestIDMiddleware; the format is switchable to plain text viaLOG_FORMAT=plain. - Sentry is initialised when
SENTRY_DSNis set. - Prometheus metrics are exposed at
/metrics(django-prometheus defaults plus the customadvisoryhub_publication_*,advisoryhub_celery_task_*, andadvisoryhub_backlogseries; the worker exports its own series on a separate port). A dev/demo Prometheus + Grafana stack (opt-inobservabilitycompose profile) ships example dashboards, alert rules, and documented SLOs (availability ≥ 99.5%, p95 latency < 1s, publication success ≥ 90%). In production the operator scrapes/metricswith their own Prometheus — AdvisoryHub ships no production monitoring infrastructure of its own. - Public intake is rate-limited per-IP for anonymous submitters
and per-user for authenticated ones; the global toggle
RATELIMIT_ENABLEexists for tests and local debugging. - The retention commands
prune_auditandforget_userhandle long-term audit hygiene and per-user PII scrubbing respectively; each records its own run on the durable ledger (AUDIT_PRUNED/USER_FORGOTTEN). - A
seed_demomanagement command builds the dev fixture data used by the docker-compose dev environment.
7.6 Internationalisation¶
The UI carries English and French locales (LANGUAGES = [("en", …),
("fr", …)]) with locale-aware middleware between sessions and
CommonMiddleware. Locale files live under locale/.
8. Cross-reference index¶
invariant.md— load-bearing rules, stable IDs.advisory-lifecycle.md— state diagrams, transition tables, publication sequence diagram.permissions.md— actors, roles, full capability matrix, enforcement surfaces.architecture.md— technology stack, internal structure, operations, testing.../../CLAUDE.md— agent-facing operational notes (app layout, common commands, persistence rules).../../README.md— setup instructions.