Development · Architecture

Architecture

A Cargo workspace contains the root azdocs package (shared lib.rs plus a thin CLI main.rs) and desktop/src-tauri (azdocs-desktop). Both use one lockfile and target directory; the desktop depends on the shared library.

Dependency flow

Strictly one-way:

flowchart TD
    cli[cli.rs<br/>clap surface] --> commands[commands/*]
    commands --> collect[collect/*]
    commands --> report[report/*]
    commands --> diagram[diagram/*]
    commands --> tui[tui/*]
    desktop[desktop/src-tauri<br/>typed Tauri IPC] --> store
    desktop --> collect
    collect --> arg[arg/*<br/>Resource Graph client]
    arg --> auth[auth/*<br/>TokenProvider]
    collect --> store[(store/*<br/>SQLite)]
    report --> store
    diagram --> store
    tui --> store
    collect --> qp[querypack/*]
    store --- model[model/*]
    report --> context[ReportContext]
    context --> print[PrintDocument<br/>PDF + DOCX composition]
    print --> pdf[Typst PDF renderer]
    print --> docx[OOXML DOCX renderer]

The load-bearing rule: report, diagram, and TUI code read only from SQLite, never the network. Stored-evidence exploration and exports work offline, which enables golden-file testing and air-gapped report generation. Desktop connection tests, collection and website capture are explicit online paths; see Website screenshots.

The collect pipeline

flowchart LR
    qp[Query pack<br/>TOML, embedded] --> runner[Bounded-concurrency<br/>runner]
    runner -->|rows| ingest[ingest.rs<br/>route to tables]
    ingest --> db[(SQLite)]
    db --> extract[extractors.rs<br/>edges from properties]
    db --> audit[audit.rs<br/>required-tags findings]
    extract --> db
    audit --> db

Module map

PathPurpose
src/cli.rsThe whole clap surface — the contract for every command
src/config.rs, src/config/Resolved runtime settings, versioned editable documents, inheritance, atomic conflict-checked TOML edits and native secret-store abstraction (deny_unknown_fields)
src/auth/TokenProvider, cached OAuth2 credentials flow and shared advisory Azure RBAC diagnostics
src/arg/ARG client: $skipToken pagination, 429 backoff, quota-header pacing
src/querypack/QueryDef TOML model; built-ins embedded, user queries.d/ merged by name
src/labels/Every user-facing string, typed from data/labels/en.toml; user files deep-merged by name
src/store/All SQL. Versioned migrations, snapshot-scoped tables, cascade delete
src/model/Plain data types + azure_types.rs display names
src/collect/Runner, ingest.rs, extractors.rs, audit.rs, and website endpoint discovery / Front Door enrichment in websites.rs
src/report/ReportContext → all report data; assessment.rs composes assessments and references using the shared PrintDocument blocks in document.rs, while markdown / html / site / csv / xlsx consume ReportContext directly. Styled outputs use BrandingContext. governance.rs owns the tag thresholds and the analysis applying them, shared with the desktop.
src/diagram/EstateGraph builders (incl. per-VNet/per-RG fan-out) → page (A4 fractions, density rungs) → layout (measure/justify) → route (orthogonal connectors) → mermaid / drawio (single + workbook) / svg / png emitters
src/tui/ratatui browse; App is a pure state machine, ui.rs renders it
desktop/src-tauri/Thin Tauri v2 boundary; opens the shared Store per command and maps core models to serialisable DTOs. topology.rs builds the explorer’s view-ready relationship graphs (estate lanes, group drill-in with folding, ×N aggregation and cross-group ghost stubs, bounded-depth neighbourhoods) with honest drawn/folded/aggregated counts
desktop/src/React/TypeScript estate explorer and lazy-loaded Cytoscape.js relationship canvas; topology-layout.ts owns deterministic zones/cameras, topology-presentation.ts owns synthetic boundary ports and DOM label placement, and CytoscapeResourceGraph.tsx coordinates paint and interaction. No direct file, database, credential-store or Azure access; new secret entry is a write-only Rust command input. See Desktop map.

Design decisions

ARM ids are lowercased at every join. ARG returns inconsistent casing across queries; every key id goes through model::normalize_arm_id, original casing kept in display_id. Skipping this creates phantom duplicate resources — the classic Azure inventory bug.

Edges are derived in Rust, not queried. collect/extractors.rs walks the already-stored properties JSON: per-type handlers for the network/compute chain (VNet → subnets/peerings, NIC → subnet/NSG/VM, private endpoint → target, load balancer, application gateway, VMSS, AKS, App Service, storage and key-vault network ACLs) plus two generic passes that apply to every resource — child types link to the ARM parent their id nests under, and identity.userAssignedIdentities links to the managed identity. Zero extra API calls, and extraction itself is an offline Rust post-pass.

Auth stays hand-rolled behind TokenProvider. The client-credentials flow is one POST. azure_identity was rejected for API churn and unneeded surface. The TokenProvider trait (static dispatch) keeps it swappable and testable.

Queries are data, not code. A new audit check is a TOML file. Routing in collect/ingest.rs: the three core inventory queries fill typed tables, other inventory rows land in query_results for report tables, finding rows become findings. Display names, themes and labels follow the same rule: embedded TOML, user overrides from the config dir, no branch on a name in Rust.

One graph, many emitters. Diagram builders produce a single EstateGraph (typed nodes with parent containment + styled edges); the Mermaid and draw.io emitters both consume it. Report emitters share one ReportContext. The two native print formats go one step further: report/assessment.rs turns that data, branding and diagram bundle into an ordered PrintDocument, then the Typst and OOXML backends render the same semantic block stream. Content, hierarchy, labels, captions and asset placement therefore have one edit point; only native layout mechanics remain renderer-specific. report/analysis.rs groups stored findings without losing occurrences or severities, resolves typed relationships and selects distinct group studies. The main assessment and the optional technical reference have separate compositions in report/assessment.rs. The reference includes every resource but selects operational configuration through data/reference_fields.toml, with wording in the labels pack. Findings are counted by check, resource and severity; query coverage replaces raw evidence tables. Full data remains in SQLite and the data exports. Non-print report composition is unchanged.

Report selection is not a rendering engine. diagram/graph/assessment.rs selects and aggregates connection-focused EstateGraphs. diagram/assets.rs renders them through the established page, layout, route, icons, svg and png modules. Fix geometry and label fitting there, never in PDF/DOCX or a parallel SVG/card renderer.

A judgement is computed once, and travels as a verdict. Tag governance is the worked example: report/governance.rs holds HEALTHY_TAG_COVERAGE_PERCENT and FLAGGED_NON_COMPLIANT_SHARE and the analysis that applies them, over the stored missing_required_tags findings rather than whatever required_tags happens to say today. ReportContext.governance renders the print and Markdown Governance chapter; EstateSnapshot.governance renders the explorer’s workspace. Both receive healthy and flagged booleans, not the thresholds — sending the numbers is what let the frontend keep its own copy of the rule.

Tenant configuration boundary

flowchart LR
    settings[Desktop Settings / config CLI] --> doc[ConfigDocument: editable values + revision]
    doc --> resolved[Config: selected tenant + inherited defaults]
    resolved -->|online only| secret[SecretStore: native store / explicit env reference]
    secret --> provider[TokenProvider]
    provider --> diagnostic[Shared ARM permission diagnostic]
    resolved --> store[Store: central tenant filter]
    store --> offline[ReportContext / diagrams / TUI / desktop DTOs]

SettingsValues is secret-free and sparse override structs preserve absent vs explicit-empty list semantics. ConfigDocument owns raw comments and legacy secrets, does not implement Debug, and sanitises parse errors. Runtime resolution never reads the secret store. Only live operations resolve credentials. SecretStore is injectable in tests; the native implementation uses keyring 4’s v1 API feature. Config writes stage and verify new secret entries, lock/recheck file revisions, write a protected backup, and atomically replace TOML. Failed writes roll back new entries. Previous entries remain usable by backups and copied configs.

Rust-generated DTOs return redacted settings, summaries, validation errors and connection diagnostics. Existing secrets and tokens never go to the webview. The frontend can submit a newly typed secret once; it clears the field after submission. Draft tests may return an opaque token for a Rust-only secret held for at most 15 minutes. Identity/source changes, discard and context switching invalidate pending secrets. No new credential is written to TOML, logs or DTOs.

Store::with_tenant owns filtering for history, latest, comparison and deletion. Cross-tenant comparisons are rejected even when explicit IDs are supplied. Snapshot rows remain unchanged; profile references are not historical keys. Reporting resolves branding against the snapshot’s tenant profile and defaults when that profile is absent. All SQL remains under src/store/.

The desktop’s Session holds config path, tenant ID, DB path and resolved labels. Collection/capture acquire the context lease and clone operation context before starting; exports clone their context. Frontend generation guards reject stale snapshot and draft-test results. Config/tenant preferences persist locally and are distinct from the editable TOML and application appearance preference.

Imported from the azdocs repository during this build.View source on GitHub