feat: Add Playbook Manager, Saved Searches, and Timeline View components

- Implemented PlaybookManager for creating and managing investigation playbooks with templates.
- Added SavedSearches component for managing bookmarked queries and recurring scans.
- Introduced TimelineView for visualizing forensic event timelines with zoomable charts.
- Enhanced backend processing with auto-queued jobs for dataset uploads and improved database concurrency.
- Updated frontend components for better user experience and performance optimizations.
- Documented changes in update log for future reference.
This commit is contained in:
2026-02-23 14:23:07 -05:00
parent 37a9584d0c
commit 5a2ad8ec1c
110 changed files with 10537 additions and 1185 deletions

View File

@@ -0,0 +1,84 @@
"""Tests for network inventory endpoints and cache/polling behavior."""
import io
import pytest
from app.services.host_inventory import inventory_cache
from tests.conftest import SAMPLE_CSV
@pytest.mark.asyncio
async def test_inventory_status_none_for_unknown_hunt(client):
hunt_id = "hunt-does-not-exist"
inventory_cache.invalidate(hunt_id)
inventory_cache.clear_building(hunt_id)
res = await client.get(f"/api/network/inventory-status?hunt_id={hunt_id}")
assert res.status_code == 200
body = res.json()
assert body["hunt_id"] == hunt_id
assert body["status"] == "none"
@pytest.mark.asyncio
async def test_host_inventory_cold_cache_returns_202(client):
# Create hunt and upload dataset linked to that hunt
hunt = await client.post("/api/hunts", json={"name": "Net Hunt"})
hunt_id = hunt.json()["id"]
files = {"file": ("network.csv", io.BytesIO(SAMPLE_CSV), "text/csv")}
up = await client.post("/api/datasets/upload", files=files, params={"hunt_id": hunt_id})
assert up.status_code == 200
# Ensure cache is cold for this hunt
inventory_cache.invalidate(hunt_id)
inventory_cache.clear_building(hunt_id)
res = await client.get(f"/api/network/host-inventory?hunt_id={hunt_id}")
assert res.status_code == 202
body = res.json()
assert body["status"] == "building"
@pytest.mark.asyncio
async def test_host_inventory_ready_cache_returns_200(client):
hunt = await client.post("/api/hunts", json={"name": "Ready Hunt"})
hunt_id = hunt.json()["id"]
mock_inventory = {
"hosts": [
{
"id": "host-1",
"hostname": "HOST-1",
"fqdn": "HOST-1.local",
"client_id": "C.1234abcd",
"ips": ["10.0.0.10"],
"os": "Windows 10",
"users": ["alice"],
"datasets": ["test"],
"row_count": 5,
}
],
"connections": [],
"stats": {
"total_hosts": 1,
"hosts_with_ips": 1,
"hosts_with_users": 1,
"total_datasets_scanned": 1,
"total_rows_scanned": 5,
},
}
inventory_cache.put(hunt_id, mock_inventory)
res = await client.get(f"/api/network/host-inventory?hunt_id={hunt_id}")
assert res.status_code == 200
body = res.json()
assert body["stats"]["total_hosts"] == 1
assert len(body["hosts"]) == 1
assert body["hosts"][0]["hostname"] == "HOST-1"
status_res = await client.get(f"/api/network/inventory-status?hunt_id={hunt_id}")
assert status_res.status_code == 200
assert status_res.json()["status"] == "ready"