updated new features

This commit is contained in:
rarebuffalo
2026-05-07 22:32:20 +05:30
parent 17c14dabf9
commit 61797fbb97
9 changed files with 327 additions and 17 deletions

View File

@@ -18,9 +18,17 @@ from app.schemas.scan import (
ThreatNarrativeResponse,
ScanDiffResponse,
ScheduledScanResponse,
NucleiResultResponse,
RemediationPlan,
)
from app.services.ai import chat_with_scan_context, generate_threat_narrative, generate_diff_narrative
from app.services.ai import (
chat_with_scan_context,
generate_threat_narrative,
generate_diff_narrative,
generate_remediation_plan,
)
from app.models.nuclei_result import NucleiScanResult
router = APIRouter(prefix="/scans", tags=["history"])
@@ -236,3 +244,81 @@ async def diff_scans(
score_change=score_change,
narrative=narrative,
)
@router.get("/{scan_id}/nuclei", response_model=NucleiResultResponse)
async def get_nuclei_result(
scan_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
Retrieve the Nuclei active scan result for a given website scan.
Nuclei runs as a background task after the main scan, so this result may
not be available immediately. Poll this endpoint until status is not
'pending'. If the Nuclei binary is not installed, status will be 'skipped'.
"""
# Verify the parent scan belongs to the requesting user
scan_check = await db.execute(
select(ScanResult).where(
ScanResult.id == scan_id,
ScanResult.user_id == current_user.id,
)
)
if not scan_check.scalar_one_or_none():
raise HTTPException(status_code=404, detail="Scan not found")
result = await db.execute(
select(NucleiScanResult).where(NucleiScanResult.scan_result_id == scan_id)
)
nuclei_row = result.scalar_one_or_none()
if not nuclei_row:
raise HTTPException(
status_code=404,
detail="Nuclei result not available yet. The background scan may still be running.",
)
return nuclei_row
@router.post("/{scan_id}/remediation-plan", response_model=RemediationPlan)
async def get_remediation_plan(
scan_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
Generate an AI-powered prioritized remediation roadmap for a scan.
The AI receives the full list of issues found in the scan and returns a
sequenced fix plan: what to do first, how hard each fix is, and a
realistic total effort estimate. Each call to this endpoint triggers a
fresh AI generation — results are not cached.
"""
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=404, detail="Scan not found")
plan_data = await generate_remediation_plan(scan.issues, scan.url)
# Validate the AI response matches our schema before returning
try:
return RemediationPlan(
summary=plan_data.get("summary", ""),
steps=plan_data.get("steps", []),
estimated_total_effort=plan_data.get("estimated_total_effort", "N/A"),
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to parse remediation plan from AI response: {e}",
)

View File

@@ -1,3 +1,4 @@
import asyncio
import logging
import httpx
@@ -24,6 +25,12 @@ from app.services.scoring import calculate_layer_statuses, calculate_score
from app.services.ai import enhance_security_issues
from app.services.threat_intel import get_threat_intel_summary
from app.services.webhook_dispatcher import dispatch_webhooks
from app.services.nuclei_scanner import run_nuclei_scan
from app.services.alerting import (
send_slack_alert,
send_email_alert,
build_scan_email_body,
)
from app.utils.validators import validate_url
logger = logging.getLogger(__name__)
@@ -39,6 +46,41 @@ dns_scanner = DNSScanner()
port_scanner = PortScanner()
async def _post_scan_tasks(
user_id: str,
user_email: str,
scan_id: str,
url: str,
score: int,
issue_count: int,
db: AsyncSession,
) -> None:
"""
Groups all post-scan side-effects that run as a background task:
- Dispatch webhooks
- Send Slack alert
- Send email alert
- Trigger Nuclei active scan
These all run after the response has been sent to the client, so they
never add latency to the scan endpoint.
"""
scan_summary = {"scan_id": scan_id, "url": url, "score": score}
await dispatch_webhooks(user_id, scan_summary, db)
slack_msg = f"URL: {url}\nScore: {score}/100 | Issues found: {issue_count}"
await send_slack_alert(title="SecureLens Scan Complete", message=slack_msg)
email_body = build_scan_email_body(url, score, issue_count)
await send_email_alert(
to_email=user_email,
subject=f"SecureLens: Scan complete for {url}",
html_body=email_body,
)
# Nuclei runs last — it creates its own DB session and takes the longest
await run_nuclei_scan(scan_id, url)
@router.post("/scan", response_model=ScanResponse)
@limiter.limit(settings.rate_limit)
@@ -52,11 +94,8 @@ async def scan_website(
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))
# Step 3: Run threat intel lookup concurrently — zero extra latency
threat_intel_task = asyncio.create_task(get_threat_intel_summary(url))
async with httpx.AsyncClient(
@@ -71,8 +110,6 @@ async def scan_website(
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)
threat_intel = await threat_intel_task
@@ -111,12 +148,16 @@ async def scan_website(
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)
background_tasks.add_task(
_post_scan_tasks,
current_user.id,
current_user.email,
scan_id,
url,
score,
len(all_issues),
db,
)
return ScanResponse(
id=scan_id,
@@ -125,7 +166,7 @@ async def scan_website(
layers=layers,
issues=all_issues,
created_at=created_at,
threat_intel=threat_intel, # Step 3: attach threat intelligence
threat_intel=threat_intel,
)
except httpx.HTTPError as e:
@@ -134,3 +175,4 @@ async def scan_website(
status_code=502,
content={"error": f"Could not reach {url}: {str(e)}"},
)