mirror of
https://github.com/mblanke/ThreatHunt.git
synced 2026-03-01 14:00:20 -05:00
Implement Phase 4: ML threat detection, automated playbooks, and advanced reporting
Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com>
This commit is contained in:
144
backend/app/api/routes/playbooks.py
Normal file
144
backend/app/api/routes/playbooks.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_active_user, require_role, get_tenant_id
|
||||
from app.core.playbook_engine import get_playbook_engine
|
||||
from app.models.user import User
|
||||
from app.models.playbook import Playbook, PlaybookExecution
|
||||
from app.schemas.playbook import PlaybookCreate, PlaybookRead, PlaybookUpdate, PlaybookExecutionRead
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PlaybookRead])
|
||||
async def list_playbooks(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List playbooks scoped to user's tenant"""
|
||||
playbooks = db.query(Playbook).filter(
|
||||
Playbook.tenant_id == tenant_id
|
||||
).offset(skip).limit(limit).all()
|
||||
return playbooks
|
||||
|
||||
|
||||
@router.post("/", response_model=PlaybookRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_playbook(
|
||||
playbook_data: PlaybookCreate,
|
||||
current_user: User = Depends(require_role(["admin"])),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new playbook (admin only)"""
|
||||
playbook = Playbook(
|
||||
tenant_id=tenant_id,
|
||||
created_by=current_user.id,
|
||||
**playbook_data.dict()
|
||||
)
|
||||
db.add(playbook)
|
||||
db.commit()
|
||||
db.refresh(playbook)
|
||||
return playbook
|
||||
|
||||
|
||||
@router.get("/{playbook_id}", response_model=PlaybookRead)
|
||||
async def get_playbook(
|
||||
playbook_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get playbook by ID"""
|
||||
playbook = db.query(Playbook).filter(
|
||||
Playbook.id == playbook_id,
|
||||
Playbook.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
if not playbook:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Playbook not found"
|
||||
)
|
||||
|
||||
return playbook
|
||||
|
||||
|
||||
@router.post("/{playbook_id}/execute", response_model=PlaybookExecutionRead)
|
||||
async def execute_playbook(
|
||||
playbook_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Execute a playbook"""
|
||||
playbook = db.query(Playbook).filter(
|
||||
Playbook.id == playbook_id,
|
||||
Playbook.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
if not playbook:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Playbook not found"
|
||||
)
|
||||
|
||||
if not playbook.is_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Playbook is disabled"
|
||||
)
|
||||
|
||||
# Create execution record
|
||||
execution = PlaybookExecution(
|
||||
playbook_id=playbook_id,
|
||||
tenant_id=tenant_id,
|
||||
status="running",
|
||||
triggered_by=current_user.id
|
||||
)
|
||||
db.add(execution)
|
||||
db.commit()
|
||||
db.refresh(execution)
|
||||
|
||||
# Execute playbook asynchronously
|
||||
try:
|
||||
engine = get_playbook_engine()
|
||||
result = await engine.execute_playbook(
|
||||
{"actions": playbook.actions},
|
||||
{"tenant_id": tenant_id, "user_id": current_user.id}
|
||||
)
|
||||
|
||||
execution.status = result["status"]
|
||||
execution.result = result
|
||||
from datetime import datetime, timezone
|
||||
execution.completed_at = datetime.now(timezone.utc)
|
||||
except Exception as e:
|
||||
execution.status = "failed"
|
||||
execution.error_message = str(e)
|
||||
|
||||
db.commit()
|
||||
db.refresh(execution)
|
||||
|
||||
return execution
|
||||
|
||||
|
||||
@router.get("/{playbook_id}/executions", response_model=List[PlaybookExecutionRead])
|
||||
async def list_playbook_executions(
|
||||
playbook_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List executions for a playbook"""
|
||||
executions = db.query(PlaybookExecution).filter(
|
||||
PlaybookExecution.playbook_id == playbook_id,
|
||||
PlaybookExecution.tenant_id == tenant_id
|
||||
).order_by(PlaybookExecution.started_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
return executions
|
||||
120
backend/app/api/routes/reports.py
Normal file
120
backend/app/api/routes/reports.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_active_user, get_tenant_id
|
||||
from app.models.user import User
|
||||
from app.models.report_template import ReportTemplate, Report
|
||||
from app.schemas.report import ReportTemplateCreate, ReportTemplateRead, ReportCreate, ReportRead
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/templates", response_model=List[ReportTemplateRead])
|
||||
async def list_report_templates(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List report templates scoped to tenant"""
|
||||
templates = db.query(ReportTemplate).filter(
|
||||
ReportTemplate.tenant_id == tenant_id
|
||||
).offset(skip).limit(limit).all()
|
||||
return templates
|
||||
|
||||
|
||||
@router.post("/templates", response_model=ReportTemplateRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_report_template(
|
||||
template_data: ReportTemplateCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new report template"""
|
||||
template = ReportTemplate(
|
||||
tenant_id=tenant_id,
|
||||
created_by=current_user.id,
|
||||
**template_data.dict()
|
||||
)
|
||||
db.add(template)
|
||||
db.commit()
|
||||
db.refresh(template)
|
||||
return template
|
||||
|
||||
|
||||
@router.post("/generate", response_model=ReportRead, status_code=status.HTTP_201_CREATED)
|
||||
async def generate_report(
|
||||
report_data: ReportCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Generate a new report
|
||||
|
||||
This is a simplified implementation. In production, this would:
|
||||
1. Fetch relevant data based on report type
|
||||
2. Apply template formatting
|
||||
3. Generate PDF/HTML output
|
||||
4. Store file and return path
|
||||
"""
|
||||
report = Report(
|
||||
tenant_id=tenant_id,
|
||||
template_id=report_data.template_id,
|
||||
title=report_data.title,
|
||||
report_type=report_data.report_type,
|
||||
format=report_data.format,
|
||||
status="generating",
|
||||
generated_by=current_user.id
|
||||
)
|
||||
db.add(report)
|
||||
db.commit()
|
||||
|
||||
# Simulate report generation
|
||||
# In production, this would be an async task
|
||||
report.status = "completed"
|
||||
report.file_path = f"/reports/{report.id}.{report_data.format}"
|
||||
db.commit()
|
||||
db.refresh(report)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ReportRead])
|
||||
async def list_reports(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List generated reports"""
|
||||
reports = db.query(Report).filter(
|
||||
Report.tenant_id == tenant_id
|
||||
).order_by(Report.generated_at.desc()).offset(skip).limit(limit).all()
|
||||
return reports
|
||||
|
||||
|
||||
@router.get("/{report_id}", response_model=ReportRead)
|
||||
async def get_report(
|
||||
report_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a specific report"""
|
||||
report = db.query(Report).filter(
|
||||
Report.id == report_id,
|
||||
Report.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
if not report:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Report not found"
|
||||
)
|
||||
|
||||
return report
|
||||
127
backend/app/api/routes/threat_intel.py
Normal file
127
backend/app/api/routes/threat_intel.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_active_user, get_tenant_id
|
||||
from app.core.threat_intel import get_threat_analyzer
|
||||
from app.models.user import User
|
||||
from app.models.threat_score import ThreatScore
|
||||
from app.models.host import Host
|
||||
from app.models.artifact import Artifact
|
||||
from app.schemas.threat_score import ThreatScoreRead, ThreatScoreCreate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/analyze/host/{host_id}", response_model=ThreatScoreRead)
|
||||
async def analyze_host(
|
||||
host_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Analyze a host for threats using ML
|
||||
"""
|
||||
host = db.query(Host).filter(
|
||||
Host.id == host_id,
|
||||
Host.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
if not host:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Host not found"
|
||||
)
|
||||
|
||||
# Analyze host
|
||||
analyzer = get_threat_analyzer()
|
||||
analysis = analyzer.analyze_host({
|
||||
"hostname": host.hostname,
|
||||
"ip_address": host.ip_address,
|
||||
"os": host.os,
|
||||
"host_metadata": host.host_metadata
|
||||
})
|
||||
|
||||
# Store threat score
|
||||
threat_score = ThreatScore(
|
||||
tenant_id=tenant_id,
|
||||
host_id=host_id,
|
||||
score=analysis["score"],
|
||||
confidence=analysis["confidence"],
|
||||
threat_type=analysis["threat_type"],
|
||||
indicators=analysis["indicators"],
|
||||
ml_model_version=analysis["ml_model_version"]
|
||||
)
|
||||
db.add(threat_score)
|
||||
db.commit()
|
||||
db.refresh(threat_score)
|
||||
|
||||
return threat_score
|
||||
|
||||
|
||||
@router.post("/analyze/artifact/{artifact_id}", response_model=ThreatScoreRead)
|
||||
async def analyze_artifact(
|
||||
artifact_id: int,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Analyze an artifact for threats
|
||||
"""
|
||||
artifact = db.query(Artifact).filter(Artifact.id == artifact_id).first()
|
||||
|
||||
if not artifact:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Artifact not found"
|
||||
)
|
||||
|
||||
# Analyze artifact
|
||||
analyzer = get_threat_analyzer()
|
||||
analysis = analyzer.analyze_artifact({
|
||||
"artifact_type": artifact.artifact_type,
|
||||
"value": artifact.value
|
||||
})
|
||||
|
||||
# Store threat score
|
||||
threat_score = ThreatScore(
|
||||
tenant_id=tenant_id,
|
||||
artifact_id=artifact_id,
|
||||
score=analysis["score"],
|
||||
confidence=analysis["confidence"],
|
||||
threat_type=analysis["threat_type"],
|
||||
indicators=analysis["indicators"],
|
||||
ml_model_version=analysis["ml_model_version"]
|
||||
)
|
||||
db.add(threat_score)
|
||||
db.commit()
|
||||
db.refresh(threat_score)
|
||||
|
||||
return threat_score
|
||||
|
||||
|
||||
@router.get("/scores", response_model=List[ThreatScoreRead])
|
||||
async def list_threat_scores(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
min_score: float = 0.0,
|
||||
threat_type: str = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
List threat scores with filtering
|
||||
"""
|
||||
query = db.query(ThreatScore).filter(ThreatScore.tenant_id == tenant_id)
|
||||
|
||||
if min_score:
|
||||
query = query.filter(ThreatScore.score >= min_score)
|
||||
if threat_type:
|
||||
query = query.filter(ThreatScore.threat_type == threat_type)
|
||||
|
||||
scores = query.order_by(ThreatScore.score.desc()).offset(skip).limit(limit).all()
|
||||
return scores
|
||||
Reference in New Issue
Block a user