Skip to content

UML diagrams (generated)

Regenerated from the actual code by pyreverse on every docs build (make docs, CI). Never edit by hand — they cannot be stale.

Several diagrams, one per group of modules, rather than one for the whole package. A single dump of revalid is 101 classes joined by 31 relations, which is a wall of disconnected boxes and says nothing about how the system fits together. The groups follow the same lines as the API reference — with app split out, because its 35 request and response models earn a diagram of their own — so the two pages can be read side by side: this page gives the shape, that one gives the docstrings.

These groups are a reading order, not a second architecture. The layered view of the system is the C4 model, which is the authority on where a module belongs, and the curated counterpart to this page is the class model — four hand-drawn diagrams that select the structures carrying a design decision and pair each with the decision it encodes. Use that page to understand the design and this one to look something up: it is exhaustive precisely because nobody chose what goes in it.

Each group pulls in one level of ancestors and associations from outside its own modules, so an edge that crosses a boundary — FindingExport pointing at the domain Finding, AdjudicateRequest at VerdictStatus — stays visible instead of being cut at the group edge. That is why a class such as Severity appears in more than one diagram: it is the same class, seen from each group that depends on it. Only classes revalid itself defines are drawn; library base types (pydantic.BaseModel and friends) are pruned, since their boxes are larger than anything in this codebase and describe the dependency rather than the system.

Overview — package dependencies

The module-level view of the whole package: who imports whom. app is the composition root and depends on nearly everything; domain sits at the bottom and depends on nothing.

classDiagram
  class revalid {
  }
  class app {
  }
  class audit {
  }
  class db {
  }
  class deltas {
  }
  class domain {
  }
  class eval {
  }
  class export {
  }
  class extract {
  }
  class findings {
  }
  class ingest {
  }
  class llm {
  }
  class pdf {
  }
  class plan {
  }
  class reports_chat {
  }
  class retest_agent {
  }
  class retest_session {
  }
  class sandbox {
  }
  class scope {
  }
  class settings {
  }
  app --> revalid
  app --> audit
  app --> db
  app --> deltas
  app --> domain
  app --> export
  app --> extract
  app --> findings
  app --> ingest
  app --> llm
  app --> pdf
  app --> plan
  app --> reports_chat
  app --> retest_agent
  app --> retest_session
  app --> sandbox
  app --> settings
  audit --> db
  audit --> domain
  db --> domain
  eval --> domain
  eval --> export
  export --> revalid
  export --> db
  export --> domain
  export --> findings
  extract --> domain
  extract --> llm
  extract --> pdf
  findings --> db
  findings --> domain
  ingest --> domain
  llm --> domain
  plan --> domain
  plan --> llm
  reports_chat --> db
  reports_chat --> findings
  reports_chat --> llm
  retest_agent --> domain
  retest_agent --> llm
  retest_agent --> sandbox
  retest_session --> db
  retest_session --> deltas
  retest_session --> domain
  retest_session --> retest_agent
  retest_session --> sandbox
  retest_session --> scope
  sandbox --> scope
  settings --> db
  settings --> domain
  settings --> llm

Core domain

domain holds the vocabulary the rest of the system agrees on — Finding and its CVSS/MITRE enrichment, plus the enums that drive every state machine (Severity, VerdictStatus, FindingStage, RetestSessionStatus). It imports nothing from the other layers, which is the point.

