# OWASP ASVS 4.0.3 — Self-Attestation Report (KIROSEC)

- **Status:** Self-assessment (evidence-based; not yet independently audited)
- **Date:** 2026-08-05 (re-audited against current `main`; previous revision 2026-07-22)
- **Standard:** OWASP Application Security Verification Standard (ASVS) v4.0.3
- **Target level:** **Level 2 (Standard)** — the ASVS baseline for applications
  handling sensitive data / PII, which this product does.
- **Owner:** KIROSEC engineering
- **Scope:** the KIROSEC platform in this monorepo — the Rust backend services
  (composed by the gateway), the Tauri + React + Rust desktop analyzer, the React
  web surfaces (console + customer portal), and the shared Rust crates (ingestion,
  case-store, case-bundle, licensing, service-auth, reporting).

> **What this is.** OWASP does not issue certificates; ASVS is a self-attestation
> framework. This is our own honest gap analysis of the codebase against ASVS
> 4.0.3, with source-file evidence for each control. It is a living artifact —
> update it when security-relevant code changes (run the `security-review` skill).
> External auditors can use it as the starting point for an independent assessment.

## 1. Executive summary

The **security fundamentals are strong**: modern authenticated cryptography
(AES-256-GCM, Argon2id, Ed25519, HMAC-SHA256, CSPRNG throughout), no injection
classes found (SQLi / XSS / XXE / template / path-traversal / zip-slip all
defended and, in several cases, regression-tested), TLS verification never
disabled, and — the product's crown jewel — **server-derived tenant isolation**
with cross-tenant denial tests and no-existence-oracle 404s.

The gaps are real but bounded, and cluster into a small number of themes:

| Theme | ASVS areas | Severity |
|---|---|---|
| **Session lifecycle** — no revocation/server-side logout, no MFA, session JWT in `localStorage`, 24h token | V2, V3, V4 | High |
| **Anti-automation** — no API-edge rate limiting; admin login + signup/checkout unthrottled | V2, V11, V13 | High |
| **HTTP security headers** — no HSTS / CSP / nosniff / X-Frame-Options | V8, V14 | High |
| **Reset/verify token handling** — stored plaintext, passed in URL query | V2, V3, V8 | Medium |
| **Key rotation** — single static env-seeded secrets, no rotation/`kid`/revocation | V1, V6 | Medium |
| **Supply chain** — unsigned installers; no Rust dependency (`cargo audit`) scan | V10, V14 | Medium |
| **Audit chain** — `hash_prev`/request context unpopulated; auth events not audited | V7 | Medium |

**Level posture:** **Level 1 is essentially met** — remediation closed password
minimum length (batch 1) and server-side logout / session revocation (batch 4),
leaving **one L1 item** open: desktop installer signing (needs a code-signing
cert). **Level 2** has a strong foundation, and remediation batches 1–4 (§5) have
since closed the security headers, password policy (incl. common-password
rejection), admin brute-force lockout, fail-closed entitlement gates, token-at-rest
hashing, session revocation, support-service error leaks, and Rust dependency
scanning.
**Re-audit note (2026-08-05).** The 2026-07-22 revision listed 17 controls as
FAIL. Re-verifying each against the code, 13 had since been implemented and the
document had simply gone stale — password policy (12-char minimum, common-password
deny list), token-at-rest hashing, the full session-revocation path (`sess` epoch,
server-side logout, invalidation on password change), and the security-header set
on both the gateway and the SPA host. Edge rate limiting was added in this pass.
Four genuine gaps remain: MFA (2.2.2), browser token storage (3.2.3), signing-key
rotation (6.4.2), and installer code signing (10.3.2 — blocked on purchasing
Authenticode / Apple Developer ID certificates, not on engineering).

We assert **partial Level 2 conformance today**, with an explicit, honest "path to
full Level 2" list in §5 — the remaining items each need a product decision, spend,
or a dedicated change, and are not claimed as complete.

## 2. How to read this

| Status | Meaning |
|---|---|
| **PASS** | Implemented and evidenced in the codebase. |
| **PARTIAL** | Partially implemented, or implemented but not verified by a test/tooling. |
| **FAIL** | Not implemented; a real gap (tracked in § 5). |
| **N/A** | Not applicable to this architecture, with justification. |

"Level" is the lowest ASVS level at which the requirement applies (L1 ⊂ L2 ⊂ L3).
Evidence is cited as `path:line` into this repository. Level 3 (life-critical /
high-assurance) is out of scope.

