From bb7b5ee4d86b30aa10df0e4152930d08da662d43 Mon Sep 17 00:00:00 2001 From: Somil450 Date: Thu, 28 May 2026 23:39:22 +0530 Subject: [PATCH 1/4] feat: Add webhook notifications for scan completion (Slack, Discord, Custom) --- backend/requirements.txt | 1 + backend/secuscan/executor.py | 26 + backend/secuscan/models.py | 357 ++-- backend/secuscan/notifications.py | 99 + backend/secuscan/routes.py | 40 +- backend/test_webhooks.py | 32 + frontend/src/api.ts | 26 + frontend/src/pages/Settings.tsx | 76 +- issues.json | 3126 +++++++++++++++++++++++++++++ 9 files changed, 3603 insertions(+), 180 deletions(-) create mode 100644 backend/secuscan/notifications.py create mode 100644 backend/test_webhooks.py create mode 100644 issues.json diff --git a/backend/requirements.txt b/backend/requirements.txt index b7d7a851..e4291eb4 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,4 @@ python-multipart>=0.0.9 xhtml2pdf>=0.2.17 aiosqlite>=0.20.0 python-whois>=0.9.4 +httpx>=0.28.1 diff --git a/backend/secuscan/executor.py b/backend/secuscan/executor.py index 44bda2ae..21ddd96c 100644 --- a/backend/secuscan/executor.py +++ b/backend/secuscan/executor.py @@ -384,6 +384,32 @@ async def execute_task(self, task_id: str): await self._broadcast(task_id, "status", final_status) await self._invalidate_cached_views() + # Trigger Webhook Notifications + try: + webhook_rows = await db.fetchall("SELECT key, value FROM settings WHERE type = 'webhook'") + if webhook_rows: + from .models import WebhookConfig + from .notifications import notify_scan_completion + webhook_config_dict = {row["key"]: row["value"] for row in webhook_rows} + webhook_config = WebhookConfig(**webhook_config_dict) + + if any([webhook_config.slack_url, webhook_config.discord_url, webhook_config.custom_url]): + findings_rows = await db.fetchall("SELECT severity, count(*) as count FROM findings WHERE task_id = ? GROUP BY severity", (task_id,)) + findings_summary = {row["severity"]: row["count"] for row in findings_rows} + + asyncio.create_task( + notify_scan_completion( + task_id=task_id, + target=target, + status=final_status, + duration=duration, + findings_summary=findings_summary, + config=webhook_config + ) + ) + except Exception as e: + logger.error(f"Failed to trigger webhooks for task {task_id}: {e}") + # Log completion await db.log_audit( "task_completed", diff --git a/backend/secuscan/models.py b/backend/secuscan/models.py index 5d4b52e5..341112aa 100644 --- a/backend/secuscan/models.py +++ b/backend/secuscan/models.py @@ -1,175 +1,182 @@ -""" -Pydantic models for API requests and responses -""" - -from typing import Optional, Dict, Any, List, Annotated -from datetime import datetime -from pydantic import BaseModel, Field, RootModel -from enum import Enum - - -MAX_BULK_DELETE = 500 - -class SafetyLevel(str, Enum): - """Plugin safety level classification""" - SAFE = "safe" - INTRUSIVE = "intrusive" - EXPLOIT = "exploit" - - -class TaskStatus(str, Enum): - """Task execution status""" - QUEUED = "queued" - RUNNING = "running" - COMPLETED = "completed" - FAILED = "failed" - CANCELLED = "cancelled" - - -class PluginFieldType(str, Enum): - """Plugin field input types""" - STRING = "string" - TEXT = "text" - INTEGER = "integer" - BOOLEAN = "boolean" - SELECT = "select" - MULTISELECT = "multiselect" - FILE = "file" - KEYVALUE = "keyvalue" - - -class PluginField(BaseModel): - """Plugin input field definition""" - id: str - label: str - type: PluginFieldType - required: bool = False - default: Optional[Any] = None - placeholder: Optional[str] = None - validation: Optional[Dict[str, Any]] = None - help: Optional[str] = None - options: Optional[List[Dict[str, str]]] = None - - -class PluginMetadata(BaseModel): - """Plugin metadata schema""" - id: str - name: str - version: str - description: str - long_description: Optional[str] = None - category: str - author: Optional[Dict[str, str]] = None - license: Optional[str] = "MIT" - icon: Optional[str] = "🔧" - - engine: Dict[str, str] - command_template: List[str] - fields: List[PluginField] - presets: Dict[str, Dict[str, Any]] - - output: Dict[str, Any] - safety: Dict[str, Any] - learning: Optional[Dict[str, Any]] = None - dependencies: Optional[Dict[str, List[str]]] = None - docker_image: Optional[str] = None - - checksum: Optional[str] = None - signature: Optional[str] = None - - -class TaskCreateRequest(BaseModel): - """Request to create a new task""" - plugin_id: str - preset: Optional[str] = None - inputs: Dict[str, Any] - consent_granted: bool = False - - -class TaskResponse(BaseModel): - """Task information response""" - task_id: str - plugin_id: str - tool: str - target: str - status: TaskStatus - created_at: datetime - started_at: Optional[datetime] = None - completed_at: Optional[datetime] = None - duration_seconds: Optional[float] = None - inputs: Optional[Dict[str, Any]] = None - preset: Optional[str] = None - error_message: Optional[str] = None - exit_code: Optional[int] = None - - -class Finding(BaseModel): - """Structured security finding""" - id: Optional[str] = None - title: str - category: str - severity: str - target: str - description: str - remediation: Optional[str] = "" - cvss: Optional[float] = None - cve: Optional[str] = None - proof: Optional[str] = None - discovered_at: Optional[datetime] = None - metadata: Dict[str, Any] = Field(default_factory=dict) - exploitability: Optional[float] = None - confidence: Optional[float] = None - asset_exposure: Optional[str] = None - risk_score: Optional[float] = None - risk_factors: List[Dict[str, Any]] = Field(default_factory=list) - - -class TaskResult(BaseModel): - """Task execution result""" - task_id: str - plugin_id: str - tool: str - target: str - timestamp: datetime - duration_seconds: Optional[float] - status: TaskStatus - - summary: List[str] = [] - severity_counts: Dict[str, int] = Field(default_factory=dict) - findings: List[Finding] = Field(default_factory=list) - structured: Dict[str, Any] = Field(default_factory=dict) - raw_output_path: Optional[str] = None - raw_output_excerpt: Optional[str] = None - - errors: List[Dict[str, Any]] = [] - error_message: Optional[str] = None - exit_code: Optional[int] = None - metadata: Dict[str, Any] = Field(default_factory=dict) - - -class HealthResponse(BaseModel): - """Health check response""" - status: str - version: str - uptime_seconds: Optional[int] = None - system: Dict[str, Any] - limits: Optional[Dict[str, int]] = None - - -class PluginListResponse(BaseModel): - """List of available plugins""" - plugins: List[Dict[str, Any]] - total: int - - -class ErrorResponse(BaseModel): - """Error response""" - error: str - message: str - field: Optional[str] = None - details: Optional[Dict[str, Any]] = None - - -class BulkDeleteRequest(RootModel[Annotated[List[str], Field(max_length=MAX_BULK_DELETE)]]): - """Accepts a JSON array of task IDs directly. Max 500 per request.""" - pass \ No newline at end of file +""" +Pydantic models for API requests and responses +""" + +from typing import Optional, Dict, Any, List, Annotated +from datetime import datetime +from pydantic import BaseModel, Field, RootModel +from enum import Enum + + +MAX_BULK_DELETE = 500 + +class SafetyLevel(str, Enum): + """Plugin safety level classification""" + SAFE = "safe" + INTRUSIVE = "intrusive" + EXPLOIT = "exploit" + + +class TaskStatus(str, Enum): + """Task execution status""" + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class PluginFieldType(str, Enum): + """Plugin field input types""" + STRING = "string" + TEXT = "text" + INTEGER = "integer" + BOOLEAN = "boolean" + SELECT = "select" + MULTISELECT = "multiselect" + FILE = "file" + KEYVALUE = "keyvalue" + + +class PluginField(BaseModel): + """Plugin input field definition""" + id: str + label: str + type: PluginFieldType + required: bool = False + default: Optional[Any] = None + placeholder: Optional[str] = None + validation: Optional[Dict[str, Any]] = None + help: Optional[str] = None + options: Optional[List[Dict[str, str]]] = None + + +class PluginMetadata(BaseModel): + """Plugin metadata schema""" + id: str + name: str + version: str + description: str + long_description: Optional[str] = None + category: str + author: Optional[Dict[str, str]] = None + license: Optional[str] = "MIT" + icon: Optional[str] = "🔧" + + engine: Dict[str, str] + command_template: List[str] + fields: List[PluginField] + presets: Dict[str, Dict[str, Any]] + + output: Dict[str, Any] + safety: Dict[str, Any] + learning: Optional[Dict[str, Any]] = None + dependencies: Optional[Dict[str, List[str]]] = None + docker_image: Optional[str] = None + + checksum: Optional[str] = None + signature: Optional[str] = None + + +class TaskCreateRequest(BaseModel): + """Request to create a new task""" + plugin_id: str + preset: Optional[str] = None + inputs: Dict[str, Any] + consent_granted: bool = False + + +class TaskResponse(BaseModel): + """Task information response""" + task_id: str + plugin_id: str + tool: str + target: str + status: TaskStatus + created_at: datetime + started_at: Optional[datetime] = None + completed_at: Optional[datetime] = None + duration_seconds: Optional[float] = None + inputs: Optional[Dict[str, Any]] = None + preset: Optional[str] = None + error_message: Optional[str] = None + exit_code: Optional[int] = None + + +class Finding(BaseModel): + """Structured security finding""" + id: Optional[str] = None + title: str + category: str + severity: str + target: str + description: str + remediation: Optional[str] = "" + cvss: Optional[float] = None + cve: Optional[str] = None + proof: Optional[str] = None + discovered_at: Optional[datetime] = None + metadata: Dict[str, Any] = Field(default_factory=dict) + exploitability: Optional[float] = None + confidence: Optional[float] = None + asset_exposure: Optional[str] = None + risk_score: Optional[float] = None + risk_factors: List[Dict[str, Any]] = Field(default_factory=list) + + +class TaskResult(BaseModel): + """Task execution result""" + task_id: str + plugin_id: str + tool: str + target: str + timestamp: datetime + duration_seconds: Optional[float] + status: TaskStatus + + summary: List[str] = [] + severity_counts: Dict[str, int] = Field(default_factory=dict) + findings: List[Finding] = Field(default_factory=list) + structured: Dict[str, Any] = Field(default_factory=dict) + raw_output_path: Optional[str] = None + raw_output_excerpt: Optional[str] = None + + errors: List[Dict[str, Any]] = [] + error_message: Optional[str] = None + exit_code: Optional[int] = None + metadata: Dict[str, Any] = Field(default_factory=dict) + + +class HealthResponse(BaseModel): + """Health check response""" + status: str + version: str + uptime_seconds: Optional[int] = None + system: Dict[str, Any] + limits: Optional[Dict[str, int]] = None + + +class PluginListResponse(BaseModel): + """List of available plugins""" + plugins: List[Dict[str, Any]] + total: int + + +class ErrorResponse(BaseModel): + """Error response""" + error: str + message: str + field: Optional[str] = None + details: Optional[Dict[str, Any]] = None + + +class BulkDeleteRequest(RootModel[Annotated[List[str], Field(max_length=MAX_BULK_DELETE)]]): + """Accepts a JSON array of task IDs directly. Max 500 per request.""" + pass + + +class WebhookConfig(BaseModel): + """Configuration for external webhook notifications""" + slack_url: Optional[str] = None + discord_url: Optional[str] = None + custom_url: Optional[str] = None \ No newline at end of file diff --git a/backend/secuscan/notifications.py b/backend/secuscan/notifications.py new file mode 100644 index 00000000..a45238d6 --- /dev/null +++ b/backend/secuscan/notifications.py @@ -0,0 +1,99 @@ +import httpx +import logging +import asyncio +from typing import Dict, Any +from .models import WebhookConfig + +logger = logging.getLogger(__name__) + +async def _send_with_retry(url: str, payload: Dict[str, Any], headers: Dict[str, str] = None, max_retries: int = 3): + """Send an HTTP request with exponential backoff retry logic.""" + if not headers: + headers = {"Content-Type": "application/json"} + + async with httpx.AsyncClient() as client: + for attempt in range(max_retries): + try: + response = await client.post(url, json=payload, headers=headers, timeout=10.0) + if response.status_code < 400: + return response + logger.warning(f"Webhook request to {url} failed with status {response.status_code}. Attempt {attempt+1}/{max_retries}") + except httpx.RequestError as e: + logger.warning(f"Webhook request to {url} failed: {e}. Attempt {attempt+1}/{max_retries}") + + if attempt < max_retries - 1: + await asyncio.sleep(2 ** attempt) + + logger.error(f"Failed to send webhook to {url} after {max_retries} attempts.") + +async def send_slack_webhook(url: str, payload: Dict[str, Any]): + await _send_with_retry(url, payload) + +async def send_discord_webhook(url: str, payload: Dict[str, Any]): + await _send_with_retry(url, payload) + +async def send_custom_webhook(url: str, payload: Dict[str, Any]): + await _send_with_retry(url, payload) + + +async def test_webhook_config(config: WebhookConfig): + payload = { + "text": "This is a test notification from SecuScan.", + "content": "This is a test notification from SecuScan.", + "message": "This is a test notification from SecuScan." + } + + tasks = [] + if config.slack_url: + tasks.append(send_slack_webhook(config.slack_url, {"text": payload["text"]})) + if config.discord_url: + tasks.append(send_discord_webhook(config.discord_url, {"content": payload["content"]})) + if config.custom_url: + tasks.append(send_custom_webhook(config.custom_url, payload)) + + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + else: + raise ValueError("No webhook URLs provided to test.") + +async def notify_scan_completion(task_id: str, target: str, status: str, duration: float, findings_summary: Dict[str, int], config: WebhookConfig): + duration_str = f"{duration:.2f}s" if duration else "Unknown" + + severity_text = ", ".join(f"{k}: {v}" for k, v in findings_summary.items() if v > 0) + if not severity_text: + severity_text = "No findings" + + text_content = ( + f"🎯 *Scan Completed*\n" + f"Target: `{target}`\n" + f"Status: {status.upper()}\n" + f"Duration: {duration_str}\n" + f"Findings: {severity_text}" + ) + + slack_payload = { + "text": text_content + } + + discord_payload = { + "content": text_content.replace("*", "**") # Discord uses double asterisk for bold + } + + custom_payload = { + "task_id": task_id, + "target": target, + "status": status, + "duration_seconds": duration, + "findings_summary": findings_summary + } + + tasks = [] + if config.slack_url: + tasks.append(send_slack_webhook(config.slack_url, slack_payload)) + if config.discord_url: + tasks.append(send_discord_webhook(config.discord_url, discord_payload)) + if config.custom_url: + tasks.append(send_custom_webhook(config.custom_url, custom_payload)) + + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) diff --git a/backend/secuscan/routes.py b/backend/secuscan/routes.py index 1258fa94..40d24bee 100644 --- a/backend/secuscan/routes.py +++ b/backend/secuscan/routes.py @@ -91,7 +91,7 @@ def build_report_filename(task: Dict[str, Any], extension: str) -> str: from .cache import get_cache from .models import ( TaskCreateRequest, TaskResponse, TaskResult, - PluginListResponse, ErrorResponse, BulkDeleteRequest + PluginListResponse, ErrorResponse, WebhookConfig, BulkDeleteRequest ) from .config import settings from .database import get_db @@ -106,6 +106,7 @@ def build_report_filename(task: Dict[str, Any], extension: str) -> str: from .reporting import reporting from .vault import VaultCrypto from .workflows import scheduler +from .notifications import test_webhook_config from sse_starlette.sse import EventSourceResponse @@ -1230,3 +1231,40 @@ async def get_assets(): rows = await db.fetchall("SELECT DISTINCT target FROM tasks UNION SELECT DISTINCT target FROM findings") assets = [{"id": str(uuid.uuid4()), "name": row["target"]} for row in rows] return {"assets": assets} + + +@router.get("/settings/webhooks", response_model=WebhookConfig) +async def get_webhooks(): + db = await get_db() + rows = await db.fetchall("SELECT key, value FROM settings WHERE type = 'webhook'") + config = {} + for r in rows: + config[r["key"]] = r["value"] + return WebhookConfig(**config) + + +@router.post("/settings/webhooks", response_model=WebhookConfig) +async def update_webhooks(config: WebhookConfig): + db = await get_db() + data = config.model_dump(exclude_unset=True) + for k, v in data.items(): + if v is not None: + await db.execute( + "INSERT INTO settings (key, value, type) VALUES (?, ?, ?) " + "ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=datetime('now')", + (k, v, "webhook") + ) + else: + await db.execute("DELETE FROM settings WHERE key = ? AND type = 'webhook'", (k,)) + return await get_webhooks() + + +@router.post("/settings/webhooks/test") +async def test_webhooks(config: WebhookConfig): + try: + await test_webhook_config(config) + return {"status": "success", "message": "Test payload sent"} + except Exception as e: + logger.error(f"Webhook test failed: {e}") + raise HTTPException(status_code=400, detail=str(e)) + diff --git a/backend/test_webhooks.py b/backend/test_webhooks.py new file mode 100644 index 00000000..93ca7436 --- /dev/null +++ b/backend/test_webhooks.py @@ -0,0 +1,32 @@ +import asyncio +import os +import sys + +from secuscan.config import settings +from secuscan.database import init_db, get_db +from secuscan.models import WebhookConfig +from secuscan.notifications import test_webhook_config, notify_scan_completion + +async def main(): + await init_db() + + config = WebhookConfig( + custom_url="http://httpbin.org/post" + ) + print("Testing webhook test_webhook_config...") + await test_webhook_config(config) + print("Test passed.") + + print("Testing notify_scan_completion...") + await notify_scan_completion( + task_id="test_task_123", + target="example.com", + status="completed", + duration=42.5, + findings_summary={"high": 2, "medium": 5}, + config=config + ) + print("Notify passed.") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index ec750a5e..2bf225b8 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -292,3 +292,29 @@ export function deleteWorkflow(workflowId: string): Promise<{ deleted: boolean } method: 'DELETE', }) } + +export interface WebhookConfig { + slack_url?: string; + discord_url?: string; + custom_url?: string; +} + +export async function getWebhooks(): Promise { + return request('/settings/webhooks') +} + +export async function updateWebhooks(data: WebhookConfig): Promise { + return request('/settings/webhooks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) +} + +export async function testWebhooks(data: WebhookConfig): Promise<{ status: string; message: string }> { + return request<{ status: string; message: string }>('/settings/webhooks/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) +} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 14061158..2600d654 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { useTheme } from '../components/ThemeContext' import { useToast } from '../components/ToastContext' +import { getWebhooks, updateWebhooks, testWebhooks, WebhookConfig } from '../api' const itemVariants = { hidden: { opacity: 0, y: 20 }, @@ -48,6 +49,8 @@ export default function Settings() { }) const [systemTimezone, setSystemTimezone] = useState('Detecting...') + const [webhookConfig, setWebhookConfig] = useState({}) + const [testingWebhook, setTestingWebhook] = useState(false) useEffect(() => { try { @@ -55,13 +58,34 @@ export default function Settings() { } catch (e) { setSystemTimezone('UTC') } + + getWebhooks() + .then(data => setWebhookConfig(data)) + .catch(e => console.error("Failed to fetch webhooks:", e)) }, []) - const handleSave = () => { + const handleSave = async () => { localStorage.setItem('secuscan-config', JSON.stringify(config)) - addToast("Operational parameters synchronized", "success") - if (config.theme !== theme) { - setTheme(config.theme) + try { + await updateWebhooks(webhookConfig) + addToast("Operational parameters synchronized", "success") + if (config.theme !== theme) { + setTheme(config.theme) + } + } catch (e) { + addToast("Failed to sync webhook configuration", "error") + } + } + + const handleTestWebhook = async () => { + setTestingWebhook(true) + try { + await testWebhooks(webhookConfig) + addToast("Test webhook payload transmitted", "success") + } catch (e) { + addToast("Failed to transmit test webhook", "error") + } finally { + setTestingWebhook(false) } } @@ -263,6 +287,50 @@ export default function Settings() { +
+
+

