mirror of
https://github.com/mblanke/ThreatHunt.git
synced 2026-03-01 14:00:20 -05:00
Implement Phase 3: Advanced search, real-time notifications, and Velociraptor integration
Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com>
This commit is contained in:
50
backend/alembic/versions/b2c3d4e5f6g7_add_phase_3_tables.py
Normal file
50
backend/alembic/versions/b2c3d4e5f6g7_add_phase_3_tables.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Add Phase 3 tables
|
||||
|
||||
Revision ID: b2c3d4e5f6g7
|
||||
Revises: a1b2c3d4e5f6
|
||||
Create Date: 2025-12-09 17:30:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b2c3d4e5f6g7'
|
||||
down_revision: Union[str, Sequence[str], None] = 'a1b2c3d4e5f6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema for Phase 3."""
|
||||
|
||||
# Create notifications table
|
||||
op.create_table(
|
||||
'notifications',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(), nullable=False),
|
||||
sa.Column('message', sa.Text(), nullable=False),
|
||||
sa.Column('notification_type', sa.String(), nullable=False),
|
||||
sa.Column('is_read', sa.Boolean(), nullable=False),
|
||||
sa.Column('link', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_notifications_id'), 'notifications', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_notifications_created_at'), 'notifications', ['created_at'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema for Phase 3."""
|
||||
|
||||
# Drop notifications table
|
||||
op.drop_index(op.f('ix_notifications_created_at'), table_name='notifications')
|
||||
op.drop_index(op.f('ix_notifications_id'), table_name='notifications')
|
||||
op.drop_table('notifications')
|
||||
@@ -37,14 +37,43 @@ class HostRead(BaseModel):
|
||||
async def list_hosts(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
hostname: Optional[str] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
os: Optional[str] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_desc: bool = False,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
tenant_id: int = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
List hosts scoped to user's tenant
|
||||
List hosts scoped to user's tenant with advanced filtering
|
||||
|
||||
Supports:
|
||||
- Filtering by hostname, IP address, OS
|
||||
- Sorting by any field
|
||||
- Pagination
|
||||
"""
|
||||
hosts = db.query(Host).filter(Host.tenant_id == tenant_id).offset(skip).limit(limit).all()
|
||||
query = db.query(Host).filter(Host.tenant_id == tenant_id)
|
||||
|
||||
# Apply filters
|
||||
if hostname:
|
||||
query = query.filter(Host.hostname.ilike(f"%{hostname}%"))
|
||||
if ip_address:
|
||||
query = query.filter(Host.ip_address.ilike(f"%{ip_address}%"))
|
||||
if os:
|
||||
query = query.filter(Host.os.ilike(f"%{os}%"))
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
sort_column = getattr(Host, sort_by, None)
|
||||
if sort_column:
|
||||
if sort_desc:
|
||||
query = query.order_by(sort_column.desc())
|
||||
else:
|
||||
query = query.order_by(sort_column)
|
||||
|
||||
hosts = query.offset(skip).limit(limit).all()
|
||||
return hosts
|
||||
|
||||
|
||||
|
||||
164
backend/app/api/routes/notifications.py
Normal file
164
backend/app/api/routes/notifications.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, WebSocket, WebSocketDisconnect
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_active_user
|
||||
from app.models.user import User
|
||||
from app.models.notification import Notification
|
||||
from app.schemas.notification import NotificationRead, NotificationUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Store active WebSocket connections
|
||||
active_connections: dict[int, List[WebSocket]] = {}
|
||||
|
||||
|
||||
@router.get("/", response_model=List[NotificationRead])
|
||||
async def list_notifications(
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
unread_only: bool = False,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
List notifications for current user
|
||||
|
||||
Supports filtering by read/unread status.
|
||||
"""
|
||||
query = db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.tenant_id == current_user.tenant_id
|
||||
)
|
||||
|
||||
if unread_only:
|
||||
query = query.filter(Notification.is_read == False)
|
||||
|
||||
notifications = query.order_by(Notification.created_at.desc()).offset(skip).limit(limit).all()
|
||||
return notifications
|
||||
|
||||
|
||||
@router.put("/{notification_id}", response_model=NotificationRead)
|
||||
async def update_notification(
|
||||
notification_id: int,
|
||||
notification_update: NotificationUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Update notification (mark as read/unread)
|
||||
"""
|
||||
notification = db.query(Notification).filter(
|
||||
Notification.id == notification_id,
|
||||
Notification.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if not notification:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Notification not found"
|
||||
)
|
||||
|
||||
notification.is_read = notification_update.is_read
|
||||
db.commit()
|
||||
db.refresh(notification)
|
||||
|
||||
return notification
|
||||
|
||||
|
||||
@router.post("/mark-all-read")
|
||||
async def mark_all_read(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Mark all notifications as read for current user
|
||||
"""
|
||||
db.query(Notification).filter(
|
||||
Notification.user_id == current_user.id,
|
||||
Notification.is_read == False
|
||||
).update({"is_read": True})
|
||||
db.commit()
|
||||
|
||||
return {"message": "All notifications marked as read"}
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
WebSocket endpoint for real-time notifications
|
||||
|
||||
Clients should send their token on connect, then receive notifications in real-time.
|
||||
"""
|
||||
await websocket.accept()
|
||||
user_id = None
|
||||
|
||||
try:
|
||||
# Wait for authentication message
|
||||
auth_data = await websocket.receive_text()
|
||||
auth_json = json.loads(auth_data)
|
||||
token = auth_json.get("token")
|
||||
|
||||
# Validate token and get user (simplified - in production use proper auth)
|
||||
from app.core.security import verify_token
|
||||
payload = verify_token(token)
|
||||
if payload:
|
||||
user_id = payload.get("sub")
|
||||
|
||||
# Register connection
|
||||
if user_id not in active_connections:
|
||||
active_connections[user_id] = []
|
||||
active_connections[user_id].append(websocket)
|
||||
|
||||
# Send confirmation
|
||||
await websocket.send_json({"type": "connected", "user_id": user_id})
|
||||
|
||||
# Keep connection alive
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
# Echo back for keepalive
|
||||
await websocket.send_json({"type": "pong"})
|
||||
else:
|
||||
await websocket.close(code=1008) # Policy violation
|
||||
|
||||
except WebSocketDisconnect:
|
||||
# Remove connection
|
||||
if user_id and user_id in active_connections:
|
||||
active_connections[user_id].remove(websocket)
|
||||
if not active_connections[user_id]:
|
||||
del active_connections[user_id]
|
||||
except Exception as e:
|
||||
print(f"WebSocket error: {e}")
|
||||
await websocket.close(code=1011) # Internal error
|
||||
|
||||
|
||||
async def send_notification_to_user(user_id: int, notification: dict):
|
||||
"""
|
||||
Send notification to all connected clients for a user
|
||||
|
||||
Args:
|
||||
user_id: User ID to send notification to
|
||||
notification: Notification data to send
|
||||
"""
|
||||
if user_id in active_connections:
|
||||
disconnected = []
|
||||
for websocket in active_connections[user_id]:
|
||||
try:
|
||||
await websocket.send_json({
|
||||
"type": "notification",
|
||||
"data": notification
|
||||
})
|
||||
except:
|
||||
disconnected.append(websocket)
|
||||
|
||||
# Clean up disconnected websockets
|
||||
for ws in disconnected:
|
||||
active_connections[user_id].remove(ws)
|
||||
|
||||
if not active_connections[user_id]:
|
||||
del active_connections[user_id]
|
||||
197
backend/app/api/routes/velociraptor.py
Normal file
197
backend/app/api/routes/velociraptor.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_active_user, require_role
|
||||
from app.core.velociraptor import get_velociraptor_client
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class VelociraptorConfig(BaseModel):
|
||||
"""Velociraptor server configuration"""
|
||||
base_url: str
|
||||
api_key: str
|
||||
|
||||
|
||||
class ArtifactCollectionRequest(BaseModel):
|
||||
"""Request to collect an artifact"""
|
||||
client_id: str
|
||||
artifact_name: str
|
||||
parameters: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class HuntCreateRequest(BaseModel):
|
||||
"""Request to create a hunt"""
|
||||
hunt_name: str
|
||||
artifact_name: str
|
||||
description: str
|
||||
parameters: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
# In a real implementation, this would be stored per-tenant in database
|
||||
# For now, using a simple in-memory store
|
||||
velociraptor_configs: Dict[int, VelociraptorConfig] = {}
|
||||
|
||||
|
||||
@router.post("/config")
|
||||
async def set_velociraptor_config(
|
||||
config: VelociraptorConfig,
|
||||
current_user: User = Depends(require_role(["admin"])),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Configure Velociraptor integration (admin only)
|
||||
|
||||
Stores Velociraptor server URL and API key for the tenant.
|
||||
"""
|
||||
velociraptor_configs[current_user.tenant_id] = config
|
||||
return {"message": "Velociraptor configuration saved"}
|
||||
|
||||
|
||||
@router.get("/clients")
|
||||
async def list_velociraptor_clients(
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
List clients from Velociraptor server
|
||||
"""
|
||||
config = velociraptor_configs.get(current_user.tenant_id)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Velociraptor not configured for this tenant"
|
||||
)
|
||||
|
||||
client = get_velociraptor_client(config.base_url, config.api_key)
|
||||
try:
|
||||
clients = await client.list_clients(limit=limit)
|
||||
return {"clients": clients}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch clients: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/clients/{client_id}")
|
||||
async def get_velociraptor_client_info(
|
||||
client_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get information about a specific Velociraptor client
|
||||
"""
|
||||
config = velociraptor_configs.get(current_user.tenant_id)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Velociraptor not configured for this tenant"
|
||||
)
|
||||
|
||||
client = get_velociraptor_client(config.base_url, config.api_key)
|
||||
try:
|
||||
client_info = await client.get_client(client_id)
|
||||
return client_info
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch client: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/collect")
|
||||
async def collect_artifact(
|
||||
request: ArtifactCollectionRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Collect an artifact from a Velociraptor client
|
||||
"""
|
||||
config = velociraptor_configs.get(current_user.tenant_id)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Velociraptor not configured for this tenant"
|
||||
)
|
||||
|
||||
client = get_velociraptor_client(config.base_url, config.api_key)
|
||||
try:
|
||||
result = await client.collect_artifact(
|
||||
request.client_id,
|
||||
request.artifact_name,
|
||||
request.parameters
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to collect artifact: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/hunts")
|
||||
async def create_hunt(
|
||||
request: HuntCreateRequest,
|
||||
current_user: User = Depends(require_role(["admin"])),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a new hunt (admin only)
|
||||
"""
|
||||
config = velociraptor_configs.get(current_user.tenant_id)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Velociraptor not configured for this tenant"
|
||||
)
|
||||
|
||||
client = get_velociraptor_client(config.base_url, config.api_key)
|
||||
try:
|
||||
result = await client.create_hunt(
|
||||
request.hunt_name,
|
||||
request.artifact_name,
|
||||
request.description,
|
||||
request.parameters
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create hunt: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/hunts/{hunt_id}/results")
|
||||
async def get_hunt_results(
|
||||
hunt_id: str,
|
||||
limit: int = 1000,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get results from a hunt
|
||||
"""
|
||||
config = velociraptor_configs.get(current_user.tenant_id)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Velociraptor not configured for this tenant"
|
||||
)
|
||||
|
||||
client = get_velociraptor_client(config.base_url, config.api_key)
|
||||
try:
|
||||
results = await client.get_hunt_results(hunt_id, limit=limit)
|
||||
return {"results": results}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to fetch hunt results: {str(e)}"
|
||||
)
|
||||
194
backend/app/core/velociraptor.py
Normal file
194
backend/app/core/velociraptor.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Velociraptor API Client
|
||||
|
||||
This module provides integration with Velociraptor servers for artifact
|
||||
collection, hunt management, and client operations.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class VelociraptorClient:
|
||||
"""Client for interacting with Velociraptor API"""
|
||||
|
||||
def __init__(self, base_url: str, api_key: str):
|
||||
"""
|
||||
Initialize Velociraptor client
|
||||
|
||||
Args:
|
||||
base_url: Base URL of Velociraptor server
|
||||
api_key: API key for authentication
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_key = api_key
|
||||
self.headers = {
|
||||
"Authorization": f"******",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def list_clients(self, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all clients
|
||||
|
||||
Args:
|
||||
limit: Maximum number of clients to return
|
||||
|
||||
Returns:
|
||||
List of client information dictionaries
|
||||
"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/api/v1/clients",
|
||||
headers=self.headers,
|
||||
params={"count": limit}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_client(self, client_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get information about a specific client
|
||||
|
||||
Args:
|
||||
client_id: Client ID
|
||||
|
||||
Returns:
|
||||
Client information dictionary
|
||||
"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/api/v1/clients/{client_id}",
|
||||
headers=self.headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def collect_artifact(
|
||||
self,
|
||||
client_id: str,
|
||||
artifact_name: str,
|
||||
parameters: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Collect an artifact from a client
|
||||
|
||||
Args:
|
||||
client_id: Client ID
|
||||
artifact_name: Name of artifact to collect
|
||||
parameters: Optional parameters for artifact collection
|
||||
|
||||
Returns:
|
||||
Collection flow information
|
||||
"""
|
||||
payload = {
|
||||
"client_id": client_id,
|
||||
"artifacts": [artifact_name],
|
||||
"parameters": parameters or {}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/api/v1/flows",
|
||||
headers=self.headers,
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def create_hunt(
|
||||
self,
|
||||
hunt_name: str,
|
||||
artifact_name: str,
|
||||
description: str,
|
||||
parameters: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new hunt
|
||||
|
||||
Args:
|
||||
hunt_name: Name of the hunt
|
||||
artifact_name: Artifact to collect
|
||||
description: Hunt description
|
||||
parameters: Optional parameters for artifact
|
||||
|
||||
Returns:
|
||||
Hunt information
|
||||
"""
|
||||
payload = {
|
||||
"hunt_name": hunt_name,
|
||||
"description": description,
|
||||
"artifacts": [artifact_name],
|
||||
"parameters": parameters or {}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/api/v1/hunts",
|
||||
headers=self.headers,
|
||||
json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_hunt_results(
|
||||
self,
|
||||
hunt_id: str,
|
||||
limit: int = 1000
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get results from a hunt
|
||||
|
||||
Args:
|
||||
hunt_id: Hunt ID
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
List of hunt results
|
||||
"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/api/v1/hunts/{hunt_id}/results",
|
||||
headers=self.headers,
|
||||
params={"count": limit}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_flow_results(
|
||||
self,
|
||||
client_id: str,
|
||||
flow_id: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get results from a flow
|
||||
|
||||
Args:
|
||||
client_id: Client ID
|
||||
flow_id: Flow ID
|
||||
|
||||
Returns:
|
||||
List of flow results
|
||||
"""
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{self.base_url}/api/v1/clients/{client_id}/flows/{flow_id}/results",
|
||||
headers=self.headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_velociraptor_client(base_url: str, api_key: str) -> VelociraptorClient:
|
||||
"""
|
||||
Factory function to create Velociraptor client
|
||||
|
||||
Args:
|
||||
base_url: Base URL of Velociraptor server
|
||||
api_key: API key for authentication
|
||||
|
||||
Returns:
|
||||
Configured VelociraptorClient instance
|
||||
"""
|
||||
return VelociraptorClient(base_url, api_key)
|
||||
@@ -1,13 +1,13 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.api.routes import auth, users, tenants, hosts, ingestion, vt, audit
|
||||
from app.api.routes import auth, users, tenants, hosts, ingestion, vt, audit, notifications, velociraptor
|
||||
from app.core.config import settings
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
description="Multi-tenant threat hunting companion for Velociraptor",
|
||||
version="0.2.0"
|
||||
version="0.3.0"
|
||||
)
|
||||
|
||||
# Configure CORS
|
||||
@@ -27,6 +27,8 @@ app.include_router(hosts.router, prefix="/api/hosts", tags=["Hosts"])
|
||||
app.include_router(ingestion.router, prefix="/api/ingestion", tags=["Ingestion"])
|
||||
app.include_router(vt.router, prefix="/api/vt", tags=["VirusTotal"])
|
||||
app.include_router(audit.router, prefix="/api/audit", tags=["Audit Logs"])
|
||||
app.include_router(notifications.router, prefix="/api/notifications", tags=["Notifications"])
|
||||
app.include_router(velociraptor.router, prefix="/api/velociraptor", tags=["Velociraptor"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -34,7 +36,7 @@ async def root():
|
||||
"""Root endpoint"""
|
||||
return {
|
||||
"message": f"Welcome to {settings.app_name}",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
|
||||
23
backend/app/models/notification.py
Normal file
23
backend/app/models/notification.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
notification_type = Column(String, nullable=False) # info, warning, error, success
|
||||
is_read = Column(Boolean, default=False, nullable=False)
|
||||
link = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), index=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User")
|
||||
tenant = relationship("Tenant")
|
||||
34
backend/app/schemas/notification.py
Normal file
34
backend/app/schemas/notification.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NotificationBase(BaseModel):
|
||||
"""Base notification schema"""
|
||||
title: str
|
||||
message: str
|
||||
notification_type: str
|
||||
link: Optional[str] = None
|
||||
|
||||
|
||||
class NotificationCreate(NotificationBase):
|
||||
"""Schema for creating a notification"""
|
||||
user_id: int
|
||||
tenant_id: int
|
||||
|
||||
|
||||
class NotificationRead(NotificationBase):
|
||||
"""Schema for reading notification data"""
|
||||
id: int
|
||||
user_id: int
|
||||
tenant_id: int
|
||||
is_read: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NotificationUpdate(BaseModel):
|
||||
"""Schema for updating a notification"""
|
||||
is_read: bool
|
||||
@@ -11,3 +11,5 @@ pydantic-settings==2.1.0
|
||||
pyotp==2.9.0
|
||||
qrcode[pil]==7.4.2
|
||||
websockets==12.0
|
||||
httpx==0.26.0
|
||||
email-validator==2.1.0
|
||||
|
||||
Reference in New Issue
Block a user