From c31f985e7a3d82b78a61a3a142fd94da9d3f3aae Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 20:55:32 +0200 Subject: [PATCH 1/6] docs(sprint-10): plan flip verify_tls default to false (redteam-friendly) --- tasks/todo.md | 148 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 95 insertions(+), 53 deletions(-) diff --git a/tasks/todo.md b/tasks/todo.md index 49d6d85..887b4d5 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,73 +1,115 @@ -# Sprint 9 — UI: engagement 2-col + global contrast pass +# Sprint 10 — C2 TLS verify: redteam-friendly defaults + warning suppression -**Base**: `sprint/8-c2` (sprint 8 not yet merged on origin/main, but its `C2ConfigCard` is the right pane). -**Scope**: frontend-only. No backend, no schema. No new features. +**Base**: `origin/main` (PR #11 merged — sprint 8 + 9 are in). +**Branch**: `sprint/10-c2-tls-default`. +**Scope**: tiny correctness sprint. No new feature. Flip a security-relevant default to match the actual operator usage of this tool. + +--- + +## Symptom (user report) + +> "Impossible de se connecter à mon C2 à cause de la vérification du certificat SSL (self signed)" + +Operator opens C2 config card, fills URL + token, hits **Test connection** without noticing the checkbox → SSL VERIFY FAILED against self-signed Mythic. + +## Root cause (workflow diagnosis confirmed) + +The `verify_tls` chain is **fully intact** end-to-end : +React `verifyTls` state → `C2ConfigInput.verify_tls` → PUT body → `C2Config.verify_tls` column → `cfg.verify_tls` in API loader → `get_adapter(verify_tls=)` → `MythicAdapter._verify` → `requests.post(verify=self._verify)`. + +The bug is the **default** at every layer is `True` : +- `backend/app/models/c2_config.py:22` — `default=True` +- `backend/migrations/versions/0006_c2_layer.py:29` — `server_default=sa.true()` +- `backend/app/api/c2.py:87` — `data.get("verify_tls", True)` +- `backend/app/services/c2/mythic.py:116` — `verify_tls: bool = True` +- `backend/app/services/c2/factory.py:9` — `verify_tls=True` +- `frontend/src/components/C2ConfigCard.tsx:27` — `useState(true)` +- `frontend/src/components/C2ConfigCard.tsx:74` — reset on delete `setVerifyTls(true)` + +Mimic is a BAS / red-team lab tool ; the dominant case is a **self-signed Mythic instance**, not a publicly-trusted cert chain. Defaulting `verify=True` is hostile to the actual workflow. + +Secondary defect : `urllib3.exceptions.InsecureRequestWarning` is never suppressed (zero hits for `disable_warnings` / `urllib3` in `backend/`). When operators correctly uncheck verify, stderr gets spammed once per HTTP call. --- ## Decisions (locked) -1. **Engagement page** : passer en **2 colonnes** sur desktop (`lg:grid-cols-2`), `[engagement form | C2ConfigCard]`. Mobile/tablet : stack vertical (comportement actuel). -2. **Contraste global** : le problème est que `canvas` (page bg) et `paper` (card bg) sont **tous deux `#ffffff`** en light mode. Les cartes ne ressortent que par leur hairline 1px → fatigue oculaire confirmée par l'utilisateur. -3. **Fix retenu** : **tinter le canvas light** d'un neutre froid très pâle. `paper` reste blanc pur. Les cartes "lèvent" naturellement sans casser le brutalisme. - - Proposition : `canvas` light `#f3f5f8` (gris-bleu très pâle, cohérent avec l'electric blue), `paper` light `#ffffff`. - - Dark mode **inchangé** (`canvas #111827` / `paper #1f2937` déjà différenciés). -4. **Pas de shadow**, pas de radius. La brutalité reste intacte — seul le contraste de surface change. -5. **Hairline** : à vérifier sur le nouveau canvas. Si nécessaire, passer `hairline` light de la valeur actuelle à un poil plus sombre pour préserver la lisibilité du bord sur tinted canvas. Mais éviter si la lecture est déjà bonne. +1. **Flip default to `False` at every layer** — model, API fallback, adapter, factory, React state, delete reset. +2. **New migration 0008** : flip `server_default` to `sa.false()`. Existing rows are NOT mutated (their stored boolean is preserved). +3. **Suppress `urllib3.InsecureRequestWarning` ONLY when `verify_tls=False`** — gated inside `MythicAdapter.__init__`. Keeps the warning live for any future code that legitimately verifies. +4. **Add helper text under checkbox** : "Leave unchecked for lab Mythic with self-signed certificates." Operators see why the box matters. +5. **No API contract change** — `C2ConfigInput.verify_tls: boolean` stays required. The fallback in `data.get("verify_tls", False)` only matters for hand-crafted requests. + +## Out of scope + +- Don't touch existing C2 endpoints behavior (route paths, payload shapes). +- Don't change the `verify_tls` field type or remove the column. +- Don't change the FakeAdapter (it makes no HTTP calls). --- -## Task A — EngagementFormPage 2-col - -**File** : `frontend/src/pages/EngagementFormPage.tsx` - -- Remplacer le wrapper `
` par un container plus large + grid 2-col responsive. -- Header reste en haut, full width. -- Body : `grid grid-cols-1 lg:grid-cols-2 gap-xl` avec : - - Col gauche : `
` engagement (déjà en `card-product`). - - Col droite : `` (seulement quand `editing && canEditEngagements`). -- Si pas en edit (création) : col droite vide → garder la grid mais que la col gauche se déploie via `lg:col-span-2` (pour pas avoir un vide à droite). Acceptable alternative : `flex` + `max-w-2xl` quand non-editing. -- Pas de modif sur la logique de form, validation, mutations. - -## Task B — Contrast pass (tokens) +## Task A — Backend (backend-builder) **Files** : -- `DESIGN.md` § Surface : mettre à jour `canvas` light = `#f3f5f8`, conserver `paper` light = `#ffffff`. Documenter dans la même section que "canvas tints lift paper cards in light mode without violating brutalism". -- Token source de vérité (Tailwind config ou CSS vars). Localiser et appliquer la même MAJ. Probablement `frontend/tailwind.config.js` ou un `frontend/src/styles/tokens.css` / `index.css`. -- Vérifier qu'aucun composant ne hardcode `#ffffff` pour la page bg (devrait utiliser `bg-canvas`). -- Tests CSS smoke : `bg-canvas` continue de matcher, dark mode inchangé. +- `backend/app/models/c2_config.py` — line 22 : `default=True` → `default=False` +- `backend/app/api/c2.py` — line 87 : `data.get("verify_tls", True)` → `data.get("verify_tls", False)` +- `backend/app/services/c2/factory.py` — line 9 : `verify_tls: bool = True` → `verify_tls: bool = False` +- `backend/app/services/c2/mythic.py` — line 116 : `verify_tls: bool = True` → `verify_tls: bool = False`, AND add at top of file `import urllib3` + `from urllib3.exceptions import InsecureRequestWarning`, AND inside `__init__` after `self._verify = verify_tls`: + ```python + if not verify_tls: + urllib3.disable_warnings(InsecureRequestWarning) + ``` +- **NEW migration** `backend/migrations/versions/0008_c2_verify_tls_default_false.py` — flip `server_default` to `sa.false()`. Use `op.batch_alter_table("c2_config")` for SQLite compatibility (Mimic uses SQLite per sprint 1 SPEC). Down-migration restores `sa.true()`. +- **Tests** : grep `backend/tests/` for `verify_tls` and flip every assertion that presupposed the old `True` default (likely in `test_c2_config*.py` PUT-without-verify-tls tests and GET-fresh-row tests). Don't add new tests — adapt existing ones. -## Task C — Visual regression check +**Constraints** : +- `pytest` baseline 468/468 must hold (or grow ; never shrink). +- `ruff` + `mypy --strict` clean. +- Migration 0008 must be reversible — round-trip `alembic upgrade head` then `alembic downgrade -1` then `alembic upgrade head` must work on a fresh SQLite DB. +- Don't restructure or refactor anything else. Minimum surface. -- `pnpm vitest run` clean. -- `pnpm tsc --noEmit` clean. -- `pnpm lint` clean. -- Screenshots avant/après (au moins) : - - EngagementsListPage (cards-on-canvas) - - EngagementDetailPage - - EngagementFormPage (edit, avec C2ConfigCard à droite) - - SimulationFormPage (déjà 2-col sprint 7, vérifier que le tinted canvas n'écrase pas) - - LoginPage -- Dark mode : passe rapide pour confirmer aucune régression. +## Task B — Frontend (frontend-builder) ---- +**Files** : +- `frontend/src/components/C2ConfigCard.tsx` : + - Line 27 : `useState(true)` → `useState(false)` + - Line 74 (delete handler) : `setVerifyTls(true)` → `setVerifyTls(false)` + - Under the checkbox JSX (around lines 169-182) : add a `

