mirror of
https://github.com/Rarebuffalo/securelens-backend.git
synced 2026-06-19 07:00:30 +00:00
updated the architecture
This commit is contained in:
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
76
app/routers/apikey.py
Normal file
76
app/routers/apikey.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.apikey import ApiKey
|
||||
from app.models.user import User
|
||||
from app.schemas.apikey import ApiKeyCreate, ApiKeyCreateResponse, ApiKeyResponse
|
||||
|
||||
router = APIRouter(prefix="/api-keys", tags=["apikeys"])
|
||||
|
||||
|
||||
@router.post("", response_model=ApiKeyCreateResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_api_key(
|
||||
data: ApiKeyCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
raw_key = f"sl_{secrets.token_urlsafe(32)}"
|
||||
key_prefix = raw_key[:10]
|
||||
hashed_key = hashlib.sha256(raw_key.encode()).hexdigest()
|
||||
|
||||
api_key = ApiKey(
|
||||
user_id=current_user.id,
|
||||
name=data.name,
|
||||
key_prefix=key_prefix,
|
||||
hashed_key=hashed_key,
|
||||
)
|
||||
db.add(api_key)
|
||||
await db.commit()
|
||||
await db.refresh(api_key)
|
||||
|
||||
return ApiKeyCreateResponse(
|
||||
id=api_key.id,
|
||||
name=api_key.name,
|
||||
key_prefix=api_key.key_prefix,
|
||||
created_at=api_key.created_at,
|
||||
key=raw_key,
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[ApiKeyResponse])
|
||||
async def list_api_keys(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ApiKey)
|
||||
.where(ApiKey.user_id == current_user.id)
|
||||
.order_by(ApiKey.created_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_api_key(
|
||||
key_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ApiKey).where(ApiKey.id == key_id, ApiKey.user_id == current_user.id)
|
||||
)
|
||||
api_key = result.scalar_one_or_none()
|
||||
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found"
|
||||
)
|
||||
|
||||
await db.delete(api_key)
|
||||
await db.commit()
|
||||
61
app/routers/auth.py
Normal file
61
app/routers/auth.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import (
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
TokenResponse,
|
||||
UserResponse,
|
||||
)
|
||||
from app.utils.auth import create_access_token, hash_password, verify_password
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def register(data: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(User).where((User.email == data.email) | (User.username == data.username))
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
if existing:
|
||||
field = "email" if existing.email == data.email else "username"
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"A user with this {field} already exists",
|
||||
)
|
||||
|
||||
user = User(
|
||||
email=data.email,
|
||||
username=data.username,
|
||||
hashed_password=hash_password(data.password),
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
token = create_access_token(user.id)
|
||||
return TokenResponse(access_token=token)
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(data: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.email == data.email))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None or not verify_password(data.password, user.hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid email or password",
|
||||
)
|
||||
|
||||
token = create_access_token(user.id)
|
||||
return TokenResponse(access_token=token)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_me(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
19
app/routers/health.py
Normal file
19
app/routers/health.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.config import settings
|
||||
|
||||
router = APIRouter(tags=["health"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
return {"message": f"{settings.app_name} backend running 🚀"}
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"app": settings.app_name,
|
||||
"version": settings.app_version,
|
||||
}
|
||||
226
app/routers/history.py
Normal file
226
app/routers/history.py
Normal file
@@ -0,0 +1,226 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.scan import ScanResult
|
||||
from app.models.user import User
|
||||
from app.schemas.scan import (
|
||||
Issue,
|
||||
LayerStatus,
|
||||
ScanHistoryItem,
|
||||
ScanHistoryResponse,
|
||||
ScanResponse,
|
||||
DashboardTrendsResponse,
|
||||
ChatRequest,
|
||||
ChatResponse,
|
||||
ThreatNarrativeResponse,
|
||||
ScanDiffResponse,
|
||||
)
|
||||
|
||||
from app.services.ai import chat_with_scan_context, generate_threat_narrative
|
||||
|
||||
router = APIRouter(prefix="/scans", tags=["history"])
|
||||
|
||||
|
||||
@router.get("", response_model=ScanHistoryResponse)
|
||||
async def list_scans(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
count_result = await db.execute(
|
||||
select(func.count()).select_from(ScanResult).where(ScanResult.user_id == current_user.id)
|
||||
)
|
||||
total = count_result.scalar_one()
|
||||
|
||||
result = await db.execute(
|
||||
select(ScanResult)
|
||||
.where(ScanResult.user_id == current_user.id)
|
||||
.order_by(ScanResult.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
)
|
||||
scans = result.scalars().all()
|
||||
|
||||
return ScanHistoryResponse(
|
||||
scans=[ScanHistoryItem.model_validate(s) for s in scans],
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/trends", response_model=DashboardTrendsResponse)
|
||||
async def get_trends(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
count_result = await db.execute(
|
||||
select(func.count()).select_from(ScanResult).where(ScanResult.user_id == current_user.id)
|
||||
)
|
||||
total_scans = count_result.scalar_one()
|
||||
|
||||
avg_result = await db.execute(
|
||||
select(func.avg(ScanResult.security_score)).where(ScanResult.user_id == current_user.id)
|
||||
)
|
||||
avg_score = avg_result.scalar_one() or 0.0
|
||||
|
||||
recent_result = await db.execute(
|
||||
select(ScanResult)
|
||||
.where(ScanResult.user_id == current_user.id)
|
||||
.order_by(ScanResult.created_at.desc())
|
||||
.limit(5)
|
||||
)
|
||||
recent_scans = recent_result.scalars().all()
|
||||
|
||||
return DashboardTrendsResponse(
|
||||
total_scans=total_scans,
|
||||
average_score=float(avg_score),
|
||||
recent_scans=[ScanHistoryItem.model_validate(s) for s in recent_scans]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{scan_id}", response_model=ScanResponse)
|
||||
async def get_scan(
|
||||
scan_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ScanResult).where(
|
||||
ScanResult.id == scan_id,
|
||||
ScanResult.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if scan is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
|
||||
|
||||
return ScanResponse(
|
||||
id=scan.id,
|
||||
url=scan.url,
|
||||
security_score=scan.security_score,
|
||||
layers={k: LayerStatus(**v) for k, v in scan.layers.items()},
|
||||
issues=[Issue(**i) for i in scan.issues],
|
||||
created_at=scan.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{scan_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_scan(
|
||||
scan_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ScanResult).where(
|
||||
ScanResult.id == scan_id,
|
||||
ScanResult.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if scan is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
|
||||
|
||||
await db.delete(scan)
|
||||
|
||||
|
||||
@router.post("/{scan_id}/chat", response_model=ChatResponse)
|
||||
async def chat_about_scan(
|
||||
scan_id: str,
|
||||
data: ChatRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ScanResult).where(
|
||||
ScanResult.id == scan_id,
|
||||
ScanResult.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
|
||||
|
||||
context_data = {
|
||||
"url": scan.url,
|
||||
"score": scan.security_score,
|
||||
"layers": scan.layers,
|
||||
"issues": scan.issues,
|
||||
}
|
||||
|
||||
reply = await chat_with_scan_context(scan_id, context_data, data.message)
|
||||
return ChatResponse(reply=reply)
|
||||
|
||||
|
||||
@router.get("/{scan_id}/threat-narrative", response_model=ThreatNarrativeResponse)
|
||||
async def get_threat_narrative(
|
||||
scan_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ScanResult).where(
|
||||
ScanResult.id == scan_id,
|
||||
ScanResult.user_id == current_user.id,
|
||||
)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
|
||||
|
||||
context_data = {
|
||||
"url": scan.url,
|
||||
"score": scan.security_score,
|
||||
"layers": scan.layers,
|
||||
"issues": scan.issues,
|
||||
}
|
||||
|
||||
narrative = await generate_threat_narrative(context_data)
|
||||
return ThreatNarrativeResponse(narrative=narrative)
|
||||
|
||||
|
||||
@router.get("/{old_id}/diff/{new_id}", response_model=ScanDiffResponse)
|
||||
async def diff_scans(
|
||||
old_id: str,
|
||||
new_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ScanResult).where(
|
||||
ScanResult.id.in_([old_id, new_id]),
|
||||
ScanResult.user_id == current_user.id
|
||||
)
|
||||
)
|
||||
scans = result.scalars().all()
|
||||
|
||||
if len(scans) != 2:
|
||||
raise HTTPException(status_code=404, detail="One or both scans not found, or access denied.")
|
||||
|
||||
s_old = scans[0] if scans[0].id == old_id else scans[1]
|
||||
s_new = scans[1] if scans[1].id == new_id else scans[0]
|
||||
|
||||
# Convert to set-like structures using issue names
|
||||
old_map = {i.get("issue"): i for i in s_old.issues}
|
||||
new_map = {i.get("issue"): i for i in s_new.issues}
|
||||
|
||||
resolved = [v for k, v in old_map.items() if k not in new_map]
|
||||
new_issues = [v for k, v in new_map.items() if k not in old_map]
|
||||
persisting = [v for k, v in new_map.items() if k in old_map]
|
||||
|
||||
return ScanDiffResponse(
|
||||
resolved_issues=resolved,
|
||||
new_issues=new_issues,
|
||||
persisting_issues=persisting,
|
||||
score_change=s_new.security_score - s_old.security_score
|
||||
)
|
||||
116
app/routers/report.py
Normal file
116
app/routers/report.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fpdf import FPDF
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.scan import ScanResult
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/scans", tags=["report"])
|
||||
|
||||
|
||||
def _generate_csv(scan: ScanResult) -> io.StringIO:
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
writer.writerow(["SecureLens AI Scan Report"])
|
||||
writer.writerow(["URL", scan.url])
|
||||
writer.writerow(["Date", scan.created_at.strftime("%Y-%m-%d %H:%M:%S")])
|
||||
writer.writerow(["Security Score", scan.security_score])
|
||||
writer.writerow([])
|
||||
|
||||
writer.writerow(["Issue", "Severity", "Layer", "Fix", "Contextual Severity", "Explanation"])
|
||||
for i in scan.issues:
|
||||
writer.writerow([
|
||||
i.get("issue"),
|
||||
i.get("severity"),
|
||||
i.get("layer"),
|
||||
i.get("fix"),
|
||||
i.get("contextual_severity", ""),
|
||||
i.get("explanation", ""),
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
|
||||
def _generate_pdf(scan: ScanResult) -> io.BytesIO:
|
||||
pdf = FPDF()
|
||||
pdf.add_page()
|
||||
|
||||
pdf.set_font("helvetica", "B", 16)
|
||||
pdf.cell(0, 10, "SecureLens AI Scan Report", new_x="LMARGIN", new_y="NEXT", align="C")
|
||||
|
||||
pdf.set_font("helvetica", "", 12)
|
||||
pdf.cell(0, 10, f"URL: {scan.url}", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.cell(0, 10, f"Date: {scan.created_at.strftime('%Y-%m-%d %H:%M:%S')}", new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.cell(0, 10, f"Security Score: {scan.security_score}/100", new_x="LMARGIN", new_y="NEXT")
|
||||
|
||||
pdf.ln(5)
|
||||
pdf.set_font("helvetica", "B", 14)
|
||||
pdf.cell(0, 10, "Discovered Issues", new_x="LMARGIN", new_y="NEXT")
|
||||
|
||||
for i in scan.issues:
|
||||
pdf.set_font("helvetica", "B", 12)
|
||||
pdf.cell(0, 8, f"Issue: {i.get('issue')} [{i.get('severity')}]", new_x="LMARGIN", new_y="NEXT")
|
||||
|
||||
pdf.set_font("helvetica", "", 10)
|
||||
pdf.multi_cell(0, 6, f"Layer: {i.get('layer')}")
|
||||
pdf.multi_cell(0, 6, f"Fix: {i.get('fix')}")
|
||||
|
||||
if i.get("explanation"):
|
||||
pdf.multi_cell(0, 6, f"AI Context: {i.get('explanation')}")
|
||||
pdf.ln(4)
|
||||
|
||||
pdf_bytes = pdf.output()
|
||||
return io.BytesIO(pdf_bytes)
|
||||
|
||||
|
||||
@router.get("/{scan_id}/export/csv")
|
||||
async def export_csv(
|
||||
scan_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ScanResult).where(ScanResult.id == scan_id, ScanResult.user_id == current_user.id)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
|
||||
|
||||
csv_data = _generate_csv(scan)
|
||||
response = StreamingResponse(iter([csv_data.getvalue()]), media_type="text/csv")
|
||||
response.headers["Content-Disposition"] = f"attachment; filename=scan_{scan_id}.csv"
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/{scan_id}/export/pdf")
|
||||
async def export_pdf(
|
||||
scan_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(
|
||||
select(ScanResult).where(ScanResult.id == scan_id, ScanResult.user_id == current_user.id)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scan not found")
|
||||
|
||||
try:
|
||||
pdf_data = _generate_pdf(scan)
|
||||
response = StreamingResponse(pdf_data, media_type="application/pdf")
|
||||
response.headers["Content-Disposition"] = f"attachment; filename=scan_{scan_id}.pdf"
|
||||
return response
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"PDF Generation failed: {str(e)}")
|
||||
154
app/routers/scan.py
Normal file
154
app/routers/scan.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, Request, BackgroundTasks
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_optional_user
|
||||
from app.middleware.rate_limiter import limiter
|
||||
from app.models.scan import ScanResult
|
||||
from app.models.user import User
|
||||
from app.models.webhook import Webhook
|
||||
from app.schemas.scan import ScanRequest, ScanResponse
|
||||
from app.services.scanner.cookies import CookieScanner
|
||||
from app.services.scanner.exposure import ExposureScanner
|
||||
from app.services.scanner.headers import HeaderScanner
|
||||
from app.services.scanner.ssl_checker import SSLScanner
|
||||
from app.services.scanner.transport import TransportScanner
|
||||
from app.services.scanner.dns import DNSScanner
|
||||
from app.services.scanner.ports import PortScanner
|
||||
from app.services.scoring import calculate_layer_statuses, calculate_score
|
||||
from app.services.ai import enhance_security_issues
|
||||
from app.utils.validators import validate_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["scan"])
|
||||
|
||||
transport_scanner = TransportScanner()
|
||||
ssl_scanner = SSLScanner()
|
||||
header_scanner = HeaderScanner()
|
||||
cookie_scanner = CookieScanner()
|
||||
exposure_scanner = ExposureScanner()
|
||||
dns_scanner = DNSScanner()
|
||||
port_scanner = PortScanner()
|
||||
|
||||
|
||||
async def dispatch_webhooks(user_id: str, scan_data: dict, db_session):
|
||||
import hmac, hashlib, json
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await db_session.execute(
|
||||
select(Webhook).where(Webhook.user_id == user_id, Webhook.is_active == True)
|
||||
)
|
||||
hooks = result.scalars().all()
|
||||
if not hooks:
|
||||
return
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
payload = json.dumps(scan_data).encode("utf-8")
|
||||
for hook in hooks:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if hook.secret_key:
|
||||
sig = hmac.new(hook.secret_key.encode(), payload, hashlib.sha256).hexdigest()
|
||||
headers["X-SecureLens-Signature"] = sig
|
||||
|
||||
try:
|
||||
await client.post(hook.target_url, content=payload, headers=headers, timeout=5.0)
|
||||
except Exception as e:
|
||||
logger.warning(f"Webhook {hook.target_url} failed: {e}")
|
||||
|
||||
|
||||
@router.post("/scan", response_model=ScanResponse)
|
||||
@limiter.limit(settings.rate_limit)
|
||||
async def scan_website(
|
||||
data: ScanRequest,
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User | None = Depends(get_optional_user),
|
||||
):
|
||||
url = validate_url(data.url)
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
dns_task = asyncio.create_task(dns_scanner.scan(url))
|
||||
port_task = asyncio.create_task(port_scanner.scan(url))
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(settings.scan_timeout),
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
response = await client.get(url)
|
||||
|
||||
all_issues = []
|
||||
all_issues.extend(await transport_scanner.scan(url, response))
|
||||
all_issues.extend(await ssl_scanner.scan(url, response))
|
||||
all_issues.extend(await header_scanner.scan(url, response))
|
||||
all_issues.extend(await cookie_scanner.scan(url, response))
|
||||
all_issues.extend(await exposure_scanner.scan(url, response))
|
||||
|
||||
# Await infrastructure scans
|
||||
all_issues.extend(await dns_task)
|
||||
all_issues.extend(await port_task)
|
||||
|
||||
score = calculate_score(all_issues)
|
||||
layers = calculate_layer_statuses(all_issues)
|
||||
|
||||
if settings.openai_api_key and all_issues:
|
||||
issues_dict_list = [i.model_dump() for i in all_issues]
|
||||
ai_data = await enhance_security_issues(issues_dict_list)
|
||||
enhanced_list = ai_data.get("enhanced_issues", [])
|
||||
enhancement_map = {e.get("issue"): e for e in enhanced_list}
|
||||
for original in all_issues:
|
||||
enh = enhancement_map.get(original.issue)
|
||||
if enh:
|
||||
original.contextual_severity = enh.get("contextual_severity")
|
||||
original.explanation = enh.get("explanation")
|
||||
original.remediation_snippet = enh.get("remediation_snippet")
|
||||
|
||||
scan_id = None
|
||||
created_at = None
|
||||
|
||||
if current_user is not None:
|
||||
layers_dict = {k: v.model_dump() for k, v in layers.items()}
|
||||
issues_list = [i.model_dump() for i in all_issues]
|
||||
|
||||
scan_record = ScanResult(
|
||||
user_id=current_user.id,
|
||||
url=url,
|
||||
security_score=score,
|
||||
layers=layers_dict,
|
||||
issues=issues_list,
|
||||
)
|
||||
db.add(scan_record)
|
||||
await db.flush()
|
||||
scan_id = scan_record.id
|
||||
created_at = scan_record.created_at
|
||||
|
||||
scan_summary = {
|
||||
"scan_id": scan_id,
|
||||
"url": url,
|
||||
"score": score
|
||||
}
|
||||
background_tasks.add_task(dispatch_webhooks, current_user.id, scan_summary, db)
|
||||
|
||||
return ScanResponse(
|
||||
id=scan_id,
|
||||
url=url,
|
||||
security_score=score,
|
||||
layers=layers,
|
||||
issues=all_issues,
|
||||
created_at=created_at,
|
||||
)
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Scan failed for {url}: {e}")
|
||||
return JSONResponse(
|
||||
status_code=502,
|
||||
content={"error": f"Could not reach {url}: {str(e)}"},
|
||||
)
|
||||
56
app/routers/webhook.py
Normal file
56
app/routers/webhook.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import secrets
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.webhook import Webhook
|
||||
from app.schemas.webhook import WebhookCreate, WebhookResponse
|
||||
|
||||
router = APIRouter(prefix="/webhooks", tags=["webhooks"])
|
||||
|
||||
|
||||
@router.post("", response_model=WebhookResponse)
|
||||
async def create_webhook(
|
||||
hook_in: WebhookCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
secret = hook_in.secret_key or secrets.token_hex(16)
|
||||
db_hook = Webhook(
|
||||
user_id=current_user.id,
|
||||
target_url=str(hook_in.target_url),
|
||||
secret_key=secret
|
||||
)
|
||||
db.add(db_hook)
|
||||
await db.commit()
|
||||
await db.refresh(db_hook)
|
||||
return db_hook
|
||||
|
||||
|
||||
@router.get("", response_model=list[WebhookResponse])
|
||||
async def list_webhooks(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Webhook).where(Webhook.user_id == current_user.id))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.delete("/{hook_id}")
|
||||
async def delete_webhook(
|
||||
hook_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Webhook).where(Webhook.id == hook_id, Webhook.user_id == current_user.id))
|
||||
hook = result.scalar_one_or_none()
|
||||
|
||||
if not hook:
|
||||
raise HTTPException(status_code=404, detail="Webhook not found")
|
||||
|
||||
await db.delete(hook)
|
||||
await db.commit()
|
||||
return {"status": "success", "message": "Webhook deleted"}
|
||||
Reference in New Issue
Block a user