Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions backend/secuscan/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,85 @@ async def build():
return await get_or_set_cached(f"reports:list:{owner}", build)


@router.get("/analytics/vulnerability-trends", dependencies=[Depends(read_heavy_limiter)])
async def get_vulnerability_trends(owner: str = Depends(get_current_owner)):
"""Return daily vulnerability counts for the last 30 days plus a simple forecast."""

async def build():
db = await get_db()

rows = await db.fetchall(
"""
SELECT
date(discovered_at) AS day,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE severity = 'critical') AS critical,
COUNT(*) FILTER (WHERE severity = 'high') AS high,
COUNT(*) FILTER (WHERE severity = 'medium') AS medium,
COUNT(*) FILTER (WHERE severity = 'low') AS low,
COUNT(*) FILTER (WHERE severity = 'info') AS info,
AVG(CASE WHEN risk_score IS NOT NULL THEN risk_score END) AS avg_risk_score
FROM findings
WHERE owner_id = ?
AND discovered_at >= date('now', '-30 days')
GROUP BY day
ORDER BY day ASC
""",
(owner,),
)

daily = [
{
"date": row["day"],
"total": int(row["total"]),
"critical": int(row["critical"]),
"high": int(row["high"]),
"medium": int(row["medium"]),
"low": int(row["low"]),
"info": int(row["info"]),
"avg_risk_score": round(row["avg_risk_score"], 1) if row["avg_risk_score"] is not None else None,
}
for row in rows
]

# Forecast: rolling averages over the last 7 and previous 7 available days.
totals = [d["total"] for d in daily]
last_7 = totals[-7:] if len(totals) >= 1 else []
prev_7 = totals[-14:-7] if len(totals) >= 8 else []

if last_7:
daily_average = round(sum(last_7) / len(last_7), 1)
next_7_days_total = round(daily_average * 7)

if prev_7:
prev_avg = sum(prev_7) / len(prev_7)
if prev_avg == 0:
trend = "increasing" if daily_average > 0 else "stable"
elif daily_average > prev_avg * 1.10:
trend = "increasing"
elif daily_average < prev_avg * 0.90:
trend = "decreasing"
else:
trend = "stable"
else:
trend = "insufficient_data"
else:
daily_average = 0.0
next_7_days_total = 0
trend = "insufficient_data"

return {
"daily": daily,
"forecast": {
"next_7_days_total": next_7_days_total,
"daily_average": daily_average,
"trend": trend,
},
}

return await get_or_set_cached(f"analytics:vulnerability-trends:{owner}", build)


@router.get("/tasks", dependencies=[Depends(read_heavy_limiter)])
async def list_tasks(
page: int = Query(1, ge=1),
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,32 @@ export function getReports() {
return request('/reports')
}

export type VulnerabilityTrendPoint = {
date: string
total: number
critical: number
high: number
medium: number
low: number
info: number
avg_risk_score: number | null
}

export type VulnerabilityTrendForecast = {
next_7_days_total: number
daily_average: number
trend: 'increasing' | 'decreasing' | 'stable' | 'insufficient_data'
}

export type VulnerabilityTrendResponse = {
daily: VulnerabilityTrendPoint[]
forecast: VulnerabilityTrendForecast
}

export function getVulnerabilityTrends() {
return request<VulnerabilityTrendResponse>('/analytics/vulnerability-trends')
}

export type NotificationChannelType = 'webhook' | 'email'
export type NotificationSeverityThreshold = 'critical' | 'high' | 'medium' | 'low' | 'info'

Expand Down
Loading