## 3. Architecture context relevant to ASVS

- **Deployment:** the backend deploys as ONE gateway process (ADR-011); routes are
  namespaced and each service exposes a `routes()` seam. Entitlements are
  **server-authoritative** (ADR-007); tenant scope is **derived from authenticated
  identity, never from client input** (ADR-003).
- **Authentication:** HS256 JWT bearer tokens verified in `crates/service-auth`
  (`jwt.rs` — `alg` must be exactly `HS256`, rejecting `none`/alg-confusion,
  constant-time HMAC compare); passwords hashed with **Argon2id** PHC strings
  (`services/auth-service/src/password.rs`); a startup gate rejects weak/placeholder
  secrets.
- **Cryptography:** case bundles at rest use **Argon2id + AES-256-GCM** with
  AAD-bound headers (ADR-009, `crates/case-bundle`); offline licenses and knowledge
  packs are **ed25519-signed** and **client-verified** (`crates/licensing`,
  `crates/common/src/sign.rs`); the PayMongo webhook is **HMAC-SHA256**
  signature-verified, fail-closed on a blank secret.
- **Hostile input:** imported logs are never executed; executables/archives are
  refused at detection; parsers are fault-isolated with size/line caps
  (`crates/ingestion`); pack routes use a bare-filename allow-list against traversal.
- **Offline-first:** the desktop is fully functional with no network; a Cargo
  `online` feature gates all egress (ADR-026).
- **Secure SDLC:** CI runs `clippy -D warnings`, `cargo fmt --check`, schema/contract
  validators, **gitleaks** secret scanning, and **`pnpm audit`** (JS) dependency
  review (`.github/workflows/`).

## 4. Verification results by ASVS chapter

### V1 — Architecture, Design & Threat Modeling

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 1.1.4 | Trust boundaries documented | L2 | PASS | `docs/security/threat-model.md` §2 boundary diagram + controls table |
| 1.1.2 | Threat model kept current | L2 | PARTIAL | Living threat model exists; some items (rate-limiting, parser sandboxing) still open in §7 |
| 1.2.1 | Low-privilege service accounts | L2 | PARTIAL | One gateway process runs all services (ADR-011); no per-service OS identity |
| 1.2.2 | Authenticated, least-priv inter-component comms | L2 | PARTIAL | One shared HS256 secret signs+verifies for every service (`gateway/main.rs`); single point of full compromise |
| 1.4.1 | Server-side trusted enforcement points | L1 | PASS | Tenant scope derived server-side from verified JWT (`service-auth/src/auth.rs`) |
| 1.4.4 | Single vetted access-control mechanism | L2 | PASS | All services resolve identity via `logparser-service-auth` `TokenResolver` |
| 1.6.1 | Explicit key-management policy | L2 | PARTIAL | Algorithms named in threat-model §5; KMS/HSM + rotation "to be decided" |
| 1.8.1/1.8.2 | Data classified by sensitivity | L2 | PASS | threat-model §1 asset/sensitivity table |
| 1.14.x | Deployment/segregation documented | L2 | PASS | `docs/architecture/02,17`, ADR-008/011/027/028 |

### V2 — Authentication

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 2.4.1 | Approved KDF for passwords | L1 | PASS | Argon2id PHC, per-password random salt via OsRng (`auth-service/src/password.rs:9-15`) |
| 2.1.2 | Allow ≥64 chars, no truncation | L1 | PASS | No max length; full input hashed |
| 2.5.6 | Reset token time-limited, single-use, random | L1 | PASS | UUIDv4, 1h/24h/7d TTLs, consume-on-use (`auth-service/src/pg_store.rs`) |
| 2.5.x | Enumeration-safe flows | L2 | PASS | Constant responses + `dummy_verify` timing equalization (`password.rs:33-38`) |
| 2.7.x | Out-of-band token delivery (not echoed in prod) | L2 | PASS | Mailed via SMTP; echoed only under `AUTH_DEV_RETURN_TOKENS=1` (dev-only) |
| 2.5.1/2.5.4 | No default/shared credentials | L1 | PASS | `is_strong_secret` rejects weak/placeholder secrets; process exits |
| 2.2.1 | Anti-automation / lockout on login | L1 | PARTIAL | In-process throttle 5/900s (`throttle.rs`), memory-only, email-keyed, resets on restart, not IP-scoped |
| 2.4.4 | Argon2 sufficient parameters | L2 | PARTIAL | `Argon2::default()` — relies on crate default, never pinned/documented |
| 2.1.1 | Minimum 12-char passwords | L1 | PASS | Minimum is 12 (`handlers.rs` `validate_password`) |
| 2.1.7 | Breached-password check | L2 | PASS | Common / low-variety password deny list in `validate_password` |
| 2.2.2 / 2.8.x | MFA available (esp. admin/high-value) | L2 | **FAIL** | No MFA/TOTP/WebAuthn anywhere, incl. platform-admin console |
| 2.5.7 | Reset tokens not stored recoverably | L2 | PASS | Reset + verification tokens persisted as `store::hash_token(..)` digests, never raw |