classDiagram
  direction LR
  class AgenticEvidence {
    command : str
    elapsed_ms : float
    exit_code : int | None
    explanation : str
    output : str
  }
  class CvssCode {
    base_score : float | None
    inferred : bool
    vector : str
  }
  class Finding {
    affected_endpoints : tuple[str, ...]
    attack_vector : str
    cvss : Optional[CvssCode]
    description : str
    impact : str
    mitre : Optional[MitreMapping]
    raw : Optional[dict[str, Any]]
    reproduction_steps : tuple[str, ...]
    severity
    title : Optional[str]
  }
  class FindingOrigin {
    name
  }
  class FindingStage {
    name
  }
  class MitreMapping {
    inferred : bool
    techniques : tuple[str, ...]
  }
  class ReportStatus {
    name
  }
  class RetestSessionStatus {
    name
  }
  class SessionEventKind {
    name
  }
  class Settings {
    api_key : str | None
    base_url : str | None
    model : Optional[str]
  }
  class Severity {
    name
  }
  class VerdictStatus {
    name
  }
  Finding --> Severity : severity

Report ingestion and understanding

The three doors into the corpus and the enrichment they share: pdf and extract for the LLM-assisted PDF path, ingest for DefectDojo JSON and manual entry, findings for versioning, notes and CVSS/MITRE enrichment, llm for the model plumbing underneath. findings and llm contribute no boxes — they are function-only modules, documented in the API reference; their work shows up here as the classes the other three exchange.

classDiagram
  direction LR
  class EnrichmentReport {
    enriched : int
    failed : int
    findings : tuple[Finding, ...]
  }
  class ExtractedFinding {
    affected_endpoints : tuple[str, ...]
    attack_vector : str
    cvss : Optional[CvssCode]
    description : str
    impact : str
    mitre : Optional[MitreMapping]
    reproduction_steps : tuple[str, ...]
    severity
    title : Optional[str]
  }
  class ExtractionFailure {
    error : str
    source_text : str
  }
  class ExtractionRegistry {
    attach(report_id: int, loop: asyncio.AbstractEventLoop, task: asyncio.Task[ExtractionReport]) None
    cancel_reason(report_id: int) str | None
    clear(report_id: int) None
    request_cancel(report_id: int, reason: str) None
  }
  class ExtractionReport {
    cancelled : bool
    failures : tuple[ExtractionFailure, ...]
    findings : tuple[Finding, ...]
  }
  class FindingTaxonomy {
    cvss_base_score : float | None
    cvss_vector : str
    mitre_techniques : tuple[str, ...]
  }
  class IngestError {
  }
  class PdfError {
  }
  class PdfPage {
    number : Optional[int]
    text : str
  }
  class PdfReport {
    page_count : Optional[int]
    pages : tuple[PdfPage, ...]
    text : Optional[str]
  }
  class Person {
    name : str
    role : str
  }
  class ReportMetadata {
    author : str
    people : tuple[Person, ...]
    product : str
    report_date : str
  }
  class Severity {
    name
  }
  ExtractedFinding --> Severity : severity

Retest goal and agentic session

plan produces the retest goal, scope parses a scope endpoint to the host the sandbox is provisioned against, sandbox is the egress-locked execution environment, retest_agent is the Pydantic AI agent with its tools, retest_session is the orchestrator owning the lifecycle, transcript and approval gate, and deltas is the transient reasoning-token channel. scope contributes no boxes — it is function-only, like findings and llm above.

