Skip to content

Threat model ​

Status: updated 2026-10-14 (hosted edition operations and billing) and 2026-10-13 (Bitbucket, Gitea, Forgejo and release hardening); earlier updates added GitLab, Azure DevOps and work trackers and consolidated the baseline. Reviewed with every change that adds an entry point, a write path or an agent capability. Exit criterion for such a change: every threat has a mitigation mapped to a test or control in the column "Test / control". An independent penetration test has not taken place yet.

Feature-specific analyses extend this document: triage-and-replies.md (TR, RP, CH, DT1–DT2) and fix-agent-and-sandbox.md (SB, FC, SC, FX, AC, DT5).

1. System and trust zones ​

ZoneContentsTrust
Z0 untrusted contentWebhook payloads, issue/PR text, diffs, repository files, LLM output, identity-provider responsesNever trusted; never instructions
Z1 agent runtimeLLM calls with read-only toolsNo write tokens
Z2 publisherValidated, typed outputs; installation tokensOnly path to external writes
Z3 sandboxUntrusted repository code, scanners, fix-agent testsNo tokens, no secrets, egress denied by default; separate runner service with signed jobs; Docker with runsc, Firecracker, or Kubernetes pods with the gVisor RuntimeClass
Control planeAPI, worker, Postgres, Valkey, identity providers (OIDC), observability stackAuthenticated, least privilege

