fix: web UI error handling (#289)

* Add failed tasks counter and error reporting

* Remove failed tasks tab

* Simplify changes

* Fix pov ui error

* Fix PoV dashboard tab. Fix lint.

* Update tests
This commit is contained in:
Evan Downing
2025-08-18 13:47:09 -04:00
committed by GitHub
parent aa2af7fce7
commit 2cd76b303a
8 changed files with 851 additions and 179 deletions
@@ -56,7 +56,7 @@ _settings: Settings | None = None
_database_manager: DatabaseManager | None = None
# Compatibility - keeping dashboard_stats as backup/cache
dashboard_stats = {"activeTasks": 0, "totalPovs": 0, "totalPatches": 0, "totalBundles": 0}
dashboard_stats = {"activeTasks": 0, "totalPovs": 0, "totalPatches": 0, "totalBundles": 0, "failedTasks": 0}
# Dashboard models
@@ -77,12 +77,19 @@ class TaskInfo(BaseModel):
bundles: List[Dict[str, Any]] = []
created_at: str
# CRS submission status and error information
crs_submission_status: Optional[str] = None # 'pending', 'success', 'failed'
crs_submission_error: Optional[str] = None
crs_error_details: Optional[str] = None # JSON string
crs_submission_timestamp: Optional[str] = None
class DashboardStats(BaseModel):
activeTasks: int
totalPovs: int
totalPatches: int
totalBundles: int
failedTasks: int
class Challenge(BaseModel):
@@ -291,12 +298,23 @@ if static_dir.exists():
# Utility functions for task management
def calculate_task_status(deadline: datetime) -> str:
"""Calculate task status based on deadline."""
def calculate_task_status(task: Task) -> str:
"""Calculate task status based on deadline and CRS submission status."""
try:
now = datetime.now(deadline.tzinfo)
# First check if the task failed to submit to CRS
crs_status = getattr(task, "crs_submission_status", None)
if crs_status == "failed":
return "failed"
# Then check deadline
deadline = getattr(task, "deadline", None)
if deadline is None:
return "active" # Default to active if no deadline
now = datetime.now(deadline.tzinfo) if deadline.tzinfo else datetime.now()
if now > deadline:
return "expired"
return "active"
except Exception:
return "active" # Default to active if parsing fails
@@ -310,10 +328,14 @@ def update_dashboard_stats(database_manager: DatabaseManager) -> None:
total_povs = 0
total_patches = 0
total_bundles = 0
failed_tasks_count = 0
with database_manager.get_all_tasks() as tasks:
for task in tasks:
if calculate_task_status(task.deadline) == "active":
task_status = calculate_task_status(task)
if task_status == "active":
active_count += 1
if task_status == "failed":
failed_tasks_count += 1
total_povs += len(task.povs)
total_patches += len(task.patches)
@@ -325,76 +347,104 @@ def update_dashboard_stats(database_manager: DatabaseManager) -> None:
"totalPovs": total_povs,
"totalPatches": total_patches,
"totalBundles": total_bundles,
"failedTasks": failed_tasks_count,
}
)
def pov_to_pov_info(pov: POV) -> dict[str, Any]:
return {
"pov_id": pov.pov_id,
"timestamp": pov.created_at,
"architecture": pov.architecture,
"engine": pov.engine,
"fuzzer_name": pov.fuzzer_name,
"sanitizer": pov.sanitizer,
"testcase": base64.b64encode(pov.testcase),
}
try:
return {
"pov_id": getattr(pov, "pov_id", "unknown"),
"timestamp": getattr(pov, "created_at", datetime.now()),
"architecture": getattr(pov, "architecture", "unknown"),
"engine": getattr(pov, "engine", "unknown"),
"fuzzer_name": getattr(pov, "fuzzer_name", "unknown"),
"sanitizer": getattr(pov, "sanitizer", "unknown"),
"testcase": base64.b64encode(getattr(pov, "testcase", b"")),
}
except Exception as e:
logger.error(f"Error converting POV to info: {e}")
return {"pov_id": "error", "error": str(e)}
def patch_to_patch_info(patch: Patch) -> dict[str, Any]:
return {
"patch_id": patch.patch_id,
"timestamp": patch.created_at,
"patch": base64.b64encode(patch.patch.encode("utf-8", errors="ignore")),
}
try:
return {
"patch_id": getattr(patch, "patch_id", "unknown"),
"timestamp": getattr(patch, "created_at", datetime.now()),
"patch": base64.b64encode(getattr(patch, "patch", "").encode("utf-8", errors="ignore")),
}
except Exception as e:
logger.error(f"Error converting patch to info: {e}")
return {"patch_id": "error", "error": str(e)}
def bundle_to_bundle_info(bundle: Bundle) -> dict[str, Any]:
return {
"bundle_id": bundle.bundle_id,
"timestamp": bundle.created_at,
"description": bundle.description,
"broadcast_sarif_id": bundle.broadcast_sarif_id,
"freeform_id": bundle.freeform_id,
"patch_id": bundle.patch_id,
"pov_id": bundle.pov_id,
"submitted_sarif_id": bundle.submitted_sarif_id,
}
try:
return {
"bundle_id": getattr(bundle, "bundle_id", "unknown"),
"timestamp": getattr(bundle, "created_at", datetime.now()),
"description": getattr(bundle, "description", ""),
"broadcast_sarif_id": getattr(bundle, "broadcast_sarif_id", None),
"freeform_id": getattr(bundle, "freeform_id", None),
"patch_id": getattr(bundle, "patch_id", None),
"pov_id": getattr(bundle, "pov_id", None),
"submitted_sarif_id": getattr(bundle, "submitted_sarif_id", None),
}
except Exception as e:
logger.error(f"Error converting bundle to info: {e}")
return {"bundle_id": "error", "error": str(e)}
def task_to_task_info(task: Task) -> TaskInfo:
"""Convert a task to a TaskInfo object."""
povs = []
for pov in task.povs:
povs.append(pov_to_pov_info(pov))
try:
povs = []
for pov in task.povs:
povs.append(pov_to_pov_info(pov))
patches = []
for patch in task.patches:
patches.append(patch_to_patch_info(patch))
patches = []
for patch in task.patches:
patches.append(patch_to_patch_info(patch))
bundles = []
for bundle in task.bundles:
bundles.append(bundle_to_bundle_info(bundle))
bundles = []
for bundle in task.bundles:
bundles.append(bundle_to_bundle_info(bundle))
task_data = {
"task_id": task.task_id,
"name": task.name,
"project_name": task.project_name,
"status": calculate_task_status(task.deadline),
"duration": task.duration,
"deadline": datetime.isoformat(task.deadline),
"challenge_repo_url": task.challenge_repo_url,
"challenge_repo_head_ref": task.challenge_repo_head_ref,
"challenge_repo_base_ref": task.challenge_repo_base_ref,
"fuzz_tooling_url": task.fuzz_tooling_url,
"fuzz_tooling_ref": task.fuzz_tooling_ref,
"created_at": datetime.isoformat(task.created_at),
"povs": povs,
"patches": patches,
"bundles": bundles,
}
task_data = {
"task_id": task.task_id,
"name": getattr(task, "name", None),
"project_name": getattr(task, "project_name", "Unknown"),
"status": calculate_task_status(task),
"duration": getattr(task, "duration", 0),
"deadline": getattr(task, "deadline", datetime.now()).isoformat()
if getattr(task, "deadline", None)
else None,
"challenge_repo_url": getattr(task, "challenge_repo_url", None),
"challenge_repo_head_ref": getattr(task, "challenge_repo_head_ref", None),
"challenge_repo_base_ref": getattr(task, "challenge_repo_base_ref", None),
"fuzz_tooling_url": getattr(task, "fuzz_tooling_url", None),
"fuzz_tooling_ref": getattr(task, "fuzz_tooling_ref", None),
"created_at": getattr(task, "created_at", datetime.now()).isoformat()
if getattr(task, "created_at", None)
else None,
"povs": povs,
"patches": patches,
"bundles": bundles,
# CRS submission status and error information
"crs_submission_status": getattr(task, "crs_submission_status", None),
"crs_submission_error": getattr(task, "crs_submission_error", None),
"crs_error_details": getattr(task, "crs_error_details", None),
"crs_submission_timestamp": getattr(task, "crs_submission_timestamp", datetime.fromtimestamp(0)).isoformat()
if getattr(task, "crs_submission_timestamp", None)
else None,
}
return TaskInfo(**task_data)
return TaskInfo(**task_data)
except Exception as e:
logger.error(f"Error in task_to_task_info for task {getattr(task, 'task_id', 'unknown')}: {e}", exc_info=True)
raise
def _create_task(
@@ -418,6 +468,8 @@ def _create_task(
now = datetime.now()
deadline = now + timedelta(seconds=challenge.duration)
# Always create the task in the database first
logger.info(f"Creating task {task_id} in database for challenge {challenge.name}")
database_manager.create_task(
task_id=task_id,
name=name,
@@ -431,18 +483,104 @@ def _create_task(
fuzz_tooling_url=challenge.fuzz_tooling_url,
fuzz_tooling_ref=challenge.fuzz_tooling_ref,
)
logger.info(f"Task {task_id} created successfully in database")
# Send task to CRS via POST /v1/task endpoint
if crs_client.submit_task(task):
logger.info(f"Submitting task {task_id} to CRS...")
crs_response = crs_client.submit_task(task)
# Update the task with CRS submission results
if crs_response.success:
logger.info(f"Task {task_id} submitted successfully to CRS")
return Message(message=f"Task {task_id} created and submitted to CRS for challenge {challenge.name}")
database_manager.update_task_crs_status(task_id=task_id, crs_submission_status="success")
logger.info(f"Task {task_id} CRS status updated to 'success'")
return Message(
message=f"Task {task_id} created and submitted successfully to CRS for challenge {challenge.name}"
)
else:
logger.error(f"Failed to submit task {task_id} to CRS")
return Error(message="Failed to submit task to CRS")
# Generate user-friendly error message
error_message = crs_response.get_user_friendly_error_message("Failed to submit task to CRS")
logger.error(f"Task {task_id} error message: {error_message}")
# Update the task with error information
logger.info(f"Updating task {task_id} CRS status to 'failed' with error details")
database_manager.update_task_crs_status(
task_id=task_id,
crs_submission_status="failed",
crs_submission_error=error_message,
crs_error_details=crs_response.error_details,
)
logger.info(f"Task {task_id} CRS status updated to 'failed' with error details")
# Verify the task was updated correctly
with database_manager.get_task(task_id) as updated_task:
if updated_task:
logger.info(f"Verified task {task_id} in database:")
logger.info(f" - CRS status: {getattr(updated_task, 'crs_submission_status', 'N/A')}")
logger.info(f" - CRS error: {getattr(updated_task, 'crs_submission_error', 'N/A')}")
logger.info(f" - CRS error details: {getattr(updated_task, 'crs_error_details', 'N/A')}")
else:
logger.error(f"Task {task_id} not found in database after update!")
# Return success message since the task was created, but include the CRS error
return Message(
message=f"Task {task_id} created but failed to submit to CRS. "
f"Task is visible in the dashboard with error details. "
f"Error: {error_message}",
color="error",
)
except Exception as e:
logger.error(f"Error creating task for challenge {challenge.name}: {e}")
return Error(message=f"Failed to create task: {str(e)}")
# Create a failed task in the database even when the challenge service fails
try:
# Generate a unique task ID for the failed task
failed_task_id = str(uuid.uuid4())
name = challenge.name or f"failed-{failed_task_id[:8]}"
now = datetime.now()
deadline = now + timedelta(seconds=challenge.duration)
logger.info(f"Creating failed task {failed_task_id} in database for challenge {challenge.name}")
database_manager.create_task(
task_id=failed_task_id,
name=name,
project_name=challenge.fuzz_tooling_project_name,
status="failed",
duration=challenge.duration,
deadline=deadline,
challenge_repo_url=challenge.challenge_repo_url,
challenge_repo_head_ref=challenge.challenge_repo_head_ref,
challenge_repo_base_ref=challenge.challenge_repo_base_ref,
fuzz_tooling_url=challenge.fuzz_tooling_url,
fuzz_tooling_ref=challenge.fuzz_tooling_ref,
)
# Mark it as failed with the error details
error_message = f"Failed to create task: {str(e)}"
database_manager.update_task_crs_status(
task_id=failed_task_id,
crs_submission_status="failed",
crs_submission_error=error_message,
crs_error_details={"exception": str(e), "error_type": "challenge_service_failure"},
)
logger.info(f"Failed task {failed_task_id} created in database with error details")
# Return a message indicating the task was created but failed
return Message(
message=f"Task {failed_task_id} created but failed during setup. "
f"Task is visible in the dashboard with error details. "
f"Error: {error_message}",
color="error",
)
except Exception as db_error:
logger.error(f"Failed to create failed task in database: {db_error}")
# If we can't even create the failed task, return the original error
return Error(message=f"Failed to create task: {str(e)}")
def _create_sarif_broadcast(
@@ -460,10 +598,22 @@ def _create_sarif_broadcast(
sarif = body["sarif"]
broadcast = challenge_service.create_sarif_broadcast(task_id, sarif)
if crs_client.submit_sarif_broadcast(broadcast):
crs_response = crs_client.submit_sarif_broadcast(broadcast)
if crs_response.success:
return Message(message=f"SARIF Broadcast for Task {task_id} created and submitted to CRS")
else:
return Error(message=f"Failed to submit SARIF Broadcast for Task {task_id} to CRS")
# Use the helper method to generate user-friendly error message
error_message = crs_response.get_user_friendly_error_message(
f"Failed to submit SARIF Broadcast for Task {task_id} to CRS"
)
# Return a message with the error details for better user experience
return Message(
message=f"SARIF Broadcast for Task {task_id} created but failed to submit to CRS. Error: {error_message}",
color="error",
)
@app.get("/v1/ping/", response_model=PingResponse, tags=["ping"])
@@ -536,6 +686,74 @@ def get_dashboard_tasks(database_manager: DatabaseManager = Depends(get_database
return tasks_list
@app.get("/v1/dashboard/tasks/failed", response_model=List[TaskInfo], tags=["dashboard"])
def get_failed_tasks(database_manager: DatabaseManager = Depends(get_database_manager)) -> List[TaskInfo]:
"""
Get list of failed tasks for debugging
"""
logger.info("get_failed_tasks endpoint called")
try:
failed_tasks = database_manager.get_tasks_by_crs_status("failed")
logger.info(f"Found {len(failed_tasks)} failed tasks in database")
if failed_tasks:
logger.info(f"First failed task ID: {failed_tasks[0].task_id}")
logger.info(f"First failed task CRS status: {getattr(failed_tasks[0], 'crs_submission_status', 'N/A')}")
logger.info(f"First failed task CRS error: {getattr(failed_tasks[0], 'crs_submission_error', 'N/A')}")
tasks_list = []
for task in failed_tasks:
try:
task_info = task_to_task_info(task)
tasks_list.append(task_info)
logger.info(f"Converted task {task.task_id} to TaskInfo with status: {task_info.status}")
except Exception as task_error:
logger.error(f"Error converting task {task.task_id} to TaskInfo: {task_error}", exc_info=True)
# Skip this task and continue with others
continue
# Sort by created_at descending (newest first)
tasks_list.sort(key=lambda x: x.created_at, reverse=True)
logger.info(f"Returning {len(tasks_list)} failed tasks")
return tasks_list
except Exception as e:
logger.error(f"Error in get_failed_tasks: {e}", exc_info=True)
raise
@app.get("/v1/dashboard/tasks/active", response_model=List[TaskInfo], tags=["dashboard"])
def get_active_tasks(database_manager: DatabaseManager = Depends(get_database_manager)) -> List[TaskInfo]:
"""
Get list of active tasks
"""
active_tasks = database_manager.get_tasks_by_status("active")
tasks_list = []
for task in active_tasks:
tasks_list.append(task_to_task_info(task))
# Sort by created_at descending (newest first)
tasks_list.sort(key=lambda x: x.created_at, reverse=True)
return tasks_list
@app.get("/v1/dashboard/tasks/expired", response_model=List[TaskInfo], tags=["dashboard"])
def get_expired_tasks(database_manager: DatabaseManager = Depends(get_database_manager)) -> List[TaskInfo]:
"""
Get list of expired tasks
"""
expired_tasks = database_manager.get_tasks_by_status("expired")
tasks_list = []
for task in expired_tasks:
tasks_list.append(task_to_task_info(task))
# Sort by created_at descending (newest first)
tasks_list.sort(key=lambda x: x.created_at, reverse=True)
return tasks_list
@app.get("/v1/dashboard/tasks/{task_id}", response_model=TaskInfo, tags=["dashboard"])
def get_dashboard_task(task_id: str, database_manager: DatabaseManager = Depends(get_database_manager)) -> TaskInfo:
"""
@@ -548,6 +766,36 @@ def get_dashboard_task(task_id: str, database_manager: DatabaseManager = Depends
return task_to_task_info(task)
@app.get("/v1/dashboard/tasks/{task_id}/crs-status", tags=["dashboard"])
def get_task_crs_status(task_id: str, database_manager: DatabaseManager = Depends(get_database_manager)) -> dict:
"""
Get detailed CRS submission status and error information for a specific task
"""
with database_manager.get_task(task_id) as task:
if not task:
raise HTTPException(status_code=404, detail="Task not found")
import json
# Parse error details if present
error_details = None
if hasattr(task, "crs_error_details") and task.crs_error_details:
try:
error_details = json.loads(task.crs_error_details)
except json.JSONDecodeError:
error_details = {"raw": task.crs_error_details}
return {
"task_id": task_id,
"crs_submission_status": getattr(task, "crs_submission_status", None),
"crs_submission_error": getattr(task, "crs_submission_error", None),
"crs_error_details": error_details,
"crs_submission_timestamp": getattr(task, "crs_submission_timestamp", datetime.fromtimestamp(0)).isoformat()
if getattr(task, "crs_submission_timestamp", None)
else None,
}
@app.get(
"/v1/request/list/",
response_model=RequestListResponse,
@@ -49,7 +49,8 @@ class FuzzingEngine(Enum):
class Message(BaseModel):
message: Optional[str] = None
message: str
color: Optional[str] = "default" # "default", "success", "warning", "error"
class POVSubmission(BaseModel):
@@ -2,19 +2,190 @@ from __future__ import annotations
import logging
import requests
from typing import Optional, Dict, Any
from buttercup.orchestrator.ui.competition_api.models.crs_types import Task, SARIFBroadcast
logger = logging.getLogger(__name__)
class CRSResponse:
"""Response from CRS API calls with detailed status and error information."""
def __init__(
self, success: bool, status_code: int, response_text: str = "", error_details: Optional[Dict[str, Any]] = None
):
self.success = success
self.status_code = status_code
self.response_text = response_text
self.error_details = error_details or {}
def get_user_friendly_error_message(self, base_message: str = "Operation failed") -> str:
"""
Generate a user-friendly error message from the response details.
Args:
base_message: Base message to start with
Returns:
Formatted error message suitable for display to users
"""
if self.success:
return ""
error_message = base_message
# Add specific error information
if self.error_details.get("error_message"):
error_message += f": {self.error_details['error_message']}"
# Add validation errors if present
validation_errors = self.error_details.get("validation_errors", {})
if validation_errors:
if isinstance(validation_errors, dict):
# Format validation errors in a user-friendly way
formatted_errors = []
for field, error in validation_errors.items():
if isinstance(error, list):
formatted_errors.append(f"{field}: {', '.join(error)}")
else:
formatted_errors.append(f"{field}: {error}")
error_message += f". Validation errors: {'; '.join(formatted_errors)}"
else:
error_message += f". Validation errors: {validation_errors}"
# Add error code if present
if self.error_details.get("error_code"):
error_message += f" (Error code: {self.error_details['error_code']})"
# Add HTTP status code information if it's not a standard success code
if self.status_code not in (200, 202):
error_message += f" (HTTP status: {self.status_code})"
return error_message
def log_detailed_response(self, logger_instance: logging.Logger, operation: str = "CRS operation") -> None:
"""
Log detailed response information for debugging purposes.
Args:
logger_instance: Logger instance to use
operation: Description of the operation being logged
"""
if self.success:
logger_instance.info(f"{operation} completed successfully (HTTP {self.status_code})")
else:
logger_instance.error(f"{operation} failed (HTTP {self.status_code})")
logger_instance.error(f"Response text: {self.response_text}")
if self.error_details:
logger_instance.error(f"Error details: {self.error_details}")
# Log specific error information
if self.error_details.get("error_message"):
logger_instance.error(f"Error message: {self.error_details['error_message']}")
if self.error_details.get("error_code"):
logger_instance.error(f"Error code: {self.error_details['error_code']}")
if self.error_details.get("validation_errors"):
logger_instance.error(f"Validation errors: {self.error_details['validation_errors']}")
if self.error_details.get("status"):
logger_instance.error(f"Status: {self.error_details['status']}")
if self.error_details.get("success_indicator") is not None:
logger_instance.error(f"Success indicator: {self.error_details['success_indicator']}")
if self.error_details.get("accepted") is not None:
logger_instance.error(f"Accepted: {self.error_details['accepted']}")
if self.error_details.get("valid") is not None:
logger_instance.error(f"Valid: {self.error_details['valid']}")
@classmethod
def from_response(cls, response: requests.Response) -> CRSResponse:
"""Create a CRSResponse from a requests.Response object."""
try:
# Try to parse JSON response for detailed error information
response_data = response.json()
# Check if the response indicates success despite HTTP status
# Look for common error indicators in the response body
if isinstance(response_data, dict):
# Check for various error fields that might be present in the response
error_message = (
response_data.get("error", "")
or response_data.get("message", "")
or response_data.get("detail", "")
or response_data.get("reason", "")
)
error_code = response_data.get("error_code", "") or response_data.get("code", "")
validation_errors = (
response_data.get("validation_errors", {})
or response_data.get("errors", {})
or response_data.get("field_errors", {})
or response_data.get("constraint_violations", {})
)
# Check for success/failure indicators in the response
status = response_data.get("status", "").lower()
success_indicator = response_data.get("success", None)
# Look for additional failure indicators
has_failure_indicators = (
error_message
or error_code
or validation_errors
or status in ["error", "failed", "failure", "invalid", "rejected"]
or success_indicator is False
or response_data.get("accepted", True) is False
or response_data.get("valid", True) is False
)
# Determine if this is actually a success or failure
# Even with 200/202 status, the response body might indicate failure
is_success = response.status_code in (200, 202) and not has_failure_indicators
return cls(
success=is_success,
status_code=response.status_code,
response_text=response.text,
error_details={
"error_message": error_message,
"error_code": error_code,
"validation_errors": validation_errors,
"status": status,
"success_indicator": success_indicator,
"accepted": response_data.get("accepted"),
"valid": response_data.get("valid"),
"raw_response": response_data,
},
)
else:
# Non-JSON response, treat as success if status is 200/202
return cls(
success=response.status_code in (200, 202),
status_code=response.status_code,
response_text=response.text,
)
except (ValueError, TypeError):
# Response is not valid JSON, treat as success if status is 200/202
return cls(
success=response.status_code in (200, 202),
status_code=response.status_code,
response_text=response.text,
)
class CRSClient:
def __init__(self, crs_base_url: str, username: str | None = None, password: str | None = None) -> None:
self.crs_base_url = crs_base_url.rstrip("/")
self.username = username
self.password = password
def submit_task(self, task: Task) -> bool:
def submit_task(self, task: Task) -> CRSResponse:
"""
Submit a task to the CRS via POST /v1/task endpoint
@@ -22,7 +193,7 @@ class CRSClient:
task: Task object to submit
Returns:
True if successful, False otherwise
CRSResponse object with detailed status and error information
"""
url = f"{self.crs_base_url}/v1/task/"
@@ -42,18 +213,19 @@ class CRSClient:
timeout=30,
)
if response.status_code in (202, 200):
logger.info(f"Task {task.tasks[0].task_id} submitted successfully to CRS")
return True
else:
logger.error(f"Failed to submit task to CRS. Status: {response.status_code}, Response: {response.text}")
return False
# Create detailed response object
crs_response = CRSResponse.from_response(response)
# Log detailed response information
crs_response.log_detailed_response(logger, f"Task submission for {task.tasks[0].task_id}")
return crs_response
except Exception as e:
logger.error(f"Error submitting task to CRS: {e}")
return False
return CRSResponse(success=False, status_code=0, response_text=str(e), error_details={"exception": str(e)})
def submit_sarif_broadcast(self, broadcast: SARIFBroadcast) -> bool:
def submit_sarif_broadcast(self, broadcast: SARIFBroadcast) -> CRSResponse:
"""
Submit a SARIF Broadcast to the CRS via POST /v1/sarif/ endpoint
"""
@@ -75,18 +247,19 @@ class CRSClient:
timeout=30,
)
if response.status_code in (202, 200):
logger.info("SARIF Broadcasts submitted successfully to CRS")
return True
else:
logger.error(
f"Failed to submit SARIF Broadcasts to CRS. Status: {response.status_code}, Response: {response.text}"
)
return False
# Create detailed response object
crs_response = CRSResponse.from_response(response)
# Log detailed response information
crs_response.log_detailed_response(
logger, f"SARIF Broadcast submission for {len(broadcast.broadcasts)} tasks"
)
return crs_response
except Exception as e:
logger.error(f"Error submitting SARIF Broadcasts to CRS: {e}")
return False
return CRSResponse(success=False, status_code=0, response_text=str(e), error_details={"exception": str(e)})
def ping(self) -> bool:
"""
@@ -43,6 +43,12 @@ class Task(Base):
fuzz_tooling_ref: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
# Error tracking fields
crs_submission_error: Mapped[str | None] = mapped_column(Text, nullable=True)
crs_error_details: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON string for detailed error info
crs_submission_status: Mapped[str | None] = mapped_column(String, nullable=True) # 'pending', 'success', 'failed'
crs_submission_timestamp: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
# Relationships
povs: Mapped[list["POV"]] = relationship("POV", back_populates="task", cascade="all, delete-orphan")
patches: Mapped[list["Patch"]] = relationship("Patch", back_populates="task", cascade="all, delete-orphan")
@@ -133,6 +139,35 @@ class DatabaseManager:
yield task
session.commit()
def get_tasks_by_crs_status(self, crs_status: str) -> list[Task]:
"""Get all tasks with a specific CRS submission status."""
with self.get_session() as session:
tasks = session.query(Task).filter(Task.crs_submission_status == crs_status).all()
session.commit()
return tasks
def get_tasks_by_status(self, status: str) -> list[Task]:
"""Get all tasks with a specific overall status (active, expired, failed)."""
with self.get_session() as session:
if status == "failed":
# For failed status, check CRS submission status
tasks = session.query(Task).filter(Task.crs_submission_status == "failed").all()
else:
# For other statuses, we need to calculate based on deadline
from datetime import datetime
now = datetime.now()
if status == "active":
tasks = session.query(Task).filter(Task.deadline > now).all()
elif status == "expired":
tasks = session.query(Task).filter(Task.deadline <= now).all()
else:
tasks = []
session.commit()
return tasks
@contextmanager
def get_povs_for_task(self, task_id: str) -> Iterator[list[POV]]:
"""Context manager: yields all POVs for a task with session open."""
@@ -250,6 +285,32 @@ class DatabaseManager:
logger.info(f"Created task: {task.task_id}")
return task
def update_task_crs_status(
self,
*,
task_id: str,
crs_submission_status: str,
crs_submission_error: str | None = None,
crs_error_details: dict | None = None,
) -> None:
"""Update the CRS submission status and error information for a task."""
import json
with self.get_session() as session:
task = session.query(Task).filter(Task.task_id == task_id).first()
if task:
task.crs_submission_status = crs_submission_status
task.crs_submission_error = crs_submission_error
task.crs_submission_timestamp = datetime.now()
if crs_error_details:
task.crs_error_details = json.dumps(crs_error_details, indent=2)
session.commit()
logger.info(f"Updated CRS status for task {task_id}: {crs_submission_status}")
else:
logger.warning(f"Task {task_id} not found for CRS status update")
# POV operations
def create_pov(
self, *, task_id: str, architecture: str, engine: str, fuzzer_name: str, sanitizer: str, testcase: bytes
@@ -26,6 +26,11 @@
<div class="stat-number" id="active-tasks">-</div>
<div class="stat-label">Currently running</div>
</div>
<div class="card failed-tasks" disabled>
<h3>Failed Tasks</h3>
<div class="stat-number" id="failed-tasks">-</div>
<div class="stat-label">CRS submissions failed</div>
</div>
<div class="card">
<h3>Total PoVs</h3>
<div class="stat-number" id="total-povs">-</div>
@@ -61,6 +66,7 @@
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="expired">Expired</option>
<option value="failed">Failed</option>
</select>
</div>
</div>
@@ -32,13 +32,15 @@ const elements = {
patchesContainer: document.getElementById('patches-container'),
statusFilter: document.getElementById('status-filter'),
activeTasks: document.getElementById('active-tasks'),
failedTasks: document.getElementById('failed-tasks'),
totalPovs: document.getElementById('total-povs'),
totalPatches: document.getElementById('total-patches'),
totalBundles: document.getElementById('total-bundles'),
detailTitle: document.getElementById('detail-title'),
detailContent: document.getElementById('detail-content'),
tabButtons: document.querySelectorAll('.tab-button'),
tabPanes: document.querySelectorAll('.tab-pane')
tabPanes: document.querySelectorAll('.tab-pane'),
notifications: document.getElementById('notifications')
};
// Initialize the dashboard
@@ -205,7 +207,8 @@ async function loadDashboard() {
// Load tasks from API
async function loadTasks() {
try {
const response = await fetch(`${API_BASE}/v1/dashboard/tasks`);
// Add timestamp to prevent caching
const response = await fetch(`${API_BASE}/v1/dashboard/tasks?t=${Date.now()}`);
if (response.ok) {
tasks = await response.json();
} else {
@@ -308,6 +311,7 @@ function calculateStatsFromTasks() {
function updateDashboard() {
// Update stats
elements.activeTasks.textContent = dashboardStats.activeTasks;
elements.failedTasks.textContent = dashboardStats.failedTasks || 0;
elements.totalPovs.textContent = dashboardStats.totalPovs;
elements.totalPatches.textContent = dashboardStats.totalPatches;
elements.totalBundles.textContent = dashboardStats.totalBundles;
@@ -336,6 +340,11 @@ async function loadAndRenderPatches() {
// Render PoVs list
function renderPovs() {
if (!elements.povsContainer) {
console.error('PoVs container not found!');
return;
}
if (allPovs.length === 0) {
elements.povsContainer.innerHTML = `
<div class="no-data">
@@ -395,20 +404,26 @@ function renderPatches() {
// Render tasks list
function renderTasks() {
const filteredTasks = filterTasksByStatus();
if (!elements.tasksContainer) {
console.error('Tasks container not found!');
return;
}
const filteredTasks = filterTasksByStatus(tasks);
if (filteredTasks.length === 0) {
elements.tasksContainer.innerHTML = `
<div class="no-data">
<div class="no-data-icon">📋</div>
<div class="no-data-icon">📝</div>
<p>No tasks found</p>
<p class="no-data-subtitle">Create a new task to get started</p>
</div>
`;
return;
}
elements.tasksContainer.innerHTML = filteredTasks.map(task => `
<div class="task-item" onclick="showTaskDetail('${task.task_id}')">
<div class="task-item ${task.crs_submission_status === 'failed' ? 'task-failed' : ''}" onclick="showTaskDetail('${task.task_id}')">
<div class="task-info">
<div class="task-name">${task.name || task.project_name}</div>
<div class="task-id">ID: ${task.task_id}</div>
@@ -418,6 +433,11 @@ function renderTasks() {
<span>Created: ${formatTimestamp(task.created_at)}</span>
<span>Deadline: ${formatTimestamp(task.deadline)}</span>
</div>
${task.crs_submission_error ? `
<div class="crs-error-details">
<strong>Error:</strong> ${task.crs_submission_error}
</div>
` : ''}
</div>
<div class="task-status">
<span class="status-badge status-${task.status}">${task.status}</span>
@@ -482,12 +502,29 @@ async function handleExampleTaskSubmission() {
body: JSON.stringify(exampleTaskData)
});
const result = await response.json();
console.log('Example task submission response:', result);
console.log('Response message:', result.message);
console.log('Response color:', result.color);
if (response.ok) {
const result = await response.json();
showNotification('Example libpng task submitted successfully!', 'success');
// Refresh dashboard after a short delay
setTimeout(loadDashboard, 1000);
// Check if the response indicates a CRS submission failure or setup failure
if (result.message && (result.message.includes('failed to submit to CRS') || result.message.includes('failed during setup'))) {
console.log('Example task creation/submission failed, showing error notification');
// Task was created but failed - show notification with proper color
showNotification(result.message, null, result.color);
// Force refresh failed tasks and main tasks list
setTimeout(async () => {
await forceRefreshFailedTasks();
}, 1000);
} else {
// Complete success
showNotification(result.message || 'Example libpng task submitted successfully!', 'success');
// Refresh dashboard after a short delay
setTimeout(loadDashboard, 1000);
}
} else {
const error = await response.json();
showNotification(`Error: ${error.message || 'Failed to submit example task'}`, 'error');
@@ -527,17 +564,37 @@ async function handleTaskSubmission(event) {
body: JSON.stringify(taskData)
});
const result = await response.json();
console.log('Task submission response:', result);
console.log('Response message:', result.message);
console.log('Response color:', result.color);
if (response.ok) {
const result = await response.json();
showNotification('Task submitted successfully!', 'success');
elements.taskModal.style.display = 'none';
elements.taskForm.reset();
// Refresh dashboard after a short delay
setTimeout(loadDashboard, 1000);
// Check if the response indicates a CRS submission failure or setup failure
if (result.message && (result.message.includes('failed to submit to CRS') || result.message.includes('failed during setup'))) {
console.log('Task creation/submission failed, showing error notification');
// Task was created but failed - show notification with proper color
showNotification(result.message, null, result.color);
elements.taskModal.style.display = 'none';
elements.taskForm.reset();
// Force refresh failed tasks and main tasks list
setTimeout(async () => {
await forceRefreshFailedTasks();
}, 1000);
} else {
// Complete success
showNotification(result.message || 'Task submitted successfully!', 'success');
elements.taskModal.style.display = 'none';
elements.taskForm.reset();
// Refresh dashboard after a short delay
setTimeout(loadDashboard, 1000);
}
} else {
const error = await response.json();
showNotification(`Error: ${error.message || 'Failed to submit task'}`, 'error');
// HTTP error - show error message
const errorMessage = result.message || result.detail || 'Failed to submit task';
showNotification(`Error: ${errorMessage}`, 'error');
}
} catch (error) {
console.error('Error submitting task:', error);
@@ -578,28 +635,20 @@ function renderTaskDetail(task) {
<div class="detail-section">
<h3>Task Information</h3>
<div class="detail-grid">
<div class="detail-label">Task ID:</div>
<div class="detail-value">${task.task_id}</div>
<div class="detail-label">Name:</div>
<div class="detail-value">${task.name || 'N/A'}</div>
<div class="detail-value">${task.name || task.project_name}</div>
<div class="detail-label">ID:</div>
<div class="detail-value">${task.task_id}</div>
<div class="detail-label">Project:</div>
<div class="detail-value">${task.project_name}</div>
<div class="detail-label">Status:</div>
<div class="detail-value">
<span class="status-badge status-${task.status}">${task.status}</span>
</div>
<div class="detail-label">Duration:</div>
<div class="detail-value">${formatDuration(task.duration)}</div>
<div class="detail-label">Created:</div>
<div class="detail-value">${formatTimestamp(task.created_at)}</div>
<div class="detail-label">Deadline:</div>
<div class="detail-value">${formatTimestamp(task.deadline)}</div>
<div class="detail-label">Repository:</div>
<div class="detail-value">${task.challenge_repo_url || 'N/A'}</div>
<div class="detail-label">Head Ref:</div>
<div class="detail-value">${task.challenge_repo_head_ref || 'N/A'}</div>
<div class="detail-label">Base Ref:</div>
<div class="detail-value">${task.challenge_repo_base_ref || 'N/A'}</div>
</div>
</div>
@@ -825,6 +874,9 @@ function renderArtifactDetail(detailData, type) {
// Not base64, use as is
}
}
const patchPreview = patchContent.length > 300
? patchContent.substring(0, 300) + '...'
: patchContent;
specificContent = `
<div class="detail-label">Patch Size:</div>
<div class="detail-value">${originalSize} characters (${patchContent.length} decoded)</div>
@@ -879,44 +931,29 @@ function formatTimestamp(timestamp) {
return new Date(timestamp).toLocaleString();
}
function showNotification(message, type = 'info') {
// Create a simple notification system
function showNotification(message, type = 'info', color = null) {
// If color is provided from backend Message, use it to override type
if (color === 'error') {
type = 'error';
} else if (color === 'warning') {
type = 'warning';
} else if (color === 'success') {
type = 'success';
}
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
// Add notification styles if not already added
if (!document.querySelector('style[data-notifications]')) {
const style = document.createElement('style');
style.setAttribute('data-notifications', 'true');
style.textContent = `
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 4px;
color: white;
font-weight: 500;
z-index: 2000;
animation: slideIn 0.3s ease;
}
.notification-success { background-color: #4caf50; }
.notification-error { background-color: #f44336; }
.notification-info { background-color: #2196f3; }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
}
// Add to notifications container
const container = document.getElementById('notifications') || document.body;
container.appendChild(notification);
document.body.appendChild(notification);
// Remove notification after 5 seconds
// Auto-remove after 5 seconds
setTimeout(() => {
notification.remove();
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
@@ -1019,4 +1056,4 @@ function getMockTasks() {
bundles: []
}
];
}
}
@@ -55,15 +55,26 @@ body {
.card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.2s ease;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.card.failed-tasks {
border-left: 4px solid #f44336;
background: linear-gradient(135deg, #fff 0%, #ffebee 100%);
}
.card.failed-tasks:hover {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
}
.card h3 {
@@ -148,13 +159,22 @@ body {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #eee;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e0e0e0;
}
.section-header h2 {
margin: 0;
color: #333;
font-size: 1.25rem;
font-size: 1.5rem;
}
.section-description {
margin: 0.5rem 0 0 0;
color: #666;
font-size: 0.9rem;
font-style: italic;
}
.filter-controls select {
@@ -215,29 +235,160 @@ body {
gap: 0.5rem;
}
/* Task status badges */
.status-badge {
padding: 0.25rem 0.5rem;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-active {
background-color: #e3f2fd;
color: #1976d2;
}
.status-completed {
background-color: #e8f5e8;
color: #2e7d32;
background-color: #4caf50;
color: white;
}
.status-expired {
background-color: #fce4ec;
color: #c2185b;
background-color: #ff9800;
color: white;
}
.status-failed {
background-color: #f44336;
color: white;
}
/* CRS status indicators */
.crs-error-info,
.crs-success-info,
.crs-pending-info {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.crs-error-info {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
.crs-success-info {
background-color: #e8f5e8;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.crs-pending-info {
background-color: #fff3e0;
color: #ef6c00;
border: 1px solid #ffcc02;
}
.crs-error-icon,
.crs-success-icon,
.crs-pending-icon {
font-size: 1rem;
}
/* Failed task styling */
.task-failed {
border-left: 4px solid #f44336;
background-color: #ffebee;
}
.task-failed:hover {
background-color: #ffcdd2;
}
/* CRS error details */
.crs-error-details {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #fff3e0;
border: 1px solid #ffcc02;
border-radius: 4px;
font-size: 0.8rem;
color: #e65100;
}
.crs-error-details strong {
color: #bf360c;
}
/* Notification system */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 4px;
color: white;
font-weight: 500;
z-index: 2000;
animation: slideIn 0.3s ease;
max-width: 400px;
word-wrap: break-word;
}
.notification-success {
background-color: #4caf50;
}
.notification-error {
background-color: #f44336;
}
.notification-warning {
background-color: #ff9800;
}
.notification-info {
background-color: #2196f3;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Error message styling for backend errors */
.error-message {
color: #D8000C !important;
background-color: #FFBABA !important;
border: 1px solid #D8000C !important;
padding: 10px !important;
margin: 10px 0 !important;
border-radius: 5px !important;
display: flex !important;
align-items: center !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
font-size: 14px !important;
line-height: 1.4 !important;
box-shadow: 0 2px 8px rgba(216, 0, 12, 0.15) !important;
}
.error-message::before {
content: '⚠' !important;
font-size: 20px !important;
margin-right: 10px !important;
flex-shrink: 0 !important;
}
/* Make dashboard stats clickable */
.task-stats {
display: flex;
gap: 1rem;
@@ -483,14 +634,20 @@ form {
/* No data states */
.no-data {
text-align: center;
padding: 2rem;
padding: 3rem 2rem;
color: #666;
}
.no-data-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.3;
opacity: 0.6;
}
.no-data-subtitle {
margin-top: 0.5rem;
font-size: 0.9rem;
color: #888;
}
/* Artifacts container */
@@ -609,15 +766,4 @@ form {
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
}
/* Make dashboard stats clickable */
.status-card {
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.status-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
+4 -4
View File
@@ -55,7 +55,7 @@ class TestCRSClient:
result = crs_client.submit_task(sample_task)
assert result is True
assert result.success is True
# Verify request was made correctly
mock_post.assert_called_once_with(
@@ -76,7 +76,7 @@ class TestCRSClient:
result = crs_client_no_auth.submit_task(sample_task)
assert result is True
assert result.success is True
# Verify request was made without auth
mock_post.assert_called_once_with(
@@ -98,7 +98,7 @@ class TestCRSClient:
result = crs_client.submit_task(sample_task)
assert result is False
assert result.success is False
@patch("requests.post")
def test_submit_task_exception(self, mock_post, crs_client, sample_task):
@@ -108,7 +108,7 @@ class TestCRSClient:
result = crs_client.submit_task(sample_task)
assert result is False
assert result.success is False
@patch("requests.get")
def test_ping_success_ready(self, mock_get, crs_client):