classDiagram
  direction LR
  class AwaitOperator {
    message : Optional[str]
  }
  class CommandResult {
    elapsed_ms : int
    exit_code : int
    stderr : str
    stdout : str
  }
  class ConcludeOutput {
    rationale : Optional[str]
    status
  }
  class DeltaChannel {
    clear(session_id: int) None
    publish(session_id: int, chunk: str) None
    read_after(session_id: int, cursor: int) tuple[str, int]
  }
  class DockerSandbox {
    exec(command: str) CommandResult
    start(scope_hosts: tuple[str, ...]) None
    stop() None
  }
  class FakeSandbox {
    commands : list[str]
    scope_hosts : tuple[str, ...]
    started : bool
    stopped : bool
    timeouts : list[float]
    exec(command: str) CommandResult
    start(scope_hosts: tuple[str, ...]) None
    stop() None
  }
  class GeneratedGoal {
    steps : Optional[tuple[str, ...]]
  }
  class LiveSession {
    abort_retry : bool
    agent
    free_launch : bool
    human_messages : list[str]
    lock
    messages : list[ModelMessage]
    observations : list[str]
    pending_call_id : str | None
    pending_goal : list[str] | None
    sandbox
    stopped : bool
    attach_run(loop: asyncio.AbstractEventLoop, task: asyncio.Task[Any]) None
    consume_restart() bool
    detach_run() None
    drain() list[str]
    drain_goal() list[str] | None
    drain_messages() list[str]
    has_queued_messages() bool
    observe(summary: str) None
    receive_message(text: str) None
    request_cancel() bool
    request_restart() bool
    set_pending_goal(steps: list[str]) None
  }
  class RetestSessionDeps {
    drain_observations : Callable[[], list[str]]
    emit_message : Callable[[str], None]
    emit_output : Callable[[str, CommandResult], None]
    free_launch : bool
    sandbox
    scope_hosts : tuple[str, ...]
  }
  class Sandbox {
    exec(command: str)* CommandResult
    start(scope_hosts: tuple[str, ...])* None
    stop()* None
  }
  class SandboxUnavailableError {
  }
  class SessionRegistry {
    drop(session_id: int) None
    get(session_id: int) LiveSession | None
    put(session_id: int, live: LiveSession) None
  }
  class VerdictStatus {
    name
  }
  class _Buffer {
    chunks : list[str]
    dropped : int
  }
  class _TurnAbortedError {
  }
  ConcludeOutput --> VerdictStatus : status
  RetestSessionDeps --> Sandbox : sandbox
  LiveSession --> Sandbox : sandbox

Verdicts, audit and export

What comes out the far end: audit re-derives verdicts to check they still hold, export serialises a run against the published JSON schema, and eval scores a run against ground truth.

classDiagram
  direction LR
  class AuditReport {
    discrepancies : tuple[Discrepancy, ...]
    ok : bool
    reproduced : int
    total : int
  }
  class Classification {
    name
  }
  class Discrepancy {
    finding_id : int
    rederived : str
    stored : str
    verdict_id : int
  }
  class EvalReport {
    confidently_wrong : int
    correct : int
    correct_pct : float
    inconclusive : int
    mean_elapsed_ms : float
    nfr01_pass : bool
    no_verdict : int
    rows : tuple[EvalRow, ...]
    total : int
    total_elapsed_ms : float
    unmatched_findings : tuple[str, ...]
    unmatched_ground_truth : tuple[str, ...]
    weighted_error : int
    wrong : int
    wrong_on_ambiguous : int
  }
  class EvalRow {
    actual : VerdictStatus | None
    ambiguous : bool
    classification
    confidently_wrong : bool
    elapsed_ms : float
    expected
    finding : str
  }
  class Finding {
    affected_endpoints : tuple[str, ...]
    attack_vector : str
    cvss : Optional[CvssCode]
    description : str
    impact : str
    mitre : Optional[MitreMapping]
    raw : Optional[dict[str, Any]]
    reproduction_steps : tuple[str, ...]
    severity : Severity
    title : Optional[str]
  }
  class FindingExport {
    finding
    id : int
    notes : tuple[NoteExport, ...]
    report_id : int | None
    version : int
    versions : tuple[FindingVersionExport, ...]
  }
  class FindingVersionExport {
    created_at : datetime
    edited_by : str | None
    finding
    origin : str
    reason : str
    version : int
  }
  class Generator {
    tool : str
    version : str
  }
  class GroundTruth {
    findings : tuple[GroundTruthEntry, ...]
    source_report : str
    target : str
  }
  class GroundTruthEntry {
    ambiguous : bool
    expected
    finding : str
    note : str
  }
  class NoteExport {
    author : str
    body : str
    created_at : datetime
    id : int
    stage : str
  }
  class ReportExport {
    created_at : datetime
    filename : str
    finding_count : int
    id : int
    model : str
    status : str
  }
  class RunExport {
    findings : tuple[FindingExport, ...]
    generated_at : datetime
    generator
    metrics
    reports : tuple[ReportExport, ...]
    schema_version : str
    verdicts : tuple[VerdictExport, ...]
  }
  class RunMetrics {
    findings : int
    mean_elapsed_ms : float
    reports : int
    total_elapsed_ms : float
    verdicts : int
    verdicts_by_status : dict[str, int]
  }
  class VerdictExport {
    actor : str
    created_at : datetime
    evidence : AgenticEvidence | None
    finding_id : int
    id : int
    matched_indicators : tuple[str, ...]
    rationale : str
    reason_code : str
    session_id : int | None
    status
  }
  class VerdictStatus {
    name
  }
  FindingExport --> Finding : finding
  FindingVersionExport --> Finding : finding
  EvalRow --> VerdictStatus : expected
  GroundTruthEntry --> VerdictStatus : expected
  VerdictExport --> VerdictStatus : status
  EvalRow --> Classification : classification
  RunExport --> Generator : generator
  RunExport --> RunMetrics : metrics