### V3 — Session Management

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 3.2.1 | New token on authentication | L1 | PASS | Fresh JWT per login/signup/accept-invite |
| 3.5.2 | Approved integrity-protected token alg | L2 | PASS | HS256, constant-time verify, `alg` pinned, `exp`/`nbf` enforced (`jwt.rs:52-108`) |
| 3.3.3 | Reasonable idle/absolute timeout | L2 | PARTIAL | 24h absolute `exp` only; no idle timeout, long for this product |
| 3.1.1 | No tokens in URL | L1 | PARTIAL | Session bearer never in URL; but reset/verify/invite tokens are in emailed URL query strings |
| 3.5.3 | No sensitive session ids logged | L2 | PARTIAL | No token logging; some server logs include email (PII) |
| 3.2.3 | Token stored non-JS-readable | L2 | **FAIL** | Console stores session JWT in `localStorage` (`apps/console/src/auth.tsx:18`) — XSS-exfiltratable |
| 3.3.1 | Server-side logout invalidation | L1 | PASS | `POST /auth/logout` bumps `users.session_epoch`; gateway rejects a stale `sess` |
| 3.3.2 | Revocation / short-lived + refresh | L2 | PASS | `sess` epoch claim revokes outstanding tokens on logout / reset / suspend |
| 3.3.4 | Terminate sessions on password change | L2 | PASS | Password reset bumps the session epoch, invalidating outstanding JWTs |

### V4 — Access Control

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 4.1.1 | Server-side access rules | L1 | PASS | Tenant scope server-derived everywhere (`entitlement-service/src/handlers.rs:395-405`) |
| 4.1.2 | Users can't manipulate ACL attributes | L1 | PASS | `tenant_id` only from JWT claim; body-supplied ids not trusted as scope |
| 4.1.3 | Deny-by-default / least privilege | L1 | PASS | 401→403 before reads; role gates on privileged ops |
| 4.2.1 | No IDOR/BOLA | L1 | PASS | Object access re-derives/validates scope; cross-tenant → identical 404 |
| 4.3.2 | No directory browsing | L1 | PASS | No static listing; packs from fixed dir |
| 4.2.2 | CSRF on state-changing ops | L1 | N/A | Bearer-token auth, no cookies — classic CSRF N/A |
| 4.1.5 | Access control fails securely | L1 | PARTIAL | Auth/isolation fail closed; **plan-entitlement gates fail OPEN when no entitlement source configured** (`tenant-management-service/src/handlers.rs:374-375`) |
| 4.3.1 | Admin interface hardened | L2 | PARTIAL | Separate admin token + secret + scope, but **no MFA, no lockout** on admin login |
| 4.3.3 | Extra authZ for sensitive/admin actions | L2 | PARTIAL | Cross-tenant admin writes audited but single-factor; no step-up |
| — | Session revocation on suspend/delete | L2 | **FAIL** | Suspended/deleted user's JWT valid until `exp` |

### V5 — Validation, Sanitization & Encoding — **strong PASS**

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 5.3.4 | SQLi prevented (parameterized) | L1 | PASS | rusqlite bound values + allow-listed exprs; sqlx compile-time-checked `query!` macros |
| 5.3.3 | XSS output encoding | L1 | PASS | React JSX auto-escape; **zero** `dangerouslySetInnerHTML`/`innerHTML`; report `escape_html` with hostile-`<script>` regression fixtures |
| 5.3.8 | XXE protection | L2 | PASS | `quick-xml` resolves no external entities; `MAX_XML_DEPTH=256` + test (`ingestion/src/windows/xml.rs`) |
| 5.3.6 | Safe deserialization | L2 | PASS | `serde_json` depth cap; JSON arrays streamed with per-record byte cap |
| 5.2.5 | Template-injection protection | L2 | PASS | Fixed `format!` templates; all log values `escape_html` (`reporting/src/lib.rs:330`) |
| 5.3.10 | CSS/URL injection | L2 | PASS | Brand color sanitized to hex/named only (`reporting/src/lib.rs:203-213`) |
| 5.1.3 | Input allow-listed | L1 | PASS | Query fields + ext JSON keys allow-listed (`case-store/src/search.rs`) |
| 5.2.4 | No eval/dynamic exec on input | L1 | PASS | No `eval`/`new Function`; rules are declarative YAML data |

