Traceability Matrix
Acceptance Criteria Coverage
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AC-1 |
Valid login returns JWT |
FT-P-01, NFT-PERF-01, NFT-RES-01, NFT-RES-LIM-04 |
Covered |
| AC-2 |
Unknown email returns code 10 |
FT-N-01 |
Covered |
| AC-3 |
Wrong password returns code 30 |
FT-N-02 |
Covered |
| AC-4 |
JWT lifetime 4 hours |
FT-P-03, NFT-SEC-04 |
Covered |
| AC-5 |
Email min 8 chars |
FT-P-02, FT-N-03 |
Covered |
| AC-6 |
Email format validation |
FT-P-02, FT-N-04 |
Covered |
| AC-7 |
Password min 8 chars |
FT-P-02, FT-N-08 |
Covered |
| AC-8 |
Duplicate email returns code 20 |
FT-N-07 |
Covered |
| AC-9 |
Only ApiAdmin can manage users |
FT-P-06, FT-P-07, FT-P-11, FT-P-12, FT-P-13, NFT-SEC-02, NFT-SEC-06 |
Covered |
| AC-10 |
First hardware check stores |
FT-P-04, NFT-RES-03 |
Covered |
| AC-11 |
Subsequent hardware check validates |
FT-P-05, NFT-RES-03 |
Covered |
| AC-12 |
Hardware mismatch returns code 40 |
FT-N-06 |
Covered |
| AC-13 |
Max upload 200 MB |
FT-P-08, NFT-RES-LIM-01, NFT-RES-LIM-02, NFT-PERF-03 |
Covered |
| AC-14 |
AES-256-CBC encryption |
FT-P-09, FT-P-10, NFT-PERF-02, NFT-PERF-03, NFT-RES-LIM-03 |
Covered |
| AC-15 |
Encrypt-decrypt round-trip |
FT-P-10 |
Covered |
| AC-16 |
Empty file upload returns code 70 |
FT-N-05 |
Covered |
| AC-17 |
SHA-384 password hashing |
NFT-SEC-03 |
Covered |
| AC-18 |
All non-login endpoints require auth |
FT-P-09, NFT-SEC-01, NFT-RES-02, NFT-RES-LIM-04 |
Covered |
| AC-19 |
Encryption key derived from email+password+hw |
FT-P-10, NFT-SEC-05 |
Covered |
Restrictions Coverage
| Restriction ID |
Restriction |
Test IDs |
Coverage |
| RESTRICT-SW-01 |
.NET 10.0 runtime |
All tests (implicit — Docker build uses .NET 10.0) |
Covered |
| RESTRICT-SW-02 |
PostgreSQL database |
All DB tests (implicit — docker-compose uses PostgreSQL) |
Covered |
| RESTRICT-SW-03 |
Max request body 200 MB |
NFT-RES-LIM-01, NFT-RES-LIM-02 |
Covered |
| RESTRICT-SW-04 |
JWT HMAC-SHA256 signing |
FT-P-03, NFT-SEC-04 |
Covered |
| RESTRICT-HW-01 |
ARM64 target architecture |
— |
NOT COVERED — CI builds ARM64; tests run on dev x64 host |
| RESTRICT-ENV-01 |
Secrets via env vars |
All tests (implicit — docker-compose passes env vars) |
Covered |
| RESTRICT-ENV-02 |
CORS admin.azaion.com |
— |
NOT COVERED — CORS is browser-enforced, not testable at API level |
| RESTRICT-OP-01 |
Serilog logging |
— |
NOT COVERED — log output verification not in scope |
Coverage Summary
| Category |
Total Items |
Covered |
Not Covered |
Coverage % |
| Acceptance Criteria (baseline) |
19 |
19 |
0 |
100% |
| Acceptance Criteria (cycle 1) |
24 |
24 |
0 |
100% |
| Acceptance Criteria (cycle 2) |
6 |
6 |
0 |
100% |
| Restrictions |
8 |
5 |
3 |
63% |
| Total |
57 |
54 |
3 |
95% |
Uncovered Items Analysis
| Item |
Reason Not Covered |
Risk |
Mitigation |
| RESTRICT-HW-01 (ARM64) |
Tests run on x64 dev/CI host; cross-architecture testing requires ARM hardware |
Low — .NET runtime handles arch differences; no arch-specific code in application |
CI builds ARM64 image; manual smoke test on target device |
| RESTRICT-ENV-02 (CORS) |
CORS is enforced by browsers, not by server-to-server HTTP calls |
Low — CORS policy is declarative in Program.cs |
Visual inspection of CORS configuration in code |
| RESTRICT-OP-01 (Logging) |
Log output format/content verification adds complexity without proportional value |
Low — Serilog configuration is declarative |
Code review of Serilog setup |
Cycle 1 Additions (2026-05-13) — AZ-513, AZ-196, AZ-183, AZ-197
Appended during the existing-code cycle 1 Test-Spec Sync (autodev Step 12). Cycle 1 ACs are namespaced by their tracker ID to avoid colliding with the baseline AC-1..AC-19 numbering above.
AZ-513 — Detection Classes CRUD
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-513 AC-1 |
POST /classes creates a class |
FT-P-14 |
Covered |
| AZ-513 AC-2 |
POST /classes requires ApiAdmin authorization |
FT-N-09 |
Covered |
| AZ-513 AC-3 |
PATCH /classes/{id} updates an existing class (full body) |
FT-P-15 |
Covered |
| AZ-513 AC-4 |
PATCH /classes/{id} accepts partial body (partial-merge) |
FT-P-16 |
Covered |
| AZ-513 AC-5 |
PATCH /classes/{id} returns 404 for unknown id |
FT-N-10 |
Covered |
| AZ-513 AC-6 |
PATCH /classes/{id} requires ApiAdmin authorization |
FT-N-11 |
Covered |
| AZ-513 AC-7 |
DELETE /classes/{id} removes a class |
FT-P-17 |
Covered |
| AZ-513 AC-8 |
DELETE /classes/{id} returns 404 for unknown id |
FT-N-12 |
Covered |
| AZ-513 AC-9 |
DELETE /classes/{id} requires ApiAdmin authorization |
FT-N-13 |
Covered |
| AZ-513 AC-10 |
UI add/delete/edit affordances work end-to-end |
— |
Cross-workspace (ui/ e2e harness) — out of scope for this workspace |
AZ-196 — Device Auto-Registration
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-196 AC-1 |
First device gets serial azj-0000 (shape: serial / email / 32-hex password) |
FT-P-18 |
Covered |
| AZ-196 AC-2 |
Sequential numbering on subsequent calls |
FT-P-19 |
Covered |
| AZ-196 AC-3 |
Persisted user has Role=CompanionPC, IsEnabled=true |
FT-P-20 |
Covered (verified via successful login → role-gated behaviour) |
| AZ-196 AC-4 |
Returned plaintext password is hashed (SHA-384) in DB, not stored plaintext |
FT-P-20 |
Covered (verified via successful login round-trip) |
| AZ-196 AC-5 |
Requires ApiAdmin authorization |
FT-N-14 |
Covered |
AZ-183 — Resources OTA Update Check (REVERTED post-cycle-1)
The OTA Update Check & Publish feature shipped in cycle 1 was reverted later the same day after the security audit (finding F-1: /get-update disclosed plaintext per-resource encryption keys to any authenticated caller; the OTA delivery model itself was deemed obsolete in the target architecture). The endpoints, service, entity, table, request DTOs, response DTO, cache key, master-key config field, and the e2e test class ResourceUpdateTests were all removed.
| AC ID |
Acceptance Criterion |
Test IDs |
Status |
| AZ-183 AC-1 |
Resources table created with required columns |
— |
Reverted — table dropped from migration set (env/db/05_resources.sql deleted) |
| AZ-183 AC-2 |
POST /get-update returns newer resources |
FT-P-21 |
Reverted — endpoint and test deleted |
| AZ-183 AC-3 |
POST /get-update returns empty when device already current |
FT-P-22 |
Reverted — endpoint and test deleted |
| AZ-183 AC-4 |
Memory cache avoids DB pressure under 2000-device polling |
— |
Reverted — cache key removed |
| AZ-183 AC-5 |
Cache invalidated on CI/CD publish |
FT-P-23 |
Reverted — endpoint and test deleted |
AZ-197 — Hardware-Binding Removal
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-197 AC-1 |
Resource download works without Hardware field |
FT-P-09 / FT-P-10 (legacy bodies retained; wire shape now omits the field) |
Covered (e2e ResourceTests updated by AZ-197 batch 6) |
| AZ-197 AC-2 |
PUT /users/hardware/set and POST /resources/check return 404 |
FT-N-15 |
Covered |
| AZ-197 AC-3 |
Security.GetApiEncryptionKey signature simplified to (email, password) |
— |
Internal signature — covered by Azaion.Test/SecurityTest unit tests, not blackbox |
| AZ-197 AC-4 |
HardwareBindingTests removed; no remaining test asserts code 40 / hardware-hash binding |
— |
Build/CI invariant — verified by test-suite enumeration |
| AZ-197 AC-5 |
Resource calls in remaining tests do not send Hardware |
— |
Build/CI invariant — verified by source review during AZ-197 batch 6 |
| AZ-197 AC-6 |
ExceptionEnum no longer carries HardwareIdMismatch / BadHardware |
— |
Build/CI invariant — verified by enum read |
| AZ-197 AC-7 |
dotnet build is clean (no new warnings) |
— |
Build invariant |
| AZ-197 AC-8 |
Test suite passes (excluding deleted HardwareBindingTests) |
All e2e tests + Azaion.Test |
Covered by Step 11 Run Tests (48/48 e2e + 2/2 unit, 2026-05-13) |
Obsoleted Baseline Entries (superseded by AZ-197)
The matrix rows below are kept for ID stability but no longer reflect production behaviour. They are superseded by the AZ-197 entries above and by FT-N-15 in blackbox-tests.md. Do NOT regenerate or delete these in cycle-update mode — wait for a full /test-spec rerun.
| Legacy Matrix Row |
Status |
| AC-10 (First hardware check stores) |
Obsoleted by AZ-197 — endpoint removed |
| AC-11 (Subsequent hardware check validates) |
Obsoleted by AZ-197 — endpoint removed |
| AC-12 (Hardware mismatch returns code 40) |
Obsoleted by AZ-197 — ExceptionEnum value removed |
| AC-19 (Encryption key derived from email+password+hw) |
Partially obsoleted — derivation is now email + password only |
Cycle 2 Cleanup (2026-05-14) — Obsolete Resource Endpoints Removed
The encrypted-download and installer-download endpoints were removed as obsolete. Affected matrix rows below are kept for ID stability but the underlying behaviour is gone; they are superseded by FT-N-16 in blackbox-tests.md.
| Removed surface |
Endpoint(s) |
Affected legacy entries |
Status |
| Per-user encrypted resource download |
POST /resources/get/{dataFolder?} |
AC-14 (AES-256-CBC encryption), AC-15 (round-trip), AC-19 (key derivation), FT-P-09, FT-P-10 |
Reverted — endpoint deleted; Security.GetApiEncryptionKey / EncryptTo / DecryptTo and ResourcesService.GetEncryptedResource deleted; GetResourceRequest DTO deleted; e2e tests Encrypted_download_returns_octet_stream_and_non_empty_body and Encryption_round_trip_decrypt_matches_original_bytes deleted from ResourceTests.cs; e2e test Per_user_encryption_produces_distinct_ciphertext_for_same_file deleted from SecurityTests.cs; Azaion.Test/SecurityTest.cs deleted (and the now-empty Azaion.Test project removed from the solution). |
| Installer download (production + staging) |
GET /resources/get-installer, GET /resources/get-installer/stage |
AC-23 (latest installer), ResourcesConfig.SuiteInstallerFolder / SuiteStageInstallerFolder references |
Reverted — endpoints deleted; ResourcesService.GetInstaller deleted; both config properties removed from appsettings.json, .env.example, secrets/staging.public.env, secrets/production.public.env, and docker-compose.test.yml. No e2e tests had been written for these endpoints, so no tests required removal. |
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| Cycle-2 AC-1 |
POST /resources/get/{dataFolder?} returns 404 |
FT-N-16 |
Covered |
| Cycle-2 AC-2 |
GET /resources/get-installer returns 404 |
FT-N-16 |
Covered |
| Cycle-2 AC-3 |
GET /resources/get-installer/stage returns 404 |
FT-N-16 |
Covered |
| Cycle-2 AC-4 |
ExceptionEnum no longer carries WrongResourceName (50); the gap is preserved |
— |
Build/CI invariant — verified by enum read |
| Cycle-2 AC-5 |
Azaion.Test project no longer in solution; build is clean |
— |
Build invariant — dotnet build Azaion.AdminApi.sln clean post-cleanup |
| Cycle-2 AC-6 |
E2E suite passes after the test deletions above |
All e2e tests |
Covered by Step 11 Run Tests post-cleanup (2026-05-14) |
Cycle 2 Additions (2026-05-14) — Auth Modernization (AZ-529 + AZ-530)
Appended during the existing-code cycle 2 Test-Spec Sync (autodev Step 12) for the eight tasks delivered by the auth-modernization + CMMC-hardening epics. Rows below are namespaced by tracker ID; functional scenarios live in blackbox-tests.md, security-only invariants in security-tests.md. Existing AC/test IDs from earlier cycles are preserved unchanged.
AZ-536 — Argon2id Password Hashing (epic AZ-530, 5 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-536 AC-1 |
New users get Argon2id hashes (PHC, m ≥ 64 MiB, t ≥ 3, p ≥ 1) |
NFT-SEC-07 |
Covered |
| AZ-536 AC-2 |
Legacy SHA-384 hashes still validate |
FT-P-24 |
Covered |
| AZ-536 AC-3 |
Successful legacy login transparently re-hashes to Argon2id |
FT-P-25 |
Covered |
| AZ-536 AC-4 |
Wrong password fails for both formats with the same error code |
FT-N-17 |
Covered |
| AZ-536 AC-5 |
Verify is constant-time (no remotely observable timing leak) |
NFT-SEC-08 |
Covered (with known suite-concurrency flake — see cycle-2 carry-forward F6) |
AZ-537 — /login Rate Limit + Account Lockout (epic AZ-530, 6 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-537 AC-1 |
Per-IP rate limit triggers HTTP 429 with Retry-After |
NFT-SEC-09 |
Covered (legitimate environment-mismatch skip in shared-IP container env) |
| AZ-537 AC-2 |
Per-account rate limit triggers HTTP 429 across IPs |
NFT-SEC-10 |
Covered |
| AZ-537 AC-3 |
Account lockout after 10 failures returns 423 even on correct password |
NFT-SEC-11 |
Covered |
| AZ-537 AC-4 |
Successful login resets failed_login_count and clears lockout_until |
FT-P-26 |
Covered |
| AZ-537 AC-5 |
Lockout auto-expires after configured duration |
FT-P-27 |
Covered |
| AZ-537 AC-6 |
Audit-log entry written on each lockout event |
NFT-SEC-12 |
Covered |
AZ-538 — CORS HTTPS-Only + HSTS (epic AZ-530, 5 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-538 AC-1 |
HTTP origin gets no Access-Control-Allow-Origin header |
NFT-SEC-13 |
Covered |
| AZ-538 AC-2 |
HTTPS origin preflight echoes credentials flag |
FT-P-28 |
Covered |
| AZ-538 AC-3 |
HSTS header present in production responses |
NFT-SEC-14 |
Covered (legitimate Production-only environment-mismatch skip in dev test harness — verified by code inspection of Program.cs UseHsts) |
| AZ-538 AC-4 |
HTTP request returns 307 to HTTPS in production |
NFT-SEC-15 |
Covered (legitimate Production-only environment-mismatch skip in dev test harness — verified by code inspection of Program.cs UseHttpsRedirection) |
| AZ-538 AC-5 |
Development env unchanged (no redirect, no HSTS) |
FT-P-29 |
Covered |
AZ-531 — Refresh-Token Flow (epic AZ-529, 5 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-531 AC-1 |
/login returns dual tokens, session row persisted |
FT-P-30 |
Covered |
| AZ-531 AC-2 |
/token/refresh rotates refresh + chains via parent_session_id |
FT-P-31 |
Covered |
| AZ-531 AC-3 |
Reuse-detection kills the entire session family |
NFT-SEC-16 |
Covered |
| AZ-531 AC-4 |
Sliding window + 12 h absolute family expiry |
FT-P-32 |
Covered |
| AZ-531 AC-5 |
Refresh tokens are opaque, hashed at rest, never logged in raw form |
NFT-SEC-17 |
Covered |
AZ-532 — Asymmetric Signing + JWKS (epic AZ-529, 5 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-532 AC-1 |
Access tokens carry alg=ES256 + kid |
NFT-SEC-18 |
Covered |
| AZ-532 AC-2 |
GET /.well-known/jwks.json serves the active public key with cache headers |
FT-P-33 |
Covered |
| AZ-532 AC-3 |
Two-key overlap during rotation (both JWKS entries valid) |
FT-P-34 |
Covered |
| AZ-532 AC-4 |
JWKS never exposes private material |
NFT-SEC-19 |
Covered |
| AZ-532 AC-5 |
alg-confusion forgery (HS256 with public key as secret) is rejected |
NFT-SEC-20 |
Covered |
AZ-533 — Mission-Token Issuance for UAV (epic AZ-529, 6 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-533 AC-1 |
Mission token issued with correct lifetime (planned_duration_h + 1h) |
FT-P-35 |
Covered |
| AZ-533 AC-2 |
Hard cap of 12 h enforced (HTTP 400 with cap message) |
FT-N-19 |
Covered |
| AZ-533 AC-3 |
Mission token carries mission_id, aircraft_id, aud, permissions, sid, jti |
FT-P-36 |
Covered |
| AZ-533 AC-4 |
Mission session auto-revoked when aircraft user reconnects |
FT-P-37 |
Covered |
| AZ-533 AC-5 |
Endpoint requires authenticated session |
FT-N-18 |
Covered |
| AZ-533 AC-6 |
MFA step-up required (amr must include mfa) |
NFT-SEC-21 |
Spec only — pending wire-up post-AZ-534 (cycle-2 carry-forward F1) |
AZ-534 — TOTP-Based 2FA at Login (epic AZ-529, 6 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-534 AC-1 |
Enrollment returns secret + QR + 10 recovery codes |
FT-P-38 |
Covered |
| AZ-534 AC-2 |
Confirm with valid TOTP completes enrollment |
FT-P-39 |
Covered |
| AZ-534 AC-3 |
Two-step /login → /login/mfa flow; access-token amr=["pwd","mfa"] |
FT-P-40 |
Covered |
| AZ-534 AC-4 |
Recovery code substitutes for TOTP and is single-use |
FT-P-41 |
Covered |
| AZ-534 AC-5 |
Disable requires password + valid TOTP |
FT-P-42 |
Covered |
| AZ-534 AC-6 |
TOTP secret encrypted at rest in users.mfa_secret |
NFT-SEC-22 |
Covered |
AZ-535 — Logout + Revocation Surface (epic AZ-529, 5 ACs)
| AC ID |
Acceptance Criterion |
Test IDs |
Coverage |
| AZ-535 AC-1 |
POST /logout revokes the current session and kills refresh |
FT-P-43 |
Covered |
| AZ-535 AC-2 |
POST /logout/all revokes every session for the user |
FT-P-44 |
Covered |
| AZ-535 AC-3 |
Admin can revoke any session by id; row records actor |
FT-P-45 |
Covered |
| AZ-535 AC-4 |
GET /sessions/revoked?since=… returns recent, non-expired entries |
FT-P-46 |
Covered |
| AZ-535 AC-5 |
POST /logout is idempotent (no second DB write) |
FT-P-47 |
Covered |
Cycle 2 Coverage Update
| Category |
Total Items |
Covered |
Not Yet Wired |
Coverage % |
| Acceptance Criteria (cycle 2 — auth modernization) |
43 |
42 |
1 (AZ-533 AC-6 — pending wire-up F1) |
98% |
| Acceptance Criteria — combined total (baseline + cycle 1 + cycle 2 cleanup + cycle 2 auth) |
100 |
96 |
1 (F1) + 3 baseline restrictions still uncovered |
96% |
The single uncovered cycle-2 AC (AZ-533 AC-6) is documented in the cycle-2 implementation report as carry-forward item F1 — the /sessions/mission amr=mfa enforcement was deferred during AZ-533, became implementable once AZ-534 shipped, and is filed as a follow-up ticket to be picked up in a later cycle.