# 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) |