├── server.py Uvicorn entrypoint (SIGTERM handler)
│ ├── init_mongo.py MongoDB initialisation CLI (see also POST /admin/storage/prepare-mongodb, the live admin-panel equivalent)
│ └── grant_superadmin.py Bootstrap/rotate a platform superadmin against a live database
├── config.py Pydantic AppConfig — typed, validated at load time
│ ├── app.py Application factory — create_platform_app() (multi-tenant base only) + create_app() (= platform + _install_product()), _init_storage, lifespan
│ ├── dependencies.py FastAPI deps: get_storage, get_registry, require_principal(min_role) (fails closed on an org-less None role), require_authenticated (any principal, org optional — /auth/me, /auth/me/organizations, /auth/session/switch, POST /orgs). _resolve_session re-reads the user document on every session-authenticated request — a deactivated account or a revoked session generation 401s on its next call, not when its JWT expires
│ ├── mcp_server.py create_mcp_server() — FastMCP app mounted alongside the REST API
│ ├── storage/ persistence layer (was the misleadingly-named cache.py)
│ │ ├── lifecycle.py StorageLifecycle (setup/close) + CacheError/KeyAlreadyExistsError/KeyNotFoundError — domain-neutral, shared by both halves
│ │ ├── platform_base.py PlatformStorageBackend ABC — orgs, users, memberships, API keys, teams, invites, SSO/SCIM, pricing, settings, usage events, spans, audit log
│ │ ├── product_base.py ProductStorageBackend ABC — resources/namespaces, findings, jobs, custom rules, categories, templates, report links, LLM credentials, allowed models, integrations
│ │ ├── base.py StorageBackend = both halves; re-exports the error contract
│ │ ├── memory.py InMemoryStorage — dict-based, no persistence (dev/tests)
│ │ ├── mongo.py MongoBackend + ensure_mongo_indexes (org_id-scoped)
│ │ └── mongo_admin.py prepare_mongodb()/build_mongo_storage() — POST /admin/storage/prepare-mongodb and /admin/storage/switch
│ ├── (opentremor_platform) services/sessions.py revoke_user_sessions() — the only writer of User.session_token_version; advancing it invalidates every session JWT that account holds, at each one's next request. Called on deactivation, admin password/2FA reset, superadmin grant/revoke, and self-service password change
│ ├── (opentremor_platform) services/jobs_sweep.py run_stuck_job_sweep() — marks org-scoped jobs with no progress update in over jobs.stuck_timeout_minutes as failed, across every org
│ ├── (opentremor_platform) services/webhooks.py send_org_webhook() — org-scoped outbound delivery by (org_id, provider), HMAC-signed over the exact request bytes, best-effort (never raises)
│ ├── (opentremor_platform) libs/github_client.py verify_webhook_signature() + GitHubClient — RS256 app JWT, installation token, PR diff, comment, commit status; exchange_manifest_code()
│ ├── shared/ the low-level layer belonging to neither domain — imported by both
│ │ ├── exception_handlers.py Exception → JSON response mappings
│ │ ├── org_bootstrap.py run_org_bootstrap_hooks() — "an org was just created, seed what it needs"; lets the platform's three org-creation paths seed product defaults without importing anything product-side
│ │ ├── startup.py run_startup_hooks() — one-time per-app startup seeding a mounted product needs
│ │ ├── middleware.py RequestIDMiddleware + TelemetryMiddleware (pure ASGI)
│ │ ├── telemetry.py ContextVars for key_id/user_id/org_id propagation + fire_span() helper — key_id (API-key principal) and user_id (session principal) are independent, never merged, so a span/audit event can be attributed to a genuine human actor without conflating it with a machine one
│ │ └── prometheus.py /metrics endpoint + metric definitions, see Metrics
│ ├── services/ domain business logic, one module per concern
│ │ ├── analysis.py record_analysis() — shared side-effects (metrics, span, findings upsert with review's computed initial_status, review_webhook notification on auto-needs_review) for every analysis, client- or server-led
│ │ ├── audit.py record_audit_event() — fire-and-forget structured audit-event write (actor, action, target, before/after) for privileged mutations; audit_events_to_csv() for the export endpoints, see Metrics / Audit
│ │ ├── analysis_runner.py run_analysis()/count_units() — server-side analysis orchestration; resume_analysis() is the second half of run_analysis (everything after ingest_units), split out so POST /jobs/{job_id}/retry can resume a job without the original raw_input, which is never persisted
│ │ ├── review.py compute_initial_status() — open vs needs_review for a brand-new finding: a matched rule marked requires_review, or LOW/UNKNOWN LLM confidence
│ │ ├── review_webhook.py notify_needs_review() — names the event and picks which finding fields go in the payload; delivery (lookup, HMAC signing, best-effort) is the platform's send_org_webhook()
│ │ ├── report.py build_report_data() — namespace resources joined against findings-triage status; shared by the report endpoints and the GitHub webhook job
│ │ ├── rules.py get_effective_rules() — composes an org's enabled custom rules after an analyzer's base rules, grouped by category (resolved against the org's rule_categories list, alphabetical by name, "Uncategorized" fallback); shared by GET /{analyzer}/rules and the server-led analyze path
│ │ ├── rule_categories.py unique_category_id()/validate_category() — org-owned rule-category id generation + existence check
│ │ ├── team.py unique_team_id()/validate_team_membership() — org-owned team id generation + "can only add an existing org member" check, mirrors rule_categories.py
│ │ ├── template.py resolve_and_render() — picks explicit template_id > org's default template for a format > the built-in file, for every report-rendering call site; render_custom() turns any Jinja2/Python rendering error into a 422
│ │ ├── slug.py slugify()/unique_slug() — shared by rules.py (rule_id), rule_categories.py (category_id), and template.py (template_id)
│ │ ├── llm_models.py is_model_allowed(storage, llm_backend, model) — allowed-models enforcement; empty catalog == unrestricted
│ │ ├── entitlements.py The single owner of what a plan_tier means — nothing outside this module compares one to a literal. org_is_entitled() (deployment-mode gate: self_hosted always true; cloud requires a paying tier or the 100-analysis free demo), consumes_demo_quota()/has_demo_quota(), is_metered(), and the enterprise-commitment readers active_contract()/included_resources_per_month()/billable_resource_count()/contract_expired(). See Billing / quota enforcement below
│ │ ├── billing.py Degressive per-resource pricing/metering once an org is entitled and paid
│ │ ├── retention.py run_retention_sweep() — runs one sweep: times it, contains partial failure, persists state. Knows nothing of what gets swept, see Data Retention
│ │ ├── product_retention.py the analysis engine's sweep steps — severity-tiered findings purge + raw resource-content redaction, registered into the sweep above
│ │ ├── sweep_registry.py register_sweep_step()/run_steps() — how a domain contributes work to a platform-owned sweep
│ │ ├── org_purge_registry.py register_org_purge_step()/run_org_purge_steps() — how a domain gets its own org-scoped rows deleted when an organization is purged
│ │ ├── product_offboarding.py the analysis engine's org-purge step — the eight collections it keys by org_id, registered into the cascade above
│ │ ├── settings_registry.py declares a config section once; FIELD_MAP, the managed-section list, the excluded-section list and the PATCH body model all derive from it
│ │ ├── product_settings.py the analysis engine's own settings sections (the product halves of retention/server)
│ │ ├── settings_sections.py composition: imports every domain's registrations in order, then verify_complete() asserts no AppConfig section lacks a disposition
│ │ ├── org_offboarding.py purge_pending_organizations() — runs each registered purge step, then cascade-deletes the platform's own collections for every organization past its deletion grace period, preserving audit_events/usage_events, see Deleting an organization
│ │ └── settings.py with_hardcoded_platform_defaults()/apply_platform_settings() + the platform's own section declarations — the admin-settings overlay, see Configuration reference
│ ├── seed/ built-in data imported into a fresh org/platform at setup time
│ │ ├── rules.py seed_builtin_rules_for_org() — imports an analyzer's built-in ruleset into custom_rules at org setup, disabled by default
│ │ ├── categories.py seed_default_categories_for_org() — imports the 9 default rule categories at org setup (ids match the old fixed enum's values); must run before rules.py's seeding
│ │ ├── templates.py seed_builtin_templates_for_org() — imports the 3 built-in report templates into report_templates at org setup, not default
│ │ └── models.py seed_default_allowed_models() — platform-wide allowed-models catalog seeding at startup
│ │ └── base.py BaseAnalyzer ABC (+ .manifest) + AnalyzerRegistry (skips is_ready=False)
│ │ No built-in analyzer packages — installed separately,
│ │ discovered via the opentremor_core.analyzers
│ │ entry-point group at startup (see below)
│ ├── health.py GET /health (no auth)
│ ├── session.py POST /auth/register, POST/DELETE /auth/session (human login)
│ ├── auth.py POST/GET/DELETE /auth/keys (org-admin only)
│ ├── orgs.py Org info, members (now joined against team_members for each member's team names), invitation links, analyzer load/unload, LLM credentials, quota/usage, report-link-ttl
│ │ + invite_router: GET /invite/{token} (public preview), POST /invite/{token}/accept (any authenticated principal)
│ ├── resources.py Resource CRUD + namespace operations (org-scoped)
│ │ + public_router: GET /reports/{token} (no auth, time-limited)
│ ├── analyzers.py Analyzer discovery, rules, ingest, server-side analyze (org-scoped)
│ ├── rules.py POST/GET /orgs/{org_id}/rules, GET/PATCH/DELETE /orgs/{org_id}/rules/{rule_id} (custom rules CRUD, org-scoped)
│ ├── rule_categories.py POST/GET /orgs/{org_id}/rule-categories, GET/PATCH/DELETE /orgs/{org_id}/rule-categories/{category_id}, POST .../seed-defaults (org-owned rule categories, fully editable/deletable)
│ ├── teams.py POST/GET /orgs/{org_id}/teams, GET/PATCH/DELETE /orgs/{org_id}/teams/{team_id}, GET/POST /orgs/{org_id}/teams/{team_id}/members, DELETE .../members/{user_id} (pure grouping, no access-control effect; admin mutates, member reads)
│ ├── report_templates.py POST/GET /orgs/{org_id}/report-templates, GET/PATCH/DELETE /orgs/{org_id}/report-templates/{template_id}, POST .../seed-builtin, POST .../preview (custom report templates CRUD, org-scoped)
│ ├── jobs.py GET /jobs, GET /jobs/{job_id} (async-analysis status polling), POST /jobs/{job_id}/retry (resume a failed api-triggered job under a new job_id)
│ ├── findings.py GET /findings, GET /findings/summary, PATCH /findings/{hash}/{rule_id} (status incl. needs_review; setting it fires review_webhook_service)
│ ├── review_webhook.py GET/PUT/DELETE /orgs/{org_id}/integrations/review-webhook (admin-only; provider="review_webhook" on the shared integrations collection)
│ ├── integrations.py GitHub App connect/disconnect (admin), manifest-flow trigger/callback for custom per-org Apps, + public webhook receiver — commit status: failure (open CRITICAL/HIGH) > pending (needs_review) > success
│ ├── metrics.py GET /metrics/summary|spans|usage|audit, GET /metrics/audit/export (org-admin only, org-scoped)
│ ├── llm_models.py GET /models (member), GET/POST /admin/models + PATCH/DELETE /admin/models/{llm_backend}/{model} (superadmin) — allowed-models catalog, platform-wide
│ ├── admin.py GET /admin/organizations(/{org_id}), GET /admin/usage, GET /admin/metrics/summary, GET /admin/organizations/{org_id}/audit, GET /admin/audit(/export), POST /admin/users/{user_id}/superadmin, PATCH /admin/organizations/{org_id}/lock|plan|2fa-policy, DELETE /admin/organizations/{org_id} + POST .../cancel-deletion + POST /admin/organizations/purge-pending, GET/PATCH /admin/settings, POST /admin/storage/prepare-mongodb, GET /admin/storage, POST /admin/storage/switch, POST /admin/retention/run, GET /admin/retention/status (superadmin only, platform-wide)
│ ├── product_admin.py POST /admin/jobs/sweep, GET /admin/jobs/sweep-status, /admin/custom-rules CRUD — the product-owned half of the /admin surface, same prefix, separate router (superadmin only)
│ ├── pricing.py GET /pricing (public), GET/PATCH /admin/pricing (superadmin) — degressive per-resource pricing table, see billing.py
│ ├── sso.py POST/GET/PATCH/DELETE /orgs/{org_id}/sso (org-admin OIDC connection CRUD)
│ │ + public_router: POST /auth/sso/discover, GET /auth/sso/{org_id}/start, GET /auth/sso/callback (no auth — login redirect/callback, JIT provisioning)
│ └── scim.py POST/GET/PATCH/DELETE /orgs/{org_id}/scim + POST .../rotate-token (org-admin SCIM config CRUD)
│ + protocol_router: /scim/v2/{org_id}/{Users,Groups,ServiceProviderConfig,ResourceTypes,Schemas} (bearer-token auth, own dependency — never a session/API key; Groups map 1:1 to Team, group-membership changes recompute role via group_role_mappings)
│ ├── hash.py Content hash (SHA-256 after normalisation)
│ ├── auth.py Key/password hashing (bcrypt), generate_id(), session JWT create/decode
│ ├── crypto.py encrypt_secret()/decrypt_secret() — reversible Fernet encryption for stored LLM keys
│ ├── llm/ LLMClient ABC + get_llm_client() factory + Anthropic/OpenAI/Mistral implementations
│ ├── resource.py AnalysisUnit, Finding, UnitAnalysis, UnitMetadata — analyzer-agnostic, no custom to_dict, use model_dump()
│ ├── auth.py OrgRole, OrgQuota, OrgContract (enterprise commitment: included_resources_per_month/committed_amount_usd/expires_at/reference — read only via entitlements.active_contract, since it survives a downgrade), Organization, User, Membership (provisioned_via: "scim"|"sso"|"invite"|null — provenance only, no access-control effect), MembershipInfo (teams: list[str], populated from team_members by routers/orgs.py, not stored on the membership itself), ApiKeyDocument, AuthContext
│ ├── analysis.py AnalyzeRequest/AnalyzeResponse/JobAccepted/LLMCredentialCreate; JobStatus — widens the platform's JobRecord with this product's fields (namespace, analyzer, llm_backend, detections, current_resource, ...)
│ ├── findings.py FindingStatus (incl. needs_review), FindingRecord, FindingStatusUpdate, FindingsSummary
│ ├── rules.py CustomRuleCreate, CustomRuleUpdate, CustomRule (category: str, a rule_categories.category_id reference; requires_review: bool routes a match straight to needs_review), RuleCategoryCreate, RuleCategoryUpdate, RuleCategory
│ ├── teams.py TeamCreate, TeamUpdate, Team, TeamMemberAdd, TeamMemberInfo — pure grouping, team_id a slug of name, immutable
│ ├── templates.py ReportFormat, ReportTemplateCreate, ReportTemplateUpdate, ReportTemplate, ReportTemplatePreviewRequest/Response
│ ├── integrations.py GitHubIntegrationCreate/GitHubIntegrationInfo/GitHubAppInfo/GitHubAppManifestResponse
│ ├── webhooks.py ReviewWebhookCreate/ReviewWebhookInfo — the needs-review notification's config shape (has_secret only, never the raw secret)
│ ├── metrics.py Span model
│ ├── audit.py AuditEvent (actor_user_id XOR actor_key_id, before/after only the changed fields, never a secret), AuditEventList
│ ├── llm_models.py AllowedModel/AllowedModelCreate/AllowedModelUpdate — allowed-models catalog (platform-wide, no org_id)
│ ├── admin.py PlatformOrgSummary (incl. pending_deletion_at)/PlatformOrgDetail, SuperadminToggle, OrgLockUpdate, OrgPlanUpdate, OrgDeletionScheduled/OrgDeletionCancelled, OrgPurgeSummary/OrgPurgeResult/OrgPurgeError, PlatformOrgUsage/PlatformUsageSummary, PlatformOrgMetrics/PlatformMetricsSummary — platform-wide org listing/management for a superadmin
│ ├── billing.py PricingTier, PricingConfig/PricingConfigUpdate — degressive per-resource pricing, GET /pricing; UsageSummary — incl. the enterprise commitment fields (included_resources_*/overage_resources/committed_amount_usd/contract_expired)
│ ├── settings.py PlatformSettingsPatch/PlatformSettingsResponse, ExcludedSection — the GET/PATCH /admin/settings shapes; PrepareMongoDBRequest/Result, MongoPrepareState, StorageInfo, StorageSwitchRequest/Result — the storage-prep/switch shapes
│ ├── retention.py RetentionSweepResult — POST /admin/retention/run and GET /admin/retention/status response shape
│ ├── sso.py SSOConnectionCreate/Update/Info, SSODiscoverRequest/Response — org OIDC connection config (encrypted_client_secret never returned)
│ └── scim.py GroupRoleMapping, ScimConfigCreate/Update/Info, ScimTokenCreated — org SCIM provisioning config (token_hash never returned); ScimUser/GroupResource, ScimListResponse, ScimError — RFC 7643 wire shapes, outbound-only (inbound bodies parsed as loose dicts)
├── formatter.py OutputFormatter — Jinja2 SandboxedEnvironment; format_markdown/format_markdown_light/format_html render the 3 built-in files, format_custom() renders org-authored source (no template loader, autoescape keyed off format)
├── markdown_report.md.j2
├── markdown_report_light.md.j2