mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-05-30 01:39:28 +02:00
Add files via upload
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# API package
|
||||
@@ -0,0 +1 @@
|
||||
# API v1 package
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
NeuroSploit v3 - Dashboard API Endpoints
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.db.database import get_db
|
||||
from backend.models import Scan, Vulnerability, Endpoint
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_dashboard_stats(db: AsyncSession = Depends(get_db)):
|
||||
"""Get overall dashboard statistics"""
|
||||
# Total scans
|
||||
total_scans_result = await db.execute(select(func.count()).select_from(Scan))
|
||||
total_scans = total_scans_result.scalar() or 0
|
||||
|
||||
# Running scans
|
||||
running_result = await db.execute(
|
||||
select(func.count()).select_from(Scan).where(Scan.status == "running")
|
||||
)
|
||||
running_scans = running_result.scalar() or 0
|
||||
|
||||
# Completed scans
|
||||
completed_result = await db.execute(
|
||||
select(func.count()).select_from(Scan).where(Scan.status == "completed")
|
||||
)
|
||||
completed_scans = completed_result.scalar() or 0
|
||||
|
||||
# Total vulnerabilities by severity
|
||||
vuln_counts = {}
|
||||
for severity in ["critical", "high", "medium", "low", "info"]:
|
||||
result = await db.execute(
|
||||
select(func.count()).select_from(Vulnerability).where(Vulnerability.severity == severity)
|
||||
)
|
||||
vuln_counts[severity] = result.scalar() or 0
|
||||
|
||||
total_vulns = sum(vuln_counts.values())
|
||||
|
||||
# Total endpoints
|
||||
endpoints_result = await db.execute(select(func.count()).select_from(Endpoint))
|
||||
total_endpoints = endpoints_result.scalar() or 0
|
||||
|
||||
# Recent activity (last 7 days)
|
||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||||
recent_scans_result = await db.execute(
|
||||
select(func.count()).select_from(Scan).where(Scan.created_at >= week_ago)
|
||||
)
|
||||
recent_scans = recent_scans_result.scalar() or 0
|
||||
|
||||
recent_vulns_result = await db.execute(
|
||||
select(func.count()).select_from(Vulnerability).where(Vulnerability.created_at >= week_ago)
|
||||
)
|
||||
recent_vulns = recent_vulns_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"scans": {
|
||||
"total": total_scans,
|
||||
"running": running_scans,
|
||||
"completed": completed_scans,
|
||||
"recent": recent_scans
|
||||
},
|
||||
"vulnerabilities": {
|
||||
"total": total_vulns,
|
||||
"critical": vuln_counts["critical"],
|
||||
"high": vuln_counts["high"],
|
||||
"medium": vuln_counts["medium"],
|
||||
"low": vuln_counts["low"],
|
||||
"info": vuln_counts["info"],
|
||||
"recent": recent_vulns
|
||||
},
|
||||
"endpoints": {
|
||||
"total": total_endpoints
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/recent")
|
||||
async def get_recent_activity(
|
||||
limit: int = 10,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get recent scan activity"""
|
||||
# Recent scans
|
||||
scans_query = select(Scan).order_by(Scan.created_at.desc()).limit(limit)
|
||||
scans_result = await db.execute(scans_query)
|
||||
recent_scans = scans_result.scalars().all()
|
||||
|
||||
# Recent vulnerabilities
|
||||
vulns_query = select(Vulnerability).order_by(Vulnerability.created_at.desc()).limit(limit)
|
||||
vulns_result = await db.execute(vulns_query)
|
||||
recent_vulns = vulns_result.scalars().all()
|
||||
|
||||
return {
|
||||
"recent_scans": [s.to_dict() for s in recent_scans],
|
||||
"recent_vulnerabilities": [v.to_dict() for v in recent_vulns]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/findings")
|
||||
async def get_recent_findings(
|
||||
limit: int = 20,
|
||||
severity: str = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get recent vulnerability findings"""
|
||||
query = select(Vulnerability).order_by(Vulnerability.created_at.desc())
|
||||
|
||||
if severity:
|
||||
query = query.where(Vulnerability.severity == severity)
|
||||
|
||||
query = query.limit(limit)
|
||||
result = await db.execute(query)
|
||||
vulnerabilities = result.scalars().all()
|
||||
|
||||
return {
|
||||
"findings": [v.to_dict() for v in vulnerabilities],
|
||||
"total": len(vulnerabilities)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/vulnerability-types")
|
||||
async def get_vulnerability_distribution(db: AsyncSession = Depends(get_db)):
|
||||
"""Get vulnerability distribution by type"""
|
||||
query = select(
|
||||
Vulnerability.vulnerability_type,
|
||||
func.count(Vulnerability.id).label("count")
|
||||
).group_by(Vulnerability.vulnerability_type)
|
||||
|
||||
result = await db.execute(query)
|
||||
distribution = result.all()
|
||||
|
||||
return {
|
||||
"distribution": [
|
||||
{"type": row[0], "count": row[1]}
|
||||
for row in distribution
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/scan-history")
|
||||
async def get_scan_history(
|
||||
days: int = 30,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get scan history for charts"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# Get scans grouped by date
|
||||
scans = await db.execute(
|
||||
select(Scan).where(Scan.created_at >= start_date).order_by(Scan.created_at)
|
||||
)
|
||||
all_scans = scans.scalars().all()
|
||||
|
||||
# Group by date
|
||||
history = {}
|
||||
for scan in all_scans:
|
||||
date_str = scan.created_at.strftime("%Y-%m-%d")
|
||||
if date_str not in history:
|
||||
history[date_str] = {
|
||||
"date": date_str,
|
||||
"scans": 0,
|
||||
"vulnerabilities": 0,
|
||||
"critical": 0,
|
||||
"high": 0
|
||||
}
|
||||
history[date_str]["scans"] += 1
|
||||
history[date_str]["vulnerabilities"] += scan.total_vulnerabilities
|
||||
history[date_str]["critical"] += scan.critical_count
|
||||
history[date_str]["high"] += scan.high_count
|
||||
|
||||
return {"history": list(history.values())}
|
||||
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
NeuroSploit v3 - Prompts API Endpoints
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.db.database import get_db
|
||||
from backend.models import Prompt
|
||||
from backend.schemas.prompt import (
|
||||
PromptCreate, PromptUpdate, PromptResponse, PromptParse, PromptParseResult, PromptPreset
|
||||
)
|
||||
from backend.core.prompt_engine.parser import PromptParser
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Preset prompts
|
||||
PRESET_PROMPTS = [
|
||||
{
|
||||
"id": "full_pentest",
|
||||
"name": "Full Penetration Test",
|
||||
"description": "Comprehensive security assessment covering all vulnerability categories",
|
||||
"category": "pentest",
|
||||
"content": """Perform a comprehensive penetration test on the target application.
|
||||
|
||||
Test for ALL vulnerability categories:
|
||||
- Injection vulnerabilities (XSS, SQL Injection, Command Injection, LDAP, XPath, Template Injection)
|
||||
- Authentication flaws (Broken auth, session management, JWT issues, OAuth flaws)
|
||||
- Authorization issues (IDOR, BOLA, privilege escalation, access control bypass)
|
||||
- File handling vulnerabilities (LFI, RFI, path traversal, file upload, XXE)
|
||||
- Request forgery (SSRF, CSRF)
|
||||
- API security issues (rate limiting, mass assignment, excessive data exposure)
|
||||
- Client-side vulnerabilities (CORS misconfig, clickjacking, open redirect)
|
||||
- Information disclosure (error messages, stack traces, sensitive data exposure)
|
||||
- Infrastructure issues (security headers, SSL/TLS, HTTP methods)
|
||||
- Business logic flaws (race conditions, workflow bypass)
|
||||
|
||||
Use thorough testing with multiple payloads and bypass techniques.
|
||||
Generate detailed PoC for each vulnerability found.
|
||||
Provide remediation recommendations."""
|
||||
},
|
||||
{
|
||||
"id": "owasp_top10",
|
||||
"name": "OWASP Top 10",
|
||||
"description": "Test for OWASP Top 10 2021 vulnerabilities",
|
||||
"category": "compliance",
|
||||
"content": """Test for OWASP Top 10 2021 vulnerabilities:
|
||||
|
||||
A01:2021 - Broken Access Control
|
||||
- IDOR, privilege escalation, access control bypass, CORS misconfig
|
||||
|
||||
A02:2021 - Cryptographic Failures
|
||||
- Sensitive data exposure, weak encryption, cleartext transmission
|
||||
|
||||
A03:2021 - Injection
|
||||
- SQL injection, XSS, command injection, LDAP injection
|
||||
|
||||
A04:2021 - Insecure Design
|
||||
- Business logic flaws, missing security controls
|
||||
|
||||
A05:2021 - Security Misconfiguration
|
||||
- Default configs, unnecessary features, missing headers
|
||||
|
||||
A06:2021 - Vulnerable Components
|
||||
- Outdated libraries, known CVEs
|
||||
|
||||
A07:2021 - Identification and Authentication Failures
|
||||
- Weak passwords, session fixation, credential stuffing
|
||||
|
||||
A08:2021 - Software and Data Integrity Failures
|
||||
- Insecure deserialization, CI/CD vulnerabilities
|
||||
|
||||
A09:2021 - Security Logging and Monitoring Failures
|
||||
- Missing audit logs, insufficient monitoring
|
||||
|
||||
A10:2021 - Server-Side Request Forgery (SSRF)
|
||||
- Internal network access, cloud metadata exposure"""
|
||||
},
|
||||
{
|
||||
"id": "api_security",
|
||||
"name": "API Security Testing",
|
||||
"description": "Focused testing for REST and GraphQL APIs",
|
||||
"category": "api",
|
||||
"content": """Perform API security testing:
|
||||
|
||||
Authentication & Authorization:
|
||||
- Test JWT implementation (algorithm confusion, signature bypass, claim manipulation)
|
||||
- OAuth/OIDC flow testing
|
||||
- API key exposure and validation
|
||||
- Rate limiting bypass
|
||||
- BOLA/IDOR on all endpoints
|
||||
|
||||
Input Validation:
|
||||
- SQL injection on API parameters
|
||||
- NoSQL injection
|
||||
- Command injection
|
||||
- Parameter pollution
|
||||
- Mass assignment vulnerabilities
|
||||
|
||||
Data Exposure:
|
||||
- Excessive data exposure in responses
|
||||
- Sensitive data in error messages
|
||||
- Information disclosure in headers
|
||||
- Debug endpoints exposure
|
||||
|
||||
GraphQL Specific (if applicable):
|
||||
- Introspection enabled
|
||||
- Query depth attacks
|
||||
- Batching attacks
|
||||
- Field suggestion exploitation
|
||||
|
||||
API Abuse:
|
||||
- Rate limiting effectiveness
|
||||
- Resource exhaustion
|
||||
- Denial of service vectors"""
|
||||
},
|
||||
{
|
||||
"id": "bug_bounty",
|
||||
"name": "Bug Bounty Hunter",
|
||||
"description": "Focus on high-impact, bounty-worthy vulnerabilities",
|
||||
"category": "bug_bounty",
|
||||
"content": """Hunt for high-impact vulnerabilities suitable for bug bounty:
|
||||
|
||||
Priority 1 - Critical Impact:
|
||||
- Remote Code Execution (RCE)
|
||||
- SQL Injection leading to data breach
|
||||
- Authentication bypass
|
||||
- SSRF to internal services/cloud metadata
|
||||
- Privilege escalation to admin
|
||||
|
||||
Priority 2 - High Impact:
|
||||
- Stored XSS
|
||||
- IDOR on sensitive resources
|
||||
- Account takeover vectors
|
||||
- Payment/billing manipulation
|
||||
- PII exposure
|
||||
|
||||
Priority 3 - Medium Impact:
|
||||
- Reflected XSS
|
||||
- CSRF on sensitive actions
|
||||
- Information disclosure
|
||||
- Rate limiting bypass
|
||||
- Open redirects (if exploitable)
|
||||
|
||||
Look for:
|
||||
- Unique attack chains
|
||||
- Business logic flaws
|
||||
- Edge cases and race conditions
|
||||
- Bypass techniques for existing security controls
|
||||
|
||||
Document with clear PoC and impact assessment."""
|
||||
},
|
||||
{
|
||||
"id": "quick_scan",
|
||||
"name": "Quick Security Scan",
|
||||
"description": "Fast scan for common vulnerabilities",
|
||||
"category": "quick",
|
||||
"content": """Perform a quick security scan for common vulnerabilities:
|
||||
|
||||
- Reflected XSS on input parameters
|
||||
- Basic SQL injection testing
|
||||
- Directory traversal/LFI
|
||||
- Security headers check
|
||||
- SSL/TLS configuration
|
||||
- Common misconfigurations
|
||||
- Information disclosure
|
||||
|
||||
Use minimal payloads for speed.
|
||||
Focus on quick wins and obvious issues."""
|
||||
},
|
||||
{
|
||||
"id": "auth_testing",
|
||||
"name": "Authentication Testing",
|
||||
"description": "Focus on authentication and session management",
|
||||
"category": "auth",
|
||||
"content": """Test authentication and session management:
|
||||
|
||||
Login Functionality:
|
||||
- Username enumeration
|
||||
- Password brute force protection
|
||||
- Account lockout bypass
|
||||
- Credential stuffing protection
|
||||
- SQL injection in login
|
||||
|
||||
Session Management:
|
||||
- Session token entropy
|
||||
- Session fixation
|
||||
- Session timeout
|
||||
- Cookie security flags (HttpOnly, Secure, SameSite)
|
||||
- Session invalidation on logout
|
||||
|
||||
Password Reset:
|
||||
- Token predictability
|
||||
- Token expiration
|
||||
- Account enumeration
|
||||
- Host header injection
|
||||
|
||||
Multi-Factor Authentication:
|
||||
- MFA bypass techniques
|
||||
- Backup codes weakness
|
||||
- Rate limiting on OTP
|
||||
|
||||
OAuth/SSO:
|
||||
- State parameter validation
|
||||
- Redirect URI manipulation
|
||||
- Token leakage"""
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@router.get("/presets", response_model=List[PromptPreset])
|
||||
async def get_preset_prompts():
|
||||
"""Get list of preset prompts"""
|
||||
return [
|
||||
PromptPreset(
|
||||
id=p["id"],
|
||||
name=p["name"],
|
||||
description=p["description"],
|
||||
category=p["category"],
|
||||
vulnerability_count=len(p["content"].split("\n"))
|
||||
)
|
||||
for p in PRESET_PROMPTS
|
||||
]
|
||||
|
||||
|
||||
@router.get("/presets/{preset_id}")
|
||||
async def get_preset_prompt(preset_id: str):
|
||||
"""Get a specific preset prompt by ID"""
|
||||
for preset in PRESET_PROMPTS:
|
||||
if preset["id"] == preset_id:
|
||||
return preset
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
|
||||
@router.post("/parse", response_model=PromptParseResult)
|
||||
async def parse_prompt(prompt_data: PromptParse):
|
||||
"""Parse a prompt to extract vulnerability types and testing scope"""
|
||||
parser = PromptParser()
|
||||
result = await parser.parse(prompt_data.content)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("", response_model=List[PromptResponse])
|
||||
async def list_prompts(
|
||||
category: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List all custom prompts"""
|
||||
query = select(Prompt).where(Prompt.is_preset == False)
|
||||
if category:
|
||||
query = query.where(Prompt.category == category)
|
||||
query = query.order_by(Prompt.created_at.desc())
|
||||
|
||||
result = await db.execute(query)
|
||||
prompts = result.scalars().all()
|
||||
|
||||
return [PromptResponse(**p.to_dict()) for p in prompts]
|
||||
|
||||
|
||||
@router.post("", response_model=PromptResponse)
|
||||
async def create_prompt(prompt_data: PromptCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""Create a custom prompt"""
|
||||
# Parse vulnerabilities from content
|
||||
parser = PromptParser()
|
||||
parsed = await parser.parse(prompt_data.content)
|
||||
|
||||
prompt = Prompt(
|
||||
name=prompt_data.name,
|
||||
description=prompt_data.description,
|
||||
content=prompt_data.content,
|
||||
category=prompt_data.category,
|
||||
is_preset=False,
|
||||
parsed_vulnerabilities=[v.dict() for v in parsed.vulnerabilities_to_test]
|
||||
)
|
||||
db.add(prompt)
|
||||
await db.commit()
|
||||
await db.refresh(prompt)
|
||||
|
||||
return PromptResponse(**prompt.to_dict())
|
||||
|
||||
|
||||
@router.get("/{prompt_id}", response_model=PromptResponse)
|
||||
async def get_prompt(prompt_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a prompt by ID"""
|
||||
result = await db.execute(select(Prompt).where(Prompt.id == prompt_id))
|
||||
prompt = result.scalar_one_or_none()
|
||||
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found")
|
||||
|
||||
return PromptResponse(**prompt.to_dict())
|
||||
|
||||
|
||||
@router.put("/{prompt_id}", response_model=PromptResponse)
|
||||
async def update_prompt(
|
||||
prompt_id: str,
|
||||
prompt_data: PromptUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update a prompt"""
|
||||
result = await db.execute(select(Prompt).where(Prompt.id == prompt_id))
|
||||
prompt = result.scalar_one_or_none()
|
||||
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found")
|
||||
|
||||
if prompt.is_preset:
|
||||
raise HTTPException(status_code=400, detail="Cannot modify preset prompts")
|
||||
|
||||
if prompt_data.name is not None:
|
||||
prompt.name = prompt_data.name
|
||||
if prompt_data.description is not None:
|
||||
prompt.description = prompt_data.description
|
||||
if prompt_data.content is not None:
|
||||
prompt.content = prompt_data.content
|
||||
# Re-parse vulnerabilities
|
||||
parser = PromptParser()
|
||||
parsed = await parser.parse(prompt_data.content)
|
||||
prompt.parsed_vulnerabilities = [v.dict() for v in parsed.vulnerabilities_to_test]
|
||||
if prompt_data.category is not None:
|
||||
prompt.category = prompt_data.category
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(prompt)
|
||||
|
||||
return PromptResponse(**prompt.to_dict())
|
||||
|
||||
|
||||
@router.delete("/{prompt_id}")
|
||||
async def delete_prompt(prompt_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Delete a prompt"""
|
||||
result = await db.execute(select(Prompt).where(Prompt.id == prompt_id))
|
||||
prompt = result.scalar_one_or_none()
|
||||
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=404, detail="Prompt not found")
|
||||
|
||||
if prompt.is_preset:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete preset prompts")
|
||||
|
||||
await db.delete(prompt)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Prompt deleted"}
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_prompt(file: UploadFile = File(...)):
|
||||
"""Upload a prompt file (.md or .txt)"""
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No file provided")
|
||||
|
||||
ext = "." + file.filename.split(".")[-1].lower() if "." in file.filename else ""
|
||||
if ext not in {".md", ".txt"}:
|
||||
raise HTTPException(status_code=400, detail="Invalid file type. Use .md or .txt")
|
||||
|
||||
content = await file.read()
|
||||
try:
|
||||
text = content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
raise HTTPException(status_code=400, detail="Unable to decode file")
|
||||
|
||||
# Parse the prompt
|
||||
parser = PromptParser()
|
||||
parsed = await parser.parse(text)
|
||||
|
||||
return {
|
||||
"filename": file.filename,
|
||||
"content": text,
|
||||
"parsed": parsed.dict()
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
NeuroSploit v3 - Reports API Endpoints
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pathlib import Path
|
||||
|
||||
from backend.db.database import get_db
|
||||
from backend.models import Scan, Report, Vulnerability
|
||||
from backend.schemas.report import ReportGenerate, ReportResponse, ReportListResponse
|
||||
from backend.core.report_engine.generator import ReportGenerator
|
||||
from backend.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=ReportListResponse)
|
||||
async def list_reports(
|
||||
scan_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List all reports"""
|
||||
query = select(Report).order_by(Report.generated_at.desc())
|
||||
|
||||
if scan_id:
|
||||
query = query.where(Report.scan_id == scan_id)
|
||||
|
||||
result = await db.execute(query)
|
||||
reports = result.scalars().all()
|
||||
|
||||
return ReportListResponse(
|
||||
reports=[ReportResponse(**r.to_dict()) for r in reports],
|
||||
total=len(reports)
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=ReportResponse)
|
||||
async def generate_report(
|
||||
report_data: ReportGenerate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Generate a new report for a scan"""
|
||||
# Get scan
|
||||
scan_result = await db.execute(select(Scan).where(Scan.id == report_data.scan_id))
|
||||
scan = scan_result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
# Get vulnerabilities
|
||||
vulns_result = await db.execute(
|
||||
select(Vulnerability).where(Vulnerability.scan_id == report_data.scan_id)
|
||||
)
|
||||
vulnerabilities = vulns_result.scalars().all()
|
||||
|
||||
# Generate report
|
||||
generator = ReportGenerator()
|
||||
report_path, executive_summary = await generator.generate(
|
||||
scan=scan,
|
||||
vulnerabilities=vulnerabilities,
|
||||
format=report_data.format,
|
||||
title=report_data.title,
|
||||
include_executive_summary=report_data.include_executive_summary,
|
||||
include_poc=report_data.include_poc,
|
||||
include_remediation=report_data.include_remediation
|
||||
)
|
||||
|
||||
# Save report record
|
||||
report = Report(
|
||||
scan_id=scan.id,
|
||||
title=report_data.title or f"Report - {scan.name}",
|
||||
format=report_data.format,
|
||||
file_path=str(report_path),
|
||||
executive_summary=executive_summary
|
||||
)
|
||||
db.add(report)
|
||||
await db.commit()
|
||||
await db.refresh(report)
|
||||
|
||||
return ReportResponse(**report.to_dict())
|
||||
|
||||
|
||||
@router.get("/{report_id}", response_model=ReportResponse)
|
||||
async def get_report(report_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Get report details"""
|
||||
result = await db.execute(select(Report).where(Report.id == report_id))
|
||||
report = result.scalar_one_or_none()
|
||||
|
||||
if not report:
|
||||
raise HTTPException(status_code=404, detail="Report not found")
|
||||
|
||||
return ReportResponse(**report.to_dict())
|
||||
|
||||
|
||||
@router.get("/{report_id}/view")
|
||||
async def view_report(report_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""View report in browser (HTML)"""
|
||||
result = await db.execute(select(Report).where(Report.id == report_id))
|
||||
report = result.scalar_one_or_none()
|
||||
|
||||
if not report:
|
||||
raise HTTPException(status_code=404, detail="Report not found")
|
||||
|
||||
if not report.file_path:
|
||||
raise HTTPException(status_code=404, detail="Report file not found")
|
||||
|
||||
file_path = Path(report.file_path)
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Report file not found on disk")
|
||||
|
||||
if report.format == "html":
|
||||
content = file_path.read_text()
|
||||
return HTMLResponse(content=content)
|
||||
else:
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type="application/octet-stream",
|
||||
filename=file_path.name
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{report_id}/download/{format}")
|
||||
async def download_report(
|
||||
report_id: str,
|
||||
format: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Download report in specified format"""
|
||||
result = await db.execute(select(Report).where(Report.id == report_id))
|
||||
report = result.scalar_one_or_none()
|
||||
|
||||
if not report:
|
||||
raise HTTPException(status_code=404, detail="Report not found")
|
||||
|
||||
# Get scan and vulnerabilities for generating report
|
||||
scan_result = await db.execute(select(Scan).where(Scan.id == report.scan_id))
|
||||
scan = scan_result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found for report")
|
||||
|
||||
vulns_result = await db.execute(
|
||||
select(Vulnerability).where(Vulnerability.scan_id == report.scan_id)
|
||||
)
|
||||
vulnerabilities = vulns_result.scalars().all()
|
||||
|
||||
# Always generate fresh report file (handles auto-generated reports without file_path)
|
||||
generator = ReportGenerator()
|
||||
report_path, _ = await generator.generate(
|
||||
scan=scan,
|
||||
vulnerabilities=vulnerabilities,
|
||||
format=format,
|
||||
title=report.title
|
||||
)
|
||||
file_path = Path(report_path)
|
||||
|
||||
# Update report with file path if not set
|
||||
if not report.file_path:
|
||||
report.file_path = str(file_path)
|
||||
report.format = format
|
||||
await db.commit()
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Report file not found")
|
||||
|
||||
media_types = {
|
||||
"html": "text/html",
|
||||
"pdf": "application/pdf",
|
||||
"json": "application/json"
|
||||
}
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type=media_types.get(format, "application/octet-stream"),
|
||||
filename=file_path.name
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{report_id}")
|
||||
async def delete_report(report_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Delete a report"""
|
||||
result = await db.execute(select(Report).where(Report.id == report_id))
|
||||
report = result.scalar_one_or_none()
|
||||
|
||||
if not report:
|
||||
raise HTTPException(status_code=404, detail="Report not found")
|
||||
|
||||
# Delete file if exists
|
||||
if report.file_path:
|
||||
file_path = Path(report.file_path)
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
await db.delete(report)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Report deleted"}
|
||||
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
NeuroSploit v3 - Scans API Endpoints
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from backend.db.database import get_db
|
||||
from backend.models import Scan, Target, Endpoint, Vulnerability
|
||||
from backend.schemas.scan import ScanCreate, ScanUpdate, ScanResponse, ScanListResponse, ScanProgress
|
||||
from backend.services.scan_service import run_scan_task
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=ScanListResponse)
|
||||
async def list_scans(
|
||||
page: int = 1,
|
||||
per_page: int = 10,
|
||||
status: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List all scans with pagination"""
|
||||
query = select(Scan).order_by(Scan.created_at.desc())
|
||||
|
||||
if status:
|
||||
query = query.where(Scan.status == status)
|
||||
|
||||
# Get total count
|
||||
count_query = select(func.count()).select_from(Scan)
|
||||
if status:
|
||||
count_query = count_query.where(Scan.status == status)
|
||||
total_result = await db.execute(count_query)
|
||||
total = total_result.scalar()
|
||||
|
||||
# Apply pagination
|
||||
query = query.offset((page - 1) * per_page).limit(per_page)
|
||||
result = await db.execute(query)
|
||||
scans = result.scalars().all()
|
||||
|
||||
# Load targets for each scan
|
||||
scan_responses = []
|
||||
for scan in scans:
|
||||
targets_query = select(Target).where(Target.scan_id == scan.id)
|
||||
targets_result = await db.execute(targets_query)
|
||||
targets = targets_result.scalars().all()
|
||||
|
||||
scan_dict = scan.to_dict()
|
||||
scan_dict["targets"] = [t.to_dict() for t in targets]
|
||||
scan_responses.append(ScanResponse(**scan_dict))
|
||||
|
||||
return ScanListResponse(
|
||||
scans=scan_responses,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=ScanResponse)
|
||||
async def create_scan(
|
||||
scan_data: ScanCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create a new scan with optional authentication for authenticated testing"""
|
||||
# Process authentication config
|
||||
auth_type = None
|
||||
auth_credentials = None
|
||||
if scan_data.auth:
|
||||
auth_type = scan_data.auth.auth_type
|
||||
auth_credentials = {}
|
||||
if scan_data.auth.cookie:
|
||||
auth_credentials["cookie"] = scan_data.auth.cookie
|
||||
if scan_data.auth.bearer_token:
|
||||
auth_credentials["bearer_token"] = scan_data.auth.bearer_token
|
||||
if scan_data.auth.username:
|
||||
auth_credentials["username"] = scan_data.auth.username
|
||||
if scan_data.auth.password:
|
||||
auth_credentials["password"] = scan_data.auth.password
|
||||
if scan_data.auth.header_name and scan_data.auth.header_value:
|
||||
auth_credentials["header_name"] = scan_data.auth.header_name
|
||||
auth_credentials["header_value"] = scan_data.auth.header_value
|
||||
|
||||
# Create scan
|
||||
scan = Scan(
|
||||
name=scan_data.name or f"Scan {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
||||
scan_type=scan_data.scan_type,
|
||||
recon_enabled=scan_data.recon_enabled,
|
||||
custom_prompt=scan_data.custom_prompt,
|
||||
prompt_id=scan_data.prompt_id,
|
||||
config=scan_data.config,
|
||||
auth_type=auth_type,
|
||||
auth_credentials=auth_credentials,
|
||||
custom_headers=scan_data.custom_headers,
|
||||
status="pending"
|
||||
)
|
||||
db.add(scan)
|
||||
await db.flush()
|
||||
|
||||
# Create targets
|
||||
targets = []
|
||||
for url in scan_data.targets:
|
||||
parsed = urlparse(url)
|
||||
target = Target(
|
||||
scan_id=scan.id,
|
||||
url=url,
|
||||
hostname=parsed.hostname,
|
||||
port=parsed.port or (443 if parsed.scheme == "https" else 80),
|
||||
protocol=parsed.scheme or "https",
|
||||
path=parsed.path or "/"
|
||||
)
|
||||
db.add(target)
|
||||
targets.append(target)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(scan)
|
||||
|
||||
scan_dict = scan.to_dict()
|
||||
scan_dict["targets"] = [t.to_dict() for t in targets]
|
||||
|
||||
return ScanResponse(**scan_dict)
|
||||
|
||||
|
||||
@router.get("/{scan_id}", response_model=ScanResponse)
|
||||
async def get_scan(scan_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Get scan details by ID"""
|
||||
result = await db.execute(select(Scan).where(Scan.id == scan_id))
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
# Load targets
|
||||
targets_result = await db.execute(select(Target).where(Target.scan_id == scan_id))
|
||||
targets = targets_result.scalars().all()
|
||||
|
||||
scan_dict = scan.to_dict()
|
||||
scan_dict["targets"] = [t.to_dict() for t in targets]
|
||||
|
||||
return ScanResponse(**scan_dict)
|
||||
|
||||
|
||||
@router.post("/{scan_id}/start")
|
||||
async def start_scan(
|
||||
scan_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Start a scan execution"""
|
||||
result = await db.execute(select(Scan).where(Scan.id == scan_id))
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
if scan.status == "running":
|
||||
raise HTTPException(status_code=400, detail="Scan is already running")
|
||||
|
||||
# Update scan status
|
||||
scan.status = "running"
|
||||
scan.started_at = datetime.utcnow()
|
||||
scan.current_phase = "initializing"
|
||||
scan.progress = 0
|
||||
await db.commit()
|
||||
|
||||
# Start scan in background with its own database session
|
||||
background_tasks.add_task(run_scan_task, scan_id)
|
||||
|
||||
return {"message": "Scan started", "scan_id": scan_id}
|
||||
|
||||
|
||||
@router.post("/{scan_id}/stop")
|
||||
async def stop_scan(scan_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Stop a running scan"""
|
||||
result = await db.execute(select(Scan).where(Scan.id == scan_id))
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
if scan.status != "running":
|
||||
raise HTTPException(status_code=400, detail="Scan is not running")
|
||||
|
||||
scan.status = "stopped"
|
||||
scan.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Scan stopped", "scan_id": scan_id}
|
||||
|
||||
|
||||
@router.get("/{scan_id}/status", response_model=ScanProgress)
|
||||
async def get_scan_status(scan_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Get scan progress and status"""
|
||||
result = await db.execute(select(Scan).where(Scan.id == scan_id))
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
return ScanProgress(
|
||||
scan_id=scan.id,
|
||||
status=scan.status,
|
||||
progress=scan.progress,
|
||||
current_phase=scan.current_phase,
|
||||
total_endpoints=scan.total_endpoints,
|
||||
total_vulnerabilities=scan.total_vulnerabilities
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{scan_id}")
|
||||
async def delete_scan(scan_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Delete a scan"""
|
||||
result = await db.execute(select(Scan).where(Scan.id == scan_id))
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
if scan.status == "running":
|
||||
raise HTTPException(status_code=400, detail="Cannot delete running scan")
|
||||
|
||||
await db.delete(scan)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Scan deleted", "scan_id": scan_id}
|
||||
|
||||
|
||||
@router.get("/{scan_id}/endpoints")
|
||||
async def get_scan_endpoints(
|
||||
scan_id: str,
|
||||
page: int = 1,
|
||||
per_page: int = 50,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get endpoints discovered in a scan"""
|
||||
result = await db.execute(select(Scan).where(Scan.id == scan_id))
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
query = select(Endpoint).where(Endpoint.scan_id == scan_id).order_by(Endpoint.discovered_at.desc())
|
||||
|
||||
# Count
|
||||
count_result = await db.execute(select(func.count()).select_from(Endpoint).where(Endpoint.scan_id == scan_id))
|
||||
total = count_result.scalar()
|
||||
|
||||
# Paginate
|
||||
query = query.offset((page - 1) * per_page).limit(per_page)
|
||||
result = await db.execute(query)
|
||||
endpoints = result.scalars().all()
|
||||
|
||||
return {
|
||||
"endpoints": [e.to_dict() for e in endpoints],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{scan_id}/vulnerabilities")
|
||||
async def get_scan_vulnerabilities(
|
||||
scan_id: str,
|
||||
severity: Optional[str] = None,
|
||||
page: int = 1,
|
||||
per_page: int = 50,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get vulnerabilities found in a scan"""
|
||||
result = await db.execute(select(Scan).where(Scan.id == scan_id))
|
||||
scan = result.scalar_one_or_none()
|
||||
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan not found")
|
||||
|
||||
query = select(Vulnerability).where(Vulnerability.scan_id == scan_id)
|
||||
|
||||
if severity:
|
||||
query = query.where(Vulnerability.severity == severity)
|
||||
|
||||
query = query.order_by(Vulnerability.created_at.desc())
|
||||
|
||||
# Count
|
||||
count_query = select(func.count()).select_from(Vulnerability).where(Vulnerability.scan_id == scan_id)
|
||||
if severity:
|
||||
count_query = count_query.where(Vulnerability.severity == severity)
|
||||
count_result = await db.execute(count_query)
|
||||
total = count_result.scalar()
|
||||
|
||||
# Paginate
|
||||
query = query.offset((page - 1) * per_page).limit(per_page)
|
||||
result = await db.execute(query)
|
||||
vulnerabilities = result.scalars().all()
|
||||
|
||||
return {
|
||||
"vulnerabilities": [v.to_dict() for v in vulnerabilities],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
NeuroSploit v3 - Settings API Endpoints
|
||||
"""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete, text
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.db.database import get_db, engine
|
||||
from backend.models import Scan, Target, Endpoint, Vulnerability, VulnerabilityTest, Report
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class SettingsUpdate(BaseModel):
|
||||
"""Settings update schema"""
|
||||
llm_provider: Optional[str] = None
|
||||
anthropic_api_key: Optional[str] = None
|
||||
openai_api_key: Optional[str] = None
|
||||
max_concurrent_scans: Optional[int] = None
|
||||
aggressive_mode: Optional[bool] = None
|
||||
default_scan_type: Optional[str] = None
|
||||
recon_enabled_by_default: Optional[bool] = None
|
||||
|
||||
|
||||
class SettingsResponse(BaseModel):
|
||||
"""Settings response schema"""
|
||||
llm_provider: str = "claude"
|
||||
has_anthropic_key: bool = False
|
||||
has_openai_key: bool = False
|
||||
max_concurrent_scans: int = 3
|
||||
aggressive_mode: bool = False
|
||||
default_scan_type: str = "full"
|
||||
recon_enabled_by_default: bool = True
|
||||
|
||||
|
||||
# In-memory settings storage (in production, use database or config file)
|
||||
_settings = {
|
||||
"llm_provider": "claude",
|
||||
"anthropic_api_key": "",
|
||||
"openai_api_key": "",
|
||||
"max_concurrent_scans": 3,
|
||||
"aggressive_mode": False,
|
||||
"default_scan_type": "full",
|
||||
"recon_enabled_by_default": True
|
||||
}
|
||||
|
||||
|
||||
@router.get("", response_model=SettingsResponse)
|
||||
async def get_settings():
|
||||
"""Get current settings"""
|
||||
return SettingsResponse(
|
||||
llm_provider=_settings["llm_provider"],
|
||||
has_anthropic_key=bool(_settings["anthropic_api_key"]),
|
||||
has_openai_key=bool(_settings["openai_api_key"]),
|
||||
max_concurrent_scans=_settings["max_concurrent_scans"],
|
||||
aggressive_mode=_settings["aggressive_mode"],
|
||||
default_scan_type=_settings["default_scan_type"],
|
||||
recon_enabled_by_default=_settings["recon_enabled_by_default"]
|
||||
)
|
||||
|
||||
|
||||
@router.put("", response_model=SettingsResponse)
|
||||
async def update_settings(settings_data: SettingsUpdate):
|
||||
"""Update settings"""
|
||||
if settings_data.llm_provider is not None:
|
||||
_settings["llm_provider"] = settings_data.llm_provider
|
||||
|
||||
if settings_data.anthropic_api_key is not None:
|
||||
_settings["anthropic_api_key"] = settings_data.anthropic_api_key
|
||||
# Also update environment variable for LLM calls
|
||||
import os
|
||||
if settings_data.anthropic_api_key:
|
||||
os.environ["ANTHROPIC_API_KEY"] = settings_data.anthropic_api_key
|
||||
|
||||
if settings_data.openai_api_key is not None:
|
||||
_settings["openai_api_key"] = settings_data.openai_api_key
|
||||
import os
|
||||
if settings_data.openai_api_key:
|
||||
os.environ["OPENAI_API_KEY"] = settings_data.openai_api_key
|
||||
|
||||
if settings_data.max_concurrent_scans is not None:
|
||||
_settings["max_concurrent_scans"] = settings_data.max_concurrent_scans
|
||||
|
||||
if settings_data.aggressive_mode is not None:
|
||||
_settings["aggressive_mode"] = settings_data.aggressive_mode
|
||||
|
||||
if settings_data.default_scan_type is not None:
|
||||
_settings["default_scan_type"] = settings_data.default_scan_type
|
||||
|
||||
if settings_data.recon_enabled_by_default is not None:
|
||||
_settings["recon_enabled_by_default"] = settings_data.recon_enabled_by_default
|
||||
|
||||
return await get_settings()
|
||||
|
||||
|
||||
@router.post("/clear-database")
|
||||
async def clear_database(db: AsyncSession = Depends(get_db)):
|
||||
"""Clear all data from the database (reset to fresh state)"""
|
||||
try:
|
||||
# Delete in correct order to respect foreign key constraints
|
||||
await db.execute(delete(VulnerabilityTest))
|
||||
await db.execute(delete(Vulnerability))
|
||||
await db.execute(delete(Endpoint))
|
||||
await db.execute(delete(Report))
|
||||
await db.execute(delete(Target))
|
||||
await db.execute(delete(Scan))
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": "Database cleared successfully",
|
||||
"status": "success"
|
||||
}
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"Failed to clear database: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_database_stats(db: AsyncSession = Depends(get_db)):
|
||||
"""Get database statistics"""
|
||||
from sqlalchemy import func
|
||||
|
||||
scans_count = (await db.execute(select(func.count()).select_from(Scan))).scalar() or 0
|
||||
vulns_count = (await db.execute(select(func.count()).select_from(Vulnerability))).scalar() or 0
|
||||
endpoints_count = (await db.execute(select(func.count()).select_from(Endpoint))).scalar() or 0
|
||||
reports_count = (await db.execute(select(func.count()).select_from(Report))).scalar() or 0
|
||||
|
||||
return {
|
||||
"scans": scans_count,
|
||||
"vulnerabilities": vulns_count,
|
||||
"endpoints": endpoints_count,
|
||||
"reports": reports_count
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tools")
|
||||
async def get_installed_tools():
|
||||
"""Check which security tools are installed"""
|
||||
import asyncio
|
||||
import shutil
|
||||
|
||||
# Complete list of 40+ tools
|
||||
tools = {
|
||||
"recon": [
|
||||
"subfinder", "amass", "assetfinder", "chaos", "uncover",
|
||||
"dnsx", "massdns", "puredns", "cero", "tlsx", "cdncheck"
|
||||
],
|
||||
"web_discovery": [
|
||||
"httpx", "httprobe", "katana", "gospider", "hakrawler",
|
||||
"gau", "waybackurls", "cariddi", "getJS", "gowitness"
|
||||
],
|
||||
"fuzzing": [
|
||||
"ffuf", "gobuster", "dirb", "dirsearch", "wfuzz", "arjun", "paramspider"
|
||||
],
|
||||
"vulnerability_scanning": [
|
||||
"nuclei", "nikto", "sqlmap", "xsstrike", "dalfox", "crlfuzz"
|
||||
],
|
||||
"port_scanning": [
|
||||
"nmap", "naabu", "rustscan"
|
||||
],
|
||||
"utilities": [
|
||||
"gf", "qsreplace", "unfurl", "anew", "uro", "jq"
|
||||
],
|
||||
"tech_detection": [
|
||||
"whatweb", "wafw00f"
|
||||
],
|
||||
"exploitation": [
|
||||
"hydra", "medusa", "john", "hashcat"
|
||||
],
|
||||
"network": [
|
||||
"curl", "wget", "dig", "whois"
|
||||
]
|
||||
}
|
||||
|
||||
results = {}
|
||||
total_installed = 0
|
||||
total_tools = 0
|
||||
|
||||
for category, tool_list in tools.items():
|
||||
results[category] = {}
|
||||
for tool in tool_list:
|
||||
total_tools += 1
|
||||
# Check if tool exists in PATH
|
||||
is_installed = shutil.which(tool) is not None
|
||||
results[category][tool] = is_installed
|
||||
if is_installed:
|
||||
total_installed += 1
|
||||
|
||||
return {
|
||||
"tools": results,
|
||||
"summary": {
|
||||
"total": total_tools,
|
||||
"installed": total_installed,
|
||||
"missing": total_tools - total_installed,
|
||||
"percentage": round((total_installed / total_tools) * 100, 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
NeuroSploit v3 - Targets API Endpoints
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from urllib.parse import urlparse
|
||||
import re
|
||||
|
||||
from backend.db.database import get_db
|
||||
from backend.schemas.target import TargetCreate, TargetBulkCreate, TargetValidation, TargetResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def validate_url(url: str) -> TargetValidation:
|
||||
"""Validate and parse a URL"""
|
||||
url = url.strip()
|
||||
|
||||
if not url:
|
||||
return TargetValidation(url=url, valid=False, error="URL is empty")
|
||||
|
||||
# URL pattern
|
||||
url_pattern = re.compile(
|
||||
r'^https?://'
|
||||
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
|
||||
r'localhost|'
|
||||
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
|
||||
r'(?::\d+)?'
|
||||
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
||||
|
||||
# Try with the URL as-is
|
||||
if url_pattern.match(url):
|
||||
normalized = url
|
||||
elif url_pattern.match(f"https://{url}"):
|
||||
normalized = f"https://{url}"
|
||||
else:
|
||||
return TargetValidation(url=url, valid=False, error="Invalid URL format")
|
||||
|
||||
# Parse URL
|
||||
parsed = urlparse(normalized)
|
||||
|
||||
return TargetValidation(
|
||||
url=url,
|
||||
valid=True,
|
||||
normalized_url=normalized,
|
||||
hostname=parsed.hostname,
|
||||
port=parsed.port or (443 if parsed.scheme == "https" else 80),
|
||||
protocol=parsed.scheme
|
||||
)
|
||||
|
||||
|
||||
@router.post("/validate", response_model=TargetValidation)
|
||||
async def validate_target(target: TargetCreate):
|
||||
"""Validate a single target URL"""
|
||||
return validate_url(target.url)
|
||||
|
||||
|
||||
@router.post("/validate/bulk", response_model=List[TargetValidation])
|
||||
async def validate_targets_bulk(targets: TargetBulkCreate):
|
||||
"""Validate multiple target URLs"""
|
||||
results = []
|
||||
for url in targets.urls:
|
||||
results.append(validate_url(url))
|
||||
return results
|
||||
|
||||
|
||||
@router.post("/upload", response_model=List[TargetValidation])
|
||||
async def upload_targets(file: UploadFile = File(...)):
|
||||
"""Upload a file with URLs (one per line)"""
|
||||
if not file.filename:
|
||||
raise HTTPException(status_code=400, detail="No file provided")
|
||||
|
||||
# Check file extension
|
||||
allowed_extensions = {".txt", ".csv", ".lst"}
|
||||
ext = "." + file.filename.split(".")[-1].lower() if "." in file.filename else ""
|
||||
if ext not in allowed_extensions:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file type. Allowed: {', '.join(allowed_extensions)}"
|
||||
)
|
||||
|
||||
# Read file content
|
||||
content = await file.read()
|
||||
try:
|
||||
text = content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
text = content.decode("latin-1")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Unable to decode file")
|
||||
|
||||
# Parse URLs (one per line, or comma-separated)
|
||||
urls = []
|
||||
for line in text.split("\n"):
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
# Handle comma-separated URLs
|
||||
if "," in line and "://" in line:
|
||||
for url in line.split(","):
|
||||
url = url.strip()
|
||||
if url:
|
||||
urls.append(url)
|
||||
else:
|
||||
urls.append(line)
|
||||
|
||||
if not urls:
|
||||
raise HTTPException(status_code=400, detail="No URLs found in file")
|
||||
|
||||
# Validate all URLs
|
||||
results = []
|
||||
for url in urls:
|
||||
results.append(validate_url(url))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.post("/parse-input", response_model=List[TargetValidation])
|
||||
async def parse_target_input(input_text: str):
|
||||
"""Parse target input (comma-separated or newline-separated)"""
|
||||
urls = []
|
||||
|
||||
# Split by newlines first
|
||||
for line in input_text.split("\n"):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# Then split by commas
|
||||
for url in line.split(","):
|
||||
url = url.strip()
|
||||
if url:
|
||||
urls.append(url)
|
||||
|
||||
if not urls:
|
||||
raise HTTPException(status_code=400, detail="No URLs provided")
|
||||
|
||||
results = []
|
||||
for url in urls:
|
||||
results.append(validate_url(url))
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,389 @@
|
||||
"""
|
||||
NeuroSploit v3 - Vulnerabilities API Endpoints
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.db.database import get_db
|
||||
from backend.models import Vulnerability
|
||||
from backend.schemas.vulnerability import VulnerabilityResponse, VulnerabilityTypeInfo
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Vulnerability type definitions
|
||||
VULNERABILITY_TYPES = {
|
||||
"injection": {
|
||||
"xss_reflected": {
|
||||
"name": "Reflected XSS",
|
||||
"description": "Cross-site scripting via user input reflected in response",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-79"]
|
||||
},
|
||||
"xss_stored": {
|
||||
"name": "Stored XSS",
|
||||
"description": "Cross-site scripting stored in application database",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-79"]
|
||||
},
|
||||
"xss_dom": {
|
||||
"name": "DOM-based XSS",
|
||||
"description": "Cross-site scripting via DOM manipulation",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-79"]
|
||||
},
|
||||
"sqli_error": {
|
||||
"name": "Error-based SQL Injection",
|
||||
"description": "SQL injection detected via error messages",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-89"]
|
||||
},
|
||||
"sqli_union": {
|
||||
"name": "Union-based SQL Injection",
|
||||
"description": "SQL injection exploitable via UNION queries",
|
||||
"severity_range": "critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-89"]
|
||||
},
|
||||
"sqli_blind": {
|
||||
"name": "Blind SQL Injection",
|
||||
"description": "SQL injection without visible output",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-89"]
|
||||
},
|
||||
"sqli_time": {
|
||||
"name": "Time-based SQL Injection",
|
||||
"description": "SQL injection detected via response time",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-89"]
|
||||
},
|
||||
"command_injection": {
|
||||
"name": "Command Injection",
|
||||
"description": "OS command injection vulnerability",
|
||||
"severity_range": "critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-78"]
|
||||
},
|
||||
"ssti": {
|
||||
"name": "Server-Side Template Injection",
|
||||
"description": "Template injection allowing code execution",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-94"]
|
||||
},
|
||||
"ldap_injection": {
|
||||
"name": "LDAP Injection",
|
||||
"description": "LDAP query injection",
|
||||
"severity_range": "high",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-90"]
|
||||
},
|
||||
"xpath_injection": {
|
||||
"name": "XPath Injection",
|
||||
"description": "XPath query injection",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-643"]
|
||||
},
|
||||
"nosql_injection": {
|
||||
"name": "NoSQL Injection",
|
||||
"description": "NoSQL database injection",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-943"]
|
||||
},
|
||||
"header_injection": {
|
||||
"name": "HTTP Header Injection",
|
||||
"description": "Injection into HTTP headers",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-113"]
|
||||
},
|
||||
"crlf_injection": {
|
||||
"name": "CRLF Injection",
|
||||
"description": "Carriage return line feed injection",
|
||||
"severity_range": "medium",
|
||||
"owasp_category": "A03:2021",
|
||||
"cwe_ids": ["CWE-93"]
|
||||
}
|
||||
},
|
||||
"file_access": {
|
||||
"lfi": {
|
||||
"name": "Local File Inclusion",
|
||||
"description": "Include local files via path manipulation",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-98"]
|
||||
},
|
||||
"rfi": {
|
||||
"name": "Remote File Inclusion",
|
||||
"description": "Include remote files for code execution",
|
||||
"severity_range": "critical",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-98"]
|
||||
},
|
||||
"path_traversal": {
|
||||
"name": "Path Traversal",
|
||||
"description": "Access files outside web root",
|
||||
"severity_range": "high",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-22"]
|
||||
},
|
||||
"file_upload": {
|
||||
"name": "Arbitrary File Upload",
|
||||
"description": "Upload malicious files",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A04:2021",
|
||||
"cwe_ids": ["CWE-434"]
|
||||
},
|
||||
"xxe": {
|
||||
"name": "XML External Entity",
|
||||
"description": "XXE injection vulnerability",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-611"]
|
||||
}
|
||||
},
|
||||
"request_forgery": {
|
||||
"ssrf": {
|
||||
"name": "Server-Side Request Forgery",
|
||||
"description": "Forge requests from the server",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A10:2021",
|
||||
"cwe_ids": ["CWE-918"]
|
||||
},
|
||||
"ssrf_cloud": {
|
||||
"name": "SSRF to Cloud Metadata",
|
||||
"description": "SSRF accessing cloud provider metadata",
|
||||
"severity_range": "critical",
|
||||
"owasp_category": "A10:2021",
|
||||
"cwe_ids": ["CWE-918"]
|
||||
},
|
||||
"csrf": {
|
||||
"name": "Cross-Site Request Forgery",
|
||||
"description": "Forge requests as authenticated user",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-352"]
|
||||
}
|
||||
},
|
||||
"authentication": {
|
||||
"auth_bypass": {
|
||||
"name": "Authentication Bypass",
|
||||
"description": "Bypass authentication mechanisms",
|
||||
"severity_range": "critical",
|
||||
"owasp_category": "A07:2021",
|
||||
"cwe_ids": ["CWE-287"]
|
||||
},
|
||||
"session_fixation": {
|
||||
"name": "Session Fixation",
|
||||
"description": "Force known session ID on user",
|
||||
"severity_range": "high",
|
||||
"owasp_category": "A07:2021",
|
||||
"cwe_ids": ["CWE-384"]
|
||||
},
|
||||
"jwt_manipulation": {
|
||||
"name": "JWT Token Manipulation",
|
||||
"description": "Manipulate JWT tokens for auth bypass",
|
||||
"severity_range": "high-critical",
|
||||
"owasp_category": "A07:2021",
|
||||
"cwe_ids": ["CWE-347"]
|
||||
},
|
||||
"weak_password_policy": {
|
||||
"name": "Weak Password Policy",
|
||||
"description": "Application accepts weak passwords",
|
||||
"severity_range": "medium",
|
||||
"owasp_category": "A07:2021",
|
||||
"cwe_ids": ["CWE-521"]
|
||||
}
|
||||
},
|
||||
"authorization": {
|
||||
"idor": {
|
||||
"name": "Insecure Direct Object Reference",
|
||||
"description": "Access objects without proper authorization",
|
||||
"severity_range": "high",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-639"]
|
||||
},
|
||||
"bola": {
|
||||
"name": "Broken Object Level Authorization",
|
||||
"description": "API-level object authorization bypass",
|
||||
"severity_range": "high",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-639"]
|
||||
},
|
||||
"privilege_escalation": {
|
||||
"name": "Privilege Escalation",
|
||||
"description": "Escalate to higher privilege level",
|
||||
"severity_range": "critical",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-269"]
|
||||
}
|
||||
},
|
||||
"api_security": {
|
||||
"rate_limiting": {
|
||||
"name": "Missing Rate Limiting",
|
||||
"description": "No rate limiting on sensitive endpoints",
|
||||
"severity_range": "medium",
|
||||
"owasp_category": "A04:2021",
|
||||
"cwe_ids": ["CWE-770"]
|
||||
},
|
||||
"mass_assignment": {
|
||||
"name": "Mass Assignment",
|
||||
"description": "Modify unintended object properties",
|
||||
"severity_range": "high",
|
||||
"owasp_category": "A04:2021",
|
||||
"cwe_ids": ["CWE-915"]
|
||||
},
|
||||
"excessive_data": {
|
||||
"name": "Excessive Data Exposure",
|
||||
"description": "API returns more data than needed",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-200"]
|
||||
},
|
||||
"graphql_introspection": {
|
||||
"name": "GraphQL Introspection Enabled",
|
||||
"description": "GraphQL schema exposed via introspection",
|
||||
"severity_range": "low-medium",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-200"]
|
||||
}
|
||||
},
|
||||
"client_side": {
|
||||
"cors_misconfig": {
|
||||
"name": "CORS Misconfiguration",
|
||||
"description": "Permissive CORS policy",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-942"]
|
||||
},
|
||||
"clickjacking": {
|
||||
"name": "Clickjacking",
|
||||
"description": "Page can be framed for clickjacking",
|
||||
"severity_range": "medium",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-1021"]
|
||||
},
|
||||
"open_redirect": {
|
||||
"name": "Open Redirect",
|
||||
"description": "Redirect to arbitrary URLs",
|
||||
"severity_range": "low-medium",
|
||||
"owasp_category": "A01:2021",
|
||||
"cwe_ids": ["CWE-601"]
|
||||
}
|
||||
},
|
||||
"information_disclosure": {
|
||||
"error_disclosure": {
|
||||
"name": "Error Message Disclosure",
|
||||
"description": "Detailed error messages exposed",
|
||||
"severity_range": "low-medium",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-209"]
|
||||
},
|
||||
"sensitive_data": {
|
||||
"name": "Sensitive Data Exposure",
|
||||
"description": "Sensitive information exposed",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A02:2021",
|
||||
"cwe_ids": ["CWE-200"]
|
||||
},
|
||||
"debug_endpoints": {
|
||||
"name": "Debug Endpoints Exposed",
|
||||
"description": "Debug/admin endpoints accessible",
|
||||
"severity_range": "high",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-489"]
|
||||
}
|
||||
},
|
||||
"infrastructure": {
|
||||
"security_headers": {
|
||||
"name": "Missing Security Headers",
|
||||
"description": "Important security headers not set",
|
||||
"severity_range": "low-medium",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-693"]
|
||||
},
|
||||
"ssl_issues": {
|
||||
"name": "SSL/TLS Issues",
|
||||
"description": "Weak SSL/TLS configuration",
|
||||
"severity_range": "medium",
|
||||
"owasp_category": "A02:2021",
|
||||
"cwe_ids": ["CWE-326"]
|
||||
},
|
||||
"http_methods": {
|
||||
"name": "Dangerous HTTP Methods",
|
||||
"description": "Dangerous HTTP methods enabled",
|
||||
"severity_range": "low-medium",
|
||||
"owasp_category": "A05:2021",
|
||||
"cwe_ids": ["CWE-749"]
|
||||
}
|
||||
},
|
||||
"logic_flaws": {
|
||||
"race_condition": {
|
||||
"name": "Race Condition",
|
||||
"description": "Exploitable race condition",
|
||||
"severity_range": "medium-high",
|
||||
"owasp_category": "A04:2021",
|
||||
"cwe_ids": ["CWE-362"]
|
||||
},
|
||||
"business_logic": {
|
||||
"name": "Business Logic Flaw",
|
||||
"description": "Exploitable business logic error",
|
||||
"severity_range": "varies",
|
||||
"owasp_category": "A04:2021",
|
||||
"cwe_ids": ["CWE-840"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/types")
|
||||
async def get_vulnerability_types():
|
||||
"""Get all vulnerability types organized by category"""
|
||||
return VULNERABILITY_TYPES
|
||||
|
||||
|
||||
@router.get("/types/{category}")
|
||||
async def get_vulnerability_types_by_category(category: str):
|
||||
"""Get vulnerability types for a specific category"""
|
||||
if category not in VULNERABILITY_TYPES:
|
||||
raise HTTPException(status_code=404, detail=f"Category '{category}' not found")
|
||||
|
||||
return VULNERABILITY_TYPES[category]
|
||||
|
||||
|
||||
@router.get("/types/{category}/{vuln_type}", response_model=VulnerabilityTypeInfo)
|
||||
async def get_vulnerability_type_info(category: str, vuln_type: str):
|
||||
"""Get detailed info for a specific vulnerability type"""
|
||||
if category not in VULNERABILITY_TYPES:
|
||||
raise HTTPException(status_code=404, detail=f"Category '{category}' not found")
|
||||
|
||||
if vuln_type not in VULNERABILITY_TYPES[category]:
|
||||
raise HTTPException(status_code=404, detail=f"Type '{vuln_type}' not found in category '{category}'")
|
||||
|
||||
info = VULNERABILITY_TYPES[category][vuln_type]
|
||||
return VulnerabilityTypeInfo(
|
||||
type=vuln_type,
|
||||
category=category,
|
||||
**info
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{vuln_id}", response_model=VulnerabilityResponse)
|
||||
async def get_vulnerability(vuln_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a specific vulnerability by ID"""
|
||||
result = await db.execute(select(Vulnerability).where(Vulnerability.id == vuln_id))
|
||||
vuln = result.scalar_one_or_none()
|
||||
|
||||
if not vuln:
|
||||
raise HTTPException(status_code=404, detail="Vulnerability not found")
|
||||
|
||||
return VulnerabilityResponse(**vuln.to_dict())
|
||||
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
NeuroSploit v3 - WebSocket Manager
|
||||
"""
|
||||
from typing import Dict, List, Optional
|
||||
from fastapi import WebSocket
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manages WebSocket connections for real-time updates"""
|
||||
|
||||
def __init__(self):
|
||||
# scan_id -> list of websocket connections
|
||||
self.active_connections: Dict[str, List[WebSocket]] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def connect(self, websocket: WebSocket, scan_id: str):
|
||||
"""Accept a WebSocket connection and register it for a scan"""
|
||||
await websocket.accept()
|
||||
async with self._lock:
|
||||
if scan_id not in self.active_connections:
|
||||
self.active_connections[scan_id] = []
|
||||
self.active_connections[scan_id].append(websocket)
|
||||
print(f"WebSocket connected for scan: {scan_id}")
|
||||
|
||||
def disconnect(self, websocket: WebSocket, scan_id: str):
|
||||
"""Remove a WebSocket connection"""
|
||||
if scan_id in self.active_connections:
|
||||
if websocket in self.active_connections[scan_id]:
|
||||
self.active_connections[scan_id].remove(websocket)
|
||||
if not self.active_connections[scan_id]:
|
||||
del self.active_connections[scan_id]
|
||||
print(f"WebSocket disconnected for scan: {scan_id}")
|
||||
|
||||
async def send_to_scan(self, scan_id: str, message: dict):
|
||||
"""Send a message to all connections watching a specific scan"""
|
||||
if scan_id not in self.active_connections:
|
||||
return
|
||||
|
||||
dead_connections = []
|
||||
for connection in self.active_connections[scan_id]:
|
||||
try:
|
||||
await connection.send_text(json.dumps(message))
|
||||
except Exception:
|
||||
dead_connections.append(connection)
|
||||
|
||||
# Clean up dead connections
|
||||
for conn in dead_connections:
|
||||
self.disconnect(conn, scan_id)
|
||||
|
||||
async def broadcast_scan_started(self, scan_id: str):
|
||||
"""Notify that a scan has started"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "scan_started",
|
||||
"scan_id": scan_id
|
||||
})
|
||||
|
||||
async def broadcast_phase_change(self, scan_id: str, phase: str):
|
||||
"""Notify phase change (recon, testing, reporting)"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "phase_change",
|
||||
"scan_id": scan_id,
|
||||
"phase": phase
|
||||
})
|
||||
|
||||
async def broadcast_progress(self, scan_id: str, progress: int, message: Optional[str] = None):
|
||||
"""Send progress update"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "progress_update",
|
||||
"scan_id": scan_id,
|
||||
"progress": progress,
|
||||
"message": message
|
||||
})
|
||||
|
||||
async def broadcast_endpoint_found(self, scan_id: str, endpoint: dict):
|
||||
"""Notify a new endpoint was discovered"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "endpoint_found",
|
||||
"scan_id": scan_id,
|
||||
"endpoint": endpoint
|
||||
})
|
||||
|
||||
async def broadcast_path_crawled(self, scan_id: str, path: str, status: int):
|
||||
"""Notify a path was crawled"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "path_crawled",
|
||||
"scan_id": scan_id,
|
||||
"path": path,
|
||||
"status": status
|
||||
})
|
||||
|
||||
async def broadcast_url_discovered(self, scan_id: str, url: str):
|
||||
"""Notify a URL was discovered"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "url_discovered",
|
||||
"scan_id": scan_id,
|
||||
"url": url
|
||||
})
|
||||
|
||||
async def broadcast_test_started(self, scan_id: str, vuln_type: str, endpoint: str):
|
||||
"""Notify a vulnerability test has started"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "test_started",
|
||||
"scan_id": scan_id,
|
||||
"vulnerability_type": vuln_type,
|
||||
"endpoint": endpoint
|
||||
})
|
||||
|
||||
async def broadcast_test_completed(self, scan_id: str, vuln_type: str, endpoint: str, is_vulnerable: bool):
|
||||
"""Notify a vulnerability test has completed"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "test_completed",
|
||||
"scan_id": scan_id,
|
||||
"vulnerability_type": vuln_type,
|
||||
"endpoint": endpoint,
|
||||
"is_vulnerable": is_vulnerable
|
||||
})
|
||||
|
||||
async def broadcast_vulnerability_found(self, scan_id: str, vulnerability: dict):
|
||||
"""Notify a vulnerability was found"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "vuln_found",
|
||||
"scan_id": scan_id,
|
||||
"vulnerability": vulnerability
|
||||
})
|
||||
|
||||
async def broadcast_log(self, scan_id: str, level: str, message: str):
|
||||
"""Send a log message"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "log_message",
|
||||
"scan_id": scan_id,
|
||||
"level": level,
|
||||
"message": message
|
||||
})
|
||||
|
||||
async def broadcast_scan_completed(self, scan_id: str, summary: dict):
|
||||
"""Notify that a scan has completed"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "scan_completed",
|
||||
"scan_id": scan_id,
|
||||
"summary": summary
|
||||
})
|
||||
|
||||
async def broadcast_error(self, scan_id: str, error: str):
|
||||
"""Notify an error occurred"""
|
||||
await self.send_to_scan(scan_id, {
|
||||
"type": "error",
|
||||
"scan_id": scan_id,
|
||||
"error": error
|
||||
})
|
||||
|
||||
|
||||
# Global instance
|
||||
manager = ConnectionManager()
|
||||
Reference in New Issue
Block a user