Corpus chat, persistence and configuration

reports_chat is the corpus Q&A agent; db holds the SQLAlchemy row records (every *Record descends from Base); settings is the runtime-editable LLM backend configuration.

classDiagram
  direction LR
  class Base {
  }
  class ChatMessageRecord {
    chat_id : Mapped[int]
    content : Mapped[str]
    created_at : Mapped[datetime]
    id : Mapped[int]
    role : Mapped[str]
  }
  class ChatSessionRecord {
    created_at : Mapped[datetime]
    id : Mapped[int]
    model : Mapped[str]
    title : Mapped[str]
    updated_at : Mapped[datetime]
  }
  class CorpusOverview {
    findings_by_severity : dict[str, int]
    findings_total : int
    reports_by_status : dict[str, int]
    reports_total : int
    verdicts_by_status : dict[str, int]
    verdicts_total : int
  }
  class FindingBrief {
    affected_endpoints : list[str]
    id : int
    report_id : int | None
    severity : str
    title : str
  }
  class FindingDetail {
    affected_endpoints : list[str]
    attack_vector : str
    description : str
    id : int
    impact : str
    latest_verdict : str | None
    latest_verdict_rationale : str | None
    report_id : int | None
    reproduction_steps : list[str]
    severity : str
    title : str
  }
  class FindingNoteRecord {
    author : Mapped[str]
    body : Mapped[str]
    created_at : Mapped[datetime]
    finding_id : Mapped[int]
    id : Mapped[int]
    stage : Mapped[str]
  }
  class FindingRecord {
    created_at : Mapped[datetime]
    id : Mapped[int]
    report_id : Mapped[int | None]
  }
  class FindingSearch {
    findings : list[FindingBrief]
    shown : int
    total : int
  }
  class FindingVersionRecord {
    affected_endpoints : Mapped[list[str]]
    attack_vector : Mapped[str]
    created_at : Mapped[datetime]
    cvss : Mapped[dict[str, Any]]
    description : Mapped[str]
    edited_by : Mapped[str | None]
    finding_id : Mapped[int]
    id : Mapped[int]
    impact : Mapped[str]
    mitre : Mapped[dict[str, Any]]
    origin : Mapped[str]
    raw : Mapped[dict[str, Any]]
    reason : Mapped[str]
    reproduction_steps : Mapped[list[str]]
    severity : Mapped[str]
    title : Mapped[str]
    version : Mapped[int]
    from_domain(finding_id: int, finding: Finding) FindingVersionRecord
    to_domain() Finding
  }
  class ProbeResult {
    error : str | None
    models : tuple[str, ...]
    reachable : bool
  }
  class ReportBrief {
    archived : bool
    filename : str
    finding_count : int
    id : int
    status : str
  }
  class ReportRecord {
    archived : Mapped[bool]
    content_hash : Mapped[str | None]
    created_at : Mapped[datetime]
    doc_metadata : Mapped[dict[str, Any] | None]
    error : Mapped[str | None]
    filename : Mapped[str]
    finding_count : Mapped[int]
    id : Mapped[int]
    model : Mapped[str]
    status : Mapped[str]
  }
  class ReportsChatDeps {
    lock : AbstractContextManager[bool]
    session
  }
  class RetestSessionRecord {
    created_at : Mapped[datetime]
    ended_at : Mapped[datetime | None]
    finding_id : Mapped[int]
    free_launch : Mapped[bool]
    id : Mapped[int]
    model : Mapped[str]
    status : Mapped[str]
    verdict_rationale : Mapped[str | None]
    verdict_status : Mapped[str | None]
  }
  class SessionEventRecord {
    created_at : Mapped[datetime]
    id : Mapped[int]
    kind : Mapped[str]
    payload : Mapped[dict[str, Any]]
    seq : Mapped[int]
    session_id : Mapped[int]
  }
  class SettingsRecord {
    api_key : Mapped[str | None]
    base_url : Mapped[str | None]
    id : Mapped[int]
    model : Mapped[str]
    updated_at : Mapped[datetime]
    from_domain(cfg: Settings) SettingsRecord
    to_domain() Settings
  }
  class VerdictRecord {
    actor : Mapped[str]
    created_at : Mapped[datetime]
    evidence : Mapped[dict[str, Any] | None]
    finding_id : Mapped[int]
    id : Mapped[int]
    matched_indicators : Mapped[list[str]]
    rationale : Mapped[str]
    reason_code : Mapped[str]
    session_id : Mapped[int | None]
    status : Mapped[str]
    agentic() VerdictRecord
  }
  ChatMessageRecord --|> Base
  ChatSessionRecord --|> Base
  FindingNoteRecord --|> Base
  FindingRecord --|> Base
  FindingVersionRecord --|> Base
  ReportRecord --|> Base
  RetestSessionRecord --|> Base
  SessionEventRecord --|> Base
  SettingsRecord --|> Base
  VerdictRecord --|> Base

