181 lines
5.4 KiB
Python
181 lines
5.4 KiB
Python
"""
|
|
©AngelaMos | 2025
|
|
Coordinates scanners and saves results
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy.orm import Session
|
|
from fastapi import HTTPException, status
|
|
|
|
from core.enums import TestType
|
|
from repositories.scan_repository import ScanRepository
|
|
from repositories.test_result_repository import TestResultRepository
|
|
|
|
from scanners.base_scanner import BaseScanner
|
|
from scanners.auth_scanner import AuthScanner
|
|
from scanners.idor_scanner import IDORScanner
|
|
from scanners.sqli_scanner import SQLiScanner
|
|
from scanners.rate_limit_scanner import RateLimitScanner
|
|
from schemas.test_result_schemas import TestResultCreate
|
|
from schemas.scan_schemas import ScanRequest, ScanResponse
|
|
|
|
|
|
class ScanService:
|
|
"""
|
|
Orchestrates security scanning workflow
|
|
"""
|
|
|
|
@staticmethod
|
|
def run_scan(db: Session, user_id: int, scan_request: ScanRequest) -> ScanResponse:
|
|
"""
|
|
Execute security scan with selected tests
|
|
|
|
Args:
|
|
db: Database session
|
|
user_id: User ID initiating the scan
|
|
scan_request: Scan configuration and tests to run
|
|
|
|
Returns:
|
|
ScanResponse: Scan results with all test outcomes
|
|
"""
|
|
scan = ScanRepository.create_scan(
|
|
db=db,
|
|
user_id=user_id,
|
|
target_url=str(scan_request.target_url),
|
|
)
|
|
|
|
scanner_mapping: dict[TestType, type[BaseScanner]] = {
|
|
TestType.RATE_LIMIT: RateLimitScanner,
|
|
TestType.AUTH: AuthScanner,
|
|
TestType.SQLI: SQLiScanner,
|
|
TestType.IDOR: IDORScanner,
|
|
}
|
|
|
|
results: list[TestResultCreate] = []
|
|
|
|
for test_type in scan_request.tests_to_run:
|
|
scanner_class: type[BaseScanner] | None = scanner_mapping.get(test_type)
|
|
|
|
if not scanner_class:
|
|
continue
|
|
|
|
try:
|
|
scanner = scanner_class(
|
|
target_url=str(scan_request.target_url),
|
|
auth_token=scan_request.auth_token,
|
|
max_requests=scan_request.max_requests,
|
|
)
|
|
|
|
result = scanner.scan()
|
|
results.append(result)
|
|
|
|
except Exception as e:
|
|
results.append(
|
|
TestResultCreate(
|
|
test_name=test_type,
|
|
status="error",
|
|
severity="info",
|
|
details=f"Scanner error: {str(e)}",
|
|
evidence_json={"error": str(e)},
|
|
recommendations_json=[
|
|
"Check target URL is accessible",
|
|
"Verify authentication token if provided",
|
|
],
|
|
)
|
|
)
|
|
|
|
for result in results:
|
|
TestResultRepository.create_test_result(
|
|
db=db,
|
|
scan_id=scan.id,
|
|
test_name=result.test_name,
|
|
status=result.status,
|
|
severity=result.severity,
|
|
details=result.details,
|
|
evidence_json=result.evidence_json,
|
|
recommendations_json=result.recommendations_json,
|
|
)
|
|
|
|
db.refresh(scan)
|
|
|
|
return ScanResponse.model_validate(scan)
|
|
|
|
@staticmethod
|
|
def get_scan_by_id(db: Session, scan_id: int, user_id: int) -> ScanResponse:
|
|
"""
|
|
Get scan by ID with authorization check
|
|
|
|
Args:
|
|
db: Database session
|
|
scan_id: Scan ID to retrieve
|
|
user_id: User ID for authorization
|
|
|
|
Returns:
|
|
ScanResponse: Scan data with results
|
|
"""
|
|
scan = ScanRepository.get_by_id(db, scan_id)
|
|
|
|
if not scan:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Scan not found",
|
|
)
|
|
|
|
if scan.user_id != user_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Not authorized to access this scan",
|
|
)
|
|
|
|
return ScanResponse.model_validate(scan)
|
|
|
|
@staticmethod
|
|
def get_user_scans(
|
|
db: Session, user_id: int, skip: int = 0, limit: int | None = None
|
|
) -> list[ScanResponse]:
|
|
"""
|
|
Get all scans for a user with pagination
|
|
|
|
Args:
|
|
db: Database session
|
|
user_id: User ID
|
|
skip: Number of records to skip
|
|
limit: Maximum number of records to return
|
|
|
|
Returns:
|
|
list[ScanResponse]: List of user's scans
|
|
"""
|
|
scans = ScanRepository.get_by_user(db=db, user_id=user_id, skip=skip, limit=limit)
|
|
|
|
return [ScanResponse.model_validate(scan) for scan in scans]
|
|
|
|
@staticmethod
|
|
def delete_scan(db: Session, scan_id: int, user_id: int) -> bool:
|
|
"""
|
|
Delete scan with authorization check
|
|
|
|
Args:
|
|
db: Database session
|
|
scan_id: Scan ID to delete
|
|
user_id: User ID for authorization
|
|
|
|
Returns:
|
|
bool: True if deleted successfully
|
|
"""
|
|
scan = ScanRepository.get_by_id(db, scan_id)
|
|
|
|
if not scan:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Scan not found",
|
|
)
|
|
|
|
if scan.user_id != user_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Not authorized to delete this scan",
|
|
)
|
|
|
|
return ScanRepository.delete(db, scan_id)
|