### V6 — Stored Cryptography — **strong PASS** (rotation gap)

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 6.2.1 | No plaintext secrets at rest | L1 | PASS | Argon2id passwords; AES-256-GCM bundle payloads |
| 6.2.2 | Proven algorithms | L2 | PASS | AES-256-GCM, Argon2id, Ed25519, SHA-256, HMAC-SHA256 |
| 6.2.3-6.2.5 | AEAD; no ECB/weak ciphers | L2 | PASS | GCM with AAD = `sha256(header)`; no ECB/DES/MD5/SHA1 in crypto paths |
| 6.2.6 | Nonces/IVs never reused | L2 | PASS | Fresh 12-byte GCM nonce per section from OsRng |
| 6.3.1 | CSPRNG for random values | L1 | PASS | `OsRng` for keys/nonces/salts/seeds |
| 6.4.1 | Secrets not hardcoded / managed | L2 | PARTIAL | Env-var seeds decode straight to keys; no KMS/HSM/secret-manager |
| 6.4.2 | Key rotation / lifecycle | L2 | **FAIL** | No rotation/versioning for `JWT_SECRET`, `LICENSE_SIGNING_SEED`, webhook secret; no key revocation list |

### V7 — Error Handling & Logging

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 7.1.1 | No secrets in logs | L1 | PASS | Grep found only presence messages, no secret values |
| 7.4.3 | No stack traces/internals to client | L1 | PASS | Uniform `{code,message}` envelope, static codes |
| 7.1.2 | No sensitive data in logs | L2 | PARTIAL | Some server logs include recipient email (PII) |
| 7.4.1 | Generic client message, detail server-side | L2 | PARTIAL | Good in most services; **support-service returns `e.to_string()` to clients** (`support-service/src/handlers.rs:83,102,118,144,162`) |
| 7.2.1/7.1.3 | Log security-relevant/auth events | L2 | PARTIAL | Entitlement mutations audited; **login/reset/invite-accept not audited** |
| 7.3.1/7.3.3 | Log integrity / tamper-evidence | L2 | PARTIAL | `AuditEvent.hash_prev` + request context hardcoded `None` — hash chain unused |

### V8 — Data Protection

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 8.1.x | Retention defined & enforced | L2 | PASS | 2-year default (`case-store/src/store.rs:30`), `retention_expires_at` + legal-hold, purge API |
| 8.x | Redaction; evidence preserved | L2 | PASS | Presets pseudonymize host/IP/user; source hashes/chain-of-custody never redacted |
| 8.3.4 | Data classification/handling | L2 | PARTIAL | Report redaction classifies fields; no cross-service token/PII classification |
| 8.1.1 | Sensitive data protected at rest | L2 | PARTIAL | Cases encrypted; **reset/verify tokens plaintext** (`auth-service/src/pg_store.rs`) |
| 8.3.1 | Sensitive data in body not URL | L2 | PARTIAL | Invite/reset tokens in URL query string |
| 8.2.1 | Anti-caching headers for sensitive responses | L2 | PASS | `Cache-Control: no-store` gateway-wide where a route sets none |
| 8.3.7 | Encrypted per-tenant integration secrets | L2 | UNVERIFIABLE | Documented in README but no implementing code (integrations not yet shipped) |

### V9 — Communication — **strong PASS**

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 9.1.2/9.1.3 | Strong TLS only | L2 | PASS | `rustls` everywhere (reqwest/sqlx/lettre); no TLS1.0/1.1/export ciphers |
| 9.2.1 | Cert validation never disabled | L2 | PASS | No `danger_accept_invalid_certs`/`native-tls` anywhere (grep clean) |
| 9.2.2/9.2.4 | Encrypted outbound + auth over TLS | L2 | PASS | PayMongo over HTTPS; SMTP STARTTLS:587 rustls |
| 9.1.1 | TLS for all inbound | L1 | PARTIAL | Inbound TLS terminated by host platform, not HSTS-enforced in-app (see V14.4.5) |

