diff --git a/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py b/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py index f38d3a1a..8751172c 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py +++ b/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py @@ -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, diff --git a/orchestrator/src/buttercup/orchestrator/ui/competition_api/models/types.py b/orchestrator/src/buttercup/orchestrator/ui/competition_api/models/types.py index a3de59fa..40cb9c5b 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/competition_api/models/types.py +++ b/orchestrator/src/buttercup/orchestrator/ui/competition_api/models/types.py @@ -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): diff --git a/orchestrator/src/buttercup/orchestrator/ui/competition_api/services/crs_client.py b/orchestrator/src/buttercup/orchestrator/ui/competition_api/services/crs_client.py index 1cf9f32f..e6d3839e 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/competition_api/services/crs_client.py +++ b/orchestrator/src/buttercup/orchestrator/ui/competition_api/services/crs_client.py @@ -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: """ diff --git a/orchestrator/src/buttercup/orchestrator/ui/database.py b/orchestrator/src/buttercup/orchestrator/ui/database.py index e850156f..1eba81c5 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/database.py +++ b/orchestrator/src/buttercup/orchestrator/ui/database.py @@ -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 diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/index.html b/orchestrator/src/buttercup/orchestrator/ui/static/index.html index 17997857..17e5fa00 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/index.html +++ b/orchestrator/src/buttercup/orchestrator/ui/static/index.html @@ -26,6 +26,11 @@
No tasks found
+Create a new task to get started