mirror of
https://github.com/mblanke/ThreatHunt.git
synced 2026-03-01 14:00:20 -05:00
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:
@@ -21,9 +21,14 @@ _engine_kwargs: dict = dict(
|
||||
)
|
||||
|
||||
if _is_sqlite:
|
||||
_engine_kwargs["connect_args"] = {"timeout": 30}
|
||||
_engine_kwargs["pool_size"] = 1
|
||||
_engine_kwargs["max_overflow"] = 0
|
||||
_engine_kwargs["connect_args"] = {"timeout": 60, "check_same_thread": False}
|
||||
# NullPool: each session gets its own connection.
|
||||
# Combined with WAL mode, this allows concurrent reads while a write is in progress.
|
||||
from sqlalchemy.pool import NullPool
|
||||
_engine_kwargs["poolclass"] = NullPool
|
||||
else:
|
||||
_engine_kwargs["pool_size"] = 5
|
||||
_engine_kwargs["max_overflow"] = 10
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, **_engine_kwargs)
|
||||
|
||||
@@ -34,7 +39,7 @@ def _set_sqlite_pragmas(dbapi_conn, connection_record):
|
||||
if _is_sqlite:
|
||||
cursor = dbapi_conn.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
cursor.execute("PRAGMA busy_timeout=5000")
|
||||
cursor.execute("PRAGMA busy_timeout=30000")
|
||||
cursor.execute("PRAGMA synchronous=NORMAL")
|
||||
cursor.close()
|
||||
|
||||
@@ -46,6 +51,10 @@ async_session_factory = async_sessionmaker(
|
||||
)
|
||||
|
||||
|
||||
# Alias expected by other modules
|
||||
async_session = async_session_factory
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""Base class for all ORM models."""
|
||||
pass
|
||||
@@ -71,5 +80,5 @@ async def init_db() -> None:
|
||||
|
||||
|
||||
async def dispose_db() -> None:
|
||||
"""Dispose of the engine connection pool."""
|
||||
await engine.dispose()
|
||||
"""Dispose of the engine on shutdown."""
|
||||
await engine.dispose()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""SQLAlchemy ORM models for ThreatHunt.
|
||||
"""SQLAlchemy ORM models for ThreatHunt.
|
||||
|
||||
All persistent entities: datasets, hunts, conversations, annotations,
|
||||
hypotheses, enrichment results, users, and AI analysis tables.
|
||||
@@ -43,6 +43,7 @@ class User(Base):
|
||||
hashed_password: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(16), default="analyst")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
display_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
hunts: Mapped[list["Hunt"]] = relationship(back_populates="owner", lazy="selectin")
|
||||
@@ -399,4 +400,108 @@ class AnomalyResult(Base):
|
||||
cluster_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
is_outlier: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
explanation: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
|
||||
# -- Persistent Processing Tasks (Phase 2) ---
|
||||
|
||||
class ProcessingTask(Base):
|
||||
__tablename__ = "processing_tasks"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_new_id)
|
||||
hunt_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(32), ForeignKey("hunts.id", ondelete="CASCADE"), nullable=True, index=True
|
||||
)
|
||||
dataset_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(32), ForeignKey("datasets.id", ondelete="CASCADE"), nullable=True, index=True
|
||||
)
|
||||
job_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, index=True)
|
||||
stage: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
status: Mapped[str] = mapped_column(String(20), default="queued", index=True)
|
||||
progress: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_processing_tasks_hunt_stage", "hunt_id", "stage"),
|
||||
Index("ix_processing_tasks_dataset_stage", "dataset_id", "stage"),
|
||||
)
|
||||
|
||||
|
||||
# -- Playbook / Investigation Templates (Feature 3) ---
|
||||
|
||||
class Playbook(Base):
|
||||
__tablename__ = "playbooks"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_new_id)
|
||||
name: Mapped[str] = mapped_column(String(256), nullable=False, index=True)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_by: Mapped[Optional[str]] = mapped_column(
|
||||
String(32), ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
is_template: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
hunt_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(32), ForeignKey("hunts.id"), nullable=True
|
||||
)
|
||||
status: Mapped[str] = mapped_column(String(20), default="active")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
steps: Mapped[list["PlaybookStep"]] = relationship(
|
||||
back_populates="playbook", lazy="selectin", cascade="all, delete-orphan",
|
||||
order_by="PlaybookStep.order_index",
|
||||
)
|
||||
|
||||
|
||||
class PlaybookStep(Base):
|
||||
__tablename__ = "playbook_steps"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
playbook_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("playbooks.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
order_index: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String(256), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
step_type: Mapped[str] = mapped_column(String(32), default="manual")
|
||||
target_route: Mapped[Optional[str]] = mapped_column(String(256), nullable=True)
|
||||
is_completed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
playbook: Mapped["Playbook"] = relationship(back_populates="steps")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_playbook_steps_playbook", "playbook_id"),
|
||||
)
|
||||
|
||||
|
||||
# -- Saved Searches (Feature 5) ---
|
||||
|
||||
class SavedSearch(Base):
|
||||
__tablename__ = "saved_searches"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_new_id)
|
||||
name: Mapped[str] = mapped_column(String(256), nullable=False, index=True)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
search_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
query_params: Mapped[dict] = mapped_column(JSON, nullable=False)
|
||||
threshold: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
||||
created_by: Mapped[Optional[str]] = mapped_column(
|
||||
String(32), ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
last_result_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_saved_searches_type", "search_type"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user