HTTP API surface

Every Pydantic request and response model exposed by app, the FastAPI composition root — the whole HTTP contract in one place, and by far the longest diagram here. Field-by-field documentation lives in the API reference; the value of the diagram is seeing which DTOs are projections of a domain type and which carry a domain enum across the wire.

classDiagram
  direction LR
  class AdjudicateRequest {
    rationale : str
    status
  }
  class AuditOut {
    discrepancies : list[DiscrepancyOut]
    ok : bool
    reproduced : int
    total : int
  }
  class BackendStatusOut {
    connected : bool
    model : str
  }
  class ChatDetailOut {
    messages : list[ChatMessageOut]
    of(record: ChatSessionRecord, messages: list[ChatMessageRecord]) 'ChatDetailOut'
  }
  class ChatMessageIn {
    content : Optional[str]
  }
  class ChatMessageOut {
    content : str
    created_at : datetime
    id : int
    role : str
    from_record(record: ChatMessageRecord) 'ChatMessageOut'
  }
  class ChatOut {
    created_at : datetime
    id : int
    model : str
    title : str
    updated_at : datetime
    from_record(record: ChatSessionRecord) 'ChatOut'
  }
  class ConcludeRequest {
    rationale : str
    status
  }
  class CvssIn {
    base_score : float | None
    vector : str
  }
  class DiscrepancyOut {
    finding_id : int
    rederived : str
    stored : str
    verdict_id : int
  }
  class Finding {
    affected_endpoints : tuple[str, ...]
    attack_vector : str
    cvss : Optional[CvssCode]
    description : str
    impact : str
    mitre : Optional[MitreMapping]
    raw : Optional[dict[str, Any]]
    reproduction_steps : tuple[str, ...]
    severity
    title : Optional[str]
  }
  class FindingEditIn {
    affected_endpoints : tuple[str, ...]
    attack_vector : str
    cvss : CvssIn | None
    description : str
    impact : str
    mitre : MitreIn | None
    reason : str
    reproduction_steps : tuple[str, ...]
    severity
    title : Optional[str]
    to_finding(current: Finding) Finding
  }
  class FindingOut {
    id : int
    report_id : int | None
    version : int
  }
  class FindingStage {
    name
  }
  class FindingVersionOut {
    created_at : datetime
    edited_by : str | None
    origin : str
    reason : str
    version : int
    from_record(record: FindingVersionRecord) 'FindingVersionOut'
  }
  class FreeLaunchRequest {
    enabled : bool
  }
  class GoalDraftOut {
    steps : list[str]
  }
  class GoalRequest {
    steps : list[str]
  }
  class HumanCommandRequest {
    command : Optional[str]
  }
  class ImportResult {
    enriched : int
    enrichment_failed : int
    imported : int
  }
  class MessageRequest {
    text : Optional[str]
  }
  class MitreIn {
    techniques : tuple[str, ...]
  }
  class NoteIn {
    body : Optional[str]
    stage
  }
  class NoteOut {
    author : str
    body : str
    created_at : datetime
    finding_id : int
    id : int
    stage : str
    from_record(record: FindingNoteRecord) 'NoteOut'
  }
  class ProbeIn {
    api_key : str | None
    base_url : str | None
    provider : str | None
  }
  class RejectRequest {
    reason : str
  }
  class ReportOut {
    archived : bool
    content_hash : str | None
    created_at : datetime
    error : str | None
    filename : str
    finding_count : int
    id : int
    metadata : ReportMetadata | None
    model : str
    status : str
    from_record(record: ReportRecord) 'ReportOut'
  }
  class ReportPatchIn {
    archived : bool
  }
  class RetestSessionOut {
    events : list[SessionEventOut]
    finding_id : int
    free_launch : bool
    id : int
    model : str
    status : str
    verdict_rationale : str | None
    verdict_status : str | None
    from_record(record: RetestSessionRecord, events: list[dict[str, Any]]) 'RetestSessionOut'
  }
  class RetestSessionSummary {
    created_at : datetime
    finding_id : int
    id : int
    status : str
    verdict_status : str | None
    from_record(record: RetestSessionRecord) 'RetestSessionSummary'
  }
  class SessionEventOut {
    kind : str
    payload : dict[str, Any]
    seq : int
  }
  class SettingsOut {
    api_key_hint : str | None
    api_key_set : bool
    base_url : str | None
    model : str
    from_domain(cfg: Settings) 'SettingsOut'
  }
  class SettingsUpdateIn {
    api_key : str | None
    base_url : str | None
    clear_key : bool
    model : Optional[str]
  }
  class Severity {
    name
  }
  class StartSessionRequest {
    deferred : bool
    free_launch : bool
    initial_goal : list[str] | None
    target_endpoints : list[str] | None
  }
  class VerdictOut {
    actor : str
    evidence : AgenticEvidence | None
    finding_id : int
    id : int
    matched_indicators : tuple[str, ...]
    rationale : str
    reason_code : str
    session_id : int | None
    status
    from_record(record: VerdictRecord) 'VerdictOut'
  }
  class VerdictStatus {
    name
  }
  ChatDetailOut --|> ChatOut
  FindingOut --|> Finding
  FindingVersionOut --|> Finding
  NoteIn --> FindingStage : stage
  FindingEditIn --> Severity : severity
  Finding --> Severity : severity
  AdjudicateRequest --> VerdictStatus : status
  ConcludeRequest --> VerdictStatus : status
  VerdictOut --> VerdictStatus : status