feat(web): attacker artifacts endpoint + UI drawer
Adds the server-side wiring and frontend UI to surface files captured
by the SSH honeypot for a given attacker.
- New repository method get_attacker_artifacts (abstract + SQLModel
impl) that joins the attacker's IP to `file_captured` log rows.
- New route GET /attackers/{uuid}/artifacts.
- New router /artifacts/{decky}/{service}/{stored_as} that streams a
quarantined file back to an authenticated viewer.
- AttackerDetail grows an ArtifactDrawer panel with per-file metadata
(sha256, size, orig_path) and a download action.
- ssh service fragment now sets NODE_NAME=decky_name so logs and the
host-side artifacts bind-mount share the same decky identifier.
This commit is contained in:
@@ -280,6 +280,61 @@ class TestGetAttackerCommands:
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
|
||||
# ─── GET /attackers/{uuid}/artifacts ─────────────────────────────────────────
|
||||
|
||||
class TestGetAttackerArtifacts:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_artifacts(self):
|
||||
from decnet.web.router.attackers.api_get_attacker_artifacts import get_attacker_artifacts
|
||||
|
||||
sample = _sample_attacker()
|
||||
rows = [
|
||||
{
|
||||
"id": 1,
|
||||
"timestamp": "2026-04-18T02:22:56+00:00",
|
||||
"decky": "decky-01",
|
||||
"service": "ssh",
|
||||
"event_type": "file_captured",
|
||||
"attacker_ip": "1.2.3.4",
|
||||
"raw_line": "",
|
||||
"msg": "",
|
||||
"fields": json.dumps({
|
||||
"stored_as": "2026-04-18T02:22:56Z_abc123def456_drop.bin",
|
||||
"sha256": "deadbeef" * 8,
|
||||
"size": "4096",
|
||||
"orig_path": "/root/drop.bin",
|
||||
}),
|
||||
},
|
||||
]
|
||||
with patch("decnet.web.router.attackers.api_get_attacker_artifacts.repo") as mock_repo:
|
||||
mock_repo.get_attacker_by_uuid = AsyncMock(return_value=sample)
|
||||
mock_repo.get_attacker_artifacts = AsyncMock(return_value=rows)
|
||||
|
||||
result = await get_attacker_artifacts(
|
||||
uuid="att-uuid-1",
|
||||
user={"uuid": "test-user", "role": "viewer"},
|
||||
)
|
||||
|
||||
assert result["total"] == 1
|
||||
assert result["data"][0]["decky"] == "decky-01"
|
||||
mock_repo.get_attacker_artifacts.assert_awaited_once_with("att-uuid-1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_404_on_unknown_uuid(self):
|
||||
from decnet.web.router.attackers.api_get_attacker_artifacts import get_attacker_artifacts
|
||||
|
||||
with patch("decnet.web.router.attackers.api_get_attacker_artifacts.repo") as mock_repo:
|
||||
mock_repo.get_attacker_by_uuid = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_attacker_artifacts(
|
||||
uuid="nonexistent",
|
||||
user={"uuid": "test-user", "role": "viewer"},
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
|
||||
# ─── Auth enforcement ────────────────────────────────────────────────────────
|
||||
|
||||
class TestAttackersAuth:
|
||||
|
||||
Reference in New Issue
Block a user