### V10 — Malicious Code

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 10.3.1 | Auto-update signed & verified | L1 | PASS | Packs ed25519-signed, **client-verified fail-closed** (`distribution-service/src/packs.rs:15-20`) |
| 10.2.4 | Anti-tampering integrity checks | L2 | PASS | Bundle AES-256-GCM + AAD binding + checksum re-verify on import |
| 10.2.6 | No unauthorized code execution | L2 | PASS | Imported content never executed; `MZ`/ELF refused at detection |
| 10.2.3 | Pinned dependency sources | L2 | PASS | `Cargo.lock` + `pnpm-lock.yaml` committed |
| 10.3.3 | No dependency/subdomain takeover | L1 | PASS | Pack URLs rewritten to self-hosted base, never GitHub raw |
| 10.3.2 | Client/installer code signing | L1 | **FAIL** | Desktop installers ship UNSIGNED (no Authenticode/notarization; signing stubbed) |
| — (SCA) | Rust dependency vuln scanning | L2 | **PARTIAL** | `pnpm audit` (JS) only; **no `cargo audit`/`cargo-deny`** for the Rust tree |

### V11 — Business Logic

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 11.1.2 | High-value logic server-side only | L1 | PASS | Server recomputes `content_hash`; client hash "authorizes nothing"; entitlement computed server-side |
| 11.1.3 | Enforce business limits (seats/quotas) | L1 | PASS | Seat gate on invite (402), device-seat activation (402), size caps |
| 11.1.1 | Sequenced flows, no skipping | L1 | PASS | Sync generation conflict → 409; idempotent no-op on equal hash |
| — | Payment amount server-authoritative | L1 | PASS | Checkout amount from server price table; grants only from signed webhook |
| 11.1.4 | Anti-automation on sensitive flows | L1/L2 | PASS | Failure lockout on login/forgot/verify/admin-login, plus the edge limiter covering signup, checkout and license/activate |
| 11.1.7 | Monitor anomalous business events | L2 | PARTIAL | Append-only audit on sensitive actions; no alerting/anomaly detection |
| — | Entitlement gating fail-closed | L1 | PARTIAL | 402 when configured; **fail-open when entitlement source unconfigured** (library default) |