` with helper text : + ```tsx +

+ Leave unchecked for lab Mythic with self-signed certificates. +

+ ``` + (Use the existing DESIGN.md tokens — `text-[12px] text-charcoal` matches the `hint` style on `FormField`. Confirm token name by reading neighboring components first.) +- **Vitest** : if `C2ConfigCard.test.tsx` exists, flip any "starts checked" assertion to "starts unchecked". -## Sequencing +**Constraints** : +- `vitest` baseline 212/212 must hold. +- `tsc --noEmit` + `eslint --max-warnings=0` clean. +- No type change to `C2ConfigInput` (the field is already required `boolean`). +- Visual: same row, just one extra `

` hint below the checkbox-label row. Same brutalist treatment, no transition. -1. **frontend-builder** : Task A + B + C. Une seule passe, commits atomiques. -2. **design-reviewer** : revue visuelle après merge des commits builder. Focus : - - Lecture confortable cards-on-tinted-canvas. - - Hairline encore visible. - - Dark mode inchangé. - - Pas de régression sur components qui pourraient ré-utiliser `bg-canvas` pour autre chose (dropdowns, modals). +## Task C — Sequencing ---- +Both tasks have **zero shared files**. Dispatch backend-builder + frontend-builder **in parallel**. No ordering constraint. + +After both report green : +- **code-reviewer** : sprint diff scan (focus : migration reversibility, urllib3 gating, no leftover hardcoded `True`). +- **design-reviewer** : helper-text placement, token compliance, focus ring still works, no regression on the card. +- **spec-reviewer** : verify SPEC.md § Intégration C2 still matches the new defaults (may need a one-line note about lab-mode default ; check before editing). ## Definition of Done -- EngagementFormPage en édition : 2 colonnes desktop, stack mobile. -- Page bg différencié de card bg en light mode (eyes confort). -- Vitest + typecheck + lint verts. -- Design-reviewer APPROVED. -- Screenshots livrés ou écueil documenté. -- Commits conventional, branche `sprint/9-ui-contrast`. +- Test connection against self-signed Mythic from a freshly-created C2 config works **without unchecking anything**. +- Existing rows are untouched (operators who saved verify_tls=true keep it until they re-save). +- `pytest` 468/468 → 468+ (no shrink), `vitest` 212/212. +- `ruff` + `mypy --strict` + `tsc --noEmit` + `eslint` clean. +- Migration 0008 round-trip OK. +- No `urllib3.InsecureRequestWarning` on stderr when `verify_tls=False`. +- Code-reviewer + design-reviewer + spec-reviewer APPROVED. +- PR opened on Gitea ; tasks/pr-body-sprint-10.md drafted by team-lead. + +## Operator notes (for PR body) + +- This sprint flips a security-relevant default. Production operators who legitimately use a publicly-trusted cert chain will need to explicitly check the box — but the dominant case in this BAS tool is lab Mythic with self-signed, so the new default matches the actual workflow. +- Existing engagements with `verify_tls=true` already stored remain unchanged. They keep their current behaviour. If the operator reported the bug because their existing row has `verify_tls=true`, they will still need to uncheck + save once after this sprint lands. -- 2.49.1 From 774ae0574837209e6d1817fbb636ac9943659488 Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 20:57:41 +0200 Subject: [PATCH 2/6] fix(frontend): default verify_tls checkbox to unchecked + add helper text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flip useState(true) → useState(false) and delete-reset from true → false so a freshly-created C2 config does not block self-signed Mythic instances. Add hint paragraph below the checkbox matching FormField hint style (text-[12px] text-graphite): "Leave unchecked for lab Mythic with self-signed certificates." Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/C2ConfigCard.tsx | 35 ++++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/C2ConfigCard.tsx b/frontend/src/components/C2ConfigCard.tsx index 1363310..ced6190 100644 --- a/frontend/src/components/C2ConfigCard.tsx +++ b/frontend/src/components/C2ConfigCard.tsx @@ -24,7 +24,7 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { const [url, setUrl] = useState(''); const [token, setToken] = useState(''); - const [verifyTls, setVerifyTls] = useState(true); + const [verifyTls, setVerifyTls] = useState(false); const [replaceToken, setReplaceToken] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null); @@ -71,7 +71,7 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { push('C2 configuration removed', 'success'); setUrl(''); setToken(''); - setVerifyTls(true); + setVerifyTls(false); setReplaceToken(false); } catch (err) { push(extractApiError(err, 'Could not remove C2 configuration'), 'error'); @@ -166,19 +166,24 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element { )} -

- setVerifyTls(e.target.checked)} - disabled={disabled} - className="h-4 w-4 accent-primary" - /> - +
+
+ setVerifyTls(e.target.checked)} + disabled={disabled} + className="h-4 w-4 accent-primary" + /> + +
+

+ Leave unchecked for lab Mythic with self-signed certificates. +

-- 2.49.1 From ee5fda605906ccca20f39e208a26cc2cd1061539 Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 21:01:49 +0200 Subject: [PATCH 3/6] fix(frontend): make verify_tls helper text spell out the MITM tradeoff Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/C2ConfigCard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/C2ConfigCard.tsx b/frontend/src/components/C2ConfigCard.tsx index ced6190..7dc2955 100644 --- a/frontend/src/components/C2ConfigCard.tsx +++ b/frontend/src/components/C2ConfigCard.tsx @@ -182,7 +182,8 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element {

- Leave unchecked for lab Mythic with self-signed certificates. + Uncheck only for lab Mythic with self-signed certificates. Disabling + verification exposes the API token to MITM.

-- 2.49.1 From 0fab40b486afd42505cc6d472c55326b8152d846 Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 21:03:05 +0200 Subject: [PATCH 4/6] feat(backend): default verify_tls to false for lab/redteam Mythic flip default to False at model, API fallback, adapter, and factory layers. add migration 0008 flipping c2_config.verify_tls server_default to false. suppress urllib3 InsecureRequestWarning when verify_tls is off. adapt c2 tests to new verify_tls default. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/api/c2.py | 2 +- backend/app/models/c2_config.py | 2 +- backend/app/services/c2/factory.py | 2 +- backend/app/services/c2/mythic.py | 8 ++++- .../0008_c2_verify_tls_default_false.py | 33 +++++++++++++++++++ backend/tests/test_c2_config.py | 6 ++-- 6 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 backend/migrations/versions/0008_c2_verify_tls_default_false.py diff --git a/backend/app/api/c2.py b/backend/app/api/c2.py index 745637c..3f2a131 100644 --- a/backend/app/api/c2.py +++ b/backend/app/api/c2.py @@ -84,7 +84,7 @@ def upsert_c2_config(eid: int): if not parsed.hostname: return jsonify({"error": "url must contain a hostname"}), 400 - verify_tls = data.get("verify_tls", True) + verify_tls = data.get("verify_tls", False) if not isinstance(verify_tls, bool): return jsonify({"error": "verify_tls must be a boolean"}), 400 diff --git a/backend/app/models/c2_config.py b/backend/app/models/c2_config.py index 4015a13..e3876a0 100644 --- a/backend/app/models/c2_config.py +++ b/backend/app/models/c2_config.py @@ -19,7 +19,7 @@ class C2Config(db.Model): # type: ignore[name-defined] ) url = db.Column(db.Text, nullable=False) api_token_encrypted = db.Column(db.Text, nullable=False) - verify_tls = db.Column(db.Boolean, nullable=False, default=True) + verify_tls = db.Column(db.Boolean, nullable=False, default=False) created_at = db.Column( db.DateTime, nullable=False, default=lambda: datetime.now(UTC) ) diff --git a/backend/app/services/c2/factory.py b/backend/app/services/c2/factory.py index 0c46370..75cce7a 100644 --- a/backend/app/services/c2/factory.py +++ b/backend/app/services/c2/factory.py @@ -6,7 +6,7 @@ import os from backend.app.services.c2.adapter import C2Adapter -def get_adapter(url: str, api_token: str, verify_tls: bool = True) -> C2Adapter: +def get_adapter(url: str, api_token: str, verify_tls: bool = False) -> C2Adapter: """Return the correct C2Adapter based on MIMIC_C2_ADAPTER (default: mythic).""" adapter_name = os.environ.get("MIMIC_C2_ADAPTER", "mythic").lower() diff --git a/backend/app/services/c2/mythic.py b/backend/app/services/c2/mythic.py index 2eec290..c430afa 100644 --- a/backend/app/services/c2/mythic.py +++ b/backend/app/services/c2/mythic.py @@ -15,6 +15,8 @@ from __future__ import annotations from datetime import datetime import requests +import urllib3 +from urllib3.exceptions import InsecureRequestWarning from backend.app.services.c2.adapter import ( C2Adapter, @@ -113,10 +115,14 @@ query GetTaskOutput($display_id: Int!) { class MythicAdapter(C2Adapter): """Real Mythic 3.x adapter using GraphQL over HTTP.""" - def __init__(self, url: str, api_token: str, verify_tls: bool = True) -> None: + def __init__(self, url: str, api_token: str, verify_tls: bool = False) -> None: self._url = url.rstrip("/") + "/graphql" self._token = api_token self._verify = verify_tls + if not verify_tls: + # Lab/red-team Mythic instances commonly use self-signed certs. + # Suppress urllib3 warnings once per process when verification is off. + urllib3.disable_warnings(InsecureRequestWarning) def _headers(self) -> dict[str, str]: return { diff --git a/backend/migrations/versions/0008_c2_verify_tls_default_false.py b/backend/migrations/versions/0008_c2_verify_tls_default_false.py new file mode 100644 index 0000000..33e1dc0 --- /dev/null +++ b/backend/migrations/versions/0008_c2_verify_tls_default_false.py @@ -0,0 +1,33 @@ +"""flip c2_config.verify_tls server_default to false + +Revision ID: 0008_c2_verify_tls_default_false +Revises: 0007 +Create Date: 2026-06-21 00:00:00.000000 +""" +import sqlalchemy as sa +from alembic import op + +revision = "0008_c2_verify_tls_default_false" +down_revision = "0007" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + with op.batch_alter_table("c2_config") as batch_op: + batch_op.alter_column( + "verify_tls", + existing_type=sa.Boolean(), + existing_nullable=False, + server_default=sa.false(), + ) + + +def downgrade() -> None: + with op.batch_alter_table("c2_config") as batch_op: + batch_op.alter_column( + "verify_tls", + existing_type=sa.Boolean(), + existing_nullable=False, + server_default=sa.true(), + ) diff --git a/backend/tests/test_c2_config.py b/backend/tests/test_c2_config.py index fd2c617..d8f4235 100644 --- a/backend/tests/test_c2_config.py +++ b/backend/tests/test_c2_config.py @@ -206,13 +206,13 @@ def test_get_config_returns_has_token_not_cleartext( assert "s3cr3t" not in str(body) -def test_get_config_verify_tls_default_true( +def test_get_config_verify_tls_default_false( client: FlaskClient, admin_token: str ) -> None: eng = _make_engagement(client, admin_token) - _put_config(client, admin_token, eng["id"]) + _put_config(client, admin_token, eng["id"], verify_tls=False) resp = client.get(f"/api/engagements/{eng['id']}/c2-config", headers=_h(admin_token)) - assert resp.get_json()["verify_tls"] is True + assert resp.get_json()["verify_tls"] is False # --------------------------------------------------------------------------- -- 2.49.1 From 91533c214c8cb16afeefb1e11324a438824a1b5d Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 21:06:10 +0200 Subject: [PATCH 5/6] docs(spec): align c2_config.verify_tls default with sprint 10 (false) --- SPEC.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index 7331b08..8ff1022 100644 --- a/SPEC.md +++ b/SPEC.md @@ -82,7 +82,7 @@ Couche d'intégration C2 permettant d'exécuter les commandes d'une simulation | `engagement_id` | int FK `engagements.id` ON DELETE CASCADE, **UNIQUE** | | | `url` | text | endpoint Mythic, ex. `https://lab.internal:7443` | | `api_token_encrypted` | text | Fernet ciphertext, jamais en clair | -| `verify_tls` | bool, défaut `true` | `false` autorisé pour labs auto-signés | +| `verify_tls` | bool, défaut `false` | Désactivé par défaut — Mimic cible des labs Mythic avec certs auto-signés. Cocher pour forcer la vérification TLS en prod. | | `created_at`, `updated_at` | datetime | | `c2_task` (lien simulation ↔ tâche Mythic) : -- 2.49.1 From 208c6e64280f63c6d9235778158736129b2f0340 Mon Sep 17 00:00:00 2001 From: Knacky Date: Sun, 21 Jun 2026 21:25:06 +0200 Subject: [PATCH 6/6] fix(c2): append trailing slash to mythic /graphql URL to avoid 301 redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nginx fronting Mythic 3.x redirects /graphql → /graphql/ (HTTP 301). The sprint 8 SSRF defense (allow_redirects=False) is correct and intentional; the fix is the URL, not the redirect policy. Upstream reference: mythic_utilities.get_http_transport hardcodes /graphql/ with trailing slash. Updated _GQL_URL in all three adapter test files to match. Added TestMythicAdapterUrlConstruction regression test to prevent silent regression on future refactors. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/services/c2/mythic.py | 5 +++-- backend/tests/test_c2_adapter_mythic.py | 14 ++++++++++++-- backend/tests/test_c2_adapter_mythic_m3.py | 2 +- backend/tests/test_c2_adapter_mythic_m4.py | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/backend/app/services/c2/mythic.py b/backend/app/services/c2/mythic.py index c430afa..cb83221 100644 --- a/backend/app/services/c2/mythic.py +++ b/backend/app/services/c2/mythic.py @@ -1,7 +1,8 @@ # Contract pinned from MythicMeta/Mythic_Scripting master @ 2026-06-10 (raw.githubusercontent.com/MythicMeta/Mythic_Scripting/master/mythic/mythic.py) +# Sprint 10 correction: /graphql/ MUST have trailing slash (matches mythic_utilities.get_http_transport). """Mythic 3.x C2 adapter. -Transport: POST https://:7443/graphql +Transport: POST https://:7443/graphql/ Header: apitoken: Backend: Hasura-proxied Postgres behind nginx. @@ -116,7 +117,7 @@ class MythicAdapter(C2Adapter): """Real Mythic 3.x adapter using GraphQL over HTTP.""" def __init__(self, url: str, api_token: str, verify_tls: bool = False) -> None: - self._url = url.rstrip("/") + "/graphql" + self._url = url.rstrip("/") + "/graphql/" self._token = api_token self._verify = verify_tls if not verify_tls: diff --git a/backend/tests/test_c2_adapter_mythic.py b/backend/tests/test_c2_adapter_mythic.py index de32055..3ec77c2 100644 --- a/backend/tests/test_c2_adapter_mythic.py +++ b/backend/tests/test_c2_adapter_mythic.py @@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error from backend.app.services.c2.mythic import MythicAdapter _BASE_URL = "https://mythic.lab:7443" -_GQL_URL = _BASE_URL + "/graphql" +_GQL_URL = _BASE_URL + "/graphql/" _TOKEN = "fake-api-token" @@ -143,9 +143,19 @@ class TestMythicAdapterNoRedirects: """Adapter must not follow HTTP redirects (allow_redirects=False).""" with rm_module.Mocker() as m: # Simulate a redirect response; requests-mock won't auto-follow it. - m.post(_GQL_URL, status_code=301, headers={"Location": "https://evil.example/graphql"}) + m.post(_GQL_URL, status_code=301, headers={"Location": "https://evil.example/graphql/"}) # With allow_redirects=False the 301 is treated as a non-2xx → raise_for_status raises. with pytest.raises(C2Error): adapter.list_callbacks() # Exactly one request was made — no follow-up to Location. assert len(m.request_history) == 1 + + +class TestMythicAdapterUrlConstruction: + def test_mythic_adapter_url_has_trailing_slash(self): + a1 = MythicAdapter(url="https://x:7443", api_token="t") + assert a1._url == "https://x:7443/graphql/" + a2 = MythicAdapter(url="https://x:7443/", api_token="t") + assert a2._url == "https://x:7443/graphql/" + a3 = MythicAdapter(url="https://x:7443///", api_token="t") + assert a3._url == "https://x:7443/graphql/" diff --git a/backend/tests/test_c2_adapter_mythic_m3.py b/backend/tests/test_c2_adapter_mythic_m3.py index e6d5963..a749616 100644 --- a/backend/tests/test_c2_adapter_mythic_m3.py +++ b/backend/tests/test_c2_adapter_mythic_m3.py @@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error from backend.app.services.c2.mythic import MythicAdapter _BASE_URL = "https://mythic.lab:7443" -_GQL_URL = _BASE_URL + "/graphql" +_GQL_URL = _BASE_URL + "/graphql/" _TOKEN = "fake-api-token" diff --git a/backend/tests/test_c2_adapter_mythic_m4.py b/backend/tests/test_c2_adapter_mythic_m4.py index 5103185..206eb76 100644 --- a/backend/tests/test_c2_adapter_mythic_m4.py +++ b/backend/tests/test_c2_adapter_mythic_m4.py @@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error, C2HistoricalTask from backend.app.services.c2.mythic import MythicAdapter _BASE_URL = "https://mythic.lab:7443" -_GQL_URL = _BASE_URL + "/graphql" +_GQL_URL = _BASE_URL + "/graphql/" _TOKEN = "fake-api-token" -- 2.49.1