Entry points: POST /webhooks/github, per-connection webhooks POST /webhooks/{gitlab,azure-devops,bitbucket,bitbucket-dc,gitea,forgejo,jira,linear,azure-boards}/:connectionId, /setup/github/*, /auth/* (GitHub and OIDC), /api/*, CLI bins on the server (keys, dsar, auth oidc test, break-glass), GitHub, GitLab, Azure DevOps, Bitbucket, Gitea, Forgejo, Jira, Linear and Atlassian/Entra OAuth responses, OIDC discovery/JWKS/token/userinfo responses, the product-telemetry endpoint (outbound, opt-in), in the hosted edition only: the public sign-up, email verification, status and sandbox-progress routes, POST /webhooks/mollie, Mollie and VIES responses, the SMTP relay and the status webhook (outbound), metrics and traces (OTLP), the dev stack ports (bound to 127.0.0.1).

2. Assets ​

AssetWhereProtectionTest / control
GitHub App private keys, client/webhook secretsprovider_apps.encrypted_credentialsAES-256-GCM envelope, context-bound (provider_apps:<id>), key rotation (ADR-0008)crypto.test.ts, encrypted-columns.test.ts, keys.int.test.ts
OIDC client secretsidentity_providers.encrypted_client_secretEncrypted with context identity_providers:<id>; returned only as a 4-character hintencrypted-columns.test.ts; oidc-identity-providers.int.test.ts
GitHub user tokensauth_sessions.encrypted_provider_tokenEncrypted, ≤ 8 h, deleted on logout/expiry; OIDC sessions store no provider tokenauth.int.test.ts
Installation tokensValkey sealed cacheEncrypted, expire ~5 min before GitHub expirymaintenance.int.test.ts (stale entries)
GitLab, Azure DevOps, Bitbucket, Gitea and Forgejo tokens, Entra client secrets, connection webhook secretsprovider_connections.encrypted_credentialsEncrypted with context provider_connections:<id>, key rotation via encrypted-columns.ts; 4-character hint; webhook secret shown onceprovider-connections.int.test.ts, encrypted-columns.test.ts
Tracker tokens (Jira API tokens and OAuth tokens, PATs, Linear keys, Entra secrets) and tracker webhook secretstracker_connections.encrypted_credentials, encrypted_webhook_secretEncrypted with context tracker_connections:<id>, key rotation covered; 4-character hint; secret shown onceapps/api/src/routes/admin/trackers.int.test.ts ("keeps credentials encrypted"), encrypted-columns.test.ts
LLM provider keysllm_credentials.encrypted_secretEncrypted, masked in responses, never logged; hosted tenants use version 3 blobs bound to the tenant (HS13)llm-credentials.int.test.ts, crypto.test.ts ("tenant keys")
Hosted SMTP password and status webhook secretsaas_settings.encrypted_secretEncrypted, registered in encryptedColumns, so quavon keys rotate covers themsaas.int.test.ts ("stores SMTP and status webhook secrets encrypted …"), encrypted-columns.test.ts
Mollie API keyEnvironment / QUAVON_MOLLIE_API_KEY_FILENever in the database, never logged; live_ keys refused outside productionhosted/config/src/config.test.ts
Tenant data (repos, PRs, issues, people, findings, analytics)Postgres tenant tablesRLS ENABLE + FORCE, withTenantrls.int.test.ts, isolation suites (13b-A)
Repository source code in the code indexcode_chunks.content and code_chunks.search (plaintext), code_symbols, code_index_files pathsRLS ENABLE + FORCE, withTenant; deleted when the index mode is set to off, the repository is removed or the tenant is deleted; at rest only volume or disk encryption (CX1)rls.int.test.ts, code-index.int.test.ts (worker: tenancy, purge)
Session and CSRF tokensBrowser cookies; only SHA-256 hashes storedHttpOnly/Secure/SameSite=Lax, double-submit CSRFauth.int.test.ts
Audit logaudit_logAppend-only trigger, hash chain per tenant and instanceaudit.int.test.ts
QUAVON_ENCRYPTION_KEYEnvironment / *_FILENever in the database; rotation procedurekeys.int.test.ts, ADR-0008
Release artefacts (images, chart, SBOM)CI, registryPinned base digests, SBOM, image scan; signing planned.github/workflows/ci.yml

3. STRIDE &ZeroWidthSpace;

#ThreatMitigationTest / control
S1Forged webhooksHMAC-SHA256 over raw body with the app's secret; unknown app → 401; host header only selects the secretwebhooks.int.test.ts
S2Session hijacking / fixationOpaque random tokens, hashes at rest, rotation at login, HttpOnly/Secure/SameSite=Lax, 8 h capauth.int.test.ts, process E2E
S3Login CSRF / code injectionstate bound to a pre-login cookie, PKCE S256, single-use Valkey entryauth.int.test.ts
S4Setup takeoverOne-time hashed setup tokens with purpose setup, CSRF-bound manifest state, single setup claimsetup.int.test.ts
S5Break-glass abuseServer-side CLI only, single use, 15 min link, 1 h session, instance administration only, auditedcontract.test.ts, process E2E
S6Setup takeover via the existing-app registrationPOST /setup/github/register requires an unused setup token (checked before any outbound call), a matching Origin, a bare hostname (no IPs, ports or userinfo), refuses when a review app exists, and in the enterprise edition rejects private hosts; token state endpoint does not distinguish unknown, used and expired tokens; concurrent registrations serialised (13b-C)setup.int.test.ts
T1Tampering with audit historyAppend-only trigger for all roles, hash chain, /audit/verifyaudit.int.test.ts
T2Tampering with ciphertextsAES-GCM authentication with context bindingcrypto.test.ts
T3PR changes the reviewer's rulesRule files and .quavon/config.yml are read only from the PR's base commit; rules can only add expectations, never suppress findings or change format, severity or thresholds; file-rule severity capped at high; config disable cannot remove UI rules; rule text never reaches the summary model; YAML loaded without aliases. Residual risk: PRs targeting a base branch the contributor controlsrules-context.test.ts, rules unit tests, review-publish.int.test.ts
T4Script injection in the dashboard via model output or repository textSPA renders previews and findings as text or allowlisted React elements only (no raw HTML), links only for http(s); Caddy CSP default-src 'self' without inline scripts or eval, frame-ancestors 'none', Referrer-Policy: no-referrerPlaywright with the production CSP; Caddyfile
R1Repudiation of admin actionsEvery mutation audited with actor, action, target, IP (real client IP via TRUST_PROXY)generated route-table audit test (routes.int.test.ts)
I1Cross-tenant data accessRLS forced on every tenant table; tenant-scoped routes authorised per request; system access restricted (ADR-0006); IDOR fuzzing over every route with idsrls.int.test.ts, route-table matrix, idor.int.test.ts (13b-A)
I2Unpublished findings exposed on public reposRepo roles require org membership; check-run output on public repos shows counts onlyauth.test.ts (isAllowed); publisher.int.test.ts
I3Secrets in logs or responsesPino redaction, no request bodies logged, secrets masked, gitleaks in CI; entropy-based redaction for unknown token formats (13b-C)redact.test.ts, admin API tests, secrets.test.ts (tooling)
I4SSRF via configurable URLs (LLM base URLs, GHES hosts, ICS feeds, OIDC issuers, telemetry endpoint)https required except explicitly private model hosts; redirects refused; enterprise edition rejects private, loopback, link-local, CGNAT and metadata addresses at save time and inside the socket DNS lookup (no rebinding window); guarded fetch for GitHub App validation in the enterprise edition (13b-C)llm-credentials.int.test.ts, endpoint-guard.test.ts, validate.test.ts
I5Tenant uses the operator's model credentialsProvider clients always get the tenant key and base URL explicitly; Bedrock ambient credentials refused; env fallback testedclient.test.ts
I6Installation token or host compromise via repository checkoutToken only via a credential helper bound to the expected host, allowlisted git environment, GIT_ALLOW_PROTOCOL=https, no redirects/hooks/fsmonitor/submodules/symlinks, fsck, size and time limits, path traversal and .git/ reads refused; worker image non-root with read-only root filesystem and /tmp tmpfscheckout.test.ts, workspace.test.ts, container E2E
I7Forged or spoofed publication stateReconcile trusts markers only on reviews authored by the app's own bot login; per-comment markers map comments to findingspublisher.int.test.ts
I8Cross-tenant leakage through Valkey keys or cachesDocumented key prefixes; tenant-scoped keys embed the tenant id; sandbox and egress ACL users cannot read quavon:*valkey-users.acl; Valkey isolation suite (13b-A)
D1Webhook floods / unsigned spamBody limit, signature check before DB writes, per-IP limit on failed deliveries, negative cache for unknown apps, outbox absorbs burstssecurity.int.test.ts
D2Brute force on auth endpointsRate limits per IP (/auth, /setup) and per session (/api), break-glass fails closed when Valkey is down; sliding-window limiter and Valkey command timeout (13b-C)security.int.test.ts
D3GitHub secondary-limit bansBudget gate per installation, documented 403/429 handlingbudget.int.test.ts (queue), provider tests
D4Lost events / duplicate effectsTransactional outbox, idempotent handlers, chaos testchaos.int.test.ts
E1Privilege escalation via missing checksEvery route must declare a permission or the API refuses to start; matrix test per routecontract.test.ts, routes.int.test.ts
E2Over-privileged GitHub accessReview app without contents: write; Fixer app separate and opt-in; scoped installation tokensprovider tests, fix-pr.int.test.ts
E3BYPASSRLS misuse@quavon/db/system import allowlist (lint), named operations on spans; quavon_app cannot SET ROLE, disable RLS or truncate the audit logESLint rule, rls.int.test.ts, RLS bypass suite (13b-A)

4. OWASP Top 10 for LLM Applications &ZeroWidthSpace;

The rows follow the 2025 list, which is the current edition on genai.owasp.org as of 2026-10-13 and the basis of promptfoo's owasp:llm plugins. A 2026 edition (reported as published on 3 or 4 August 2026) is described by secondary sources, but its official numbering could not be verified on genai.owasp.org: the project page still lists the 2025 entries, and secondary sources disagree on the position of "Hidden Context Exposure" (08 or 09). The 2026 column below uses the most frequently reported order and is unverified; re-check it before publishing externally.

2025Risk2026 (unverified)ControlTest / control
LLM01Prompt injectionLLM01 Prompt InjectionZ0 content wrapped with untrusted() (random boundary, invisible/bidi characters stripped); reader/writer split for replies; safe outputs; no write tokens in agent context; containment rather than detectionuntrusted.test.ts; eval injection items (violations gate 0); redteam-smoke-v1 and E2E red team (13b-A)
LLM02Sensitive information disclosureLLM02 Sensitive Information Disclosureredact() before logging and before prompt logs; provider error bodies never stored; raw prompts only with tenant debug opt-in, encrypted, TTL ≤ 7 days; secret scan of outgoing bodiesredact.test.ts, secrets.test.ts (publisher)
LLM03Supply chainLLM04 Supply ChainExact-pinned dependencies, licence gate, OSV-Scanner, image scan, SBOM; model providers chosen and keyed by the tenant; no third-party model weights shippedlicenses.test.ts, vulns.test.ts, ci.yml (sbom, image-scan)
LLM04Data and model poisoningLLM05 Data and Model PoisoningNo training on customer data; feedback learning only adjusts bounded per-repository parameters, counts only actors with write permission, never suppresses security findings (FB1–FB3); rules only from the base branch (T3)learning.test.ts, apply.test.ts, feedback.int.test.ts, replay gate
LLM05Improper output handlingLLM10 Improper Output HandlingPublisher validates findings, maps anchors to the head diff, offers a one-click suggestion only for the exact lines the model quoted (found once near the finding in the head file, commentable as a range) and only when no replaced line disappears without a counterpart, otherwise shows the code as a plain block, sanitises markdown (HTML stripped, mentions neutralised, link hosts allowlisted, no images), blocks and audits outgoing bodies with secrets, labels every comment as automatedsanitize.test.ts, render.test.ts, suggestion.test.ts (incident of a committed suggestion that deleted a required line), publisher.int.test.ts; publisher-output assertions in the red team (13b-A); smoke item smoke-v1/034 under the fix-validity gate. Residual risk: a replacement that rewrites a quoted line into a wrong line with the same leading name, or a deletion the model marks as intended, is still committable; the author reviews every suggestion before committing it
LLM06Excessive agencyLLM03 Excessive AgencyReview agents only read; GitHub writes only through the publisher (lint rule), never APPROVE/REQUEST_CHANGES; shadow and approval modes; Fixer tier opt-in; assignment apply behind legal sign-offtools.test.ts, publisher.int.test.ts, assignment-flow.int.test.ts, ESLint rule
LLM07System prompt leakageLLM08 Hidden Context ExposureNo secrets, credentials or tenant data of other tenants in prompts or tool schemas; system-prompt fragments are a red-team violation kindredact.test.ts; red-team canary and system-prompt checks (13b-A)
LLM08Vector and embedding weaknessesLLM09 Vector and Embedding WeaknessesTenant-scoped embeddings under RLS; search filtered by tenant and repositoryrls.int.test.ts; embedding isolation suite (13b-A)
LLM09MisinformationLLM07 MisinformationReplies need verified citations from the same repository or abstain; acceptance results marked inferred and never blocking; review never approves; visible automated-output noticereply.test.ts, check.test.ts (acceptance), render.test.ts
LLM10Unbounded consumptionLLM06 Unbounded ConsumptionTool-loop step/token/wall-clock limits; rolling EUR budgets per tenant and repository with downgrade or block; atomic budget reservation under concurrency (13b-C); queue fairness; budget alerts (11b)budget.test.ts (llm), budget.int.test.ts (queue)

5. Feature summaries &ZeroWidthSpace;

Sandbox, deterministic scanners, Issue→Draft PR and acceptance criteria (details in fix-agent-and-sandbox.md):

#ThreatMitigationTest / control
SBRepository code escapes the sandbox, reads secrets or reaches the networkSeparate runner service with signed jobs and its own Valkey ACL user; images by digest; gVisor runsc (systrap without KVM) or Firecracker; no tokens, no .git, egress none by default; limits enforced by the runnerescape.int.test.ts (runc locally, runsc/Firecracker in CI), signing.test.ts
FXFix agent pushes harmful or unauthorised changesTrigger by write users only (re-checked at publish); fixed commands from policy; patch from an in-memory overlay validated outside the sandbox; Fixer app, new bot branch, draft PR only, approval by defaultpatch.test.ts, fix-pr.int.test.ts, fix.int.test.ts
ACInjected issue text skews acceptance resultsDeterministic extraction first, inferred marking, untrusted wrapping, evidence verified against the diff, never blockingextract.test.ts, check.test.ts

Feedback learning:

#ThreatMitigationTest / control
FB1Feedback poisoning by outsidersDismissal signals count only from actors with write permission; reactions only on private repositories; suppressions are proposals until a maintainer activates them, expire, never apply to security findings or critical severityfeedback.int.test.ts, apply.test.ts
FB2Calibration drift silences a categoryMinimum sample size per scope; shadow mode; offline replay gatereplay.test.ts, replay eval gate
FB3Cross-tenant learningTenant tables under forced RLS; no shared parameters; feedback never trains modelsrls.int.test.ts

Expertise-aware assignment (ADR-0014):

#ThreatMitigationTest / control
AS1SSRF through a user-supplied ICS feed URLhttps only, no credentials in the URL, guarded fetch in every edition, redirects refused, 10 s timeout, 1 MiB cap, calendar content types only; URL encrypted, never logged or returnedavailability/index.test.ts, assign.int.test.ts
AS2Mention spam or markdown injection through suggestion commentsFixed templates and typed facts; logins, patterns and teams as code spans; sanitised, secret-scanned, marker-reconciled; at most one comment per subjectexplain/index.test.ts, assignment.int.test.ts (publisher)
AS3Per-person inference from shadow or stored dataOnly selected people with facts stored, no scores in any API; shadow rows visible to self-hosted instance admins only and to nobody in the hosted edition; per-person endpoints 404 in team-only and works-council modeprivacy/index.int.test.ts, assignment.int.test.ts (API)
AS4Assignment leaves shadow or writes without the legal and co-determination gateseffectiveAssignmentMode checked in the API, the job and the publisher; works-council mode without agreement is off; apply needs a per-repository opt-inassignment.test.ts (shared types), assignment-flow.int.test.ts

Generic OIDC login (ADR-0013):

#ThreatMitigationTest / control
ID1Group-name collision across identity providers grants rolesoidcGrants uses only mappings of the session's identity provideridentity.test.ts ("ignores mappings of another identity provider")
ID2SSRF through discovery-supplied endpoints (JWKS, token, userinfo)Every OIDC call through the guarded customFetch, redirects refused; enterprise: https and no private addresses; issuer normalised before the uniqueness checkendpoint-guard.test.ts; oidc-login.int.test.ts (enterprise private issuer), oidc-check.test.ts, oidc-identity-providers.int.test.ts
ID3Login CSRF, code injection, token substitutionState, nonce and PKCE S256 bound to a pre-login cookie; iss (RFC 9207), aud, azp, exp and signature validated; only response_type=code; plain PKCE never sentoidc-login.int.test.ts (wrong or replayed state, missing cookie, wrong nonce, wrong iss parameter and claim, wrong aud, foreign azp, expired token), oidc-dex.int.test.ts
ID4Stale IdP groups or disabled provider keep accessGrants computed per request from session groups and current mappings; session ≤ 8 h; disabled provider rejects sessions; deleting a provider cascades to its sessionsoidc-login.int.test.ts
ID5A provider bound to one tenant is used in anotherMapping, listing and grants require enabled AND (bound_tenant_id IS NULL OR = tenant); enterprise edition requires a bindingoidc-login.int.test.ts ("ignores mappings once the provider is bound to another tenant"), oidc-identity-providers.int.test.ts
ID6Group overflow or truncation changes the granting groupMore than 200 groups or a group longer than 256 characters rejects the loginidentity.test.ts, oidc.test.ts, oidc-login.int.test.ts

Team analytics:

#ThreatMitigationTest / control
AN1Per-person inference via small groups or differencing of overlapping scopesk-anonymity per point and dimension cell (k ≥ 5), no head counts, no person dimension; tenant and team scopes only with tenant:manage; differencing is a documented residual riskanalytics.test.ts; packages/db/src/analytics/analytics.int.test.ts (4 vs 5 authors, dimension cells, shared authors, bots), apps/api/src/routes/analytics/analytics.int.test.ts, apps/web/e2e/analytics.spec.ts
AN2CSV formula injection in exportsEscaping as in the audit export; cache-control: no-store; suppressed values never exportedaudit.test.ts (escaping); apps/api/src/routes/analytics/analytics.int.test.ts ("exports CSV with formula escaping and without suppressed points")
AN3Cost data exposureCost metrics only on /costs routes with tenant:manage / repo:manage; never split by personapps/api/src/routes/analytics/analytics.int.test.ts ("separates cost metrics from team metrics"); route-table fixtures
AN4Self view becomes a per-person view for othersSelf view only for the person, opt-in, never in works-council or team-only mode; 404 for every other case including administratorsanalytics.test.ts (selfViewAllowed); apps/api/src/routes/analytics/analytics.int.test.ts (self view 404 cases, works-council refusal)

Distribution, observability and product telemetry:

#ThreatMitigationTest / control
K8S1Sandbox pod reaches the control plane, Valkey, Postgres or the internetSandbox namespace with default-deny NetworkPolicy; egress only to the egress proxy for registries jobs during setup; Pod Security restricted; runner refuses a RuntimeClass whose handler is not runsctooling/ci/kind/e2e.sh (12 NetworkPolicy probes); backends/kubernetes/spec.test.ts
K8S2Runner service account over-privilegedNamespaced Role limited to pods and pods/attach; read-only runtimeclasses; self-check with SelfSubjectAccessReviewbackends/kubernetes/index.test.ts (RBAC self-check)
K8S3Autoscaler credentials read job dataDedicated Valkey ACL user limited to queue-length commands on quavon:*:waitdeploy/helm/codelotse/tests/render.sh
MT1Metrics leak tenant or personal data, or explode in cardinalityMETRICS attributes exclude tenant, repository, person and installation loginmetrics.test.ts, job-runner.metrics.test.ts, webhooks.metrics.int.test.ts
TL1Product telemetry sends data without consent or more than shownOff by default; only instance admins enable it, only in self_hosted, only after a preview exists; the sent body equals the stored preview; strict schema with bucketed counts only; enterprise and air-gap hard offanalytics.test.ts (productTelemetryUnavailable, countBucket); product-telemetry.test.ts (property test), apps/worker/src/telemetry/product.int.test.ts, apps/api/src/routes/admin/instance/telemetry.int.test.ts
TL2Telemetry endpoint used for SSRF or redirecthttps only, no userinfo, no redirects, no cookies, 10 s timeout, no retriesapps/worker/src/telemetry/product.int.test.ts

Data subject requests:

#ThreatMitigationTest / control
DS1Deleting the wrong person through a reused loginDelete requires the provider external idDsarDeleteSubject contract; packages/db/src/dsar/dsar.int.test.ts
DS2Export file or audit entry exposes the subjectExport file mode 0600; audit entry stores a hash of the subjectpackages/db/src/dsar/dsar.int.test.ts, apps/api/src/bin/dsar.int.test.ts
DS3Incomplete deletion leaves person-linked dataPersonal-data registry with a completeness test over all tablespersonal-data.test.ts

Hardening:

#ThreatMitigationTest / control
HD1DNS rebinding during GitHub App validation and manifest conversionEnterprise edition uses the guarded fetch for bothsetup.int.test.ts (rebinding resolver), github-apps.int.test.ts
HD2Concurrent review-app registrationsAdvisory lock with re-check under the locksetup.int.test.ts (race test)
HD3Rate-limit bursts at window edges, silent fail-open on a stalled ValkeySliding-window log, 250 ms timeout, throttled warning and the quavon.rate_limit.store_unavailable counter and alert; break-glass fails closedrate-limit.int.test.ts, security.int.test.ts
HD4High-entropy secrets without a known prefix in logsLogs and prompt logs also redact high-entropy tokens (git SHAs and SHA-256 digests exempt)logger.test.ts, redact.test.ts
HD5Revoked GitHub user tokens keep read accessA 401 on membership refresh ends the session (auth.session_revoked)auth.int.test.ts ("ends the session instead of using cached roles when GitHub revoked the token")
HD6Sandbox Valkey user lists queue key names (tenant, repository and job ids)-scan on the sandbox ACL userpackages/queue/src/isolation.int.test.ts ("hides quavon key names from the sandbox user")

Code hosts: GitLab and Azure DevOps Repos:

#ThreatMitigationTest / control
PV1Forged GitLab or Azure DevOps webhooksPer-connection secret (X-Gitlab-Token / basic-auth password), constant-time comparison before any DB write; unknown connection and bad secret both return 401 without detail; failed-delivery rate limitapps/api/src/routes/webhooks/connection.int.test.ts
PV2Replayed deliveriesIdempotency key (X-Gitlab-Event-UUID, Azure notification id) in webhook_deliveries, unique per provider, host and deliveryconnection.int.test.ts
PV3Theft of code-host tokensEncrypted per connection (see assets), 4-character hint, webhook secret shown onceprovider-connections.int.test.ts, encrypted-columns.test.ts
PV4SSRF through the connection base URLNo credentials, query or fragment; https only (http only for private hosts in the self-hosted edition); enterprise edition requires a public host resolved via assertPublicEndpoint, guarded fetchprovider-connections.int.test.ts
PV5Cross-tenant use of a connectionTenant RLS on provider_connections; composite FK (tenant_id, installation_id); all connection routes in the IDOR fuzzrls.int.test.ts, idor.int.test.ts
PV6Installation collisions across hostsInstallation uniqueness (provider, base_url, external_id)packages/installations/src/index.int.test.ts, migrate.int.test.ts
PV7Bidi or zero-width spoofing in published comments (red team F3, closed)The publisher strips U+200B–200F, U+202A–202E, U+2060–206F, U+FEFF and similar characters; tracker writes use the same setpackages/eval/src/redteam/redteam.test.ts ("strips bidi and zero-width control characters"), packages/publisher/src/tracker/text.test.ts
PV8Silent loss of findings on hosts without a capabilityCapability-driven degradation: fenced suggestion block, commit status plus summary comment, audited SARIF skippublisher.int.test.ts, sarif.int.test.ts, render.test.ts

Code hosts: Bitbucket Cloud, Bitbucket Data Center, Gitea and Forgejo:

#ThreatMitigationTest / control
PV9Forged Bitbucket, Gitea or Forgejo webhooksHMAC-SHA256 over the raw body with the per-connection secret (X-Hub-Signature, X-Gitea-Signature, X-Forgejo-Signature), constant-time compare before any DB write; unknown connection, wrong kind, tampered body and missing signature all return 401 without detail; failure rate limitapps/api/src/routes/webhooks/signed.int.test.ts
PV10Replayed deliveriesDelivery id (X-Request-UUID, X-Request-Id, X-Gitea-Delivery, X-Forgejo-Delivery) in webhook_deliveries per connection; duplicates answered 200 without enqueuesigned.int.test.ts
PV11Theft of Bitbucket, Gitea or Forgejo tokensSame encrypted connection storage as PV3 (context provider_connections:<id>), 4-character hint, secret shown onceprovider-connections.int.test.ts
PV12SSRF through the base URLAs PV4; the Bitbucket Cloud base URL is pinned to https://bitbucket.orgprovider-connections.int.test.ts
PV13Cross-tenant use of the new connection kindsTenant RLS; route fixtures rotate through all connection kinds in the IDOR fuzzidor.int.test.ts, routes.int.test.ts
PV14Another app's comments treated as ours on Bitbucket Cloud (no /user for access tokens)App-user accounts mapped to [bot] logins; thread linking only matches comment ids recorded as posted by usnormalize-bitbucket.test.ts, contract suite
PV15Findings lost on hosts without multi-line comments, suggestions or thread resolutionAs PV8; Forgejo thread resolution refused explicitly; Code Insights annotations stripped of invisible characterscontract suite, packages/git-provider/src/bitbucket/provider.test.ts, Forgejo/Gitea E2E
PV16Comment ids colliding across connections of one tenant (GitLab, Gitea, Forgejo and Bitbucket Data Center ids are unique per server only), so thread linking, resolution and feedback act on the wrong comment or noneposted_comments unique on (tenant_id, pull_request_id, external_id) (migrations 0029 expand, 0030 contract); every lookup by comment id is scoped by pull requestpackages/publisher/src/publisher.int.test.ts ("records equal comment ids from two connections of one tenant"), Forgejo and Gitea E2E in one tenant

Work trackers: Jira, Linear, Azure Boards:

#ThreatMitigationTest / control
TK1Forged tracker webhooksPer-connection secret, constant-time HMAC (Jira X-Hub-Signature, Linear-Signature) or hashed basic-auth compare (Azure Boards), Linear timestamp tolerance 60 s, verification before parsing, 401 without body for unknown connection, wrong source and bad signature, failure rate limitapps/api/src/routes/webhooks/trackers.int.test.ts, packages/trackers/src/keys-webhooks.test.ts, tracker contract suite
TK2Replayed or duplicated deliveries; another tenant suppressing deliveries by guessing idsDelivery id prefixed with the connection id, unique in webhook_deliveries; outbox job in the same transactiontrackers.int.test.ts
TK3Trusting webhook contentOnly the issue id is taken; the ticket is re-read with the connection credentialsapps/worker/src/processors/tracker-sync.int.test.ts
TK4SSRF through tracker base URLshttps only (http for on-premises trackers in the self-hosted edition), no credentials or query; enterprise: public-address check at create and guarded fetch (DNS pinned, no redirects) on every requestapps/api/src/routes/admin/trackers.int.test.ts ("validates the kind, credentials and base URL")
TK5Jira OAuth 3LO CSRF or code injection256-bit state hashed in Valkey, 10 min, single use, bound to the starting admin; internal redirects only; site matched against accessible resourcestrackers.int.test.ts ("connects Jira Cloud through OAuth 3LO …")
TK6Refresh-token race with rotating tokensRefresh under SELECT … FOR UPDATE with re-checktracker-sync.int.test.ts ("refreshes expiring Jira OAuth tokens once")
TK7Sync loops or write amplificationAgreed values and pending writes, in-flight projection and deferral, failed writes retried rather than reverted, per-change idempotency keyspackages/trackers/src/sync/engine.test.ts, tracker-sync.int.test.ts ("survives a bidirectional stress run without sync loops")
TK8Content injection into trackersADF built from an AST, HTML escaped, wiki control characters escaped, links only http(s)/mailto, control/bidi characters stripped, secret scan on comments and created ticketspackages/trackers/src/format/format.test.ts, packages/publisher/src/tracker/text.test.ts
TK9Prompt injection through ticket textTicket text goes through the acceptance pipeline (untrusted(), evidence verification)apps/worker/src/processors/tracker-acceptance.int.test.ts, ac-smoke-v1 injection items
TK10Tracker writes outside the publisherTracker writes only through submitTrackerWrite/publishTrackerWrite; lint rule covers updateFields, addComment, createIssue outside packages/trackers and packages/publisherESLint rule (eslint.config.js)
TK11Tenant isolation and tracker rate limitsRLS on all tracker tables, all new routes in the IDOR fuzz; budget gate per host and connection, header-driven blocks, delayed jobsrls.int.test.ts, idor.int.test.ts, contract test "turns rate limit responses into a delayed retry"

Release hardening:

#ThreatMitigationTest / control
RH1Tampered or substituted release imagesBase images pinned by digest; CycloneDX SBOM and grype scan (--only-fixed --fail-on high) per image; release.yml pushes by digest, signs with cosign keyless and attaches SLSA build provenance and SBOM attestations; nothing is published until a registry is configured.github/workflows/release.yml, apps/cli/src/commands/release/dry-run.test.ts, quavon release dry-run
RH2Attribution or private metadata in release notesNotes use conventional-commit subject lines only; bodies, trailers and "generated by" footers are never copiedapps/cli/src/commands/release/notes.test.ts
RH3Lost events or duplicate external writes when workers crash or Valkey, Postgres or the model endpoint failOutbox in the webhook transaction, BullMQ stalled-job recovery, idempotent review runs and publish intents; review publishes serialised per repository and head commit, so pull requests sharing a head commit create one check runtests/chaos/src/chaos.chaos.test.ts (nightly hardening.yml), packages/publisher/src/publisher.int.test.ts ("creates one check run when two pull requests with the same head publish at once")
RH4Webhook floods delaying acknowledgementsSignature check before DB writes, one short transaction per delivery; ack p99 measured at nominal and peak ratestests/load/src/ingress.load.test.ts (nightly)
RH5Upgrades that break stored data or the previous releaseChecksummed migrations, expand/contract lint, upgrade matrix with seeded data from every supported releasetests/upgrade/src/upgrade.upgrade.test.ts, tests/upgrade/src/releases.test.ts
RH6Pilot reports exposing individualsTeam and tenant aggregates through the analytics query with k-anonymity; no logins or ids in any outputapps/cli/src/commands/pilot/pilot.int.test.ts
RH7Deterministic findings (secrets, SAST) withheld while the model endpoint is downAfter the last attempt with a retryable model error, a review with deterministic findings completes without the model and says so in the summary; mode (shadow, approval, auto) and publish intents unchangedapps/worker/src/processors/review.int.test.ts ("publishes deterministic findings only when the model stays unavailable"), packages/agents/src/review/pipeline.test.ts, tests/chaos/src/chaos.chaos.test.ts

Hosted edition: sign-up, lifecycle, support access, status, fairness (enterprise edition only):

#ThreatMitigationTest / control
HS1Someone claims an organisation they do not administerClaim needs tenant:manage, which the membership sync grants only to org owners and admins (or the account owner) of an installation of that tenant; GitHub principals only; conditional unclaimed → active updatehosted/api/src/routes/saas/saas.int.test.ts ("claims the provisioned tenant …" with a second claim answering 409, "rejects anonymous and non-admin callers …")
HS2Data of organisations that never agreed to the service is storedUnclaimed tenants ignore every non-lifecycle event; unclaimed tenants are deleted after 7 dayshosted/worker/src/saas/saas.int.test.ts ("creates an unclaimed tenant and processes nothing until it is claimed", "removes unclaimed tenants after seven days")
HS3Forged or replayed owner email verification256-bit single-use token, only the SHA-256 hash stored, 48 h expiry, verification by explicit POST, 10/min/IPsaas.int.test.ts ("… verifies the owner email once", "expires verification links …"); hosted/web/e2e/hosted.spec.ts ("confirms the owner email only on request")
HS4SMTP downgrade or header injectionSTARTTLS required when configured (refused if not offered), TLS mode, CR/LF in headers rejected, password encrypted in saas_settingsmail.test.ts, saas.int.test.ts ("stores SMTP and status webhook secrets encrypted …")
HS5Silent operator access to tenant dataAccess only through an approved, time-boxed grant; the requester cannot approve; break-glass cannot request; enterprise-only authorizer wrappersaas.int.test.ts ("needs tenant admin approval …", "ends access when the approved time runs out …", "refuses break-glass sessions …")
HS6Support session changes data or takes an exportThe wrapper applies only to non-mutating requests; export is a POSTsaas.int.test.ts (owner email and export answer 403 during an active session)
HS7Unaudited support readsEvery request under a grant appends support_access.read with route and grant idsaas.int.test.ts (exact audit sequence)
HS8Cross-tenant access through the hosted routesTenant-scoped routes under withTenant, forced RLS on the new tables, IDOR fuzz over both editions with tenant admin and instance admin principals; black-box isolation fuzz over HTTP against a kind deployment of the hosted editionhosted/api/src/idor.int.test.ts, hosted/db/src/rls.int.test.ts (new tables), hosted/deploy/tests/isolation-fuzz.ts (run by hosted/deploy/tests/staging-kind.sh)
HS9Export leaks secrets or other tenantsDSAR secret-column rule, RLS-scoped reads, tenant admin only, 5/minsaas.int.test.ts ("exports every tenant table without secrets and nothing of other tenants")
HS10Incomplete deletionDelete in foreign key order over every tenant table except the kept ones (audit_log, saas_tenants, billing_accounts, invoices), tombstone without owner data, uninstall after the grace period and before deletehosted/worker/src/saas/saas.int.test.ts ("uninstalls the app and deletes every tenant row …", "orders every tenant table …")
HS11Status endpoint leaks internals or is abusedFixed shape (states only), cached 15 s, 60/min/IP; webhook signed, public HTTPS targets only, no redirectssaas.int.test.ts ("serves a cached public health summary …"); worker ("sends signed incident webhooks on status changes only")
HS12A noisy tenant starves othersTenant-level round-robin in the routed dispatcher, tenant-fair outbox claimsrouted.int.test.ts ("does not let a tenant with many installations starve another tenant"), dispatcher.int.test.ts ("claims the rows of a quiet tenant in the first batch behind a flood")
HS13A hosted tenant runs on an untested or foreign model keyThe enterprise gateway refuses keys without a successful test; edits clear the flag; the per-tenant key binds the blob to its tenanttenant.int.test.ts ("refuses keys that never passed a connection test …"), llm-credentials.int.test.ts, crypto.test.ts ("tenant keys")
HS14Installation id enumeration on the public checkAnswers only known for the shared app, no account data, 30/min/IPsaas.int.test.ts ("publishes the shared app install link …")
HS15Deletion cancelled by someone else, or after data removal startedtenant:manage + CSRF, never under support access (mutation), row lock, only while delete_after is later than the database clock, 409 afterwards, auditedhosted/api/src/routes/saas/saas.int.test.ts ("cancelling a requested deletion"), idor.int.test.ts, staging-kind.sh fuzz
HS16Cancellation silently resumes processing for installations suspended for other reasonsOnly installations suspended at the request time are resumedsaas.int.test.ts ("keeps installations suspended …")
HS17A cancel races the uninstall or hard delete, a cancelled tenant loses its installation, or a pending deletion is still chargedThe GitHub App is uninstalled only after the grace period, so a cancelled deletion keeps its installation and reinstalling cannot create a second tenant. Before the uninstall and the hard delete the worker re-checks deletion_pending and delete_after <= clock_timestamp() under a row lock (FOR UPDATE), the same lock the cancel route takes; once the worker has seen the deadline pass, the cancel route refuses. No renewal is charged while a tenant is deletion_pendinghosted/worker/src/saas/saas.int.test.ts ("uninstalls the app and deletes every tenant row only after the grace period", "keeps the installation of a tenant that cancels its deletion during the grace period"), hosted/billing/src/lifecycle.int.test.ts ("runs from trial to active, renews …": no renewal while deletion_pending)

Hosted edition: plans, metering, Mollie billing, invoices, sandbox progress (enterprise edition only):

#ThreatMitigationTest / control
BL1Forged Mollie webhookOnly a form id is accepted (tr_ or invoice_), queued through the outbox; the worker re-fetches the object with our key and finds the tenant in our own tables (customer or invoice id), never from the body; unknown ids ignored; failures rate limited per IPhosted/api/src/routes/billing/billing.int.test.ts (Mollie webhook), hosted/billing/src/lifecycle.int.test.ts
BL2Replayed webhooks activating twicebilling_events unique on (mollie_id, status); activation only while checkout_payment_id matches, then clearedlifecycle.int.test.ts (duplicate, ignored)
BL3Double chargingIdempotency-Key on every POST; invoice unique on (tenant, kind, period_start); the issue job skips invoices that have a Mollie id; permanent provider errors set error instead of retryinglifecycle.int.test.ts (second issue skipped), mollie/client.test.ts
BL4Cross-tenant billing accessForced RLS on billing tables, tenant:manage (summary tenant:view), tenant-scoped authorisationhosted/db/src/rls.int.test.ts, billing.int.test.ts (cross-tenant 403 for every route)
BL5Mollie key disclosure or a live key in tests*_FILE, never logged, live_ refused outside production, recorded fixtures onlyhosted/config/src/config.test.ts
BL6Price or plan tampering by a tenantThe catalogue is server config; clients send plan key and interval only; amounts are computed on the serverlifecycle.int.test.ts (amount match), billing.int.test.ts (unknown plan 400)
BL7False B2B or VAT claimsVAT ID format per country, VIES check with timestamp and consultation number, refusal when invalid or unconfirmedissuer-vies.test.ts, lifecycle.int.test.ts (VIES), tax.test.ts
BL8Public progress reveals customers or revenueRounded down to 5 %, capped at 100, 0 below five paying tenants, at most one recompute per hour, timestamp truncated to the hour, snapshot table without tenant column or identifiersusage.int.test.ts, pure.test.ts, hosted/db/src/rls.int.test.ts (columns), billing.int.test.ts
BL9Load or timing attacks on the public progress endpointServed from Valkey or the stored row, never counts subscriptions, strict per-IP bucket, Cache-Control: public, max-age=300billing.int.test.ts (watched database, rate limit)
BL10CORS abuseAllowlisted origins only, GET only, no credentials, Vary: Originbilling.int.test.ts
BL11SSRF or path injection towards Mollie or VIESFixed hosts, redirects refused, ids validated before use in paths, timeoutsmollie/client.test.ts ("refuses redirects and malformed ids")
BL12Using features outside the plan (including sandbox)Plan guard in review, SAST and triage, and job gating; hosted tenants without a plan are read-onlyreview.int.test.ts, triage.int.test.ts, hosted/worker/src/billing.int.test.ts, lifecycle.int.test.ts
BL13Output silently lost when a limit is hitShadow degrade or a recorded skip with reason; running runs are never re-checked, so security findings still publishreview.int.test.ts ("never re-checks running runs")
BL14Data loss on non-paymentGrace period, then read-only; no deletionlifecycle.int.test.ts (grace → read-only, invoices kept)
BL15Read-only bypassAPI preHandler answers 402 for tenant mutations except billing, /saas and support access, so an admin can always pay, export, delete or revoke support accessbilling.int.test.ts (read-only tenants)
BL16Usage double counting or manipulationOutbox event in the work transaction; rollups recomputed from source tables; finalised months immutableusage.int.test.ts
BL17Seat list exposes individualsAdmin only; provider and login only; no per-person activitybilling.int.test.ts (member 403)

Hosted edition: reminders, mandate revocation, retention purge, test invoices (enterprise edition only):

#ThreatMitigationTest / control
BL18Reminder flood or duplicate dunning mailsOne reminder per day of graceReminderDays (0, 7, 12) and failed period; audit entry written in the same transaction under a row lock; re-check before sendinghosted/billing/src/offboarding.int.test.ts ("mails the billing email on days 0, 7 and 12 once per failed period")
BL19Reminder mail header injection or address leak in logsShared SMTP client rejects CR/LF in headers; recipient only from the tenant's own billing account; errors logged without the addressmail.test.ts, pure.test.ts (graceReminderMail)
BL20Mandate stays valid after deletion or cancellationHourly revocation for hard-deleted tenants and ended cancellations, so within about an hour; 404/410 count as done; retried on failure; auditedoffboarding.int.test.ts ("revokes the mandate once a cancelled subscription has ended", "revokes the mandate with retries …"), mollie/client.test.ts
BL21Invoice records deleted too early or kept foreverPurge only for hard-deleted tenants, Berlin calendar-year cutoff, configurable years (default 8, pending the tax adviser), account only after the last invoice and a cleared mandate; auditedoffboarding.int.test.ts (boundary at 2034-12-31T23:00Z, "never purges the invoices of tenants that still exist"), pure.test.ts (retentionCutoff)
BL22Test-invoice script used against the live accountOnly test_ keys; aborts on a non-test response mode; no live calls in teststest-invoices.test.ts

Hosted edition: payment method change, terms acceptance, webhook flood (enterprise edition only):

#ThreatMitigationTest / control
BL23Payment method change used to activate or extend a plan, to take over another tenant's mandate, or leaves the old mandate validtenant:manage + CSRF, only for active or past_due plans, audited; the €0.01 first payment is tagged purpose: payment_method in metadata set by the server and is only accepted while checkout_payment_id matches; it replaces the stored mandate and never changes plan, period or seats; the tenant comes from our own customer id; every other mandate of the customer is revoked afterwards (retried with the job, audited) and the €0.01 is refunded with an idempotency keyhosted/billing/src/lifecycle.int.test.ts ("replaces the payment method of a paid plan …", "marks a failed payment method change …"), billing.int.test.ts ("starts a payment method change …"), mollie/client.test.ts
BL24A checkout without accepted terms, or accepting outdated termsacceptTerms: true (literal) and the current terms.version are required (400 or 422 otherwise); acceptance is audited with actor, IP and version in the transaction that records the checkoutbilling.int.test.ts ("saves billing details, starts the Mollie checkout …"), lifecycle.int.test.ts (terms_outdated, audit row), hosted/config/src/config.test.ts (https terms URL required), hosted/web/e2e/billing.spec.ts
BL25Webhook flood: valid-looking ids each queue a job that calls the Mollie APIEvery request on /webhooks/mollie counts against 600/min per IP before the body is parsed (429 with Retry-After, metric outcome rate_limited); malformed requests additionally 30/min per IP; repeated ids collapse to one outbox row per minute; Mollie retries refused callsbilling.int.test.ts ("limits every delivery per IP with a generous bucket", "rate-limits repeated malformed deliveries")

CI failure analysis (ci-analysis.md):

#ThreatMitigationTest / control
CI1Injected instructions in CI logs, check names or annotations (fork authors control what their CI prints)Every input wrapped in untrusted blocks with the CI threat model in the system prompt; model text cleaned (links, images, HTML, mentions removed) and sanitised again by the publisher; locations only when the file exists at the head and the line is inside it; on public repositories auto becomes approvalci.test.ts (agents), ci.int.test.ts ("downgrades auto to approval on public repositories", "drops locations that do not exist at the head"), ci-smoke-v1 (injection, unverified locations), red team ci items
CI2Secrets printed in CI logs reach the model, the database or the commentredact() over the whole log before the excerpt; only the redacted excerpt goes to the model; raw logs are never stored (ci_analyses keeps check metadata and the verified result only); the publisher secret scan blocks a comment that still contains oneci.test.ts ("redacts secrets"), ci-smoke-v1 (ci_prompt_secret_leaks = 0), red team items 073 and 074 (secret blocked)
CI3SSRF or token leak through the log redirectThe job-log request does not follow redirects; the Location is fetched without the GitHub token, https only, no further redirects, through the guarded fetch in the enterprise edition (no private addresses at connect time), 60 s timeout, 20 MiB cap with the tail kept; download failures fall back to annotationsprovider.int.test.ts ("sends no credentials to the blob host", "rejects a log redirect to a private address", "rejects a log redirect to plain http"), check-runs.test.ts
CI4Cost from noisy or flapping CIThree-minute debounce with one outbox key per head; one analysis per pull request and head (unique row); at most three checks; per-pull-request daily limit; budget gate; hosted plan gateingest.int.test.ts ("routes one debounced ci analysis per failed head"), ci.int.test.ts ("analyses each head only once", daily limit)
CI5Loops on CodeLotse's own check runCheck runs whose app slug is CodeLotse's are skipped before analysisci.int.test.ts ("skips its own checks …")

Notifications (ADR-0016, notifications.md):

#ThreatMitigationTest / control
NT1SSRF through a channel URLSlack (hooks.slack.com) and Teams (*.environment.api.powerplatform.com) host allowlist, https only, no credentials or fragment, checked when saving and again before every send; Slack and Teams always through the guarded fetch; generic webhooks https only, assertPublicEndpoint and the guarded fetch in the enterprise edition; no redirects, 10 s timeout, at most 64 KiB of the answer readnotifications.test.ts (shared-types), transport.test.ts ("refuses stored targets …", redirects), notifications.int.test.ts ("rejects private webhook targets and blocks them when testing")
NT2Leak of webhook URLs or signing secretsURL and signing secret encrypted with the context notification_channels:<id> and covered by keys rotate; views show the host and the last four characters only; the signing secret is returned only on create and rotate; errors and logs never contain the URLnotifications.int.test.ts ("stores Slack URLs encrypted …", "returns the webhook signing secret only on create and rotate"), notify.int.test.ts (publisher: "never logs or stores the target URL"), encrypted-columns.test.ts
NT3Content injection into Slack, Teams or e-mail (mentions, links, header injection) and leaking codeMetadata only (repository, number, title, counts); control and invisible characters removed; Slack &, <, > escaped (neutralises <!channel> and mentions); Teams Markdown escaped; e-mail plain text with fixed subjects; secret scan before sending (skipped on a hit)render.test.ts, transport.test.ts ("skips messages that look like they contain a secret")
NT4Spoofed or replayed webhooks to receiversHMAC-SHA256 over <timestamp>.<body> in X-CodeLotse-Signature, 32-byte random secret per channel, rotatable; delivery id per event for de-duplication; verification recipe with a five-minute windowrender.test.ts (signWebhookPayload), transport.test.ts ("posts signed JSON to a local HTTPS receiver")
NT5E-mail or chat spamOnly tenant:manage users create channels (audited); at most 20 recipients; events only, no free text; one delivery per channel and event key; approval digest at most hourly per channel; test sends at most every 10 s per channelnotifications.test.ts (shared-types), notify.int.test.ts (worker: digest interval, one digest per channel), notifications.int.test.ts (409 on a second test)

Browser push (ADR-0020, notifications.md):

#ThreatMitigationTest / control
PU1SSRF through a subscription endpointEndpoint must be https on the push services of the major browsers (host allowlist), no port, credentials or fragment; checked when subscribing and again before every send; guarded fetch, no redirects, 10 s timeout, at most 64 KiB readpush.test.ts (shared-types), publisher push transport tests
PU2Push to people who lost access, or about repositories they cannot seeRecipients from stored memberships refreshed within seven days; admin-only events for admins; repository events need a repository role; checked at submit and again before sendingpush.int.test.ts (publisher: eligibility matrix, recheck before sending)
PU3Leak of content through browser vendors' push servicesMetadata only, rendered like other channels; secret scan before sending; payload encrypted end to end (RFC 8291) with per-message keyswebpush.test.ts (RFC 8291 test vector), render and transport tests
PU4Managing or testing someone else's subscriptionRoutes under /api/me, cookie sessions only, rows filtered by the signed-in user; other users' ids answer 404; audited; test at most every 10 s per subscription; at most ten subscriptions per person and tenantAPI push subscription integration tests, IDOR fuzz
PU5Phishing or open redirect through a notification clickMessages carry a path, not a URL; the schema and the service worker accept only same-origin pathspush.test.ts (shared-types), web e2e
PU6Theft of subscription secretsEndpoint and keys encrypted with the context push_subscriptions:<id>, covered by keys rotate; views show the host and a hash; VAPID private key only in configuration secretsencrypted-columns.test.ts, config tests

Pull request descriptions (configuration.md):

#ThreatMitigationTest / control
PD1Injected instructions in a public pull request title, description or diff make the description publish links, mentions or HTMLTitle, description (without our own block) and diff only inside untrusted blocks with the review threat model; areas must be changed paths or their directories; text reduced to one line without fences; the publisher sanitises the block like other output and runs the secret scan; on public repositories auto becomes approval; the title is never changed by a reviewdescribe.test.ts (agents), description.int.test.ts (publisher: forged markers and links, secret blocked, public approval), describe-smoke-v1, red team items 084–089
PD2Overwriting text the author wroteOnly the text between the start and end marker is replaced; zero markers use the template placeholder or the chosen position; any other marker layout is skipped as malformed without writing; a deleted block is an opt-out for the pull request until @codelotse describedescribe.test.ts (shared-types: placeholder, top, bottom, replace, two starts, end before start, CRLF), description.int.test.ts ("skips malformed markers …", "stops after the author removed the block …")
PD3Lost author edit in the read-modify-write of the bodyThe body is read fresh right before the write and the head must still be the reviewed one. GitHub, GitLab and Gitea have no compare-and-swap for pull request bodies, so an author edit within that window can be lost; recorded as residual riskdescription.int.test.ts ("skips … stale heads")
PD4Model text forges the end marker or our publication markerThe publisher replaces every <!-- in model text and escapes <, so neither marker can be written by the model; the block ends with the publication marker for reconciliationdescription.int.test.ts ("neutralises forged markers …"), red team items 085 and 088
PD5Editing the description starts a review loopOur edit fires pull_request.edited, which updates the stored pull request; reviews are keyed by head SHA, so no review startsroute.ts key per head SHA, review_runs unique per pull request and head

Summary diagrams (configuration.md):

#ThreatMitigationTest / control
DG1Injection through diagram labelsThe model returns nodes and steps only; CodeLotse generates ids and keeps letters, digits and .,_/()- in labels (40 characters), drops the reserved word end and edges to unknown nodesdiagram.test.ts, red team items 090–093
DG2Links, scripts or init directives in Mermaid (click, href, %%{init}, style, :::)Not expressible by the builder; model text cannot open code fences in the summary or the description; the publisher renders a Mermaid fence only when every line matches the builder grammar and shows other Mermaid blocks as text; violations.ts checks every Mermaid linesanitize.test.ts ("renders only generated diagrams as Mermaid …"), pipeline.test.ts, redteam.test.ts
DG3Rendering cost on the code hostAt most 10 nodes, 15 steps and 2,000 characters; only when the policy asks for it and the host renders Mermaiddiagram.test.ts ("caps labels, nodes, edges and size")

Prompts for coding agents (configuration.md, commands.md):

#ThreatMitigationTest / control
AH1Prompt injection into the user's local coding agent through finding text produced from untrusted pull request contentFixed framing header (untrusted content, verify first, run no commands, open no links); finding text between fixed BEGIN/END FINDINGS delimiters whose spoofed copies are replaced; URLs, <!--, control, format (bidi, zero-width, tag) and variation-selector characters removed; every agent only pre-fills the prompt and the user confirms it; CodeLotse runs nothingagent-prompt.test.ts (shared-types); red-team items 094–101
AH2Private code and finding text sent to cursor.com through the https://cursor.com/link/prompt query when a comment link is clickedThe web link is added to code-host comments on public repositories only (repository visibility from the run context, unknown values count as private); the web interface uses only local schemes (cursor://, claude-cli://, vscode://, codex://)render.test.ts ("links the Cursor web prompt only on public repositories"), publisher.int.test.ts ("adds agent prompts to comments and links Cursor only on public repositories")
AH3Forged finding markers or a fence breakout from the prompt block into the commentCode fence one backtick longer than the longest run in the prompt; <!-- replaced in the prompt; the block is placed before the transparency label and the real finding marker, which stays the last marker; model-written deep links (claude-cli://, codex:// and any other scheme) are treated as foreign links by the sanitiserrender.test.ts ("adds a collapsed prompt to each comment before the label and marker"), sanitize.test.ts; red-team items 096, 097, 100
AH4Oversized comments or linksAt most 2,500 characters per finding and 8,000 per prompt (at most 20 findings); a link is left out when it exceeds its limit (Claude Code 5,000 characters of prompt, other links 10,000 encoded characters); the secret scan covers the prompt and the decoded Cursor linkagent-prompt.test.ts, publisher.int.test.ts ("blocks secrets that appear only in the agent prompt"), redteam.test.ts (decoded Cursor links)

Code index (ADR-0017, code-index.md):

#ThreatMitigationTest / control
CX1Stored source code leaks across tenants or outlives the repositorySix tenant tables with forced RLS and withTenant; foreign keys with cascade on repository removal; tenant deletion walks all tenant tables; the hourly code_index maintenance purges snapshots and blobs when the mode is off; chunk content is plaintext (full-text search needs it) and is protected at rest by volume or disk encryption, which the hosted edition requiresrls.int.test.ts (all six tables), code-index.int.test.ts (worker: "keeps index rows inside the tenant and removes them with the repository", maintenance purge)
CX2Malicious repository content: huge files, many files, parser bombsWork tree capped at 512 MiB; at most 50 000 files per repository and 256 KiB per file (instance settings); binary blobs are stored without chunks; 1 s parse deadline per file and a 10-minute budget per build (time_budget); the WASM parser runs through guarded and falls back to line windows; 4 000 characters per chunk; storage cap per tenant (storage_cap)chunk.test.ts, files.test.ts, code-index.int.test.ts ("stops at the storage cap", "caps the number of files")
CX3Code sent to an embeddings endpointOnly in the hybrid mode, only when pgvector exists and only to the tenant's routed embeddings tier; there is no default model; calls go through @quavon/llm with the guarded fetch in the enterprise editioncode-index.int.test.ts ("indexes lexically when no embeddings model is routed", "stores embeddings in hybrid mode …")
CX4Cost of embeddingsCalls are recorded with the feature index in llm_calls and admitted by the tenant budget guard; BudgetExceededError stops embedding and keeps the snapshot; at most 20 000 chunks per run; unchanged blobs are never embedded againcode-index.int.test.ts ("… caps them per run")
CX5Path traversal or symlinks in the indexed treePaths from git ls-tree must pass isSafeRelativePath and must not point into .git; symlinks (mode 120000) and submodules are skipped; files are read with readContained (O_NOFOLLOW, inside the work tree)files.test.ts ("… skips what is not indexed"), workspace.test.ts

Cross-repository context (ADR-0017):

#ThreatMitigationTest / control
XR1A review receives code of another tenantThe resolver runs in a tenant transaction (RLS) and filters every query by tenant_id as well; links reference repositories through composite tenant foreign keys; linked code is read only from the targets' index snapshots inside withTenantlinks.int.test.ts ("never resolves repositories or links of another tenant"), rls.int.test.ts (repository_links)
XR2Private code leaks into comments on a public repositoryA target is used only when its visibility is not narrower than the source's: a public source gets public targets only, an internal source no private targets; unknown values count as public for the source and private for the targetlinks.test.ts, links.int.test.ts ("never gives a public source private or internal targets"), review.int.test.ts (worker: no [linked] block after the source turns public), linked-smoke.test.ts and the clean item smoke-v1/033
XR3A pull request or a repository author links code the app should not use.quavon/config.yml is read at the base commit, so a pull request cannot add links; config names resolve only within the same installation (other_installation otherwise); links across installations and pattern repositories need an admin link; targets of suspended installations are skippedlinks.int.test.ts ("reports every status …", "prefers the repository of the same installation …")
XR4Instructions planted in linked codeLinked snippets and usages are rendered inside the untrusted repository context block like every other context; the finder prompt says linked code is never part of the diff; output sanitising and the secret scan apply unchangedred-team items 106–109 (linked_repo)
XR5Cost and prompt sizeAt most 5 linked and 3 pattern repositories per review (over_limit); linked context takes at most 25 % of the context budget; no extra embedding call (the review's query vector is reused, semantic search only for targets indexed with the same model)links.test.ts ("caps linked and pattern repositories separately"), linked.test.ts ("keeps linked context within its share of the budget")

MCP server (ADR-0018, mcp-server.md):

#ThreatMitigationTest / control
MS1Access token leak or theft256-bit random tokens with the clt_ prefix, which gitleaks, findSecrets and redact detect; only the SHA-256 hash is stored; the secret is shown once and the list shows the last four characters; expiry 1–90 days; revocable by the owner and tenant admins; accepted only in the Authorization header, never in the query string; never logged (Caddy redacts credentials in access logs, the API logs no request headers)tokens.test.ts, mcp-tokens.int.test.ts ("shows the secret once and stores only its hash and hint"), mcp.int.test.ts ("… never logs the token"), secrets.test.ts (tooling)
MS2Cookie CSRF or DNS rebinding against /mcpBearer routes ignore cookies and never accept a session principal; an Origin header must equal PUBLIC_URL; cookie routes never accept bearer principalscontract.test.ts ("bearer routes"), mcp.int.test.ts ("rejects missing, revoked, expired …")
MS3Privilege beyond readSix read-only tools, no write path, no outbound calls; bearer routes may only declare authenticated and check repo:view per call; repository roles capped at reader; one tenant per token; tenant switch off by default; break-glass cannot create tokens; instance admin is never carried overcontract.test.ts, mcp.int.test.ts ("binds grants to the token tenant and caps repository roles at reader"), mcp-tokens.int.test.ts ("refuses break-glass sessions")
MS4Stale grants after offboardingGitHub users: no refresh without a session; the cached membership must be younger than 24 h (MCP_TOKEN_GRANT_MAX_AGE_MS), otherwise 401 with "sign in to CodeLotse to refresh access". OIDC users: grants computed per request from current mappings and the provider state; the group snapshot is refreshed at each sign-in. Deleting the user deletes the tokensmcp.int.test.ts ("stops serving a GitHub token …", "deletes tokens with their user"), oidc-login.int.test.ts ("refreshes the group snapshot at sign-in …")
MS5Cross-tenant access or IDOR through tool argumentswithTenant on every query; each repository-scoped call resolves the repository inside the token tenant and checks repo:view; unknown and forbidden answer the same "Not found."; finding ids must belong to the repository; token routes in the IDOR fuzzmcp.int.test.ts ("… answers not found for forbidden repositories"), idor.int.test.ts, routes.int.test.ts
MS6CodeLotse carries prompt injection into IDE agentsEvery tool result is JSON inside an untrusted block (random boundary, invisible characters stripped) after redact; server instructions and the fix prompt frame findings as data to verify, not instructionsmcp.int.test.ts (untrusted wrapper, redacted key)
MS7Denial of servicePer-token limit of 120 requests a minute and 600 per IP; results capped at 32 KiB, findings at 50 per search and 100 per review, code hits at 20; 64 KiB request bodies; stateless JSON responsesmcp.int.test.ts ("… rate-limits per token …"), rate-limit.ts

MCP servers as review context (mcp-context.md, ADR-0019); awaiting sign-off, MCP_CONTEXT_SIGNED_OFF is false:

#ThreatMitigationTest / control
MCSSRF through server URLs, credential misdirection, prompt injection and tool poisoning through MCP output, exfiltration through tool arguments, unwanted egressStreamable HTTP only with the guarded fetch in every edition (no redirects, configured origin only, private hosts only through QUAVON_MCP_PRIVATE_HOSTS on self-hosted instances); encrypted credentials; SHA-256 pinning of admin-approved tools with flat schemas; finder-only tools, untrusted and redacted text results; secret and e-mail checks on arguments; call and byte budgets; never on public repositories; QUAVON_MCP_CONTEXT defaults to disabledMC1–MC12 in mcp-context.md; mcp.test.ts, review-mcp.int.test.ts, mcp-servers.int.test.ts, red-team items 106–114

6. Open risks (tracked) &ZeroWidthSpace;

  • MCP server: GitHub memberships of token owners refresh only through a web sign-in, so an offboarded GitHub user keeps read access for up to 24 h (MS4). OIDC group changes reach tokens at the next sign-in; removed mappings and disabled providers apply at once. Tokens are bearer secrets: anyone holding one reads what its owner can read until it expires or is revoked.

  • MCP context (once signed off): findings on private repositories may paraphrase external context, servers may misreport tool annotations, and search terms derived from private pull requests reach the configured servers by design (MC6, MC7, MC5).

  • Code index: chunk content and its search vector are stored in plaintext. Protection at rest depends on volume or disk encryption of the PostgreSQL server (CX1).

  • Pull request descriptions: an author edit made between our fresh read and the write can be overwritten, because the supported hosts offer no compare-and-swap on pull request bodies (PD3).

  • Rotated or deleted webhook secrets remain valid for up to 60 s per process.

  • IdP group changes take effect at the next sign-in (≤ 8 h), like GitHub role changes.

  • Analytics differencing across overlapping scopes (tenant minus team, team A minus team B) is mitigated by per-point k and administrator-only scopes, not eliminated.

  • gVisor on the production host runs with the systrap platform without KVM; the sandbox boundary is a user-space kernel, not a virtual machine.

  • No independent penetration test yet.

  • The OWASP LLM Top 10 2026 numbering is unverified (section 4).

  • Audit-log retention and pseudonymisation on erasure requests await counsel.

  • LLM budget checks are advisory under concurrency: calls already in flight can overshoot a cap by at most their own cost, bounded by worker concurrency. Reservation-based admission is still open. Plan limits are checked at run start, so a month can overshoot a plan limit by the runs in flight.

  • Secret redaction in logs is pattern- and entropy-based; short or low-entropy secrets can still pass, and model-visible text is not entropy-redacted.

  • A compromised model can quote parts of its system prompt in published outputs (no secrets or tenant data in prompts); (red team F2).

  • CI failure analysis: secrets in CI logs are redacted by pattern only; a secret without a known pattern can reach the model provider. Self-hosted instances download job logs from wherever GitHub redirects, including internal storage of a GHES host.

  • GitLab, Azure DevOps, Bitbucket, Gitea and Forgejo deliveries that are missed are not reconciled; the next push triggers a fresh review. GitLab draft notes belong to the token user, so concurrent or crashed runs can publish leftover drafts together.

  • Jira Data Center deliveries without X-Atlassian-Webhook-Identifier are deduplicated by body hash. Atlassian code exchange and Entra token requests are not budget-gated. Tracker webhook secrets are decrypted per request.

  • The self-hosted edition can point code-host and tracker connections at internal hosts (by design, like LLM endpoints). The same applies to generic notification webhooks.

  • Notifications: the recipients of an e-mail channel see each other; the signing secret is shown once and anyone who copies it can sign requests until it is rotated; a delivery whose target accepted the request but whose status update failed is sent again on retry (receivers de-duplicate by X-CodeLotse-Delivery).

  • Prompts for coding agents: a user can still accept a malicious instruction that survives the framing in their agent. Claude Code resolves repo only from clones it has seen, otherwise it opens in the current directory.

  • Reviews without deterministic findings still end as failed after the last attempt while the model endpoint is down; the next push reviews again.

  • Bitbucket Cloud and Data Center are verified against fixtures built from the API documentation only (no accounts; Data Center needs a licence).

  • Release signing and provenance have not run yet: the registry and the signing identity are open decisions, so release.yml publishes nothing.

  • Hosted edition: mandate revocation, the retention purge and grace reminders run in the hourly billing cycle, so each can lag its trigger by up to an hour. If the commit fails after the mail server accepted a reminder, the reminder is sent again in the next cycle.

  • Hosted edition: the default invoice retention of 8 years is pending confirmation by Quavon's tax adviser. The purge deletes only Quavon's copies; Mollie keeps its own invoice records.

  • Hosted edition: cancelling a deletion keeps the data but not the model keys and connection secrets, which are deleted when the deletion is requested.

  • Hosted edition: per-tenant model-key KEKs are derived, not stored, so deleting a tenant does not crypto-shred its blobs; deletion relies on the hard delete and backup expiry. Restoring a backup must re-run tenant deletions.

  • Hosted edition: the status webhook cannot report a Valkey outage (the monitor runs on the job queue); the pull endpoint covers it.

  • Mollie and VIES are verified against fixtures built from their documentation only; reverse-charge fields in Mollie sales invoices are unconfirmed (see the VAT treatment page of the hosted edition's billing documentation, not part of this repository).

The open core is released under the Apache License 2.0.