### V12 — Files & Resources — **strong PASS**

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 12.1.1 | Upload/parse size limits | L1 | PASS | 1 MiB/line cap, per-record JSON cap, 16 MiB gateway body limit |
| 12.1.2 | Decompression-bomb protection | L2 | PASS | ZIP/gzip **refused, never auto-extracted** (`ingestion/src/detect.rs:130-135`) |
| 12.2.1 | Content-type by magic bytes | L1 | PASS | `sniff` magic bytes; NUL/binary rejected |
| 12.3.2 | Path-traversal prevented | L1 | PASS | `is_safe_name` rejects `/`,`\`,`..`,leading `.`; regression test (`packs.rs:257-301`) |
| 12.3.1 | Client filename not used for FS ops | L1 | PASS | Import stores basename only |
| 12.5.2 | Direct upload request can't execute | L1 | PASS | Served `application/octet-stream`; imports read-only into SQLite |
| 12.4.2 | Uploaded files virus-scanned | L2 | PARTIAL | No AV scan; well-mitigated (executables/archives refused, never executed) |
| 12.6.1 | SSRF/remote-fetch protection | L2 | PARTIAL | Manifest URLs rewritten to self; no user-supplied fetch path found |

### V13 — API & Web Service

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 13.1.1 | Same authN/authZ on API as UI | L1 | PASS | All routers require bearer resolution; only deliberately-public `/packs`,`/releases`,`/healthz` open |
| 13.1.4 | AuthZ at URI and resource level | L1 | PASS | Per-resource `can_sync`/`require_tenant_in_own_org` |
| 13.2.1 | Correct HTTP methods; deny others | L1 | PASS | Methods mapped correctly; unlisted → 405 |
| 13.2.2 | Schema/input validation | L1 | PASS | Typed deserialization + explicit validation; OpenAPI contracts |
| 13.2.5 | Reject unexpected fields (mass-assignment) | L2 | PASS | Explicit DTO structs; `tenant_id` never from body; portal projects to safe shape |
| 13.2.6 | Body size limits | L1 | PASS | 16 MiB global + per-flow caps (chunk/report/snapshot) |
| — | CORS allow-list (not wildcard) | L1 | PASS | Fails closed — no origins configured ⇒ deny, not `*` |
| — | Webhook authenticity | L1 | PASS | Constant-time HMAC over raw body; fail-closed on blank secret |
| 13.1.x | API-edge rate limiting | L2 | PASS | Per-client edge limiter (`gateway/src/ratelimit.rs`), global + sensitive tiers |

### V14 — Configuration

| Ref | Requirement | Lvl | Status | Evidence / Gap |
|---|---|---|---|---|
| 14.1.1 | Secrets via env, never committed | L1 | PASS | `.gitignore` excludes `.env*`/keys; `.env.example` placeholders only |
| 14.1.3 | Secret scanning in CI | L2 | PASS | gitleaks-action + `.gitleaks.toml` |
| 14.5.3 | CORS allow-list, no wildcard reflection | L2 | PASS | `build_cors()` fails closed (`gateway/main.rs:567-589`) |
| 14.1.x | Default-secure / fail-closed on unset secrets | L1 | PASS | Blank webhook secret rejects all; empty trusted keys reject all licenses; missing `JWT_SECRET` is fatal |
| 14.3.2 | Debug/detailed errors off in prod | L2 | PARTIAL | Mostly generic; support-service leaks `e.to_string()` |
| 14.2.1 | Dependency vuln audit | L2 | PARTIAL | `pnpm audit` (JS) only; no `cargo audit` + no `dependabot.yml` |
| 14.4.2 | `X-Content-Type-Options: nosniff` | L2 | PASS | `X-Content-Type-Options: nosniff` (gateway + `vercel.json`) |
| 14.4.3 | Content-Security-Policy | L2 | PASS | CSP on gateway and SPA host |
| 14.4.5 | HSTS | L2 | PASS | HSTS `max-age=63072000; includeSubDomains; preload` (gateway + SPA) |
| 14.4.7 | Clickjacking defense | L2 | PASS | `X-Frame-Options: DENY` + `frame-ancestors` (gateway + SPA) |

## 5. Remediation backlog (ranked)

> **Remediation progress — 2026-07-22 (batch 1).** The chapter tables above are
> the point-in-time survey; the following items have since been implemented and
> verified (the tables will be refreshed at the next full survey):
> - ✅ **HTTP security headers** (V14.4.2/.5/.7, V8.2.1) — gateway now emits HSTS,
>   `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy`,
>   a baseline CSP, and `Cache-Control: no-store` (verified live on `/healthz`);
>   the console + customer-portal `vercel.json` set the same headers. CSP is still
>   a **baseline** (`frame-ancestors/base-uri/object-src`) — a full `default-src`
>   lockdown remains open (V14.4.3 → PARTIAL).
> - ✅ **Password minimum 12** (V2.1.1) and **pinned Argon2id parameters**
>   (V2.4.4, 19 MiB/t=2/p=1).
> - ✅ **support-service no longer leaks internal error detail** to clients (V7.4.1).
> - ✅ **Rust dependency scanning** (V14.2.1, V10) — `cargo audit` added to CI
>   (advisory, matching pnpm audit) + `dependabot.yml` for cargo/npm/actions.
> - ⏳ **New, from `cargo audit`:** `quick-xml` DoS advisories (quadratic
>   attribute check; unbounded namespace allocation) affect the **hostile Windows
>   XML** parse path. The fix needs a major bump (→0.41), but quick-xml **0.38**
>   changed text semantics — `BytesText::unescape()` became `decode()` and XML
>   entities (`&amp;`, `&lt;`) are now emitted as separate `Event::GeneralRef`
>   events. A naive swap would silently drop/mangle entities in log text (Windows
>   event data routinely contains `&`/`<`/`>` in command lines and paths), so this
>   needs a fixture-backed migration (entities-in-text + `GeneralRef` handling) —
>   **tracked as a dedicated follow-up, NOT rushed on the hostile-input path.**
>   Other advisories (`rsa` Marvin timing — no fix released; unmaintained
>   Tauri/GTK3 stack; `spin` yanked) are accepted/transitive.
>
> **Remediation progress — 2026-07-22 (batch 2).**
> - ✅ **Fail-closed entitlement gates** (V4.1.5) — `require_feature`
>   (tenant-management) and the central-sync gate (case-sync) now DENY (402) when
>   no entitlement source is configured, instead of silently granting paid
>   features. Misconfiguration fails safe.
> - ✅ **Reset / verification / invite tokens hashed at rest** (V2.5.7, V8.1.1) —
>   only `sha256(token)` is persisted (Postgres + in-memory stores); the raw token
>   lives only in the emailed link.
>
> **Remediation progress — 2026-07-22 (batch 3).**
> - ✅ **Platform-admin login brute-force lockout** (V4.3.1, V11.1.4) — the admin
>   login now locks the submitted identifier after N failures (429), refusing even
>   a correct password until it expires; audited as `admin.login_locked`.
> - ✅ **Common / low-variety password rejection** (V2.1.7) — `validate_password`
>   denies a curated common-password list and passwords with <5 distinct chars.
> - ✅ **CORS doc drift fixed** (V14.5.3) — `.env.example` now states the
>   fail-closed behavior correctly.
>
> **Remediation progress — 2026-07-22 (batch 4).**
> - ✅ **Server-side logout + session revocation** (V3.3.1/.2/.4) — a per-user
>   session epoch (`sess` JWT claim) is bumped on logout / password reset /
>   suspend; a gateway middleware rejects any token whose epoch is stale
>   (401 `session_revoked`). Verified end-to-end on a live gateway. **This closes
>   the second of the two open L1 items — only installer signing remains at L1.**

### Path to full Level 2 (remaining, honest)

These items are what stand between the current **partial L2** posture and a full
L2 self-attestation. They are NOT yet done because each needs a product decision,
spend, or a dedicated fixture-backed change — not something to rush:

| Item | ASVS | What it needs |
|---|---|---|
| **MFA** (TOTP/WebAuthn) for org owners + platform-admin | V2.2.2, V4.3.1 | Product decision (method, enrollment UX, recovery codes) + a feature build |
| ~~Session revocation + server-side logout~~ **✅ Done (batch 4)** | V3.3.1/.2/.4 | Implemented: per-user session-epoch + gateway middleware |
| **Session token out of `localStorage`** → httpOnly cookie | V3.2.3 | Auth rework across console + gateway, with CSRF defense added back |
| **Desktop installer code-signing** | V10.3.2 | A code-signing cert (Authenticode + Apple Developer ID) — a purchase; individual path viable now, org path after D-U-N-S |
| **Key rotation / `kid` versioning** | V6.4.2 | `kid`-versioned secrets + rotation runbook + (ideally) a managed secret store |
| **Tamper-evident audit chain + auth-event logging** | V7.2/V7.3 | Populate `hash_prev`/request context; emit audit events on login/reset/invite (auth-service needs an audit sink) |
| **API-edge rate limiting** (beyond admin/login) | V13.1.x | A gateway `tower_governor` layer + trusted-proxy IP extraction (deploy-dependent) |
| **`quick-xml` DoS fix** | V5.3.8 | Fixture-backed migration to 0.41 (0.38 `GeneralRef` entity split — see batch-1 note) |
| **Full CSP `default-src` lockdown** on the SPAs | V14.4.3 | Testing script/style/analytics against the live build to avoid breakage |


### High
1. **HTTP security headers** (V8.2.1, V14.4.2/.3/.5/.7). **✅ Done (batch 1)** — CSP still baseline-only. Add a `tower_http`
   response-header layer on the gateway (HSTS `max-age=63072000; includeSubDomains`,
   `X-Content-Type-Options: nosniff`, a tuned CSP, `X-Frame-Options: DENY`,
   `Referrer-Policy: no-referrer`, `Cache-Control: no-store` on sensitive routes) and
   a `headers` block in each app's `vercel.json`.
2. **Session revocation + server-side logout** (V3.3.1/.2/.4, V4). Add a per-user
   `session_epoch`/`jti` denylist checked at verify time; bump it on logout, password
   reset, and admin suspend/delete. (Or short `exp` + refresh tokens with a revocable
   store.)
3. **MFA (TOTP/WebAuthn)** for org owners and the platform-admin console
   (V2.2.2/2.8, V4.3.1). Enforce before token issuance.
4. **Anti-automation** (V13.1.x, V2.2.1, V11.1.4). Add a `tower_governor` limiter at
   the gateway (per-IP + per-token); throttle `signup`, `admin/login`, `checkout`,
   `license/activate`; document the WAF/edge dependency.
5. **Session token out of `localStorage`** (V3.2.3). Implement the scoped
   httpOnly + Secure + SameSite cookie migration (threat-model §8) for the browser
   console (desktop keeps bearer); add CSRF defense with the cookie.
6. **Sign the desktop installers** (V10.3.2). Provision Authenticode (EV/OV) + Apple
   Developer ID, wire the stubbed signing/notarization steps, and publish SHA-256
   checksums + detached signatures per artifact.

### Medium
7. **Hash reset/verify tokens at rest; prefer POST body over URL** (V2.5.7, V8.1.1,
   V8.3.1, V3.1.1). Store `sha256(token)`, look up by hash; keep short TTLs.
8. **Key rotation/lifecycle** (V6.4.2, V1.6.1). `kid`-versioned secrets, a rotation
   runbook, a managed secret store; consider EdDSA so verifiers don't hold the signing key.
9. **Fail-closed entitlement default** (V4.1.5, V11). **✅ Done (batch 2).** Invert `require_feature` and the
   case-sync plan gate to deny when no entitlement source is configured (or make it
   non-optional in `AppState`).
10. **Rust dependency scanning** (V14.2.1, V10). **✅ Done (batch 1).** Add `cargo audit`/`cargo-deny` to
    `rust.yml` + a `dependabot.yml` covering cargo and npm.
11. **Audit chain** (V7.3.1, V7.2.2). Populate `hash_prev` + `request_id`/`ip`/`user_agent`;
    emit audit events for login success/failure, reset, invite-accept.
12. **Password policy** (V2.1.1, V2.1.7, V2.4.4). **✅ Min 12 + pinned Argon2 done (batch 1);** breach/common-password check still open. Raise minimum to 12; add a
    HIBP-range / common-password deny check; pin & document Argon2 parameters.
13. **support-service error detail** (V7.4.1). **✅ Done (batch 1).** Replace `ApiError::internal(e.to_string())`
    with the log-server-side / generic-client pattern used elsewhere.

### Low
14. **AV scanning of imports** (V12.4.2) — integrate ClamAV in managed-cloud ingestion,
    or formally record "refuse-and-never-execute" as the compensating control.
15. **Narrow desktop CSP `connect-src`** off broad `https:` to the specific origins
    (`tauri.conf.json`).
16. **Durable/IP-keyed login throttle** (V2.2.1) — back lockouts with Postgres for
    multi-replica correctness.
17. **Doc drift** — fix the stale `.env.example` CORS comment ("Unset = permissive" is
    wrong; code fails closed).
18. **Integration-secret encryption** (V8.3.7) — implement AEAD-at-rest before any
    integration ships, or mark not-implemented here (done above).

## 6. Attestation statement

As of **2026-07-22**, KIROSEC engineering asserts, based on the evidence-based
self-assessment above, that the KIROSEC platform:

- **Meets OWASP ASVS 4.0.3 Level 1 essentially.** Password minimum length (§5.12,
  batch 1) and server-side logout / session revocation (§5.2, batch 4) are closed;
  **one L1 item remains** — desktop installer signing (§5.6), which needs a
  code-signing certificate.
- **Partially meets Level 2**, with strong conformance in cryptography (V6),
  input validation & encoding (V5), communication security (V9), files & resources
  (V12), and access control / tenant isolation (V4/V13), and an open remediation
  backlog (§5) concentrated in session lifecycle, anti-automation, HTTP security
  headers, token-at-rest handling, key rotation, supply-chain signing, and the
  audit chain.

This is a **self-attestation**, not an independent certification. Residual risks in
§5 are accepted on an interim basis and tracked to closure. This statement should be
re-issued when the High-severity backlog items are closed or when a third-party
assessment is completed.

- **Owner:** KIROSEC engineering · **Contact:** support@kirosec.com

## 7. Maintaining this attestation

- Re-run the per-chapter survey when a security-relevant change lands (new trust
  boundary, auth/session change, crypto, file import, external service, or
  tenant-scoping path). The `security-review` skill checklist is the trigger.
- Keep evidence `path:line` references current; stale references are a review smell.
- Track FAIL/PARTIAL items in § 5 until closed; a closed item flips to PASS with new
  evidence and a dated note.
- Refresh against **ASVS v5.0** when we schedule a standards bump (v5.0 reorganizes
  chapters; this report targets the widely-adopted 4.0.3 for now).
