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