Webhook_Endpoints

+
+
+
+ setWebhookConfig({...webhookConfig, slack_url: val})} + /> + setWebhookConfig({...webhookConfig, discord_url: val})} + /> +
+
+ setWebhookConfig({...webhookConfig, custom_url: val})} + /> +
+ +
+
+
+

Access_Perimeters

diff --git a/issues.json b/issues.json new file mode 100644 index 00000000..7bfe76bc --- /dev/null +++ b/issues.json @@ -0,0 +1,3126 @@ +{ + "value": [ + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/372", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/372/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/372/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/372/events", + "html_url": "https://github.com/utksh1/SecuScan/issues/372", + "id": 4538703364, + "node_id": "I_kwDOQMZG5c8AAAABDoceBA", + "number": 372, + "title": "feat(frontend): add scan history filtering system", + "user": { + "login": "sultanafariza07", + "id": 177865567, + "node_id": "U_kgDOCpoDXw", + "avatar_url": "https://avatars.githubusercontent.com/u/177865567?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/sultanafariza07", + "html_url": "https://github.com/sultanafariza07", + "followers_url": "https://api.github.com/users/sultanafariza07/followers", + "following_url": "https://api.github.com/users/sultanafariza07/following{/other_user}", + "gists_url": "https://api.github.com/users/sultanafariza07/gists{/gist_id}", + "starred_url": "https://api.github.com/users/sultanafariza07/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/sultanafariza07/subscriptions", + "organizations_url": "https://api.github.com/users/sultanafariza07/orgs", + "repos_url": "https://api.github.com/users/sultanafariza07/repos", + "events_url": "https://api.github.com/users/sultanafariza07/events{/privacy}", + "received_events_url": "https://api.github.com/users/sultanafariza07/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-28T07:52:27Z", + "updated_at": "2026-05-28T07:52:57Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "issue_dependencies_summary": { + "blocked_by": 0, + "total_blocked_by": 0, + "blocking": 0, + "total_blocking": 0 + }, + "body": "Problem Statement\n\nCurrently, the scan history dashboard in SecuScan displays previous scans without advanced filtering options. As the number of scans grows, users may find it difficult to quickly locate specific scan results.\n\nAdding filtering functionality will improve usability, navigation, and workflow efficiency for security researchers and contributors.\n\nObjective\n\nImplement a filtering system in the frontend dashboard that allows users to filter scan history based on:\n\nSeverity\nDate\nPlugin Type\nScan Status\nExpected Features\n1. Severity Filter\n\nAllow filtering scans by vulnerability severity levels:\n\nCritical\nHigh\nMedium\nLow\nInformational\nExample:\n\nShow only “High” and “Critical” scans.\n\n2. Date Filter\n\nAllow users to filter scans using:\n\nToday\nLast 7 Days\nLast 30 Days\nCustom Date Range\nOptional Enhancement:\n\nUse a calendar date picker component.\n\n3. Plugin Type Filter\n\nAllow filtering scans by plugin category/type:\n\nExamples:\n\nWeb Scan\nRecon\nContainer\nCloud\nAPI\nNetwork\n4. Status Filter\n\nAllow filtering scans by execution state:\n\nCompleted\nRunning\nFailed\nQueued\nCancelled\nSuggested UI Layout\n\nYou can place filters:\n\nAbove the scan history table\nOR\nInside a collapsible sidebar\nExample Layout\n------------------------------------------------\n| Severity ▼ | Status ▼ | Plugin ▼ | Date ▼ |\n------------------------------------------------\n| Scan History Table |\n------------------------------------------------\nTechnical Suggestions\nFrontend Stack\nReact 18\nTypeScript\nVite\nSuggested Components\n\nYou can create reusable filter components:\n\ncomponents/\n ├── filters/\n │ ├── SeverityFilter.tsx\n │ ├── StatusFilter.tsx\n │ ├── DateFilter.tsx\n │ └── PluginFilter.tsx\nSuggested State Management\n\nUsing React state:\n\nconst [severity, setSeverity] = useState(\"\");\nconst [status, setStatus] = useState(\"\");\nconst [pluginType, setPluginType] = useState(\"\");\n\nThen filter scan data dynamically:\n\nconst filteredScans = scans.filter((scan) =\u003e {\n return (\n (severity ? scan.severity === severity : true) \u0026\u0026\n (status ? scan.status === status : true)\n );\n});\nOptional Enhancements\nSearch Bar\n\nAdd keyword search:\n\nScan ID\nTarget URL\nPlugin name", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/372/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/372/timeline", + "performed_via_github_app": null, + "state_reason": null, + "pinned_comment": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/371", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/371/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/371/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/371/events", + "html_url": "https://github.com/utksh1/SecuScan/issues/371", + "id": 4538591643, + "node_id": "I_kwDOQMZG5c8AAAABDoVpmw", + "number": 371, + "title": "[DOCS] Add Windows contributor development and troubleshooting guide", + "user": { + "login": "byebyewe86-alt", + "id": 248188971, + "node_id": "U_kgDODssQKw", + "avatar_url": "https://avatars.githubusercontent.com/u/248188971?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/byebyewe86-alt", + "html_url": "https://github.com/byebyewe86-alt", + "followers_url": "https://api.github.com/users/byebyewe86-alt/followers", + "following_url": "https://api.github.com/users/byebyewe86-alt/following{/other_user}", + "gists_url": "https://api.github.com/users/byebyewe86-alt/gists{/gist_id}", + "starred_url": "https://api.github.com/users/byebyewe86-alt/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/byebyewe86-alt/subscriptions", + "organizations_url": "https://api.github.com/users/byebyewe86-alt/orgs", + "repos_url": "https://api.github.com/users/byebyewe86-alt/repos", + "events_url": "https://api.github.com/users/byebyewe86-alt/events{/privacy}", + "received_events_url": "https://api.github.com/users/byebyewe86-alt/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-28T07:34:29Z", + "updated_at": "2026-05-28T07:36:29Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "issue_dependencies_summary": { + "blocked_by": 0, + "total_blocked_by": 0, + "blocking": 0, + "total_blocking": 0 + }, + "body": "## Summary\n\nAdd a dedicated Windows contributor setup and troubleshooting guide to help new contributors configure Python, Node.js, virtual environments, Git, and Docker more easily on Windows systems.\n\n## Problem\n\nCurrent setup instructions are mostly generic and may not fully address Windows-specific issues. New contributors on Windows can face confusion with PowerShell commands, virtual environment activation, execution policies, Git workflows, Docker setup, and Python/Node.js configuration.\n\n## Where to update\n\nList the files or sections that likely need edits:\n\n* Suggested files:\n\n * `docs/windows_contributor_guide.md`\n * `CONTRIBUTING.md`\n * `README.md`\n\n* Related sections or headings:\n\n * Local setup\n * Development environment setup\n * Contributor onboarding\n * Troubleshooting\n\n## Expected outcome\n\nA contributor should be able to follow the Windows-specific guide to:\n\n* install and verify Python 3.11+\n* create and activate a virtual environment\n* install Node.js dependencies\n* configure Docker Desktop if needed\n* run backend and frontend locally\n* resolve common PowerShell and Git issues\n\n## Acceptance criteria\n\n* [ ] The affected docs are updated\n* [ ] Steps, commands, or links are accurate\n* [ ] Cross-links are added where they reduce contributor confusion\n* [ ] Contributor can follow the updated instructions without extra clarification\n\n## Verification\n\nMaintainers can verify by following the documented Windows setup steps on a fresh Windows environment and confirming that:\n\n* backend starts successfully\n* frontend starts successfully\n* commands work correctly in PowerShell\n* troubleshooting steps resolve common setup issues\n\n## Additional context\n\nPotential troubleshooting topics:\n\n* PowerShell execution policy errors\n* virtualenv activation on Windows\n* Python PATH issues\n* npm/node version verification\n* Git rebase conflict basics\n* Docker Desktop setup notes\n", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/371/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/371/timeline", + "performed_via_github_app": null, + "state_reason": null, + "pinned_comment": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/370", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/370/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/370/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/370/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/370", + "id": 4538373616, + "node_id": "PR_kwDOQMZG5c7gIIUg", + "number": 370, + "title": "feat: add versioned plugin metadata schema with migration helpers (#227)", + "user": { + "login": "Bhavy12-cell", + "id": 185819124, + "node_id": "U_kgDOCxNf9A", + "avatar_url": "https://avatars.githubusercontent.com/u/185819124?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Bhavy12-cell", + "html_url": "https://github.com/Bhavy12-cell", + "followers_url": "https://api.github.com/users/Bhavy12-cell/followers", + "following_url": "https://api.github.com/users/Bhavy12-cell/following{/other_user}", + "gists_url": "https://api.github.com/users/Bhavy12-cell/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Bhavy12-cell/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Bhavy12-cell/subscriptions", + "organizations_url": "https://api.github.com/users/Bhavy12-cell/orgs", + "repos_url": "https://api.github.com/users/Bhavy12-cell/repos", + "events_url": "https://api.github.com/users/Bhavy12-cell/events{/privacy}", + "received_events_url": "https://api.github.com/users/Bhavy12-cell/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-28T06:56:50Z", + "updated_at": "2026-05-28T06:56:50Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/370", + "html_url": "https://github.com/utksh1/SecuScan/pull/370", + "diff_url": "https://github.com/utksh1/SecuScan/pull/370.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/370.patch", + "merged_at": null + }, + "body": "Closes #227\r\n\r\nAdded versioned plugin metadata schema with migration helpers.\r\n\r\nChanges:\r\n- Added `backend/secuscan/plugin_schema.py`:\r\n - `detect_schema_version()` — detects version or defaults to v1\r\n - `validate_by_version()` — runs version-specific validation\r\n - `migrate_v1_to_v2()` — adds presets, learning, dependencies blocks\r\n - `migrate_to_latest()` — chains migrations to latest version\r\n - `load_and_migrate()` — loads and migrates a metadata.json file\r\n- Updated `plugin_validator.py`:\r\n - Added `_check_schema_version()` to warn on missing/invalid version\r\n- Added `test_plugin_schema.py`:\r\n - Tests version detection, v1/v2 validation, migration helpers\r\n - Verifies migrations produce valid v2 schema\r\n - Ensures original metadata is never mutated\r\n", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/370/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/370/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/369", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/369/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/369/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/369/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/369", + "id": 4538358042, + "node_id": "PR_kwDOQMZG5c7gIFIg", + "number": 369, + "title": "security(executor): isolate custom plugin parsers in a constrained subprocess", + "user": { + "login": "advikdivekar", + "id": 178076523, + "node_id": "U_kgDOCp07aw", + "avatar_url": "https://avatars.githubusercontent.com/u/178076523?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/advikdivekar", + "html_url": "https://github.com/advikdivekar", + "followers_url": "https://api.github.com/users/advikdivekar/followers", + "following_url": "https://api.github.com/users/advikdivekar/following{/other_user}", + "gists_url": "https://api.github.com/users/advikdivekar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/advikdivekar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/advikdivekar/subscriptions", + "organizations_url": "https://api.github.com/users/advikdivekar/orgs", + "repos_url": "https://api.github.com/users/advikdivekar/repos", + "events_url": "https://api.github.com/users/advikdivekar/events{/privacy}", + "received_events_url": "https://api.github.com/users/advikdivekar/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-28T06:54:10Z", + "updated_at": "2026-05-28T07:09:35Z", + "closed_at": null, + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/369", + "html_url": "https://github.com/utksh1/SecuScan/pull/369", + "diff_url": "https://github.com/utksh1/SecuScan/pull/369.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/369.patch", + "merged_at": null + }, + "body": "**What is the problem or what is the feature**\nSecuScan executed custom plugin `parser.py` files directly inside the backend process using `importlib.util.exec_module`. This meant any malicious or buggy parser could: read in-memory secrets and database handles; allocate unlimited memory and crash the server; run indefinitely with no timeout; access every environment variable including `SECUSCAN_VAULT_KEY` and API credentials. A single compromised plugin could fully compromise the backend process.\n\n**What was changed**\n\n| File | Change |\n|---|---|\n| `backend/secuscan/parser_sandbox.py` | New module: `run_parser_in_sandbox()` spawns a fresh Python subprocess, feeds scanner output via stdin as a JSON envelope, reads the parse result from stdout, enforces wall-clock timeout and stdout size cap, strips all secrets from the child environment |\n| `backend/secuscan/executor.py` | `_parse_results()`: replaced `importlib.exec_module` block with `run_parser_in_sandbox()`; catches `ParserSandboxError` and falls through to built-in parsers |\n| `backend/secuscan/config.py` | Added `parser_sandbox_timeout_seconds` (default 30) and `parser_sandbox_max_output_bytes` (default 8 MB) |\n| `.env.example` | Documented both new settings |\n| `testing/backend/unit/test_parser_sandbox.py` | 25 unit tests |\n\n**Why this approach**\nUsing `subprocess.run` with a self-contained bootstrap script (passed via `-c`) is the lightest-weight isolation available without requiring Docker or OS-level sandboxing tools. The child has no shared memory with the parent, cannot import backend modules, and runs with a minimal sanitised environment. Communication via stdin/stdout with JSON encoding avoids any shared file descriptor risks and makes the protocol trivially testable.\n\n**How to test**\n1. Run a plugin that has a `parser.py` — the parse step will run in a child process (visible in logs as `Parser sandbox completed successfully for plugin \u0027...\u0027`)\n2. Add an infinite loop to any parser.py, submit a scan — it should fail with a sandbox timeout error after 30s rather than hanging the backend\n3. Set `SECUSCAN_VAULT_KEY=mysecret`, add `import os; return {\"key\": os.environ.get(\"SECUSCAN_VAULT_KEY\")}` to a parser — the result will show `NOT_FOUND`\n4. Run `python3.11 -m pytest testing/backend/unit/test_parser_sandbox.py -v` — all 25 tests pass\n\n**Edge cases covered**\n- Parser times out: `ParserSandboxError` with reason, falls through to built-in parser\n- Parser crashes or calls `sys.exit`: `ParserSandboxError` with exit code and stderr excerpt\n- Parser has a syntax error: caught at import time inside child\n- Parser missing `parse()` function: child exits non-zero\n- Parser returns a list instead of dict: wrapped in `{\"findings\": [...]}`\n- Parser returns `None` or a non-dict/list: `ParserSandboxError`\n- Parser produces oversized output: `ParserSandboxError` before JSON decode\n- Empty stdout from child: treated as empty result\n- Environment secrets (SECUSCAN_VAULT_KEY, arbitrary tokens) stripped from child env\n\n**Lines changed**\n578 insertions, 15 deletions across 5 files\n\n**Verification**\n- [x] Root cause fully resolved or feature completely implemented\n- [x] All edge cases and error paths handled\n- [x] No regressions introduced\n- [x] All existing tests pass\n- [x] New tests written covering this change (25 tests, all passing)\n- [x] Code matches project style and conventions throughout\n- [x] Total lines changed confirmed at 200 or above (578)\n- [x] Not a duplicate of any existing open or closed issue or PR\n\n**Labels:** `type:security` `type:refactor` `level:critical` `gssoc:approved`\n\nPlease review and merge this under GSSoC 2026.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/369/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/369/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/368", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/368/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/368/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/368/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/368", + "id": 4538340355, + "node_id": "PR_kwDOQMZG5c7gIBf-", + "number": 368, + "title": "feat(security): add per-plugin capability permissions and pre-execution enforcement", + "user": { + "login": "advikdivekar", + "id": 178076523, + "node_id": "U_kgDOCp07aw", + "avatar_url": "https://avatars.githubusercontent.com/u/178076523?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/advikdivekar", + "html_url": "https://github.com/advikdivekar", + "followers_url": "https://api.github.com/users/advikdivekar/followers", + "following_url": "https://api.github.com/users/advikdivekar/following{/other_user}", + "gists_url": "https://api.github.com/users/advikdivekar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/advikdivekar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/advikdivekar/subscriptions", + "organizations_url": "https://api.github.com/users/advikdivekar/orgs", + "repos_url": "https://api.github.com/users/advikdivekar/repos", + "events_url": "https://api.github.com/users/advikdivekar/events{/privacy}", + "received_events_url": "https://api.github.com/users/advikdivekar/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-28T06:51:10Z", + "updated_at": "2026-05-28T07:09:20Z", + "closed_at": null, + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/368", + "html_url": "https://github.com/utksh1/SecuScan/pull/368", + "diff_url": "https://github.com/utksh1/SecuScan/pull/368.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/368.patch", + "merged_at": null + }, + "body": "**What is the problem or what is the feature**\nSecuScan had no mechanism to restrict which capabilities individual plugins could exercise at runtime. An operator running in a restricted environment (air-gapped, no-exploit policy, no credential access) had no way to prevent plugins from performing network calls, accessing secrets, running exploits, or requiring Docker — the system would simply execute anything the plugin declared. This created a governance gap where the safety-level field existed but was purely advisory with no enforcement boundary.\n\n**What was changed**\n\n| File | Change |\n|---|---|\n| `backend/secuscan/capabilities.py` | New module: `Capability` enum (network, filesystem, docker, credentials, intrusive, exploit), `CapabilityEnforcer` class, `CapabilityDeniedError`, `validate_capability_list`, `effective_capabilities`, `build_enforcer_from_settings` |\n| `backend/secuscan/models.py` | Added optional `capabilities: List[str]` field to `PluginMetadata` |\n| `backend/secuscan/config.py` | Added `denied_capabilities: List[str]` setting driven by `SECUSCAN_DENIED_CAPABILITIES` env var |\n| `backend/secuscan/executor.py` | Imports and instantiates `CapabilityEnforcer` at startup; calls `enforcer.check()` before `build_command()`; catches `CapabilityDeniedError` with its own audit log event (`task_capability_denied`) and persists the task as failed |\n| `backend/secuscan/plugins.py` | Validates declared capabilities against the known set at plugin load time; exposes `capabilities` in `list_plugins` response |\n| `plugins/*/metadata.json` (60 files) | All plugins now declare an explicit `capabilities` list matching what they actually require |\n| `testing/backend/unit/test_capabilities.py` | 36 unit tests covering allow, deny, partial denial, legacy fallback, normalisation, error metadata, and settings integration |\n| `.env.example` | Documented `SECUSCAN_DENIED_CAPABILITIES` with usage examples |\n\n**Why this approach**\nThe check runs inside `execute_task` immediately before `build_command()`, so no subprocess is ever spawned for a denied plugin. Legacy plugins without a `capabilities` field automatically inherit an implied set from their `safety.level` (safe→network, intrusive→network+intrusive, exploit→network+intrusive+exploit), ensuring enforcement works across the full existing plugin catalogue without requiring every plugin to be updated before the feature is useful. The enforcer is constructed once at executor startup from settings, keeping the hot path allocation-free.\n\n**How to test**\n1. Start the backend with `SECUSCAN_DENIED_CAPABILITIES=exploit`\n2. Submit a task for any exploit-level plugin (e.g. `sqlmap`, `zap_scanner`)\n3. Task should immediately transition to `failed` with error message referencing the denied capability and a `task_capability_denied` audit log entry\n4. Submit a task for a safe plugin (e.g. `nmap`, `whois_lookup`) — it should run normally\n5. Run `python3.11 -m pytest testing/backend/unit/test_capabilities.py -v` — all 36 tests pass\n\n**Edge cases covered**\n- Plugin with no `capabilities` field falls back to safety-level implied set (backward compatible)\n- Partial denial: only the overlapping capabilities trigger a block, not the full list\n- Capability tokens in `SECUSCAN_DENIED_CAPABILITIES` are normalised (trimmed, lowercased)\n- Empty strings and whitespace-only entries in the denied list are silently ignored\n- Unknown capability tokens in plugin metadata fail plugin load, not task execution\n- `CapabilityDeniedError` is a `PermissionError` subclass carrying `plugin_id` and `denied_capabilities` for structured logging\n- Dedicated audit log event distinguishes capability denials from other task failures\n\n**Lines changed**\n824 insertions, 122 deletions across 67 files\n\n**Verification**\n- [x] Root cause fully resolved or feature completely implemented\n- [x] All edge cases and error paths handled\n- [x] No regressions introduced\n- [x] All existing tests pass\n- [x] New tests written covering this change (36 tests, all passing)\n- [x] Code matches project style and conventions throughout\n- [x] Total lines changed confirmed at 200 or above (824)\n- [x] Not a duplicate of any existing open or closed issue or PR\n\n**Labels:** `type:security` `type:feature` `level:advanced` `gssoc:approved`\n\nPlease review and merge this under GSSoC 2026.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/368/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/368/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/367", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/367/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/367/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/367/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/367", + "id": 4537982074, + "node_id": "PR_kwDOQMZG5c7gG3rW", + "number": 367, + "title": "feat(sandbox): add plugin execution sandbox with resource limits and …", + "user": { + "login": "ask-z4ch", + "id": 229467675, + "node_id": "U_kgDODa1mGw", + "avatar_url": "https://avatars.githubusercontent.com/u/229467675?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ask-z4ch", + "html_url": "https://github.com/ask-z4ch", + "followers_url": "https://api.github.com/users/ask-z4ch/followers", + "following_url": "https://api.github.com/users/ask-z4ch/following{/other_user}", + "gists_url": "https://api.github.com/users/ask-z4ch/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ask-z4ch/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ask-z4ch/subscriptions", + "organizations_url": "https://api.github.com/users/ask-z4ch/orgs", + "repos_url": "https://api.github.com/users/ask-z4ch/repos", + "events_url": "https://api.github.com/users/ask-z4ch/events{/privacy}", + "received_events_url": "https://api.github.com/users/ask-z4ch/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-28T05:34:41Z", + "updated_at": "2026-05-28T05:37:52Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/367", + "html_url": "https://github.com/utksh1/SecuScan/pull/367", + "diff_url": "https://github.com/utksh1/SecuScan/pull/367.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/367.patch", + "merged_at": null + }, + "body": "## Purpose \u0026 Rationale\r\nPlugin subprocesses (nmap, nikto, custom parsers) previously ran with no CPU, memory, or time constraints. A single runaway plugin could degrade the entire local instance — common in the learning and lab environments SecuScan targets. This change introduces a backend sandbox layer that enforces configurable resource limits with structured timeout escalation, surfacing violation reasons in the Task Details view.\r\n\r\n## Technical Resolution Details\r\nAdded `backend/secuscan/sandbox_executor.py` with `sandbox_execute()` wrapping every `asyncio.create_subprocess_exec` call with timeout enforcement (`asyncio.wait_for`), output byte-stream capping, `resource.setrlimit` via `preexec_fn` on Linux (with `platform.system()` guard for macOS/Windows), and a SIGTERM → 3s grace → SIGKILL escalation. Created `SandboxConfig` Pydantic model (`timeout_seconds=120`, `max_memory_mb=512`, `max_output_bytes=5MB`, `allow_network`) and `SandboxViolation` model in `models.py`. Extended `PluginMetadata` with an optional `sandbox` key for per-plugin overrides. Replaced raw subprocess calls in `executor.py` (`_execute_command`) with `sandbox_execute()`, mapping `SandboxViolation` reasons to distinct `terminated:timeout`, `terminated:memory_limit`, and `terminated:output_limit` task statuses. Updated `frontend/src/pages/TaskDetails.tsx` with a labeled badge, threshold tooltip, and sandbox violation error block. Documented the `sandbox` metadata key in `PLUGINS.md`. Closes #326.\r\n\r\n## Local Testing Execution\r\n1. `pytest testing/backend/unit/ testing/backend/test_sandbox_executor.py -v --asyncio-mode=strict --timeout=60` — 254 tests pass; 2 failures and 2 errors are pre-existing infrastructure issues (missing trio dependency, Windows PermissionError on temp dir cleanup) unrelated to this change\r\n2. All 11 new sandbox-specific tests pass: timeout path, output cap, SIGKILL escalation, truncation flagging, broadcast callback, platform guard, and `resolve_sandbox_config` merging\r\n3. Manual verification confirms `SandboxViolation(reason=\"timeout\")` produced for a sleep-30 process with 1s timeout, and `SandboxViolation(reason=\"output_limit\")` for a 5000-byte output capped at 100 bytes\r\n\r\n## Desired Review Feedback Type\r\nFocus on the `sandbox_execute` SIGTERM → SIGKILL escalation timing and the output-cap truncation logic — these paths are the most safety-critical.\r\n\r\n## Integrity \u0026 AI Usage Disclosure\r\nIn compliance with the GSSoC 2026 AI Conduct rules, I disclose that AI tools were used solely as learning and boilerplate aids. The final logic was fully reviewed, tested, and manually adapted to match human styling and clean-code design.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/367/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/367/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/366", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/366/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/366/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/366/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/366", + "id": 4537933400, + "node_id": "PR_kwDOQMZG5c7gGt_z", + "number": 366, + "title": "feat(frontend+backend): add scan history diff view", + "user": { + "login": "KBarathraj", + "id": 181458458, + "node_id": "U_kgDOCtDWGg", + "avatar_url": "https://avatars.githubusercontent.com/u/181458458?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/KBarathraj", + "html_url": "https://github.com/KBarathraj", + "followers_url": "https://api.github.com/users/KBarathraj/followers", + "following_url": "https://api.github.com/users/KBarathraj/following{/other_user}", + "gists_url": "https://api.github.com/users/KBarathraj/gists{/gist_id}", + "starred_url": "https://api.github.com/users/KBarathraj/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/KBarathraj/subscriptions", + "organizations_url": "https://api.github.com/users/KBarathraj/orgs", + "repos_url": "https://api.github.com/users/KBarathraj/repos", + "events_url": "https://api.github.com/users/KBarathraj/events{/privacy}", + "received_events_url": "https://api.github.com/users/KBarathraj/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-28T05:22:58Z", + "updated_at": "2026-05-28T05:22:58Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/366", + "html_url": "https://github.com/utksh1/SecuScan/pull/366", + "diff_url": "https://github.com/utksh1/SecuScan/pull/366.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/366.patch", + "merged_at": null + }, + "body": "## Description\r\nAdds a Compare Scans feature. Users can select two historical scans of the same target and see findings categorized as new, fixed, unchanged, or severity changed. Helps track remediation progress over time.\r\n\r\n## Related Issues\r\nCloses #336\r\n\r\nissue name : Add Scan History Diff View for Comparing Historical Scan Results\r\n\r\n## Type of Change\r\n- [ ] Bug fix (non-breaking change which fixes an issue)\r\n- [x] New feature (non-breaking change which adds functionality)\r\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\r\n- [ ] Documentation update\r\n\r\n## How Has This Been Tested?\r\n\r\n- `./testing/test_python.sh` :- 9/9 passing (9 new tests for diff_service)\r\n- `cd frontend \u0026\u0026 npm run test` :- passing\r\n- `cd frontend \u0026\u0026 npm run build` :- zero TypeScript errors, zero new warnings\r\n- Manual: seeded two scans, verified 200/400/404/422 responses via Swagger, verified all four diff categories render correctly in UI\r\n\r\n## Checklist\r\n- [x] My code follows the code style of this project.\r\n- [x] I have performed a self-review of my own code.\r\n- [x] I have commented my code, particularly in hard-to-understand areas.\r\n- [x] I have made corresponding changes to the documentation.\r\n- [x] My changes generate no new warnings.\r\n\r\n## Screenshots of changes\r\nBefore:\r\n\u003cimg width=\"2464\" height=\"1445\" alt=\"Screenshot 2026-05-28 100604\" src=\"https://github.com/user-attachments/assets/f7719741-3d3f-433f-950d-96debd993f64\" /\u003e\r\nAfter :\r\n\u003cimg width=\"2468\" height=\"1466\" alt=\"Screenshot 2026-05-28 100839\" src=\"https://github.com/user-attachments/assets/1189ad7f-458e-4cc1-aca7-e7fb81c5c89a\" /\u003e\r\n\r\nComparing scans:\r\n\u003cimg width=\"2211\" height=\"1395\" alt=\"Screenshot 2026-05-28 094613\" src=\"https://github.com/user-attachments/assets/543e35d2-ac14-47b2-ae13-307a5392aa9d\" /\u003e\r\n\r\n", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/366/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/366/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/365", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/365/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/365/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/365/events", + "html_url": "https://github.com/utksh1/SecuScan/issues/365", + "id": 4537895885, + "node_id": "I_kwDOQMZG5c8AAAABDnrLzQ", + "number": 365, + "title": "[BUG] Two test-infrastructure issues found while implementing sandbox executor (#326)", + "user": { + "login": "ask-z4ch", + "id": 229467675, + "node_id": "U_kgDODa1mGw", + "avatar_url": "https://avatars.githubusercontent.com/u/229467675?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ask-z4ch", + "html_url": "https://github.com/ask-z4ch", + "followers_url": "https://api.github.com/users/ask-z4ch/followers", + "following_url": "https://api.github.com/users/ask-z4ch/following{/other_user}", + "gists_url": "https://api.github.com/users/ask-z4ch/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ask-z4ch/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ask-z4ch/subscriptions", + "organizations_url": "https://api.github.com/users/ask-z4ch/orgs", + "repos_url": "https://api.github.com/users/ask-z4ch/repos", + "events_url": "https://api.github.com/users/ask-z4ch/events{/privacy}", + "received_events_url": "https://api.github.com/users/ask-z4ch/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-28T05:13:23Z", + "updated_at": "2026-05-28T05:13:43Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "issue_dependencies_summary": { + "blocked_by": 0, + "total_blocked_by": 0, + "blocking": 0, + "total_blocking": 0 + }, + "body": "## Summary\n\nTwo pre-existing test-infrastructure bugs were discovered while working on issue #326 (plugin execution sandbox):\n\n1. **Missing `trio` dependency** — `test_cli.py` uses `@pytest.mark.anyio` which parametrizes over `[\"asyncio\", \"trio\"]` by default, but `trio` is not in `requirements-dev.txt`.\n2. **Windows `PermissionError` on temp dir cleanup** — `conftest.py`\u0027s `TemporaryDirectory` can\u0027t delete the SQLite DB because Windows holds the file handle past test teardown.\n\n## Why this matters\n\nNeither issue is caused by the sandbox work — they are pre-existing and block a clean test run. The trio failures show up on every OS; the PermissionError shows up on every Windows run. Together they create red ERROR/FAILED output that masks genuine regressions introduced by new features.\n\n## Bug 1: Missing trio dependency\n\n### Reproduction steps\n1. `pip install -r backend/requirements-dev.txt`\n2. `pytest testing/backend/unit/test_cli.py -v`\n\n### Actual behavior\n```\nFAILED test_run_scan_plugin_not_found[trio]\nFAILED test_run_scan_successful_execution[trio]\n```\nRoot cause: `ModuleNotFoundError: No module named \u0027trio\u0027` when anyio loads the `_trio` backend.\n\n\n## Bug 2: Windows PermissionError on TemporaryDirectory cleanup\n\n### Reproduction steps\n1. Run `pytest testing/backend/unit/` on Windows\n\n### Actual behavior\n```\nERROR at teardown of test_execute_task_sets_cancelled_status_in_db\nPermissionError: [WinError 32] The process cannot access the file because it is being used by another process: \u0027.../test_secuscan.db\u0027\n```\n\n## Scope\n- `backend/requirements-dev.txt` (add trio or pin backends)\n- `pyproject.toml` (optional anyio config)\n- `testing/backend/conftest.py` (ignore_cleanup_errors)\n\n## Environment\n- OS: Bug 1 = all OS; Bug 2 = Windows only\n- Python Version: 3.13\n- Test runner: pytest with anyio plugin\n\n## Definition of done\n- [ ] `pytest testing/backend/unit/test_cli.py` passes without trio-related failures\n- [ ] No `PermissionError` during temporary directory cleanup on Windows\n- [ ] Full unit suite `pytest testing/backend/unit/` remains green\n\n## Additional context\nBoth bugs were surfaced while validating that existing tests remain green after the sandbox executor implementation in PR #326. They are not caused by that feature - they pre-date it and occur on a clean `main` branch checkout.\n```", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/365/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/365/timeline", + "performed_via_github_app": null, + "state_reason": null, + "pinned_comment": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/364", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/364/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/364/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/364/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/364", + "id": 4535805121, + "node_id": "PR_kwDOQMZG5c7f_zrz", + "number": 364, + "title": "feat(docker): harden backend and frontend images with non-root user and Trivy CVE scanning", + "user": { + "login": "Aditi-24-05", + "id": 127961851, + "node_id": "U_kgDOB6CK-w", + "avatar_url": "https://avatars.githubusercontent.com/u/127961851?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Aditi-24-05", + "html_url": "https://github.com/Aditi-24-05", + "followers_url": "https://api.github.com/users/Aditi-24-05/followers", + "following_url": "https://api.github.com/users/Aditi-24-05/following{/other_user}", + "gists_url": "https://api.github.com/users/Aditi-24-05/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Aditi-24-05/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Aditi-24-05/subscriptions", + "organizations_url": "https://api.github.com/users/Aditi-24-05/orgs", + "repos_url": "https://api.github.com/users/Aditi-24-05/repos", + "events_url": "https://api.github.com/users/Aditi-24-05/events{/privacy}", + "received_events_url": "https://api.github.com/users/Aditi-24-05/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T21:17:28Z", + "updated_at": "2026-05-27T21:29:12Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/364", + "html_url": "https://github.com/utksh1/SecuScan/pull/364", + "diff_url": "https://github.com/utksh1/SecuScan/pull/364.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/364.patch", + "merged_at": null + }, + "body": "## Description\r\nHardens the Docker setup for both backend and frontend containers as outlined in issue #245. \r\n\r\nChanges include running containers as non-root users, pinning base images to specific versions, patching a critical OpenSSL CVE (CVE-2026-31789), adding a GitHub Actions workflow for automated Trivy vulnerability scanning, and documenting a base image update policy.\r\n\r\nAlso fixes two pre-existing bugs discovered during testing:\r\n- `CMD` pointed to `secuscan.api` which does not exist, corrected to `secuscan.main`\r\n- `COPY plugins` in the backend Dockerfile referenced a path outside the build context, removed since plugins are injected via volume mount at runtime\r\n\r\n## Related Issues\r\nCloses #245\r\n\r\n## Type of Change\r\n- [x] Bug fix (non-breaking change which fixes an issue)\r\n- [x] New feature (non-breaking change which adds functionality)\r\n- [x] Documentation update\r\n\r\n## How Has This Been Tested?\r\n\r\n**Integration tests**: new pytest suite covering all hardening requirements:\r\nAll tests passed:\r\n- `TestNonRootUser`- backend runs as `secuscan` (UID 1001), frontend as `nginx` (UID 101)\r\n- `TestSUIDFiles` - no unexpected SUID/SGID binaries in either image\r\n- `TestDockerfileStructure` - USER instruction, pinned base, HEALTHCHECK, apt cleanup all verified\r\n- `TestTrivyCVEGate` - zero unfixed CRITICAL CVEs in both images after patching OpenSSL\r\n\r\n**Manual smoke tests:**\r\n```bash\r\ndocker run --rm secuscan-backend:test id\r\n# uid=1001(secuscan) gid=1001(secuscan)\r\n\r\ndocker run --rm secuscan-frontend:test id \r\n# uid=101(nginx) gid=101(nginx)\r\n\r\ntrivy image --severity CRITICAL --ignore-unfixed secuscan-backend:test\r\ntrivy image --severity CRITICAL --ignore-unfixed secuscan-frontend:test\r\n# 0 vulnerabilities found in both\r\n```\r\n\r\n**Full stack:**\r\n```bash\r\ndocker compose up --build\r\n# GET /api/v1/health HTTP/1.1\" 200 OK healthcheck passing\r\n```\r\n\r\n## Checklist\r\n- [x] My code follows the code style of this project.\r\n- [x] I have performed a self-review of my own code.\r\n- [x] I have commented my code, particularly in hard-to-understand areas.\r\n- [x] I have made corresponding changes to the documentation.\r\n- [x] My changes generate no new warnings.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/364/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/364/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/363", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/363/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/363/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/363/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/363", + "id": 4535199667, + "node_id": "PR_kwDOQMZG5c7f92ra", + "number": 363, + "title": "fix: redact finding description, remediation and proof before DB insert", + "user": { + "login": "Srejoye", + "id": 177326090, + "node_id": "U_kgDOCpHICg", + "avatar_url": "https://avatars.githubusercontent.com/u/177326090?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Srejoye", + "html_url": "https://github.com/Srejoye", + "followers_url": "https://api.github.com/users/Srejoye/followers", + "following_url": "https://api.github.com/users/Srejoye/following{/other_user}", + "gists_url": "https://api.github.com/users/Srejoye/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Srejoye/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Srejoye/subscriptions", + "organizations_url": "https://api.github.com/users/Srejoye/orgs", + "repos_url": "https://api.github.com/users/Srejoye/repos", + "events_url": "https://api.github.com/users/Srejoye/events{/privacy}", + "received_events_url": "https://api.github.com/users/Srejoye/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T19:37:15Z", + "updated_at": "2026-05-27T19:39:10Z", + "closed_at": null, + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/363", + "html_url": "https://github.com/utksh1/SecuScan/pull/363", + "diff_url": "https://github.com/utksh1/SecuScan/pull/363.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/363.patch", + "merged_at": null + }, + "body": "## Problem\r\n\r\n`executor.py` called `redact(output)` on the raw CLI text file (line 302) but the parsed findings written to the `findings` table were never redacted:\r\n\r\n```python\r\n# _upsert_findings_and_report — before this fix\r\nfinding[\"description\"], # ← no redact()\r\nfinding.get(\"remediation\", \"\"), # ← no redact()\r\nfinding.get(\"proof\"), # ← no redact()\r\n```\r\n\r\n`reporting.py::_normalize_finding()` does call `redact()` — but only at PDF/HTML export time. The DB row, the `/api/v1/findings` JSON API, SARIF exports, CSV exports, and any direct DB reads all returned raw unredacted content.\r\n\r\n`redact_dict()` already existed in `redaction.py` for exactly this purpose — it was just never called before storage.\r\n\r\n## Fix\r\n\r\n**`backend/secuscan/executor.py`**\r\n\r\n1. Add `redact_dict` to the import from `.redaction`\r\n2. Call `finding = redact_dict(finding)` before the INSERT in both `_upsert_findings_and_report` and `_upsert_findings_and_report_from_scanner`\r\n\r\n```python\r\n# after fix — both INSERT loops\r\nfor finding in findings_data:\r\n finding = redact_dict(finding) # redact secrets before storage\r\n ...INSERT...\r\n```\r\n\r\n## Tests\r\n\r\nNew file `testing/backend/unit/test_findings_redaction.py` — 6 tests:\r\n\r\n| Test | What it verifies |\r\n|---|---|\r\n| `test_redact_dict_redacts_aws_key_in_description` | Secret in description/remediation is replaced with `[REDACTED]` |\r\n| `test_redact_dict_leaves_clean_finding_unchanged` | Clean findings pass through unmodified |\r\n| `test_redact_dict_handles_nested_metadata` | Nested metadata dict walked recursively; non-strings untouched |\r\n| `test_redact_dict_handles_none_proof` | `None` proof does not raise |\r\n| `test_redact_dict_handles_missing_keys_gracefully` | Minimal findings with no description/remediation work fine |\r\n| `test_upsert_findings_redacts_description_before_insert` | **Integration**: DB row verified to not contain raw secret after INSERT |\r\n\r\n## No breaking changes\r\n`redact_dict` uses existing infrastructure. Non-secret content passes through unchanged. No API or schema changes.\r\n\r\nCloses #306", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/363/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/363/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/362", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/362/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/362/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/362/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/362", + "id": 4535183783, + "node_id": "PR_kwDOQMZG5c7f9zVE", + "number": 362, + "title": "Docs/architecture guide", + "user": { + "login": "piyush10854", + "id": 225612438, + "node_id": "U_kgDODXKSlg", + "avatar_url": "https://avatars.githubusercontent.com/u/225612438?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/piyush10854", + "html_url": "https://github.com/piyush10854", + "followers_url": "https://api.github.com/users/piyush10854/followers", + "following_url": "https://api.github.com/users/piyush10854/following{/other_user}", + "gists_url": "https://api.github.com/users/piyush10854/gists{/gist_id}", + "starred_url": "https://api.github.com/users/piyush10854/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/piyush10854/subscriptions", + "organizations_url": "https://api.github.com/users/piyush10854/orgs", + "repos_url": "https://api.github.com/users/piyush10854/repos", + "events_url": "https://api.github.com/users/piyush10854/events{/privacy}", + "received_events_url": "https://api.github.com/users/piyush10854/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-27T19:34:30Z", + "updated_at": "2026-05-27T19:35:34Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/362", + "html_url": "https://github.com/utksh1/SecuScan/pull/362", + "diff_url": "https://github.com/utksh1/SecuScan/pull/362.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/362.patch", + "merged_at": null + }, + "body": "# Pull Request\r\n\r\n## Description\r\n\r\nThis PR improves contributor-facing documentation for SecuScan by adding:\r\n\r\n* Architecture documentation\r\n* Plugin development guide\r\n* API usage guide\r\n* Contributor onboarding guide\r\n\r\nThe goal is to improve the onboarding experience for new contributors and GSSOC participants.\r\n\r\n## Changes Made\r\n\r\n* Added `docs/ARCHITECTURE.md`\r\n* Added `docs/PLUGIN_DEVELOPMENT.md`\r\n* Added `docs/API_GUIDE.md`\r\n* Added `docs/CONTRIBUTOR_ONBOARDING.md`\r\n* Updated README documentation links\r\n\r\nCloses #ISSUE_NUMBER\r\n\r\n## Type of Change\r\n\r\n* [x] Documentation update\r\n\r\n## Checklist\r\n\r\n* [x] Added contributor-focused documentation\r\n* [x] Updated README links\r\n* [x] No unrelated files modified\r\n* [x] Beginner-friendly documentation structure\r\n\r\nCLOSES #358 ", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/362/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/362/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/361", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/361/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/361/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/361/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/361", + "id": 4535034812, + "node_id": "PR_kwDOQMZG5c7f9UR2", + "number": 361, + "title": "fix: treat naive DB datetime as UTC in _should_run to prevent TypeError crash", + "user": { + "login": "Srejoye", + "id": 177326090, + "node_id": "U_kgDOCpHICg", + "avatar_url": "https://avatars.githubusercontent.com/u/177326090?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Srejoye", + "html_url": "https://github.com/Srejoye", + "followers_url": "https://api.github.com/users/Srejoye/followers", + "following_url": "https://api.github.com/users/Srejoye/following{/other_user}", + "gists_url": "https://api.github.com/users/Srejoye/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Srejoye/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Srejoye/subscriptions", + "organizations_url": "https://api.github.com/users/Srejoye/orgs", + "repos_url": "https://api.github.com/users/Srejoye/repos", + "events_url": "https://api.github.com/users/Srejoye/events{/privacy}", + "received_events_url": "https://api.github.com/users/Srejoye/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T19:10:26Z", + "updated_at": "2026-05-27T19:10:26Z", + "closed_at": null, + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/361", + "html_url": "https://github.com/utksh1/SecuScan/pull/361", + "diff_url": "https://github.com/utksh1/SecuScan/pull/361.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/361.patch", + "merged_at": null + }, + "body": "## Problem\r\n\r\n`WorkflowScheduler._should_run()` crashed with `TypeError` on every scheduler tick after the first workflow run.\r\n\r\n```python\r\n# workflows.py — broken\r\nlast = datetime.fromisoformat(last_run_at.replace(\"Z\", \"+00:00\"))\r\nelapsed = (now - last).total_seconds() # TypeError if last is naive\r\n```\r\n\r\n`now` is `datetime.now(timezone.utc)` — timezone-aware.\r\n`last_run_at` comes from `datetime(\u0027now\u0027)` written by SQLite, which produces `\"2026-05-25 08:02:28\"` — no `Z`, no `+00:00`.\r\nThe `.replace(\"Z\", \"+00:00\")` does nothing for this format.\r\n`fromisoformat()` returns a naive datetime.\r\nPython 3.8+ raises `TypeError: can\u0027t subtract offset-naive and offset-aware datetimes`.\r\n\r\n**Reproducer:**\r\n```python\r\nfrom datetime import datetime, timezone\r\nlast = datetime.fromisoformat(\"2026-05-25 08:02:28\") # naive\r\nnow = datetime.now(timezone.utc) # aware\r\n(now - last).total_seconds() # TypeError\r\n```\r\n\r\n## Fix\r\n\r\nAfter parsing `last`, treat it as UTC if it has no timezone info:\r\n\r\n```python\r\nlast = datetime.fromisoformat(last_run_at.replace(\"Z\", \"+00:00\"))\r\nif last.tzinfo is None:\r\n last = last.replace(tzinfo=timezone.utc)\r\nelapsed = (now - last).total_seconds()\r\n```\r\n\r\nAll timestamps written by the scheduler use `datetime(\u0027now\u0027)` which is UTC — treating naive values as UTC is correct.\r\n\r\n## Changes\r\n\r\n- `backend/secuscan/workflows.py` — 2 lines added to `_should_run()`\r\n- `testing/backend/unit/test_workflows_scheduler.py` — new file, 8 tests\r\n\r\n## Tests\r\n\r\n| Test | Covers |\r\n|---|---|\r\n| `test_should_run_when_no_last_run` | `None` last_run_at → always run |\r\n| `test_should_run_when_elapsed_exceeds_schedule` | elapsed \u003e schedule → run |\r\n| `test_should_not_run_when_elapsed_below_schedule` | elapsed \u003c schedule → skip |\r\n| `test_should_run_at_exact_boundary` | exactly at boundary → run |\r\n| `test_sqlite_naive_datetime_does_not_raise` | **regression** — SQLite format must not raise TypeError |\r\n| `test_z_suffix_still_works` | Z-suffix strings unaffected |\r\n| `test_offset_aware_iso_string_still_works` | +00:00 strings unaffected |\r\n| `test_empty_string_treated_as_no_last_run` | empty string → run |\r\n\r\n## No breaking changes\r\nTwo lines added, nothing removed. No API or schema changes.\r\n\r\nCloses #304", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/361/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/361/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/360", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/360/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/360/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/360/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/360", + "id": 4534892603, + "node_id": "PR_kwDOQMZG5c7f8257", + "number": 360, + "title": "feat(notifications): implement high-risk finding alerting workflows and configuration UI", + "user": { + "login": "YerraguntaAjayKumar", + "id": 212876416, + "node_id": "U_kgDODLA8gA", + "avatar_url": "https://avatars.githubusercontent.com/u/212876416?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/YerraguntaAjayKumar", + "html_url": "https://github.com/YerraguntaAjayKumar", + "followers_url": "https://api.github.com/users/YerraguntaAjayKumar/followers", + "following_url": "https://api.github.com/users/YerraguntaAjayKumar/following{/other_user}", + "gists_url": "https://api.github.com/users/YerraguntaAjayKumar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/YerraguntaAjayKumar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/YerraguntaAjayKumar/subscriptions", + "organizations_url": "https://api.github.com/users/YerraguntaAjayKumar/orgs", + "repos_url": "https://api.github.com/users/YerraguntaAjayKumar/repos", + "events_url": "https://api.github.com/users/YerraguntaAjayKumar/events{/privacy}", + "received_events_url": "https://api.github.com/users/YerraguntaAjayKumar/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T18:49:13Z", + "updated_at": "2026-05-27T19:30:49Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/360", + "html_url": "https://github.com/utksh1/SecuScan/pull/360", + "diff_url": "https://github.com/utksh1/SecuScan/pull/360.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/360.patch", + "merged_at": null + }, + "body": "## Description\r\nThis PR implements production-grade notification channels for high-risk and critical findings inside SecuScan as requested in #254. \r\n\r\n### Key Contributions:\r\n- **Database Engine (`backend/secuscan/database.py`, `models.py`)**: Added SQLite schema migrations for `notification_rules` and `notification_history` tracking along with structured Pydantic schemas.\r\n- **Service Layer (`backend/secuscan/notifications.py`, `executor.py`)**: Built an isolated background notification service. Implemented a 1-hour duplicate cooldown window for deduplication, payload routing (Webhook/Email placeholders), and strict metadata data redaction to isolate raw tokens or internal code snippets.\r\n- **Frontend Panel (`frontend/src/components/NotificationSettings.tsx`, `pages/Settings.tsx`)**: Created a clean, responsive neo-brutalist alerting configuration interface tied directly into a centralized `api.ts` request pattern to eliminate lockfile or formatting churn.\r\n\r\n## Related Issues\r\nCloses #254\r\n\r\n## Type of Change\r\n- [x] New feature (non-breaking change which adds functionality)\r\n\r\n## How Has This Been Tested?\r\nImplemented a comprehensive unit test suite inside `testing/backend/unit/test_notifications.py` ensuring successful route verification, failed network handling delivery states, data redaction assertions, and 3600-second token deduplication logic.\r\n\r\n## Checklist\r\n- [x] My code follows the code style of this project.\r\n- [x] I have performed a self-review of my own code.\r\n- [x] I have commented my code, particularly in hard-to-understand areas.\r\n- [x] My changes generate no new warnings.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/360/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/360/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/359", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/359/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/359/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/359/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/359", + "id": 4534480633, + "node_id": "PR_kwDOQMZG5c7f7gqa", + "number": 359, + "title": "Add keyboard shortAdd keyboard shortcut tests for useShortcuts hookcut hook tests", + "user": { + "login": "Vishvadharman", + "id": 67373113, + "node_id": "MDQ6VXNlcjY3MzczMTEz", + "avatar_url": "https://avatars.githubusercontent.com/u/67373113?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Vishvadharman", + "html_url": "https://github.com/Vishvadharman", + "followers_url": "https://api.github.com/users/Vishvadharman/followers", + "following_url": "https://api.github.com/users/Vishvadharman/following{/other_user}", + "gists_url": "https://api.github.com/users/Vishvadharman/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Vishvadharman/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Vishvadharman/subscriptions", + "organizations_url": "https://api.github.com/users/Vishvadharman/orgs", + "repos_url": "https://api.github.com/users/Vishvadharman/repos", + "events_url": "https://api.github.com/users/Vishvadharman/events{/privacy}", + "received_events_url": "https://api.github.com/users/Vishvadharman/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10957861656, + "node_id": "LA_kwDOQMZG5c8AAAACjSOzGA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:beginner", + "name": "level:beginner", + "color": "C7D2A4", + "default": false, + "description": "20 pts difficulty label for small beginner-friendly PRs" + }, + { + "id": 10957870191, + "node_id": "LA_kwDOQMZG5c8AAAACjSPUbw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/quality:clean", + "name": "quality:clean", + "color": "8FB3A7", + "default": false, + "description": "Contributor score x1.2; mentor +5 pts" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870938, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXWg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/gssoc:approved", + "name": "gssoc:approved", + "color": "7BA37B", + "default": false, + "description": "Admin validation: approved for GSSoC scoring" + }, + { + "id": 10979545003, + "node_id": "LA_kwDOQMZG5c8AAAACjm6Pqw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:frontend", + "name": "area:frontend", + "color": "6B8DB5", + "default": false, + "description": "Frontend React/UI work" + } + ], + "state": "closed", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T17:44:50Z", + "updated_at": "2026-05-27T18:27:37Z", + "closed_at": "2026-05-27T18:27:37Z", + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/359", + "html_url": "https://github.com/utksh1/SecuScan/pull/359", + "diff_url": "https://github.com/utksh1/SecuScan/pull/359.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/359.patch", + "merged_at": "2026-05-27T18:27:37Z" + }, + "body": "## Description\r\n\r\nAdded focused unit tests for the existing `useShortcuts` hook to validate keyboard shortcut behavior, cleanup, and editable input handling.\r\n\r\n## Related Issues\r\n\r\nCloses #89\r\n\r\n## Type of Change\r\n\r\n* [x] Bug fix (non-breaking change which fixes an issue)\r\n* [ ] New feature (non-breaking change which adds functionality)\r\n* [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\r\n* [ ] Documentation update\r\n\r\n## How Has This Been Tested?\r\n\r\n* Ran `npm run test` inside the frontend directory\r\n* Verified keyboard shortcut navigation behavior\r\n* Verified cleanup of keydown event listeners on unmount\r\n* Verified shortcuts are ignored while typing in editable inputs\r\n* Verified Escape key blurs focused input fields\r\n\r\n## Checklist\r\n\r\n* [x] My code follows the code style of this project.\r\n* [x] I have performed a self-review of my own code.\r\n* [x] I have commented my code, particularly in hard-to-understand areas.\r\n* [ ] I have made corresponding changes to the documentation.\r\n* [x] My changes generate no new warnings.\r\n", + "closed_by": { + "login": "utksh1", + "id": 183999732, + "node_id": "U_kgDOCvec9A", + "avatar_url": "https://avatars.githubusercontent.com/u/183999732?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/utksh1", + "html_url": "https://github.com/utksh1", + "followers_url": "https://api.github.com/users/utksh1/followers", + "following_url": "https://api.github.com/users/utksh1/following{/other_user}", + "gists_url": "https://api.github.com/users/utksh1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/utksh1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/utksh1/subscriptions", + "organizations_url": "https://api.github.com/users/utksh1/orgs", + "repos_url": "https://api.github.com/users/utksh1/repos", + "events_url": "https://api.github.com/users/utksh1/events{/privacy}", + "received_events_url": "https://api.github.com/users/utksh1/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/359/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/359/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/358", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/358/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/358/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/358/events", + "html_url": "https://github.com/utksh1/SecuScan/issues/358", + "id": 4534071103, + "node_id": "I_kwDOQMZG5c8AAAABDkBvPw", + "number": 358, + "title": "[DOCS] Documentation Overhaul for Architecture, API, Plugins, and Contributor Onboarding", + "user": { + "login": "piyush10854", + "id": 225612438, + "node_id": "U_kgDODXKSlg", + "avatar_url": "https://avatars.githubusercontent.com/u/225612438?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/piyush10854", + "html_url": "https://github.com/piyush10854", + "followers_url": "https://api.github.com/users/piyush10854/followers", + "following_url": "https://api.github.com/users/piyush10854/following{/other_user}", + "gists_url": "https://api.github.com/users/piyush10854/gists{/gist_id}", + "starred_url": "https://api.github.com/users/piyush10854/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/piyush10854/subscriptions", + "organizations_url": "https://api.github.com/users/piyush10854/orgs", + "repos_url": "https://api.github.com/users/piyush10854/repos", + "events_url": "https://api.github.com/users/piyush10854/events{/privacy}", + "received_events_url": "https://api.github.com/users/piyush10854/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + { + "login": "piyush10854", + "id": 225612438, + "node_id": "U_kgDODXKSlg", + "avatar_url": "https://avatars.githubusercontent.com/u/225612438?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/piyush10854", + "html_url": "https://github.com/piyush10854", + "followers_url": "https://api.github.com/users/piyush10854/followers", + "following_url": "https://api.github.com/users/piyush10854/following{/other_user}", + "gists_url": "https://api.github.com/users/piyush10854/gists{/gist_id}", + "starred_url": "https://api.github.com/users/piyush10854/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/piyush10854/subscriptions", + "organizations_url": "https://api.github.com/users/piyush10854/orgs", + "repos_url": "https://api.github.com/users/piyush10854/repos", + "events_url": "https://api.github.com/users/piyush10854/events{/privacy}", + "received_events_url": "https://api.github.com/users/piyush10854/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-27T16:40:51Z", + "updated_at": "2026-05-27T18:18:41Z", + "closed_at": null, + "assignee": { + "login": "piyush10854", + "id": 225612438, + "node_id": "U_kgDODXKSlg", + "avatar_url": "https://avatars.githubusercontent.com/u/225612438?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/piyush10854", + "html_url": "https://github.com/piyush10854", + "followers_url": "https://api.github.com/users/piyush10854/followers", + "following_url": "https://api.github.com/users/piyush10854/following{/other_user}", + "gists_url": "https://api.github.com/users/piyush10854/gists{/gist_id}", + "starred_url": "https://api.github.com/users/piyush10854/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/piyush10854/subscriptions", + "organizations_url": "https://api.github.com/users/piyush10854/orgs", + "repos_url": "https://api.github.com/users/piyush10854/repos", + "events_url": "https://api.github.com/users/piyush10854/events{/privacy}", + "received_events_url": "https://api.github.com/users/piyush10854/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "author_association": "NONE", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "issue_dependencies_summary": { + "blocked_by": 0, + "total_blocked_by": 0, + "blocking": 0, + "total_blocking": 0 + }, + "body": "## Description\n\nThe repository already contains strong setup and contribution documentation, but several advanced contributor-facing areas can still be improved to make onboarding and development workflows easier for new contributors and GSSOC participants.\n\nCurrently, there is limited centralised documentation for:\n\n- System architecture\n- Internal request/data flow\n- Plugin development workflow\n- API usage examples\n- Contributor onboarding guidance\n- Development best practices\n\nImproving these areas would significantly enhance contributor experience and long-term maintainability.\n\n---\n\n## Proposed Documentation Improvements\n\n### 1. Architecture Documentation\n\nAdd documentation explaining:\n\n- Frontend ↔ backend interaction\n- Scan orchestration flow\n- Plugin execution lifecycle\n- Result normalisation pipeline\n- Reporting workflow\n- High-level system design\n\nPossible addition:\n- `docs/ARCHITECTURE.md`\n\n---\n\n### 2. Plugin Development Guide\n\nCreate contributor-facing plugin documentation covering:\n\n- Plugin structure\n- Metadata format\n- Registration flow\n- Parser integration\n- Testing plugins locally\n- Plugin best practices\n\nPossible addition:\n- `docs/PLUGIN_DEVELOPMENT.md`\n\n---\n\n### 3. API Documentation Improvements\n\nImprove backend API documentation with:\n\n- Example request/response payloads\n- Error handling examples\n- Workflow execution examples\n- Sample curl requests\n- API usage notes for frontend contributors\n\n---\n\n### 4. Contributor Onboarding Guide\n\nCreate an onboarding guide for beginners and GSSOC contributors covering:\n\n- Repository walkthrough\n- Recommended contribution paths\n- Branch workflow\n- PR creation process\n- Testing expectations\n- Common contributor mistakes\n- Troubleshooting setup issues\n\nPossible addition:\n- `docs/CONTRIBUTOR_ONBOARDING.md`\n\n---\n\n## Benefits\n\n- Easier onboarding for contributors\n- Better project maintainability\n- Improved developer experience\n- Reduced repeated setup and workflow questions\n- Better understanding of internal architecture\n\n---\n\n## Expected Outcome\n\nThis issue aims to improve overall project documentation quality while making SecuScan more contributor-friendly for both beginners and intermediate open-source contributors.\n\nI would like to work on this issue under GSSOC 2026.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/358/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/358/timeline", + "performed_via_github_app": null, + "state_reason": null, + "pinned_comment": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/357", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/357/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/357/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/357/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/357", + "id": 4533370404, + "node_id": "PR_kwDOQMZG5c7f33_v", + "number": 357, + "title": "Feat/secure plugin onboarding", + "user": { + "login": "Avnithakur731-a", + "id": 196552247, + "node_id": "U_kgDOC7cmNw", + "avatar_url": "https://avatars.githubusercontent.com/u/196552247?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Avnithakur731-a", + "html_url": "https://github.com/Avnithakur731-a", + "followers_url": "https://api.github.com/users/Avnithakur731-a/followers", + "following_url": "https://api.github.com/users/Avnithakur731-a/following{/other_user}", + "gists_url": "https://api.github.com/users/Avnithakur731-a/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Avnithakur731-a/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Avnithakur731-a/subscriptions", + "organizations_url": "https://api.github.com/users/Avnithakur731-a/orgs", + "repos_url": "https://api.github.com/users/Avnithakur731-a/repos", + "events_url": "https://api.github.com/users/Avnithakur731-a/events{/privacy}", + "received_events_url": "https://api.github.com/users/Avnithakur731-a/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10924091185, + "node_id": "LA_kwDOQMZG5c8AAAACiyBnMQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:advanced", + "name": "level:advanced", + "color": "7BA37B", + "default": false, + "description": "55 pts difficulty label for advanced contributor PRs" + }, + { + "id": 10957870885, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXJQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:feature", + "name": "type:feature", + "color": "7FA6A0", + "default": false, + "description": "Feature work category bonus label" + }, + { + "id": 10957870982, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXhg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/gssoc:invalid", + "name": "gssoc:invalid", + "color": "B8A76A", + "default": false, + "description": "Admin validation: invalid for GSSoC scoring" + }, + { + "id": 10957871083, + "node_id": "LA_kwDOQMZG5c8AAAACjSPX6w", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/gssoc:ai-slop", + "name": "gssoc:ai-slop", + "color": "A75D5D", + "default": false, + "description": "Admin validation: low-quality AI-generated submission" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + }, + { + "id": 10979545075, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P8w", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:plugins", + "name": "area:plugins", + "color": "6E9A99", + "default": false, + "description": "Scanner plugin metadata, schemas, or plugin runtime work" + } + ], + "state": "closed", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 3, + "created_at": "2026-05-27T15:00:28Z", + "updated_at": "2026-05-28T04:35:35Z", + "closed_at": "2026-05-27T18:25:38Z", + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/357", + "html_url": "https://github.com/utksh1/SecuScan/pull/357", + "diff_url": "https://github.com/utksh1/SecuScan/pull/357.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/357.patch", + "merged_at": null + }, + "body": "Fixed plugin loader integrity validation and indentation issues in plugins.py.\r\n\r\nThis PR includes the following changes:\r\n\r\n- Fixed indentation issues in plugin loader to ensure proper Python structure\r\n- Corrected plugin integrity verification logic for checksum and signature validation\r\n- Improved compute_plugin_digest consistency for reliable plugin validation\r\n- Fixed function placement issues to avoid runtime errors and ensure correct execution flow\r\n- Ensured safe plugin loading and validation process across all plugin types\r\n- Verified backend compilation successfully using python -m compileall with no errors\r\n\r\nThese changes improve the stability and reliability of the plugin system and prevent runtime crashes caused by malformed structure or incorrect validation flow.", + "closed_by": { + "login": "utksh1", + "id": 183999732, + "node_id": "U_kgDOCvec9A", + "avatar_url": "https://avatars.githubusercontent.com/u/183999732?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/utksh1", + "html_url": "https://github.com/utksh1", + "followers_url": "https://api.github.com/users/utksh1/followers", + "following_url": "https://api.github.com/users/utksh1/following{/other_user}", + "gists_url": "https://api.github.com/users/utksh1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/utksh1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/utksh1/subscriptions", + "organizations_url": "https://api.github.com/users/utksh1/orgs", + "repos_url": "https://api.github.com/users/utksh1/repos", + "events_url": "https://api.github.com/users/utksh1/events{/privacy}", + "received_events_url": "https://api.github.com/users/utksh1/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/357/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/357/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/356", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/356/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/356/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/356/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/356", + "id": 4533224142, + "node_id": "PR_kwDOQMZG5c7f3ZOP", + "number": 356, + "title": "docs(security): add secure deployment and threat model guide", + "user": { + "login": "Dharshin1", + "id": 170921077, + "node_id": "U_kgDOCjAMdQ", + "avatar_url": "https://avatars.githubusercontent.com/u/170921077?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Dharshin1", + "html_url": "https://github.com/Dharshin1", + "followers_url": "https://api.github.com/users/Dharshin1/followers", + "following_url": "https://api.github.com/users/Dharshin1/following{/other_user}", + "gists_url": "https://api.github.com/users/Dharshin1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Dharshin1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Dharshin1/subscriptions", + "organizations_url": "https://api.github.com/users/Dharshin1/orgs", + "repos_url": "https://api.github.com/users/Dharshin1/repos", + "events_url": "https://api.github.com/users/Dharshin1/events{/privacy}", + "received_events_url": "https://api.github.com/users/Dharshin1/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10946283294, + "node_id": "LA_kwDOQMZG5c8AAAACjHMHHg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:intermediate", + "name": "level:intermediate", + "color": "D6B76B", + "default": false, + "description": "35 pts difficulty label for moderate contributor PRs" + }, + { + "id": 10957870308, + "node_id": "LA_kwDOQMZG5c8AAAACjSPU5A", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:docs", + "name": "type:docs", + "color": "6B8DB5", + "default": false, + "description": "Documentation work category bonus label" + }, + { + "id": 10979545110, + "node_id": "LA_kwDOQMZG5c8AAAACjm6QFg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:docs", + "name": "area:docs", + "color": "8CA0B3", + "default": false, + "description": "Documentation or contributor guide work" + }, + { + "id": 10979545212, + "node_id": "LA_kwDOQMZG5c8AAAACjm6QfA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:security", + "name": "area:security", + "color": "A75D5D", + "default": false, + "description": "Security-sensitive implementation or tests" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T14:40:29Z", + "updated_at": "2026-05-27T18:24:05Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/356", + "html_url": "https://github.com/utksh1/SecuScan/pull/356", + "diff_url": "https://github.com/utksh1/SecuScan/pull/356.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/356.patch", + "merged_at": null + }, + "body": "## Description\r\n\r\nAdds a production-oriented secure deployment and operator threat model guide for SecuScan.\r\n\r\nThis PR introduces:\r\n\r\n* Threat model documentation\r\n* Trust boundary analysis\r\n* Local-first deployment assumptions\r\n* Authentication and credential management guidance\r\n* Secret and vault handling recommendations\r\n* Plugin security considerations\r\n* Network exposure and reverse proxy guidance\r\n* Deployment profiles for:\r\n\r\n * Local development\r\n * LAN deployments\r\n * Containerized deployments\r\n* Operator hardening checklist\r\n\r\nThe README documentation index was also updated to include the new secure deployment guide.\r\n\r\n## Related Issues\r\n\r\nCloses #247\r\n\r\n## Type of Change\r\n\r\n* Bug fix (non-breaking change which fixes an issue)\r\n* New feature (non-breaking change which adds functionality)\r\n* Breaking change (fix or feature that would cause existing functionality to not work as expected)\r\n* Documentation update\r\n\r\n## How Has This Been Tested?\r\n\r\n* Verified markdown formatting and structure manually\r\n* Reviewed links and documentation navigation updates\r\n* Confirmed changes are scoped only to documentation files\r\n* Ensured no unrelated lockfile, UI, or formatting churn was introduced\r\n\r\n## Checklist\r\n\r\n* My code follows the code style of this project.\r\n* I have performed a self-review of my own code.\r\n* I have commented my code, particularly in hard-to-understand areas.\r\n* I have made corresponding changes to the documentation.\r\n* My changes generate no new warnings.\r\n", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/356/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/356/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/355", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/355/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/355/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/355/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/355", + "id": 4532265463, + "node_id": "PR_kwDOQMZG5c7f0PW8", + "number": 355, + "title": "Fix inconsistent light mode theming across dashboard components", + "user": { + "login": "ajawankarbhavanasri-sketch", + "id": 258382139, + "node_id": "U_kgDOD2aZOw", + "avatar_url": "https://avatars.githubusercontent.com/u/258382139?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ajawankarbhavanasri-sketch", + "html_url": "https://github.com/ajawankarbhavanasri-sketch", + "followers_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/followers", + "following_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/following{/other_user}", + "gists_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/subscriptions", + "organizations_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/orgs", + "repos_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/repos", + "events_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/events{/privacy}", + "received_events_url": "https://api.github.com/users/ajawankarbhavanasri-sketch/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10924091185, + "node_id": "LA_kwDOQMZG5c8AAAACiyBnMQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:advanced", + "name": "level:advanced", + "color": "7BA37B", + "default": false, + "description": "55 pts difficulty label for advanced contributor PRs" + }, + { + "id": 10957870647, + "node_id": "LA_kwDOQMZG5c8AAAACjSPWNw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:design", + "name": "type:design", + "color": "C9A0A8", + "default": false, + "description": "Design or UX work category bonus label" + }, + { + "id": 10957870982, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXhg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/gssoc:invalid", + "name": "gssoc:invalid", + "color": "B8A76A", + "default": false, + "description": "Admin validation: invalid for GSSoC scoring" + }, + { + "id": 10979545003, + "node_id": "LA_kwDOQMZG5c8AAAACjm6Pqw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:frontend", + "name": "area:frontend", + "color": "6B8DB5", + "default": false, + "description": "Frontend React/UI work" + } + ], + "state": "closed", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-27T12:23:05Z", + "updated_at": "2026-05-27T18:25:45Z", + "closed_at": "2026-05-27T18:25:45Z", + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/355", + "html_url": "https://github.com/utksh1/SecuScan/pull/355", + "diff_url": "https://github.com/utksh1/SecuScan/pull/355.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/355.patch", + "merged_at": null + }, + "body": "## Description\r\n\r\nAdded light theme color tokens in frontend/src/index.css to improve light mode consistency across dashboard components.\r\n\r\n## Changes Made\r\n\r\n* Added .theme-light variables\r\n* Updated background colors\r\n* Improved text readability\r\n* Added light mode support for dashboard UI\r\n\r\n## Issue\r\n\r\nFixes #178\r\n", + "closed_by": { + "login": "utksh1", + "id": 183999732, + "node_id": "U_kgDOCvec9A", + "avatar_url": "https://avatars.githubusercontent.com/u/183999732?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/utksh1", + "html_url": "https://github.com/utksh1", + "followers_url": "https://api.github.com/users/utksh1/followers", + "following_url": "https://api.github.com/users/utksh1/following{/other_user}", + "gists_url": "https://api.github.com/users/utksh1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/utksh1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/utksh1/subscriptions", + "organizations_url": "https://api.github.com/users/utksh1/orgs", + "repos_url": "https://api.github.com/users/utksh1/repos", + "events_url": "https://api.github.com/users/utksh1/events{/privacy}", + "received_events_url": "https://api.github.com/users/utksh1/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/355/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/355/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/354", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/354/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/354/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/354/events", + "html_url": "https://github.com/utksh1/SecuScan/issues/354", + "id": 4532226302, + "node_id": "I_kwDOQMZG5c8AAAABDiRI_g", + "number": 354, + "title": "[DOCS] Enhance local setup instructions and add troubleshooting for new contributors", + "user": { + "login": "anjalikumari45", + "id": 262248205, + "node_id": "U_kgDOD6GXDQ", + "avatar_url": "https://avatars.githubusercontent.com/u/262248205?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/anjalikumari45", + "html_url": "https://github.com/anjalikumari45", + "followers_url": "https://api.github.com/users/anjalikumari45/followers", + "following_url": "https://api.github.com/users/anjalikumari45/following{/other_user}", + "gists_url": "https://api.github.com/users/anjalikumari45/gists{/gist_id}", + "starred_url": "https://api.github.com/users/anjalikumari45/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/anjalikumari45/subscriptions", + "organizations_url": "https://api.github.com/users/anjalikumari45/orgs", + "repos_url": "https://api.github.com/users/anjalikumari45/repos", + "events_url": "https://api.github.com/users/anjalikumari45/events{/privacy}", + "received_events_url": "https://api.github.com/users/anjalikumari45/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignees": [ + { + "login": "parneetbrar234-svg", + "id": 226455749, + "node_id": "U_kgDODX9wxQ", + "avatar_url": "https://avatars.githubusercontent.com/u/226455749?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/parneetbrar234-svg", + "html_url": "https://github.com/parneetbrar234-svg", + "followers_url": "https://api.github.com/users/parneetbrar234-svg/followers", + "following_url": "https://api.github.com/users/parneetbrar234-svg/following{/other_user}", + "gists_url": "https://api.github.com/users/parneetbrar234-svg/gists{/gist_id}", + "starred_url": "https://api.github.com/users/parneetbrar234-svg/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/parneetbrar234-svg/subscriptions", + "organizations_url": "https://api.github.com/users/parneetbrar234-svg/orgs", + "repos_url": "https://api.github.com/users/parneetbrar234-svg/repos", + "events_url": "https://api.github.com/users/parneetbrar234-svg/events{/privacy}", + "received_events_url": "https://api.github.com/users/parneetbrar234-svg/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-27T12:17:30Z", + "updated_at": "2026-05-27T18:18:38Z", + "closed_at": null, + "assignee": { + "login": "parneetbrar234-svg", + "id": 226455749, + "node_id": "U_kgDODX9wxQ", + "avatar_url": "https://avatars.githubusercontent.com/u/226455749?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/parneetbrar234-svg", + "html_url": "https://github.com/parneetbrar234-svg", + "followers_url": "https://api.github.com/users/parneetbrar234-svg/followers", + "following_url": "https://api.github.com/users/parneetbrar234-svg/following{/other_user}", + "gists_url": "https://api.github.com/users/parneetbrar234-svg/gists{/gist_id}", + "starred_url": "https://api.github.com/users/parneetbrar234-svg/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/parneetbrar234-svg/subscriptions", + "organizations_url": "https://api.github.com/users/parneetbrar234-svg/orgs", + "repos_url": "https://api.github.com/users/parneetbrar234-svg/repos", + "events_url": "https://api.github.com/users/parneetbrar234-svg/events{/privacy}", + "received_events_url": "https://api.github.com/users/parneetbrar234-svg/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "author_association": "NONE", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "issue_dependencies_summary": { + "blocked_by": 0, + "total_blocked_by": 0, + "blocking": 0, + "total_blocking": 0 + }, + "body": "## Summary \nEnhance the `README.md` and local setup documentation by introducing a dedicated \"Troubleshooting / Common Issues\" section, specifically targeting Python and Node.js environment configurations.\n\n## Problem \nCurrently, the \"Quick Start\" guide assumes a seamless local environment. First-time open-source contributors (especially those joining via GSSoC) frequently encounter environment-specific blockers—such as Python 3.11+ version mismatches, virtual environment (`venv`) activation errors, or Node version discrepancies. Without a documented troubleshooting section, these common pitfalls cause onboarding friction and lead to repetitive setup-related support issues being opened.\n\n## Where to update \n- Suggested files: `README.md` and `docs/` (if a deeper setup guide exists).\n- Related sections or headings: Add a new `### Troubleshooting` section right after \"Quick Start\".\n\n## Expected outcome \nA streamlined onboarding experience where new contributors can independently resolve common setup errors. This will significantly reduce the support burden on maintainers and accelerate the time from fork to first PR.\n\n## Acceptance criteria \n- [x] The affected docs are updated with clear troubleshooting steps.\n- [x] Steps, commands, or links are accurate and tested.\n- [x] Cross-links are added where they reduce contributor confusion.\n- [x] A contributor can follow the updated instructions without extra clarification.\n\n## Verification \nMaintainers can review the added troubleshooting steps to ensure they align with the current architecture (e.g., Python 3.11+ and Node 20+) and are easy to understand.\n\n## Additional context \nI noticed there is an influx of new contributors, and having this will make everyone\u0027s workflow smoother. I would love to work on this documentation issue as a contributor for **GSSoC\u002726**. Could you please assign this to me?", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/354/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/354/timeline", + "performed_via_github_app": null, + "state_reason": null, + "pinned_comment": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/353", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/353/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/353/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/353/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/353", + "id": 4530696222, + "node_id": "PR_kwDOQMZG5c7fvFer", + "number": 353, + "title": "fix(ui): replace window.confirm dialogs with in-app confirmation modal", + "user": { + "login": "siddiqui7864", + "id": 130930054, + "node_id": "U_kgDOB83Vhg", + "avatar_url": "https://avatars.githubusercontent.com/u/130930054?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/siddiqui7864", + "html_url": "https://github.com/siddiqui7864", + "followers_url": "https://api.github.com/users/siddiqui7864/followers", + "following_url": "https://api.github.com/users/siddiqui7864/following{/other_user}", + "gists_url": "https://api.github.com/users/siddiqui7864/gists{/gist_id}", + "starred_url": "https://api.github.com/users/siddiqui7864/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/siddiqui7864/subscriptions", + "organizations_url": "https://api.github.com/users/siddiqui7864/orgs", + "repos_url": "https://api.github.com/users/siddiqui7864/repos", + "events_url": "https://api.github.com/users/siddiqui7864/events{/privacy}", + "received_events_url": "https://api.github.com/users/siddiqui7864/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10946283294, + "node_id": "LA_kwDOQMZG5c8AAAACjHMHHg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:intermediate", + "name": "level:intermediate", + "color": "D6B76B", + "default": false, + "description": "35 pts difficulty label for moderate contributor PRs" + }, + { + "id": 10957870409, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVSQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:accessibility", + "name": "type:accessibility", + "color": "5F7FA6", + "default": false, + "description": "Accessibility work category bonus label" + }, + { + "id": 10957870885, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXJQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:feature", + "name": "type:feature", + "color": "7FA6A0", + "default": false, + "description": "Feature work category bonus label" + }, + { + "id": 10979545003, + "node_id": "LA_kwDOQMZG5c8AAAACjm6Pqw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:frontend", + "name": "area:frontend", + "color": "6B8DB5", + "default": false, + "description": "Frontend React/UI work" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T08:10:13Z", + "updated_at": "2026-05-27T18:24:10Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/353", + "html_url": "https://github.com/utksh1/SecuScan/pull/353", + "diff_url": "https://github.com/utksh1/SecuScan/pull/353.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/353.patch", + "merged_at": null + }, + "body": "Closes #33\r\n\r\n## Changes\r\n- Extracted a reusable `ConfirmModal` component to `components/ConfirmModal.tsx`\r\n- Replaced 3 `window.confirm()` calls in `Scans.tsx` (delete record, bulk delete, purge all)\r\n- Replaced 2 `window.confirm()` calls in `Settings.tsx` (engine reset, nuclear purge)\r\n- Modal matches the existing neo-brutalist design system\r\n- Supports keyboard navigation — Escape to cancel, Enter to confirm\r\n\r\n## Testing\r\nTested locally — all 5 confirm dialogs now show the in-app modal correctly.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/353/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/353/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/352", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/352/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/352/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/352/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/352", + "id": 4530287063, + "node_id": "PR_kwDOQMZG5c7ftw0Z", + "number": 352, + "title": "feat(scans): pause polling when tab is hidden (#95)", + "user": { + "login": "PrishaNagpal", + "id": 215357912, + "node_id": "U_kgDODNYZ2A", + "avatar_url": "https://avatars.githubusercontent.com/u/215357912?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/PrishaNagpal", + "html_url": "https://github.com/PrishaNagpal", + "followers_url": "https://api.github.com/users/PrishaNagpal/followers", + "following_url": "https://api.github.com/users/PrishaNagpal/following{/other_user}", + "gists_url": "https://api.github.com/users/PrishaNagpal/gists{/gist_id}", + "starred_url": "https://api.github.com/users/PrishaNagpal/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/PrishaNagpal/subscriptions", + "organizations_url": "https://api.github.com/users/PrishaNagpal/orgs", + "repos_url": "https://api.github.com/users/PrishaNagpal/repos", + "events_url": "https://api.github.com/users/PrishaNagpal/events{/privacy}", + "received_events_url": "https://api.github.com/users/PrishaNagpal/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10946283294, + "node_id": "LA_kwDOQMZG5c8AAAACjHMHHg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:intermediate", + "name": "level:intermediate", + "color": "D6B76B", + "default": false, + "description": "35 pts difficulty label for moderate contributor PRs" + }, + { + "id": 10957870191, + "node_id": "LA_kwDOQMZG5c8AAAACjSPUbw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/quality:clean", + "name": "quality:clean", + "color": "8FB3A7", + "default": false, + "description": "Contributor score x1.2; mentor +5 pts" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870469, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVhQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:performance", + "name": "type:performance", + "color": "C9A66B", + "default": false, + "description": "Performance work category bonus label" + }, + { + "id": 10957870846, + "node_id": "LA_kwDOQMZG5c8AAAACjSPW_g", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:bug", + "name": "type:bug", + "color": "B96B6B", + "default": false, + "description": "Bug fix work category bonus label" + }, + { + "id": 10957870938, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXWg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/gssoc:approved", + "name": "gssoc:approved", + "color": "7BA37B", + "default": false, + "description": "Admin validation: approved for GSSoC scoring" + }, + { + "id": 10979545003, + "node_id": "LA_kwDOQMZG5c8AAAACjm6Pqw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:frontend", + "name": "area:frontend", + "color": "6B8DB5", + "default": false, + "description": "Frontend React/UI work" + } + ], + "state": "closed", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T07:04:23Z", + "updated_at": "2026-05-27T18:27:42Z", + "closed_at": "2026-05-27T18:27:41Z", + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/352", + "html_url": "https://github.com/utksh1/SecuScan/pull/352", + "diff_url": "https://github.com/utksh1/SecuScan/pull/352.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/352.patch", + "merged_at": "2026-05-27T18:27:41Z" + }, + "body": "## Description\r\nPauses polling entirely when the browser tab is hidden to reduce unnecessary backend traffic. Resumes with an immediate fetch when the tab becomes visible again. Normal 5s polling behaviour is unchanged while the tab is active.\r\n\r\n## Related Issues\r\nCloses #95\r\n\r\n## Type of Change\r\n- [ ] Bug fix (non-breaking change which fixes an issue)\r\n- [x] New feature (non-breaking change which adds functionality)\r\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\r\n- [ ] Documentation update\r\n\r\n## How Has This Been Tested?\r\nUnit tests added at testing/unit/pages/Scans.polling.test.tsx using Vitest + jsdom with fake timers.\r\nAll 6 tests pass (npx vitest run Scans.polling):\r\n-fires one fetch on mount\r\n-polls every 5s while tab is visible\r\n-stops polling entirely when tab is hidden\r\n-resumes immediately when tab becomes visible again\r\n-does not double-poll if tab was never hidden\r\n-cleans up interval and listener on unmount\r\n\r\n\r\n\r\n## Checklist\r\n- [x] My code follows the code style of this project.\r\n- [x] I have performed a self-review of my own code.\r\n- [x] I have commented my code, particularly in hard-to-understand areas.\r\n- [ ] I have made corresponding changes to the documentation.\r\n- [x] My changes generate no new warnings.\r\n", + "closed_by": { + "login": "utksh1", + "id": 183999732, + "node_id": "U_kgDOCvec9A", + "avatar_url": "https://avatars.githubusercontent.com/u/183999732?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/utksh1", + "html_url": "https://github.com/utksh1", + "followers_url": "https://api.github.com/users/utksh1/followers", + "following_url": "https://api.github.com/users/utksh1/following{/other_user}", + "gists_url": "https://api.github.com/users/utksh1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/utksh1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/utksh1/subscriptions", + "organizations_url": "https://api.github.com/users/utksh1/orgs", + "repos_url": "https://api.github.com/users/utksh1/repos", + "events_url": "https://api.github.com/users/utksh1/events{/privacy}", + "received_events_url": "https://api.github.com/users/utksh1/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/352/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/352/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/351", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/351/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/351/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/351/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/351", + "id": 4529871979, + "node_id": "PR_kwDOQMZG5c7fsbIr", + "number": 351, + "title": "feat: standardize API error codes with machine-readable problem detai…", + "user": { + "login": "Bhavy12-cell", + "id": 185819124, + "node_id": "U_kgDOCxNf9A", + "avatar_url": "https://avatars.githubusercontent.com/u/185819124?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Bhavy12-cell", + "html_url": "https://github.com/Bhavy12-cell", + "followers_url": "https://api.github.com/users/Bhavy12-cell/followers", + "following_url": "https://api.github.com/users/Bhavy12-cell/following{/other_user}", + "gists_url": "https://api.github.com/users/Bhavy12-cell/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Bhavy12-cell/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Bhavy12-cell/subscriptions", + "organizations_url": "https://api.github.com/users/Bhavy12-cell/orgs", + "repos_url": "https://api.github.com/users/Bhavy12-cell/repos", + "events_url": "https://api.github.com/users/Bhavy12-cell/events{/privacy}", + "received_events_url": "https://api.github.com/users/Bhavy12-cell/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10924091185, + "node_id": "LA_kwDOQMZG5c8AAAACiyBnMQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:advanced", + "name": "level:advanced", + "color": "7BA37B", + "default": false, + "description": "55 pts difficulty label for advanced contributor PRs" + }, + { + "id": 10957870846, + "node_id": "LA_kwDOQMZG5c8AAAACjSPW_g", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:bug", + "name": "type:bug", + "color": "B96B6B", + "default": false, + "description": "Bug fix work category bonus label" + }, + { + "id": 10957870885, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXJQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:feature", + "name": "type:feature", + "color": "7FA6A0", + "default": false, + "description": "Feature work category bonus label" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T05:49:01Z", + "updated_at": "2026-05-27T18:24:15Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/351", + "html_url": "https://github.com/utksh1/SecuScan/pull/351", + "diff_url": "https://github.com/utksh1/SecuScan/pull/351.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/351.patch", + "merged_at": null + }, + "body": "Closes #218\r\n\r\nStandardized all API error responses with machine-readable problem details (RFC 7807-style).\r\n\r\nChanges:\r\n- Added `backend/secuscan/errors.py` with `problem()` helper and\r\n exception handlers for HTTPException, RequestValidationError,\r\n and unhandled exceptions\r\n- Each error response includes: code, message, status, request_id,\r\n hint, field, details\r\n- Registered handlers in `main.py`\r\n- Updated `ErrorResponse` model in `models.py` to match new schema\r\n- Added `test_error_codes.py` covering 404/401/409/429/500 responses,\r\n validation errors, and legacy string-only error removal\r\n\r\nTesting:\r\n- All existing tests continue to pass\r\n- New tests snapshot validation/auth/not-found/conflict responses\r\n- Negative tests ensure legacy string-only errors are removed\r\n", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/351/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/351/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/350", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/350/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/350/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/350/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/350", + "id": 4529628283, + "node_id": "PR_kwDOQMZG5c7frqLq", + "number": 350, + "title": "feat(risk): composite risk scoring model with explainable factors (#252)", + "user": { + "login": "Shikhar-404exe", + "id": 209316015, + "node_id": "U_kgDODHnorw", + "avatar_url": "https://avatars.githubusercontent.com/u/209316015?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Shikhar-404exe", + "html_url": "https://github.com/Shikhar-404exe", + "followers_url": "https://api.github.com/users/Shikhar-404exe/followers", + "following_url": "https://api.github.com/users/Shikhar-404exe/following{/other_user}", + "gists_url": "https://api.github.com/users/Shikhar-404exe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Shikhar-404exe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Shikhar-404exe/subscriptions", + "organizations_url": "https://api.github.com/users/Shikhar-404exe/orgs", + "repos_url": "https://api.github.com/users/Shikhar-404exe/repos", + "events_url": "https://api.github.com/users/Shikhar-404exe/events{/privacy}", + "received_events_url": "https://api.github.com/users/Shikhar-404exe/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10924091185, + "node_id": "LA_kwDOQMZG5c8AAAACiyBnMQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:advanced", + "name": "level:advanced", + "color": "7BA37B", + "default": false, + "description": "55 pts difficulty label for advanced contributor PRs" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870885, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXJQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:feature", + "name": "type:feature", + "color": "7FA6A0", + "default": false, + "description": "Feature work category bonus label" + }, + { + "id": 10979545003, + "node_id": "LA_kwDOQMZG5c8AAAACjm6Pqw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:frontend", + "name": "area:frontend", + "color": "6B8DB5", + "default": false, + "description": "Frontend React/UI work" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T04:54:34Z", + "updated_at": "2026-05-27T18:24:22Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/350", + "html_url": "https://github.com/utksh1/SecuScan/pull/350", + "diff_url": "https://github.com/utksh1/SecuScan/pull/350.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/350.patch", + "merged_at": null + }, + "body": "Implements a deterministic, explainable risk scoring model that prioritizes findings using five weighted factors.\r\n\r\n#252 \r\n\r\n### Scoring Formula\r\n`risk_score = severity×0.30 + exploitability×0.25 + asset_exposure×0.20 + recency×0.15 + confidence×0.10`\r\n\r\nEach factor includes a human-readable explanation of its contribution (label, raw value, score out of 10, weight, contribution, detail string).\r\n\r\n### Backend Changes\r\n- `backend/secuscan/risk_scoring.py` — scoring algorithm with `compute_risk_score()` and `generate_risk_factors()`\r\n- `backend/secuscan/models.py` — `Finding` model extended with `exploitability`, `confidence`, `asset_exposure`, `risk_score`, `risk_factors`\r\n- `backend/secuscan/database.py` — schema migration for 4 new columns\r\n- `backend/secuscan/executor.py` — `_compute_and_set_risk_score()` called on finding upsert in both sync and async paths\r\n- `backend/secuscan/routes.py` — `risk_score`, `risk_factors`, `avg_risk_score` exposed on `GET /findings`, `GET /finding/{id}`, `GET /dashboard/summary`\r\n\r\n### Frontend Changes\r\n- `Findings.tsx` — sidebar shows color-coded Risk Score card (red ≥7, amber 4–6.9, blue \u003c4) with per-factor breakdown\r\n- `TaskDetails.tsx` — FindingDrawer shows Risk Score and factor breakdown\r\n\r\n### Testing\r\n- 17 backend unit tests in `testing/backend/unit/test_risk_scoring.py` covering:\r\n - Determinism, score bounds, default values\r\n - All severity levels mapping to correct scores\r\n - Factor generation correctness\r\n - Edge cases (null fields, extreme values, backward compat)\r\n- 7 frontend tests in `frontend/testing/unit/pages/Findings.test.tsx` covering:\r\n - Risk score visibility and color coding\r\n - Factor label rendering\r\n - Absence when score is null", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/350/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/350/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/349", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/349/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/349/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/349/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/349", + "id": 4529594342, + "node_id": "PR_kwDOQMZG5c7frjMt", + "number": 349, + "title": "Fix safe mode validation for hostnames and URL targets", + "user": { + "login": "ionfwsrijan", + "id": 201338831, + "node_id": "U_kgDODAAvzw", + "avatar_url": "https://avatars.githubusercontent.com/u/201338831?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ionfwsrijan", + "html_url": "https://github.com/ionfwsrijan", + "followers_url": "https://api.github.com/users/ionfwsrijan/followers", + "following_url": "https://api.github.com/users/ionfwsrijan/following{/other_user}", + "gists_url": "https://api.github.com/users/ionfwsrijan/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ionfwsrijan/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ionfwsrijan/subscriptions", + "organizations_url": "https://api.github.com/users/ionfwsrijan/orgs", + "repos_url": "https://api.github.com/users/ionfwsrijan/repos", + "events_url": "https://api.github.com/users/ionfwsrijan/events{/privacy}", + "received_events_url": "https://api.github.com/users/ionfwsrijan/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10957870165, + "node_id": "LA_kwDOQMZG5c8AAAACjSPUVQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:critical", + "name": "level:critical", + "color": "A75D5D", + "default": false, + "description": "80 pts difficulty label for critical or high-impact PRs" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870533, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVxQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:security", + "name": "type:security", + "color": "A66A5F", + "default": false, + "description": "Security work category bonus label" + }, + { + "id": 10957870846, + "node_id": "LA_kwDOQMZG5c8AAAACjSPW_g", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:bug", + "name": "type:bug", + "color": "B96B6B", + "default": false, + "description": "Bug fix work category bonus label" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + }, + { + "id": 10979545212, + "node_id": "LA_kwDOQMZG5c8AAAACjm6QfA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:security", + "name": "area:security", + "color": "A75D5D", + "default": false, + "description": "Security-sensitive implementation or tests" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 3, + "created_at": "2026-05-27T04:46:29Z", + "updated_at": "2026-05-27T20:49:30Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/349", + "html_url": "https://github.com/utksh1/SecuScan/pull/349", + "diff_url": "https://github.com/utksh1/SecuScan/pull/349.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/349.patch", + "merged_at": null + }, + "body": "## Description\r\nThis PR fixes Safe Mode target validation so it can’t be bypassed using hostnames or URL forms.\r\n\r\nKey improvements:\r\n- Safe mode now parses `http(s)://...` targets using proper URL parsing (no substring checks).\r\n- URL IP literals (e.g. `http://8.8.8.8`) are treated the same as raw IP targets and correctly blocked in safe mode.\r\n- Hostnames are resolved (A/AAAA) in safe mode and rejected if **any** resolved address is public (fail-safe).\r\n- `settings.allowed_networks` is now enforced consistently in safe mode for IP/CIDR targets and resolved hostname targets, including basic wildcard patterns like `10.*.*.*`.\r\n\r\n## Related Issues\r\nFixes #282\r\n\r\n## Type of Change\r\n- [x] Bug fix (non-breaking change which fixes an issue)\r\n- [ ] New feature (non-breaking change which adds functionality)\r\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\r\n- [ ] Documentation update\r\n\r\n## How Has This Been Tested?\r\n- Ran backend unit tests for validation logic:\r\n\r\n```bash\r\npython -m pytest -q testing/backend/unit/test_validation.py\r\n\r\n## Notes\r\n\r\n- Added regression tests that mock DNS resolution to validate safe mode behavior for public hostnames and URL IP literals.\r\n\r\n## Checklist\r\n\r\n- [x] My code follows the code style of this project.\r\n- [x] I have performed a self-review of my own code.\r\n- [x] I have commented my code, particularly in hard-to-understand areas.\r\n- [x] I have made corresponding changes to the documentation.\r\n- [x] My changes generate no new warnings.", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/349/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/349/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/348", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/348/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/348/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/348/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/348", + "id": 4529494861, + "node_id": "PR_kwDOQMZG5c7frOzY", + "number": 348, + "title": "fix(validation): type guard for validate_port, null byte/tab sanitization, edge case tests", + "user": { + "login": "hariom888", + "id": 188325558, + "node_id": "U_kgDOCzmetg", + "avatar_url": "https://avatars.githubusercontent.com/u/188325558?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hariom888", + "html_url": "https://github.com/hariom888", + "followers_url": "https://api.github.com/users/hariom888/followers", + "following_url": "https://api.github.com/users/hariom888/following{/other_user}", + "gists_url": "https://api.github.com/users/hariom888/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hariom888/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hariom888/subscriptions", + "organizations_url": "https://api.github.com/users/hariom888/orgs", + "repos_url": "https://api.github.com/users/hariom888/repos", + "events_url": "https://api.github.com/users/hariom888/events{/privacy}", + "received_events_url": "https://api.github.com/users/hariom888/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10946283294, + "node_id": "LA_kwDOQMZG5c8AAAACjHMHHg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:intermediate", + "name": "level:intermediate", + "color": "D6B76B", + "default": false, + "description": "35 pts difficulty label for moderate contributor PRs" + }, + { + "id": 10957870191, + "node_id": "LA_kwDOQMZG5c8AAAACjSPUbw", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/quality:clean", + "name": "quality:clean", + "color": "8FB3A7", + "default": false, + "description": "Contributor score x1.2; mentor +5 pts" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870533, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVxQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:security", + "name": "type:security", + "color": "A66A5F", + "default": false, + "description": "Security work category bonus label" + }, + { + "id": 10957870846, + "node_id": "LA_kwDOQMZG5c8AAAACjSPW_g", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:bug", + "name": "type:bug", + "color": "B96B6B", + "default": false, + "description": "Bug fix work category bonus label" + }, + { + "id": 10957870938, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXWg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/gssoc:approved", + "name": "gssoc:approved", + "color": "7BA37B", + "default": false, + "description": "Admin validation: approved for GSSoC scoring" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + }, + { + "id": 10979545212, + "node_id": "LA_kwDOQMZG5c8AAAACjm6QfA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:security", + "name": "area:security", + "color": "A75D5D", + "default": false, + "description": "Security-sensitive implementation or tests" + } + ], + "state": "closed", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T04:23:41Z", + "updated_at": "2026-05-27T18:27:47Z", + "closed_at": "2026-05-27T18:27:47Z", + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/348", + "html_url": "https://github.com/utksh1/SecuScan/pull/348", + "diff_url": "https://github.com/utksh1/SecuScan/pull/348.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/348.patch", + "merged_at": "2026-05-27T18:27:47Z" + }, + "body": "## Changes\r\n\r\n### `backend/secuscan/validation.py`\r\n\r\n**`validate_port` — type guard added**\r\n`validate_port(\"80\")` previously raised `TypeError` because the comparison `port \u003c 1` fails on strings. `validate_port(80.5)` silently returned `True` for floats. `validate_port(True)` also passed because `bool` is a subclass of `int`. Added `isinstance` check with explicit `bool` exclusion.\r\n\r\n**`sanitize_input` — null byte and tab now filtered**\r\n`\\x00` (null byte) can truncate strings in C-backed tools like nmap, effectively hiding everything after it from the argument parser. `\\t` (tab) is usable in argument injection in some shell contexts. Both added to `dangerous_chars`.\r\n\r\n**`validate_port_range` — docstring updated**\r\nMixed comma+range spec support (`\"22,80,443-8080\"`) was added in the previous fix but never documented. Docstring now shows all three supported formats.\r\n\r\n### `testing/backend/unit/test_validation.py`\r\n\r\n- `test_validate_port`: add type guard cases — `str`, `float`, `bool`, `None`\r\n- `test_sanitize_input`: add null byte, tab, leading/trailing whitespace cases\r\n\r\n### `testing/backend/unit/test_task_start_validation.py`\r\n\r\nFive missing edge cases added:\r\n- `inputs` not a dict → 400\r\n- Array item exceeds `MAX_FIELD` → 400 \r\n- Integer/bool/None field values → pass (only strings/lists are length-checked)\r\n- Field value exactly at `MAX_FIELD` chars → pass (boundary check)\r\n- Error message on oversized field → does not echo the field value back (security)\r\n\r\n## No breaking changes\r\nAll changes are additive or defensive. No existing behaviour removed.\r\n\r\nCloses #307 ", + "closed_by": { + "login": "utksh1", + "id": 183999732, + "node_id": "U_kgDOCvec9A", + "avatar_url": "https://avatars.githubusercontent.com/u/183999732?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/utksh1", + "html_url": "https://github.com/utksh1", + "followers_url": "https://api.github.com/users/utksh1/followers", + "following_url": "https://api.github.com/users/utksh1/following{/other_user}", + "gists_url": "https://api.github.com/users/utksh1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/utksh1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/utksh1/subscriptions", + "organizations_url": "https://api.github.com/users/utksh1/orgs", + "repos_url": "https://api.github.com/users/utksh1/repos", + "events_url": "https://api.github.com/users/utksh1/events{/privacy}", + "received_events_url": "https://api.github.com/users/utksh1/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/348/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/348/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/347", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/347/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/347/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/347/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/347", + "id": 4529315571, + "node_id": "PR_kwDOQMZG5c7fqpfW", + "number": 347, + "title": "fix(validation): resolve hostnames to IPs to prevent SSRF attacks", + "user": { + "login": "princeyadaviiit", + "id": 175847866, + "node_id": "U_kgDOCns5ug", + "avatar_url": "https://avatars.githubusercontent.com/u/175847866?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/princeyadaviiit", + "html_url": "https://github.com/princeyadaviiit", + "followers_url": "https://api.github.com/users/princeyadaviiit/followers", + "following_url": "https://api.github.com/users/princeyadaviiit/following{/other_user}", + "gists_url": "https://api.github.com/users/princeyadaviiit/gists{/gist_id}", + "starred_url": "https://api.github.com/users/princeyadaviiit/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/princeyadaviiit/subscriptions", + "organizations_url": "https://api.github.com/users/princeyadaviiit/orgs", + "repos_url": "https://api.github.com/users/princeyadaviiit/repos", + "events_url": "https://api.github.com/users/princeyadaviiit/events{/privacy}", + "received_events_url": "https://api.github.com/users/princeyadaviiit/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10957870165, + "node_id": "LA_kwDOQMZG5c8AAAACjSPUVQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:critical", + "name": "level:critical", + "color": "A75D5D", + "default": false, + "description": "80 pts difficulty label for critical or high-impact PRs" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870533, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVxQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:security", + "name": "type:security", + "color": "A66A5F", + "default": false, + "description": "Security work category bonus label" + }, + { + "id": 10957870846, + "node_id": "LA_kwDOQMZG5c8AAAACjSPW_g", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:bug", + "name": "type:bug", + "color": "B96B6B", + "default": false, + "description": "Bug fix work category bonus label" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + }, + { + "id": 10979545212, + "node_id": "LA_kwDOQMZG5c8AAAACjm6QfA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:security", + "name": "area:security", + "color": "A75D5D", + "default": false, + "description": "Security-sensitive implementation or tests" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-27T03:44:19Z", + "updated_at": "2026-05-28T01:45:46Z", + "closed_at": null, + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/347", + "html_url": "https://github.com/utksh1/SecuScan/pull/347", + "diff_url": "https://github.com/utksh1/SecuScan/pull/347.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/347.patch", + "merged_at": null + }, + "body": "\r\nFixes #286 \r\n ## Summary\r\n Fixes a critical SSRF vulnerability where hostnames resolving to blocked IP addresses bypassed network-level security guardrails.\r\n ## Problem\r\n The `validate_target()` function only performed regex and TLD checks on hostnames without resolving them to IP addresses. This\r\n allowed attackers to:\r\n - Access cloud metadata services (169.254.169.254) via malicious DNS\r\n - Perform DNS rebinding attacks\r\n - Reach internal infrastructure through hostname resolution\r\n\r\n ## Solution\r\n - Added async DNS resolution in `validate_target_async()`\r\n - All hostnames are now resolved to IPs before validation\r\n - Each resolved IP is checked against blocked networks (link-local, multicast, loopback)\r\n - Safe mode restrictions apply to resolved IPs\r\n - Error messages don\u0027t leak hostnames for security\r\n\r\n ## Changes\r\n - `backend/secuscan/validation.py`: Added DNS resolution and IP validation\r\n - `backend/secuscan/routes.py`: Updated to use async validation\r\n - `testing/backend/unit/test_validation_ssrf.py`: 16 new SSRF-specific tests\r\n - `testing/backend/unit/test_validation.py`: Updated existing tests\r\n - `testing/backend/demo_ssrf_fix.py`: Demonstration script\r\n\r\n ## Testing\r\n - ✅ All 16 SSRF tests pass\r\n - ✅ All existing validation tests pass\r\n - ✅ Security test for hostname leakage passes\r\n - ✅ Demo script shows all attack vectors blocked\r\n\r\n ## Security Impact\r\n **Severity:** High - Prevents SSRF against cloud metadata services and internal infrastructure\r\n\r\n **Attack Scenarios Blocked:**\r\n 1. Cloud metadata SSRF (AWS/GCP/Azure IMDSv1)\r\n 2. DNS rebinding attacks\r\n 3. Internal network access via hostname resolution\r\n 4. Multicast/link-local address access", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/347/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/347/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/346", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/346/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/346/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/346/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/346", + "id": 4528908791, + "node_id": "PR_kwDOQMZG5c7fpW-W", + "number": 346, + "title": "fix(validation): resolve hostnames to IPs to prevent SSRF attacks", + "user": { + "login": "princeyadaviiit", + "id": 175847866, + "node_id": "U_kgDOCns5ug", + "avatar_url": "https://avatars.githubusercontent.com/u/175847866?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/princeyadaviiit", + "html_url": "https://github.com/princeyadaviiit", + "followers_url": "https://api.github.com/users/princeyadaviiit/followers", + "following_url": "https://api.github.com/users/princeyadaviiit/following{/other_user}", + "gists_url": "https://api.github.com/users/princeyadaviiit/gists{/gist_id}", + "starred_url": "https://api.github.com/users/princeyadaviiit/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/princeyadaviiit/subscriptions", + "organizations_url": "https://api.github.com/users/princeyadaviiit/orgs", + "repos_url": "https://api.github.com/users/princeyadaviiit/repos", + "events_url": "https://api.github.com/users/princeyadaviiit/events{/privacy}", + "received_events_url": "https://api.github.com/users/princeyadaviiit/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-27T02:10:34Z", + "updated_at": "2026-05-27T02:56:48Z", + "closed_at": "2026-05-27T02:26:42Z", + "assignee": null, + "author_association": "NONE", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/346", + "html_url": "https://github.com/utksh1/SecuScan/pull/346", + "diff_url": "https://github.com/utksh1/SecuScan/pull/346.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/346.patch", + "merged_at": null + }, + "body": "Fixes #286\r\n\r\nHostnames are now resolved to IP addresses before validation, blocking SSRF attacks against cloud metadata services (169.254.169.254) and internal infrastructure. Includes comprehensive test coverage.\r\n\r\n ## Summary\r\n Fixes a critical SSRF vulnerability where hostnames resolving to blocked IP addresses bypassed network-level security guardrails.\r\n ## Problem\r\n The `validate_target()` function only performed regex and TLD checks on hostnames without resolving them to IP addresses. This\r\n allowed attackers to:\r\n - Access cloud metadata services (169.254.169.254) via malicious DNS\r\n - Perform DNS rebinding attacks\r\n - Reach internal infrastructure through hostname resolution\r\n\r\n ## Solution\r\n - Added async DNS resolution in `validate_target_async()`\r\n - All hostnames are now resolved to IPs before validation\r\n - Each resolved IP is checked against blocked networks (link-local, multicast, loopback)\r\n - Safe mode restrictions apply to resolved IPs\r\n - Error messages don\u0027t leak hostnames for security\r\n\r\n ## Changes\r\n - `backend/secuscan/validation.py`: Added DNS resolution and IP validation\r\n - `backend/secuscan/routes.py`: Updated to use async validation\r\n - `testing/backend/unit/test_validation_ssrf.py`: 16 new SSRF-specific tests\r\n - `testing/backend/unit/test_validation.py`: Updated existing tests\r\n - `testing/backend/demo_ssrf_fix.py`: Demonstration script\r\n\r\n ## Testing\r\n - ✅ All 16 SSRF tests pass\r\n - ✅ All existing validation tests pass\r\n - ✅ Security test for hostname leakage passes\r\n - ✅ Demo script shows all attack vectors blocked\r\n\r\n ## Security Impact\r\n **Severity:** High - Prevents SSRF against cloud metadata services and internal infrastructure\r\n\r\n **Attack Scenarios Blocked:**\r\n 1. Cloud metadata SSRF (AWS/GCP/Azure IMDSv1)\r\n 2. DNS rebinding attacks\r\n 3. Internal network access via hostname resolution\r\n 4. Multicast/link-local address access\r\n", + "closed_by": { + "login": "princeyadaviiit", + "id": 175847866, + "node_id": "U_kgDOCns5ug", + "avatar_url": "https://avatars.githubusercontent.com/u/175847866?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/princeyadaviiit", + "html_url": "https://github.com/princeyadaviiit", + "followers_url": "https://api.github.com/users/princeyadaviiit/followers", + "following_url": "https://api.github.com/users/princeyadaviiit/following{/other_user}", + "gists_url": "https://api.github.com/users/princeyadaviiit/gists{/gist_id}", + "starred_url": "https://api.github.com/users/princeyadaviiit/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/princeyadaviiit/subscriptions", + "organizations_url": "https://api.github.com/users/princeyadaviiit/orgs", + "repos_url": "https://api.github.com/users/princeyadaviiit/repos", + "events_url": "https://api.github.com/users/princeyadaviiit/events{/privacy}", + "received_events_url": "https://api.github.com/users/princeyadaviiit/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/346/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/346/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/345", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/345/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/345/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/345/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/345", + "id": 4528009766, + "node_id": "PR_kwDOQMZG5c7fmfZx", + "number": 345, + "title": "feat: plugin dependency preflight with OS-specific install guidance (#228)", + "user": { + "login": "aaniya22", + "id": 230661478, + "node_id": "U_kgDODb-dZg", + "avatar_url": "https://avatars.githubusercontent.com/u/230661478?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/aaniya22", + "html_url": "https://github.com/aaniya22", + "followers_url": "https://api.github.com/users/aaniya22/followers", + "following_url": "https://api.github.com/users/aaniya22/following{/other_user}", + "gists_url": "https://api.github.com/users/aaniya22/gists{/gist_id}", + "starred_url": "https://api.github.com/users/aaniya22/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/aaniya22/subscriptions", + "organizations_url": "https://api.github.com/users/aaniya22/orgs", + "repos_url": "https://api.github.com/users/aaniya22/repos", + "events_url": "https://api.github.com/users/aaniya22/events{/privacy}", + "received_events_url": "https://api.github.com/users/aaniya22/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10924091185, + "node_id": "LA_kwDOQMZG5c8AAAACiyBnMQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:advanced", + "name": "level:advanced", + "color": "7BA37B", + "default": false, + "description": "55 pts difficulty label for advanced contributor PRs" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870885, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXJQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:feature", + "name": "type:feature", + "color": "7FA6A0", + "default": false, + "description": "Feature work category bonus label" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + }, + { + "id": 10979545075, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P8w", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:plugins", + "name": "area:plugins", + "color": "6E9A99", + "default": false, + "description": "Scanner plugin metadata, schemas, or plugin runtime work" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-26T22:45:20Z", + "updated_at": "2026-05-27T18:24:39Z", + "closed_at": null, + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/345", + "html_url": "https://github.com/utksh1/SecuScan/pull/345", + "diff_url": "https://github.com/utksh1/SecuScan/pull/345.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/345.patch", + "merged_at": null + }, + "body": "## What\r\n\r\nAdds a dedicated `preflight.py` module that detects missing binaries and\r\nPython packages for each plugin and returns copy-pasteable install commands\r\ntailored to the host OS (Linux/macOS).\r\n\r\n## Why\r\n\r\nThe previous `list_plugins` response returned a generic hardcoded string:\r\n\u003e \"Install required tools locally to enable this scanner.\"\r\n\r\nThis gave users no actionable path forward. Issue #228 asks for real\r\nOS-aware guidance surfaced in the plugin availability payload.\r\n\r\n## Changes\r\n\r\n### `backend/secuscan/preflight.py` (new)\r\n- `check_plugin_dependencies()` — pure function, fully injectable\r\n (`which_fn`, `import_fn`, `os_tag`) for hermetic testing\r\n- `PreflightResult` dataclass — `runnable`, `missing_binaries`,\r\n `missing_packages`, `install_guidance`, `status`\r\n- `_BINARY_HINTS` — install commands for 17 common tools across\r\n Linux (`apt`/`go install`) and macOS (`brew`)\r\n- Unknown binaries get a fallback `# install manually` comment\r\n rather than silently failing\r\n\r\n### `backend/secuscan/plugins.py`\r\n- `list_plugins()` now calls `check_plugin_dependencies()` instead of\r\n the old `_get_missing_binaries()` helper\r\n- Availability payload gains two new fields:\r\n - `missing_packages` — Python packages that failed `importlib` lookup\r\n - `install_guidance` — replaces the old `guidance` field; now contains\r\n real OS-specific commands instead of a generic string\r\n- `_get_missing_binaries()` removed (logic absorbed into `preflight.py`)\r\n\r\n### `testing/backend/unit/test_preflight.py` (new)\r\n- 27 hermetic unit tests, zero network/disk I/O\r\n- Covers: runnable plugins, missing binaries, partial missing,\r\n deduplication, Linux apt guidance, macOS brew guidance,\r\n unknown binary fallback, pip guidance, mixed binary+package failures,\r\n `PreflightResult.status` property\r\n\r\n## What was NOT changed\r\n\r\n- No UI changes\r\n- No doc changes\r\n- No lockfile churn\r\n- No formatting changes\r\n- All existing tests continue to pass\r\n\r\n## Verification\r\n\r\n```bash\r\npytest testing/backend/unit/test_preflight.py testing/backend/unit/test_plugins.py -v\r\n```\r\n\r\nCloses #228", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/345/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/345/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/344", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/344/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/344/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/344/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/344", + "id": 4527957237, + "node_id": "PR_kwDOQMZG5c7fmUhf", + "number": 344, + "title": "ci: add mutation testing for validation and redaction helpers (#244)", + "user": { + "login": "aaniya22", + "id": 230661478, + "node_id": "U_kgDODb-dZg", + "avatar_url": "https://avatars.githubusercontent.com/u/230661478?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/aaniya22", + "html_url": "https://github.com/aaniya22", + "followers_url": "https://api.github.com/users/aaniya22/followers", + "following_url": "https://api.github.com/users/aaniya22/following{/other_user}", + "gists_url": "https://api.github.com/users/aaniya22/gists{/gist_id}", + "starred_url": "https://api.github.com/users/aaniya22/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/aaniya22/subscriptions", + "organizations_url": "https://api.github.com/users/aaniya22/orgs", + "repos_url": "https://api.github.com/users/aaniya22/repos", + "events_url": "https://api.github.com/users/aaniya22/events{/privacy}", + "received_events_url": "https://api.github.com/users/aaniya22/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10924091185, + "node_id": "LA_kwDOQMZG5c8AAAACiyBnMQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:advanced", + "name": "level:advanced", + "color": "7BA37B", + "default": false, + "description": "55 pts difficulty label for advanced contributor PRs" + }, + { + "id": 10957870384, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVMA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:testing", + "name": "type:testing", + "color": "7A94A6", + "default": false, + "description": "Testing work category bonus label" + }, + { + "id": 10957870777, + "node_id": "LA_kwDOQMZG5c8AAAACjSPWuQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:devops", + "name": "type:devops", + "color": "6E9A99", + "default": false, + "description": "DevOps or infrastructure work category bonus label" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + }, + { + "id": 10979545148, + "node_id": "LA_kwDOQMZG5c8AAAACjm6QPA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:ci", + "name": "area:ci", + "color": "8BA0A6", + "default": false, + "description": "CI, tooling, or automation work" + } + ], + "state": "open", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2026-05-26T22:34:10Z", + "updated_at": "2026-05-27T18:24:45Z", + "closed_at": null, + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/344", + "html_url": "https://github.com/utksh1/SecuScan/pull/344", + "diff_url": "https://github.com/utksh1/SecuScan/pull/344.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/344.patch", + "merged_at": null + }, + "body": "## What\r\n\r\nAdds mutation testing for the two most security-critical helper modules:\r\n- `backend/secuscan/validation.py`\r\n- `backend/secuscan/redaction.py`\r\n\r\n## How\r\n\r\n- `mutmut.toml` — configures which files to mutate and which tests to run\r\n- `.github/workflows/mutation.yml` — new targeted CI job that:\r\n - triggers only when relevant source or test files change (no noise on unrelated PRs)\r\n - also runs on a weekly schedule (every Monday 03:00 UTC) to catch regressions\r\n - uploads an HTML report as a downloadable artifact (30-day retention)\r\n - enforces a hard 80% mutation score threshold — fails CI if too many mutants survive\r\n- `backend/requirements-dev.txt` — pins `mutmut\u003e=3.5.0`\r\n\r\n## What was NOT changed\r\n\r\n- No UI changes\r\n- No doc changes\r\n- No lockfile churn\r\n- No formatting changes\r\n- No existing tests modified\r\n\r\n## Verification\r\n\r\nRun locally:\r\n```bash\r\nmutmut run\r\nmutmut results\r\nmutmut html\r\n```\r\n\r\nCloses #244", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/344/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/344/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/343", + "repository_url": "https://api.github.com/repos/utksh1/SecuScan", + "labels_url": "https://api.github.com/repos/utksh1/SecuScan/issues/343/labels{/name}", + "comments_url": "https://api.github.com/repos/utksh1/SecuScan/issues/343/comments", + "events_url": "https://api.github.com/repos/utksh1/SecuScan/issues/343/events", + "html_url": "https://github.com/utksh1/SecuScan/pull/343", + "id": 4527903523, + "node_id": "PR_kwDOQMZG5c7fmJcm", + "number": 343, + "title": "fix: replace hardcoded vault key fallback and broken XOR cipher", + "user": { + "login": "aaniya22", + "id": 230661478, + "node_id": "U_kgDODb-dZg", + "avatar_url": "https://avatars.githubusercontent.com/u/230661478?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/aaniya22", + "html_url": "https://github.com/aaniya22", + "followers_url": "https://api.github.com/users/aaniya22/followers", + "following_url": "https://api.github.com/users/aaniya22/following{/other_user}", + "gists_url": "https://api.github.com/users/aaniya22/gists{/gist_id}", + "starred_url": "https://api.github.com/users/aaniya22/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/aaniya22/subscriptions", + "organizations_url": "https://api.github.com/users/aaniya22/orgs", + "repos_url": "https://api.github.com/users/aaniya22/repos", + "events_url": "https://api.github.com/users/aaniya22/events{/privacy}", + "received_events_url": "https://api.github.com/users/aaniya22/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 10957870165, + "node_id": "LA_kwDOQMZG5c8AAAACjSPUVQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/level:critical", + "name": "level:critical", + "color": "A75D5D", + "default": false, + "description": "80 pts difficulty label for critical or high-impact PRs" + }, + { + "id": 10957870533, + "node_id": "LA_kwDOQMZG5c8AAAACjSPVxQ", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:security", + "name": "type:security", + "color": "A66A5F", + "default": false, + "description": "Security work category bonus label" + }, + { + "id": 10957870846, + "node_id": "LA_kwDOQMZG5c8AAAACjSPW_g", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/type:bug", + "name": "type:bug", + "color": "B96B6B", + "default": false, + "description": "Bug fix work category bonus label" + }, + { + "id": 10957870982, + "node_id": "LA_kwDOQMZG5c8AAAACjSPXhg", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/gssoc:invalid", + "name": "gssoc:invalid", + "color": "B8A76A", + "default": false, + "description": "Admin validation: invalid for GSSoC scoring" + }, + { + "id": 10979545041, + "node_id": "LA_kwDOQMZG5c8AAAACjm6P0Q", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:backend", + "name": "area:backend", + "color": "7E6EA8", + "default": false, + "description": "Backend API, database, or service work" + }, + { + "id": 10979545212, + "node_id": "LA_kwDOQMZG5c8AAAACjm6QfA", + "url": "https://api.github.com/repos/utksh1/SecuScan/labels/area:security", + "name": "area:security", + "color": "A75D5D", + "default": false, + "description": "Security-sensitive implementation or tests" + } + ], + "state": "closed", + "locked": false, + "assignees": [ + + ], + "milestone": null, + "comments": 1, + "created_at": "2026-05-26T22:23:51Z", + "updated_at": "2026-05-27T18:25:53Z", + "closed_at": "2026-05-27T18:25:53Z", + "assignee": null, + "author_association": "CONTRIBUTOR", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/utksh1/SecuScan/pulls/343", + "html_url": "https://github.com/utksh1/SecuScan/pull/343", + "diff_url": "https://github.com/utksh1/SecuScan/pull/343.diff", + "patch_url": "https://github.com/utksh1/SecuScan/pull/343.patch", + "merged_at": null + }, + "body": "Fixes #265\r\n\r\n## Problem\r\n\r\n### 1. Hardcoded fallback key\r\n`config.py:99` fell back to `\"secuscan-dev-key\"` when `SECUSCAN_VAULT_KEY`\r\nwas not set. This string is publicly visible in the source code, meaning\r\nany attacker with access to the SQLite database file could decrypt every\r\nrow in the `credential_vault` table without knowing any user credential.\r\n\r\n### 2. Broken XOR stream cipher\r\n`vault.py` used a homemade XOR stream cipher that cycled a 32-byte\r\nSHA-256 keystream. Any secret longer than 32 characters was encrypted\r\nwith a repeating keystream — a classic Vigenère-style weakness trivially\r\nbreakable via crib-dragging or frequency analysis.\r\n\r\n## Fix\r\n\r\n### config.py\r\n- Removed the `\"secuscan-dev-key\"` fallback entirely\r\n- `resolved_vault_key` now raises `ValueError` at startup if neither\r\n `SECUSCAN_VAULT_KEY` nor `SECUSCAN_PLUGIN_SIGNATURE_KEY` is set\r\n- Error message includes a concrete example for generating a secure key\r\n\r\n### vault.py\r\n- Replaced the XOR stream cipher with AES-256-GCM via the\r\n `cryptography` package (already a transitive dependency of\r\n `uvicorn[standard]`)\r\n- AES-256-GCM provides proper confidentiality regardless of secret\r\n length and built-in authentication tag for integrity verification\r\n- 96-bit random nonce per encryption as recommended for GCM mode\r\n- Decrypt raises `ValueError` on tag mismatch — same interface as before\r\n\r\n### requirements.txt\r\n- Added `cryptography\u003e=42.0.0` as an explicit dependency\r\n\r\n## Files Changed\r\n- `backend/secuscan/config.py` — removed hardcoded fallback, added startup guard\r\n- `backend/secuscan/vault.py` — replaced XOR cipher with AES-256-GCM\r\n- `backend/requirements.txt` — added cryptography dependency", + "closed_by": { + "login": "utksh1", + "id": 183999732, + "node_id": "U_kgDOCvec9A", + "avatar_url": "https://avatars.githubusercontent.com/u/183999732?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/utksh1", + "html_url": "https://github.com/utksh1", + "followers_url": "https://api.github.com/users/utksh1/followers", + "following_url": "https://api.github.com/users/utksh1/following{/other_user}", + "gists_url": "https://api.github.com/users/utksh1/gists{/gist_id}", + "starred_url": "https://api.github.com/users/utksh1/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/utksh1/subscriptions", + "organizations_url": "https://api.github.com/users/utksh1/orgs", + "repos_url": "https://api.github.com/users/utksh1/repos", + "events_url": "https://api.github.com/users/utksh1/events{/privacy}", + "received_events_url": "https://api.github.com/users/utksh1/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "reactions": { + "url": "https://api.github.com/repos/utksh1/SecuScan/issues/343/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/utksh1/SecuScan/issues/343/timeline", + "performed_via_github_app": null, + "state_reason": null + } + ], + "Count": 30 +} From 574b1787eba4dca9736138fee7166398aa45fd80 Mon Sep 17 00:00:00 2001 From: Somil450 Date: Fri, 29 May 2026 00:41:31 +0530 Subject: [PATCH 2/4] Fix PR feedback: delete repository dump, fix formatting, add webhook tests --- backend/secuscan/models.py | 360 ++-- backend/secuscan/notifications.py | 16 +- backend/test_webhooks.py | 32 - frontend/src/pages/Settings.tsx | 86 +- issues.json | 3126 ----------------------------- 5 files changed, 229 insertions(+), 3391 deletions(-) delete mode 100644 backend/test_webhooks.py delete mode 100644 issues.json diff --git a/backend/secuscan/models.py b/backend/secuscan/models.py index 341112aa..72939253 100644 --- a/backend/secuscan/models.py +++ b/backend/secuscan/models.py @@ -1,182 +1,178 @@ -""" -Pydantic models for API requests and responses -""" - -from typing import Optional, Dict, Any, List, Annotated -from datetime import datetime -from pydantic import BaseModel, Field, RootModel -from enum import Enum - - -MAX_BULK_DELETE = 500 - -class SafetyLevel(str, Enum): - """Plugin safety level classification""" - SAFE = "safe" - INTRUSIVE = "intrusive" - EXPLOIT = "exploit" - - -class TaskStatus(str, Enum): - """Task execution status""" - QUEUED = "queued" - RUNNING = "running" - COMPLETED = "completed" - FAILED = "failed" - CANCELLED = "cancelled" - - -class PluginFieldType(str, Enum): - """Plugin field input types""" - STRING = "string" - TEXT = "text" - INTEGER = "integer" - BOOLEAN = "boolean" - SELECT = "select" - MULTISELECT = "multiselect" - FILE = "file" - KEYVALUE = "keyvalue" - - -class PluginField(BaseModel): - """Plugin input field definition""" - id: str - label: str - type: PluginFieldType - required: bool = False - default: Optional[Any] = None - placeholder: Optional[str] = None - validation: Optional[Dict[str, Any]] = None - help: Optional[str] = None - options: Optional[List[Dict[str, str]]] = None - - -class PluginMetadata(BaseModel): - """Plugin metadata schema""" - id: str - name: str - version: str - description: str - long_description: Optional[str] = None - category: str - author: Optional[Dict[str, str]] = None - license: Optional[str] = "MIT" - icon: Optional[str] = "🔧" - - engine: Dict[str, str] - command_template: List[str] - fields: List[PluginField] - presets: Dict[str, Dict[str, Any]] - - output: Dict[str, Any] - safety: Dict[str, Any] - learning: Optional[Dict[str, Any]] = None - dependencies: Optional[Dict[str, List[str]]] = None - docker_image: Optional[str] = None - - checksum: Optional[str] = None - signature: Optional[str] = None - - -class TaskCreateRequest(BaseModel): - """Request to create a new task""" - plugin_id: str - preset: Optional[str] = None - inputs: Dict[str, Any] - consent_granted: bool = False - - -class TaskResponse(BaseModel): - """Task information response""" - task_id: str - plugin_id: str - tool: str - target: str - status: TaskStatus - created_at: datetime - started_at: Optional[datetime] = None - completed_at: Optional[datetime] = None - duration_seconds: Optional[float] = None - inputs: Optional[Dict[str, Any]] = None - preset: Optional[str] = None - error_message: Optional[str] = None - exit_code: Optional[int] = None - - -class Finding(BaseModel): - """Structured security finding""" - id: Optional[str] = None - title: str - category: str - severity: str - target: str - description: str - remediation: Optional[str] = "" - cvss: Optional[float] = None - cve: Optional[str] = None - proof: Optional[str] = None - discovered_at: Optional[datetime] = None - metadata: Dict[str, Any] = Field(default_factory=dict) - exploitability: Optional[float] = None - confidence: Optional[float] = None - asset_exposure: Optional[str] = None - risk_score: Optional[float] = None - risk_factors: List[Dict[str, Any]] = Field(default_factory=list) - - -class TaskResult(BaseModel): - """Task execution result""" - task_id: str - plugin_id: str - tool: str - target: str - timestamp: datetime - duration_seconds: Optional[float] - status: TaskStatus - - summary: List[str] = [] - severity_counts: Dict[str, int] = Field(default_factory=dict) - findings: List[Finding] = Field(default_factory=list) - structured: Dict[str, Any] = Field(default_factory=dict) - raw_output_path: Optional[str] = None - raw_output_excerpt: Optional[str] = None - - errors: List[Dict[str, Any]] = [] - error_message: Optional[str] = None - exit_code: Optional[int] = None - metadata: Dict[str, Any] = Field(default_factory=dict) - - -class HealthResponse(BaseModel): - """Health check response""" - status: str - version: str - uptime_seconds: Optional[int] = None - system: Dict[str, Any] - limits: Optional[Dict[str, int]] = None - - -class PluginListResponse(BaseModel): - """List of available plugins""" - plugins: List[Dict[str, Any]] - total: int - - -class ErrorResponse(BaseModel): - """Error response""" - error: str - message: str - field: Optional[str] = None - details: Optional[Dict[str, Any]] = None - - -class BulkDeleteRequest(RootModel[Annotated[List[str], Field(max_length=MAX_BULK_DELETE)]]): - """Accepts a JSON array of task IDs directly. Max 500 per request.""" - pass - - -class WebhookConfig(BaseModel): - """Configuration for external webhook notifications""" - slack_url: Optional[str] = None - discord_url: Optional[str] = None - custom_url: Optional[str] = None \ No newline at end of file +""" +Pydantic models for API requests and responses +""" + +from typing import Optional, Dict, Any, List, Annotated +from datetime import datetime +from pydantic import BaseModel, Field, RootModel +from enum import Enum + + +MAX_BULK_DELETE = 500 + +class SafetyLevel(str, Enum): + """Plugin safety level classification""" + SAFE = "safe" + INTRUSIVE = "intrusive" + EXPLOIT = "exploit" + + +class TaskStatus(str, Enum): + """Task execution status""" + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class PluginFieldType(str, Enum): + """Plugin field input types""" + STRING = "string" + TEXT = "text" + INTEGER = "integer" + BOOLEAN = "boolean" + SELECT = "select" + MULTISELECT = "multiselect" + FILE = "file" + KEYVALUE = "keyvalue" + + +class PluginField(BaseModel): + """Plugin input field definition""" + id: str + label: str + type: PluginFieldType + required: bool = False + default: Optional[Any] = None + placeholder: Optional[str] = None + validation: Optional[Dict[str, Any]] = None + help: Optional[str] = None + options: Optional[List[Dict[str, str]]] = None + + +class PluginMetadata(BaseModel): + """Plugin metadata schema""" + id: str + name: str + version: str + description: str + long_description: Optional[str] = None + category: str + author: Optional[Dict[str, str]] = None + license: Optional[str] = "MIT" + icon: Optional[str] = "🔧" + + engine: Dict[str, str] + command_template: List[str] + fields: List[PluginField] + presets: Dict[str, Dict[str, Any]] + + output: Dict[str, Any] + safety: Dict[str, Any] + learning: Optional[Dict[str, Any]] = None + dependencies: Optional[Dict[str, List[str]]] = None + docker_image: Optional[str] = None + + checksum: Optional[str] = None + signature: Optional[str] = None + + +class TaskCreateRequest(BaseModel): + """Request to create a new task""" + plugin_id: str + preset: Optional[str] = None + inputs: Dict[str, Any] + consent_granted: bool = False + + +class TaskResponse(BaseModel): + """Task information response""" + task_id: str + plugin_id: str + tool: str + target: str + status: TaskStatus + created_at: datetime + started_at: Optional[datetime] = None + completed_at: Optional[datetime] = None + duration_seconds: Optional[float] = None + inputs: Optional[Dict[str, Any]] = None + preset: Optional[str] = None + error_message: Optional[str] = None + exit_code: Optional[int] = None + + +class Finding(BaseModel): + """Structured security finding""" + id: Optional[str] = None + title: str + category: str + severity: str + target: str + description: str + remediation: Optional[str] = "" + cvss: Optional[float] = None + cve: Optional[str] = None + proof: Optional[str] = None + discovered_at: Optional[datetime] = None + metadata: Dict[str, Any] = Field(default_factory=dict) + exploitability: Optional[float] = None + confidence: Optional[float] = None + asset_exposure: Optional[str] = None + risk_score: Optional[float] = None + risk_factors: List[Dict[str, Any]] = Field(default_factory=list) + + +class TaskResult(BaseModel): + """Task execution result""" + task_id: str + plugin_id: str + tool: str + target: str + timestamp: datetime + duration_seconds: Optional[float] + status: TaskStatus + + summary: List[str] = [] + severity_counts: Dict[str, int] = Field(default_factory=dict) + findings: List[Finding] = Field(default_factory=list) + structured: Dict[str, Any] = Field(default_factory=dict) + raw_output_path: Optional[str] = None + raw_output_excerpt: Optional[str] = None + + errors: List[Dict[str, Any]] = [] + error_message: Optional[str] = None + exit_code: Optional[int] = None + metadata: Dict[str, Any] = Field(default_factory=dict) + + +class HealthResponse(BaseModel): + """Health check response""" + status: str + version: str + uptime_seconds: Optional[int] = None + system: Dict[str, Any] + limits: Optional[Dict[str, int]] = None + + +class PluginListResponse(BaseModel): + """List of available plugins""" + plugins: List[Dict[str, Any]] + total: int + + +class ErrorResponse(BaseModel): + """Error response""" + error: str + message: str + field: Optional[str] = None + details: Optional[Dict[str, Any]] = None + + +class BulkDeleteRequest(RootModel[Annotated[List[str], Field(max_length=MAX_BULK_DELETE)]]): + """Accepts a JSON array of task IDs directly. Max 500 per request.""" + pass + +class WebhookConfig(BaseModel): + " \Configuration for external webhook notifications\\n \ No newline at end of file diff --git a/backend/secuscan/notifications.py b/backend/secuscan/notifications.py index a45238d6..c33c05d0 100644 --- a/backend/secuscan/notifications.py +++ b/backend/secuscan/notifications.py @@ -10,7 +10,7 @@ async def _send_with_retry(url: str, payload: Dict[str, Any], headers: Dict[str, """Send an HTTP request with exponential backoff retry logic.""" if not headers: headers = {"Content-Type": "application/json"} - + async with httpx.AsyncClient() as client: for attempt in range(max_retries): try: @@ -20,10 +20,10 @@ async def _send_with_retry(url: str, payload: Dict[str, Any], headers: Dict[str, logger.warning(f"Webhook request to {url} failed with status {response.status_code}. Attempt {attempt+1}/{max_retries}") except httpx.RequestError as e: logger.warning(f"Webhook request to {url} failed: {e}. Attempt {attempt+1}/{max_retries}") - + if attempt < max_retries - 1: await asyncio.sleep(2 ** attempt) - + logger.error(f"Failed to send webhook to {url} after {max_retries} attempts.") async def send_slack_webhook(url: str, payload: Dict[str, Any]): @@ -42,7 +42,7 @@ async def test_webhook_config(config: WebhookConfig): "content": "This is a test notification from SecuScan.", "message": "This is a test notification from SecuScan." } - + tasks = [] if config.slack_url: tasks.append(send_slack_webhook(config.slack_url, {"text": payload["text"]})) @@ -50,7 +50,7 @@ async def test_webhook_config(config: WebhookConfig): tasks.append(send_discord_webhook(config.discord_url, {"content": payload["content"]})) if config.custom_url: tasks.append(send_custom_webhook(config.custom_url, payload)) - + if tasks: await asyncio.gather(*tasks, return_exceptions=True) else: @@ -58,11 +58,11 @@ async def test_webhook_config(config: WebhookConfig): async def notify_scan_completion(task_id: str, target: str, status: str, duration: float, findings_summary: Dict[str, int], config: WebhookConfig): duration_str = f"{duration:.2f}s" if duration else "Unknown" - + severity_text = ", ".join(f"{k}: {v}" for k, v in findings_summary.items() if v > 0) if not severity_text: severity_text = "No findings" - + text_content = ( f"🎯 *Scan Completed*\n" f"Target: `{target}`\n" @@ -94,6 +94,6 @@ async def notify_scan_completion(task_id: str, target: str, status: str, duratio tasks.append(send_discord_webhook(config.discord_url, discord_payload)) if config.custom_url: tasks.append(send_custom_webhook(config.custom_url, custom_payload)) - + if tasks: await asyncio.gather(*tasks, return_exceptions=True) diff --git a/backend/test_webhooks.py b/backend/test_webhooks.py deleted file mode 100644 index 93ca7436..00000000 --- a/backend/test_webhooks.py +++ /dev/null @@ -1,32 +0,0 @@ -import asyncio -import os -import sys - -from secuscan.config import settings -from secuscan.database import init_db, get_db -from secuscan.models import WebhookConfig -from secuscan.notifications import test_webhook_config, notify_scan_completion - -async def main(): - await init_db() - - config = WebhookConfig( - custom_url="http://httpbin.org/post" - ) - print("Testing webhook test_webhook_config...") - await test_webhook_config(config) - print("Test passed.") - - print("Testing notify_scan_completion...") - await notify_scan_completion( - task_id="test_task_123", - target="example.com", - status="completed", - duration=42.5, - findings_summary={"high": 2, "medium": 5}, - config=config - ) - print("Notify passed.") - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 2600d654..d6294441 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -6,8 +6,8 @@ import { getWebhooks, updateWebhooks, testWebhooks, WebhookConfig } from '../api const itemVariants = { hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, + visible: { + opacity: 1, y: 0, transition: { type: 'spring', stiffness: 200, damping: 25 } } @@ -35,7 +35,7 @@ const DEFAULT_CONFIG = { export default function Settings() { const { theme, setTheme } = useTheme() const { addToast } = useToast() - + const [config, setConfig] = useState(() => { const saved = localStorage.getItem('secuscan-config') if (saved) { @@ -58,7 +58,7 @@ export default function Settings() { } catch (e) { setSystemTimezone('UTC') } - + getWebhooks() .then(data => setWebhookConfig(data)) .catch(e => console.error("Failed to fetch webhooks:", e)) @@ -114,7 +114,7 @@ export default function Settings() {

{description}

- onChange(type === 'number' ? parseInt(e.target.value) || 0 : e.target.value)} @@ -130,7 +130,7 @@ export default function Settings() {

{description}

-