test(m4): cover the new security guards + pin e2e to exact MITRE v19 counts

- 5 new pytest covering paths the code-reviewer flagged as un-asserted:
    * `test_seed_refuses_file_url` — `file://` scheme rejected before I/O
      (was the SSRF-to-local-FS vector).
    * `test_seed_refuses_disallowed_https_host` — non-allowlisted HTTPS
      host rejected with `MitreSourceForbidden`.
    * `test_seed_refuses_custom_url_without_sha` — end-to-end guard that
      `seed_mitre(source=<custom URL>, expected_sha256=None,
      allow_unverified=False)` raises `MitreSeedError`.
    * `test_dotted_id_fallback_resolves_orphan_subtechnique` — STIX bundle
      without `relationship[subtechnique-of]` still attaches T1059.001 to
      T1059 via the dotted-id convention.
    * `test_seed_clears_version_when_source_is_not_default` — seed from a
      local path leaves `settings.mitre_version` NULL (no stale pin).
- Existing `test_checksum_mismatch_aborts` reworked to monkey-patch
  `_ensure_host_allowed` so `file://` can drive the test past the allowlist
  gate (was relying on file:// being accepted before CR1).
- Removed unused `uuid` import.
- e2e: assertions on `tactics_upserted`/`techniques_upserted`/
  `subtechniques_upserted` switched from `>= 14/180/400` thresholds to
  `=== 15/222/475` exact counts pinned to MITRE Enterprise v19.0 + 0
  orphans. Catches parser regressions that would silently include revoked
  rows. Bump alongside MITRE_VERSION when re-pinning.
- e2e: `Math.random()` → `crypto.randomUUID().slice(0, 8)` for unique
  test-run emails (collision-safe across parallel CI workers).

DoD: 58 pytest pass (was 53), 34 Playwright pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-12 19:19:34 +02:00
parent 54adfee690
commit 8b1de6e258
2 changed files with 80 additions and 10 deletions

View File

@@ -9,7 +9,9 @@ import { expect, test, type APIRequestContext, type Page } from '@playwright/tes
* + the picker UI.
*/
const ADMIN_EMAIL = `admin-${Math.floor(Math.random() * 1e6)}@metamorph.local`;
// crypto.randomUUID() guarantees uniqueness across parallel test runs; the
// Math.random() previous pattern could collide one-in-a-million in CI.
const ADMIN_EMAIL = `admin-${crypto.randomUUID().slice(0, 8)}@metamorph.local`;
const ADMIN_PASSWORD = 'AdminPass1234!';
async function resetAndMintToken(request: APIRequestContext): Promise<string> {
@@ -55,9 +57,13 @@ test.describe('M4 — MITRE ATT&CK reference', () => {
});
expect(sync.status(), `mitre sync failed: ${await sync.text()}`).toBe(200);
const result = await sync.json();
expect(result.tactics_upserted).toBeGreaterThanOrEqual(14);
expect(result.techniques_upserted).toBeGreaterThanOrEqual(180);
expect(result.subtechniques_upserted).toBeGreaterThanOrEqual(400);
// Pinned exactly to MITRE Enterprise v19.0 — bump alongside MITRE_VERSION
// in `app/services/mitre_seed.py` when the pin changes. Exact counts catch
// parser regressions that would silently include revoked/deprecated rows.
expect(result.tactics_upserted).toBe(15);
expect(result.techniques_upserted).toBe(222);
expect(result.subtechniques_upserted).toBe(475);
expect(result.subtechniques_skipped_orphan).toBe(0);
});
test('GET /mitre/tactics returns 14+ Enterprise tactics', async ({ request }) => {
@@ -146,7 +152,7 @@ test.describe('M4 — MITRE ATT&CK reference', () => {
test('Non-admin cannot trigger /mitre/sync', async ({ page, request }) => {
// Invite a no-perm user via the admin.
const adminAccess = await loginAndGetAccess(request, ADMIN_EMAIL, ADMIN_PASSWORD);
const eveEmail = `eve-${Math.floor(Math.random() * 1e6)}@metamorph.local`;
const eveEmail = `eve-${crypto.randomUUID().slice(0, 8)}@metamorph.local`;
const inv = await request.post('/api/v1/invitations', {
headers: { Authorization: `Bearer ${adminAccess}` },
data: { email_hint: eveEmail },