Compare commits
8 Commits
sprint/9-u
...
65857134ef
| Author | SHA1 | Date | |
|---|---|---|---|
| 65857134ef | |||
|
|
208c6e6428 | ||
|
|
91533c214c | ||
|
|
0fab40b486 | ||
|
|
ee5fda6059 | ||
|
|
774ae05748 | ||
|
|
c31f985e7a | ||
| 24d4a3b146 |
2
SPEC.md
2
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** | |
|
| `engagement_id` | int FK `engagements.id` ON DELETE CASCADE, **UNIQUE** | |
|
||||||
| `url` | text | endpoint Mythic, ex. `https://lab.internal:7443` |
|
| `url` | text | endpoint Mythic, ex. `https://lab.internal:7443` |
|
||||||
| `api_token_encrypted` | text | Fernet ciphertext, jamais en clair |
|
| `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 | |
|
| `created_at`, `updated_at` | datetime | |
|
||||||
|
|
||||||
`c2_task` (lien simulation ↔ tâche Mythic) :
|
`c2_task` (lien simulation ↔ tâche Mythic) :
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def upsert_c2_config(eid: int):
|
|||||||
if not parsed.hostname:
|
if not parsed.hostname:
|
||||||
return jsonify({"error": "url must contain a hostname"}), 400
|
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):
|
if not isinstance(verify_tls, bool):
|
||||||
return jsonify({"error": "verify_tls must be a boolean"}), 400
|
return jsonify({"error": "verify_tls must be a boolean"}), 400
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class C2Config(db.Model): # type: ignore[name-defined]
|
|||||||
)
|
)
|
||||||
url = db.Column(db.Text, nullable=False)
|
url = db.Column(db.Text, nullable=False)
|
||||||
api_token_encrypted = 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(
|
created_at = db.Column(
|
||||||
db.DateTime, nullable=False, default=lambda: datetime.now(UTC)
|
db.DateTime, nullable=False, default=lambda: datetime.now(UTC)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os
|
|||||||
from backend.app.services.c2.adapter import C2Adapter
|
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)."""
|
"""Return the correct C2Adapter based on MIMIC_C2_ADAPTER (default: mythic)."""
|
||||||
adapter_name = os.environ.get("MIMIC_C2_ADAPTER", "mythic").lower()
|
adapter_name = os.environ.get("MIMIC_C2_ADAPTER", "mythic").lower()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Contract pinned from MythicMeta/Mythic_Scripting master @ 2026-06-10 (raw.githubusercontent.com/MythicMeta/Mythic_Scripting/master/mythic/mythic.py)
|
# 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.
|
"""Mythic 3.x C2 adapter.
|
||||||
|
|
||||||
Transport: POST https://<host>:7443/graphql
|
Transport: POST https://<host>:7443/graphql/
|
||||||
Header: apitoken: <token>
|
Header: apitoken: <token>
|
||||||
Backend: Hasura-proxied Postgres behind nginx.
|
Backend: Hasura-proxied Postgres behind nginx.
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ from __future__ import annotations
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import urllib3
|
||||||
|
from urllib3.exceptions import InsecureRequestWarning
|
||||||
|
|
||||||
from backend.app.services.c2.adapter import (
|
from backend.app.services.c2.adapter import (
|
||||||
C2Adapter,
|
C2Adapter,
|
||||||
@@ -113,10 +116,14 @@ query GetTaskOutput($display_id: Int!) {
|
|||||||
class MythicAdapter(C2Adapter):
|
class MythicAdapter(C2Adapter):
|
||||||
"""Real Mythic 3.x adapter using GraphQL over HTTP."""
|
"""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._url = url.rstrip("/") + "/graphql/"
|
||||||
self._token = api_token
|
self._token = api_token
|
||||||
self._verify = verify_tls
|
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]:
|
def _headers(self) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
)
|
||||||
@@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error
|
|||||||
from backend.app.services.c2.mythic import MythicAdapter
|
from backend.app.services.c2.mythic import MythicAdapter
|
||||||
|
|
||||||
_BASE_URL = "https://mythic.lab:7443"
|
_BASE_URL = "https://mythic.lab:7443"
|
||||||
_GQL_URL = _BASE_URL + "/graphql"
|
_GQL_URL = _BASE_URL + "/graphql/"
|
||||||
_TOKEN = "fake-api-token"
|
_TOKEN = "fake-api-token"
|
||||||
|
|
||||||
|
|
||||||
@@ -143,9 +143,19 @@ class TestMythicAdapterNoRedirects:
|
|||||||
"""Adapter must not follow HTTP redirects (allow_redirects=False)."""
|
"""Adapter must not follow HTTP redirects (allow_redirects=False)."""
|
||||||
with rm_module.Mocker() as m:
|
with rm_module.Mocker() as m:
|
||||||
# Simulate a redirect response; requests-mock won't auto-follow it.
|
# 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 allow_redirects=False the 301 is treated as a non-2xx → raise_for_status raises.
|
||||||
with pytest.raises(C2Error):
|
with pytest.raises(C2Error):
|
||||||
adapter.list_callbacks()
|
adapter.list_callbacks()
|
||||||
# Exactly one request was made — no follow-up to Location.
|
# Exactly one request was made — no follow-up to Location.
|
||||||
assert len(m.request_history) == 1
|
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/"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error
|
|||||||
from backend.app.services.c2.mythic import MythicAdapter
|
from backend.app.services.c2.mythic import MythicAdapter
|
||||||
|
|
||||||
_BASE_URL = "https://mythic.lab:7443"
|
_BASE_URL = "https://mythic.lab:7443"
|
||||||
_GQL_URL = _BASE_URL + "/graphql"
|
_GQL_URL = _BASE_URL + "/graphql/"
|
||||||
_TOKEN = "fake-api-token"
|
_TOKEN = "fake-api-token"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error, C2HistoricalTask
|
|||||||
from backend.app.services.c2.mythic import MythicAdapter
|
from backend.app.services.c2.mythic import MythicAdapter
|
||||||
|
|
||||||
_BASE_URL = "https://mythic.lab:7443"
|
_BASE_URL = "https://mythic.lab:7443"
|
||||||
_GQL_URL = _BASE_URL + "/graphql"
|
_GQL_URL = _BASE_URL + "/graphql/"
|
||||||
_TOKEN = "fake-api-token"
|
_TOKEN = "fake-api-token"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -206,13 +206,13 @@ def test_get_config_returns_has_token_not_cleartext(
|
|||||||
assert "s3cr3t" not in str(body)
|
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
|
client: FlaskClient, admin_token: str
|
||||||
) -> None:
|
) -> None:
|
||||||
eng = _make_engagement(client, admin_token)
|
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))
|
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
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element {
|
|||||||
|
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState('');
|
||||||
const [verifyTls, setVerifyTls] = useState(true);
|
const [verifyTls, setVerifyTls] = useState(false);
|
||||||
const [replaceToken, setReplaceToken] = useState(false);
|
const [replaceToken, setReplaceToken] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null);
|
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');
|
push('C2 configuration removed', 'success');
|
||||||
setUrl('');
|
setUrl('');
|
||||||
setToken('');
|
setToken('');
|
||||||
setVerifyTls(true);
|
setVerifyTls(false);
|
||||||
setReplaceToken(false);
|
setReplaceToken(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
push(extractApiError(err, 'Could not remove C2 configuration'), 'error');
|
push(extractApiError(err, 'Could not remove C2 configuration'), 'error');
|
||||||
@@ -166,19 +166,25 @@ export function C2ConfigCard({ engagementId }: C2ConfigCardProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex items-center gap-sm">
|
<div className="flex flex-col gap-xs">
|
||||||
<input
|
<div className="flex items-center gap-sm">
|
||||||
id="c2-verify-tls"
|
<input
|
||||||
data-testid="c2-verify-tls"
|
id="c2-verify-tls"
|
||||||
type="checkbox"
|
data-testid="c2-verify-tls"
|
||||||
checked={verifyTls}
|
type="checkbox"
|
||||||
onChange={(e) => setVerifyTls(e.target.checked)}
|
checked={verifyTls}
|
||||||
disabled={disabled}
|
onChange={(e) => setVerifyTls(e.target.checked)}
|
||||||
className="h-4 w-4 accent-primary"
|
disabled={disabled}
|
||||||
/>
|
className="h-4 w-4 accent-primary"
|
||||||
<label htmlFor="c2-verify-tls" className="text-[14px] text-ink">
|
/>
|
||||||
Verify TLS certificate
|
<label htmlFor="c2-verify-tls" className="text-[14px] text-ink">
|
||||||
</label>
|
Verify TLS certificate
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-[12px] text-graphite">
|
||||||
|
Uncheck only for lab Mythic with self-signed certificates. Disabling
|
||||||
|
verification exposes the API token to MITM.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-md flex-wrap">
|
<div className="flex items-center gap-md flex-wrap">
|
||||||
|
|||||||
148
tasks/todo.md
148
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).
|
**Base**: `origin/main` (PR #11 merged — sprint 8 + 9 are in).
|
||||||
**Scope**: frontend-only. No backend, no schema. No new features.
|
**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)
|
## Decisions (locked)
|
||||||
|
|
||||||
1. **Engagement page** : passer en **2 colonnes** sur desktop (`lg:grid-cols-2`), `[engagement form | C2ConfigCard]`. Mobile/tablet : stack vertical (comportement actuel).
|
1. **Flip default to `False` at every layer** — model, API fallback, adapter, factory, React state, delete reset.
|
||||||
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.
|
2. **New migration 0008** : flip `server_default` to `sa.false()`. Existing rows are NOT mutated (their stored boolean is preserved).
|
||||||
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.
|
3. **Suppress `urllib3.InsecureRequestWarning` ONLY when `verify_tls=False`** — gated inside `MythicAdapter.__init__`. Keeps the warning live for any future code that legitimately verifies.
|
||||||
- Proposition : `canvas` light `#f3f5f8` (gris-bleu très pâle, cohérent avec l'electric blue), `paper` light `#ffffff`.
|
4. **Add helper text under checkbox** : "Leave unchecked for lab Mythic with self-signed certificates." Operators see why the box matters.
|
||||||
- Dark mode **inchangé** (`canvas #111827` / `paper #1f2937` déjà différenciés).
|
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.
|
||||||
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.
|
## 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
|
## Task A — Backend (backend-builder)
|
||||||
|
|
||||||
**File** : `frontend/src/pages/EngagementFormPage.tsx`
|
|
||||||
|
|
||||||
- Remplacer le wrapper `<div className="flex flex-col gap-xl max-w-2xl">` 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 : `<form>` engagement (déjà en `card-product`).
|
|
||||||
- Col droite : `<C2ConfigCard>` (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)
|
|
||||||
|
|
||||||
**Files** :
|
**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".
|
- `backend/app/models/c2_config.py` — line 22 : `default=True` → `default=False`
|
||||||
- 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`.
|
- `backend/app/api/c2.py` — line 87 : `data.get("verify_tls", True)` → `data.get("verify_tls", False)`
|
||||||
- Vérifier qu'aucun composant ne hardcode `#ffffff` pour la page bg (devrait utiliser `bg-canvas`).
|
- `backend/app/services/c2/factory.py` — line 9 : `verify_tls: bool = True` → `verify_tls: bool = False`
|
||||||
- Tests CSS smoke : `bg-canvas` continue de matcher, dark mode inchangé.
|
- `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.
|
## Task B — Frontend (frontend-builder)
|
||||||
- `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.
|
|
||||||
|
|
||||||
---
|
**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 `<p>` with helper text :
|
||||||
|
```tsx
|
||||||
|
<p className="text-[12px] text-charcoal mt-xxs">
|
||||||
|
Leave unchecked for lab Mythic with self-signed certificates.
|
||||||
|
</p>
|
||||||
|
```
|
||||||
|
(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 `<p>` hint below the checkbox-label row. Same brutalist treatment, no transition.
|
||||||
|
|
||||||
1. **frontend-builder** : Task A + B + C. Une seule passe, commits atomiques.
|
## Task C — Sequencing
|
||||||
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).
|
|
||||||
|
|
||||||
---
|
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
|
## Definition of Done
|
||||||
|
|
||||||
- EngagementFormPage en édition : 2 colonnes desktop, stack mobile.
|
- Test connection against self-signed Mythic from a freshly-created C2 config works **without unchecking anything**.
|
||||||
- Page bg différencié de card bg en light mode (eyes confort).
|
- Existing rows are untouched (operators who saved verify_tls=true keep it until they re-save).
|
||||||
- Vitest + typecheck + lint verts.
|
- `pytest` 468/468 → 468+ (no shrink), `vitest` 212/212.
|
||||||
- Design-reviewer APPROVED.
|
- `ruff` + `mypy --strict` + `tsc --noEmit` + `eslint` clean.
|
||||||
- Screenshots livrés ou écueil documenté.
|
- Migration 0008 round-trip OK.
|
||||||
- Commits conventional, branche `sprint/9-ui-contrast`.
|
- 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user