diff --git a/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py b/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py
index 4122fd57..fc7bd7ea 100644
--- a/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py
+++ b/orchestrator/src/buttercup/orchestrator/ui/competition_api/main.py
@@ -768,6 +768,28 @@ def get_dashboard_task(task_id: str, database_manager: DatabaseManager = Depends
return task_to_task_info(task)
+@app.delete(
+ "/v1/dashboard/tasks/{task_id}",
+ response_model=Message,
+ tags=["dashboard"],
+)
+def cancel_dashboard_task(
+ task_id: str,
+ crs_client: CRSClient = Depends(get_crs_client),
+) -> Message | Error:
+ """Cancel a task by forwarding a DELETE request to the task server."""
+ logger.info(f"Dashboard: cancelling task {task_id}")
+ response = crs_client.cancel_task(task_id)
+ if response.success:
+ return Message(
+ message=f"Task {task_id} cancellation requested",
+ color="success",
+ )
+ error_msg = response.get_user_friendly_error_message()
+ logger.error(f"Failed to cancel task {task_id}: {error_msg}")
+ return Error(message=f"Failed to cancel task: {error_msg}")
+
+
@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"""
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 0d042643..2d6d030b 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
@@ -229,6 +229,44 @@ def submit_task(self, task: Task) -> CRSResponse:
logger.error(f"Error submitting task to CRS: {e}")
return CRSResponse(success=False, status_code=0, response_text=str(e), error_details={"exception": str(e)})
+ def cancel_task(self, task_id: str) -> CRSResponse:
+ """Cancel a task by sending a DELETE request to the CRS.
+
+ Args:
+ task_id: UUID of the task to cancel
+
+ Returns:
+ CRSResponse object with detailed status and error information
+
+ """
+ url = f"{self.crs_base_url}/v1/task/{task_id}/"
+
+ auth = None
+ if self.username and self.password:
+ auth = (self.username, self.password)
+
+ try:
+ logger.info(f"Cancelling task {task_id} via CRS at {url}")
+
+ response = requests.delete(
+ url,
+ auth=auth,
+ timeout=30,
+ )
+
+ crs_response = CRSResponse.from_response(response)
+ crs_response.log_detailed_response(logger, f"Task cancellation for {task_id}")
+ return crs_response
+
+ except Exception as e:
+ logger.error(f"Error cancelling task via CRS: {e}")
+ return CRSResponse(
+ success=False,
+ status_code=0,
+ response_text=str(e),
+ error_details={"exception": str(e)},
+ )
+
def submit_sarif_broadcast(self, broadcast: SARIFBroadcast) -> CRSResponse:
"""Submit a SARIF Broadcast to the CRS via POST /v1/sarif/ endpoint"""
url = f"{self.crs_base_url}/v1/sarif/"
diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/index.html b/orchestrator/src/buttercup/orchestrator/ui/static/index.html
index 4df4313f..75f786e0 100644
--- a/orchestrator/src/buttercup/orchestrator/ui/static/index.html
+++ b/orchestrator/src/buttercup/orchestrator/ui/static/index.html
@@ -12,6 +12,7 @@
Buttercup CRS Dashboard
+
@@ -66,6 +67,7 @@ Tasks
+
@@ -155,6 +157,8 @@ Submit New Task
placeholder="1800">
+
+
@@ -176,6 +180,36 @@
Task Details
+
+
+
diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/script.js b/orchestrator/src/buttercup/orchestrator/ui/static/script.js
index 9f6d1088..5215826e 100644
--- a/orchestrator/src/buttercup/orchestrator/ui/static/script.js
+++ b/orchestrator/src/buttercup/orchestrator/ui/static/script.js
@@ -29,7 +29,19 @@ const elements = {
closeDetailModal: document.getElementById('close-detail-modal'),
taskForm: document.getElementById('task-form'),
fillExampleBtn: document.getElementById('fill-example-btn'),
+ loadJsonBtn: document.getElementById('load-json-btn'),
+ loadJsonInput: document.getElementById('load-json-input'),
cancelBtn: document.getElementById('cancel-btn'),
+ submitSarifBtn: document.getElementById('submit-sarif-btn'),
+ sarifModal: document.getElementById('sarif-modal'),
+ sarifModalContent: document.querySelector('#sarif-modal .modal-content'),
+ closeSarifModal: document.getElementById('close-sarif-modal'),
+ sarifForm: document.getElementById('sarif-form'),
+ sarifTaskSelect: document.getElementById('sarif-task-select'),
+ sarifFileInput: document.getElementById('sarif-file-input'),
+ sarifPreview: document.getElementById('sarif-preview'),
+ sarifPreviewContent: document.getElementById('sarif-preview-content'),
+ cancelSarifBtn: document.getElementById('cancel-sarif-btn'),
tasksContainer: document.getElementById('tasks-container'),
povsContainer: document.getElementById('povs-container'),
patchesContainer: document.getElementById('patches-container'),
@@ -85,6 +97,28 @@ function setupEventListeners() {
} else {
console.error('fillExampleBtn element not found');
}
+
+ if (elements.loadJsonBtn && elements.loadJsonInput) {
+ elements.loadJsonBtn.addEventListener('click', () => {
+ elements.loadJsonInput.click();
+ });
+ elements.loadJsonInput.addEventListener('change', handleJsonFileLoad);
+ }
+
+ elements.submitSarifBtn.addEventListener('click', () => {
+ openSarifModal();
+ });
+
+ elements.closeSarifModal.addEventListener('click', () => {
+ elements.sarifModal.style.display = 'none';
+ });
+
+ elements.cancelSarifBtn.addEventListener('click', () => {
+ elements.sarifModal.style.display = 'none';
+ });
+
+ elements.sarifFileInput.addEventListener('change', handleSarifFilePreview);
+ elements.sarifForm.addEventListener('submit', handleSarifSubmission);
elements.refreshBtn.addEventListener('click', loadDashboard);
@@ -129,19 +163,24 @@ function setupEventListeners() {
let mousePressedOutside = false;
window.addEventListener('mousedown', (event) => {
- // Check if mouse was pressed outside both modal contents
- if (!elements.taskModalContent.contains(event.target) && !elements.detailModalContent.contains(event.target)) {
- mousePressedOutside = true;
- } else {
- mousePressedOutside = false;
- }
+ // Check if mouse was pressed outside all modal contents
+ const insideAnyModal =
+ elements.taskModalContent.contains(event.target) ||
+ elements.detailModalContent.contains(event.target) ||
+ elements.sarifModalContent.contains(event.target);
+ mousePressedOutside = !insideAnyModal;
});
window.addEventListener('mouseup', (event) => {
// Only close if mouse was pressed outside and released outside
- if (mousePressedOutside && !elements.taskModalContent.contains(event.target) && !elements.detailModalContent.contains(event.target)) {
+ const insideAnyModal =
+ elements.taskModalContent.contains(event.target) ||
+ elements.detailModalContent.contains(event.target) ||
+ elements.sarifModalContent.contains(event.target);
+ if (mousePressedOutside && !insideAnyModal) {
elements.taskModal.style.display = 'none';
elements.detailModal.style.display = 'none';
+ elements.sarifModal.style.display = 'none';
}
});
}
@@ -514,6 +553,11 @@ function renderTasks() {
${task.status}
+ ${task.status === 'active' ? `
+
+ ` : ''}
@@ -593,6 +637,60 @@ function fillExampleValues() {
}
}
+function handleJsonFileLoad(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ try {
+ const data = JSON.parse(e.target.result);
+
+ // Map JSON keys to form field IDs
+ const fieldMap = {
+ 'name': 'task-name',
+ 'challenge_repo_url': 'challenge-repo-url',
+ 'challenge_repo_head_ref': 'challenge-repo-head-ref',
+ 'challenge_repo_base_ref': 'challenge-repo-base-ref',
+ 'fuzz_tooling_url': 'fuzz-tooling-url',
+ 'fuzz_tooling_ref': 'fuzz-tooling-ref',
+ 'fuzz_tooling_project_name': 'fuzz-tooling-project-name',
+ 'duration': 'duration',
+ };
+
+ for (const [jsonKey, fieldId] of Object.entries(fieldMap)) {
+ if (data[jsonKey] !== undefined && data[jsonKey] !== null) {
+ const field = document.getElementById(fieldId);
+ if (field) {
+ field.value = data[jsonKey];
+ }
+ }
+ }
+
+ if (data.harnesses_included !== undefined) {
+ const checkbox = document.getElementById('harnesses-included');
+ if (checkbox) {
+ checkbox.checked = !!data.harnesses_included;
+ }
+ }
+
+ showNotification(
+ `Loaded challenge from ${file.name}`,
+ 'success',
+ );
+ } catch (err) {
+ showNotification(
+ `Failed to parse JSON file: ${err.message}`,
+ 'error',
+ );
+ }
+ };
+ reader.readAsText(file);
+
+ // Reset so the same file can be loaded again
+ event.target.value = '';
+}
+
async function handleTaskSubmission(event) {
event.preventDefault();
@@ -731,6 +829,17 @@ function renderTaskDetail(task) {
+ ${task.status === 'active' ? `
+
+
+
+
+ ` : ''}
+
${renderArtifacts('PoVs (Vulnerabilities)', task.povs || [], 'pov')}
${renderArtifacts('Patches', task.patches || [], 'patch')}
${renderArtifacts('Bundles', task.bundles || [], 'bundle')}
@@ -834,6 +943,168 @@ function renderArtifact(artifact, type) {
`;
}
+// Cancel task
+async function cancelTask(taskId) {
+ if (!confirm('Are you sure you want to cancel this task?')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`${API_BASE}/v1/dashboard/tasks/${taskId}`, {
+ method: 'DELETE',
+ });
+
+ const result = await response.json();
+
+ if (response.ok) {
+ showNotification(
+ result.message || 'Task cancellation requested',
+ null,
+ result.color,
+ );
+ loadDashboard();
+ } else {
+ const errorMessage = result.message || 'Failed to cancel task';
+ showNotification(`Error: ${errorMessage}`, 'error');
+ }
+ } catch (error) {
+ console.error('Cancel task error:', error);
+ showNotification('Error cancelling task', 'error');
+ }
+}
+
+// Open SARIF modal, optionally pre-selecting a task
+function openSarifModal(preselectedTaskId) {
+ // Populate the task dropdown with active tasks
+ const activeTaskList = tasks.filter(t => t.status === 'active');
+ elements.sarifTaskSelect.innerHTML =
+ '';
+ activeTaskList.forEach(t => {
+ const label = t.name || t.project_name;
+ const opt = document.createElement('option');
+ opt.value = t.task_id;
+ opt.textContent = `${label} (${t.task_id.substring(0, 8)}...)`;
+ elements.sarifTaskSelect.appendChild(opt);
+ });
+
+ if (preselectedTaskId) {
+ elements.sarifTaskSelect.value = preselectedTaskId;
+ }
+
+ // Reset file input and preview
+ elements.sarifFileInput.value = '';
+ elements.sarifPreview.style.display = 'none';
+ elements.sarifPreviewContent.textContent = '';
+
+ elements.sarifModal.style.display = 'block';
+}
+
+// Preview SARIF file contents when selected
+function handleSarifFilePreview(event) {
+ const file = event.target.files[0];
+ if (!file) {
+ elements.sarifPreview.style.display = 'none';
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ try {
+ const parsed = JSON.parse(e.target.result);
+ const runsCount = (parsed.runs || []).length;
+ const resultsCount = (parsed.runs || []).reduce(
+ (sum, run) => sum + (run.results || []).length,
+ 0,
+ );
+ const version = parsed.version || 'unknown';
+ const preview =
+ `SARIF version: ${version}\n` +
+ `Runs: ${runsCount}\n` +
+ `Results: ${resultsCount}\n\n` +
+ JSON.stringify(parsed, null, 2).substring(0, 500);
+ elements.sarifPreviewContent.textContent =
+ preview.length >= 500 ? preview + '\n...' : preview;
+ elements.sarifPreview.style.display = 'block';
+ } catch (err) {
+ elements.sarifPreviewContent.textContent =
+ 'Invalid JSON: ' + err.message;
+ elements.sarifPreview.style.display = 'block';
+ }
+ };
+ reader.readAsText(file);
+}
+
+// Handle SARIF form submission
+async function handleSarifSubmission(event) {
+ event.preventDefault();
+
+ const taskId = elements.sarifTaskSelect.value;
+ if (!taskId) {
+ showNotification('Please select a task', 'error');
+ return;
+ }
+
+ const file = elements.sarifFileInput.files[0];
+ if (!file) {
+ showNotification('Please select a SARIF file', 'error');
+ return;
+ }
+
+ const submitBtn = elements.sarifForm.querySelector(
+ 'button[type="submit"]',
+ );
+ const originalText = submitBtn.textContent;
+ submitBtn.disabled = true;
+ submitBtn.innerHTML = ' Submitting...';
+
+ try {
+ const text = await file.text();
+ const sarif = JSON.parse(text);
+
+ const response = await fetch(`${API_BASE}/webhook/sarif`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ task_id: taskId, sarif: sarif }),
+ });
+
+ const result = await response.json();
+
+ if (response.ok) {
+ if (
+ result.message &&
+ result.message.includes('failed')
+ ) {
+ showNotification(result.message, null, result.color);
+ } else {
+ showNotification(
+ result.message || 'SARIF submitted successfully!',
+ 'success',
+ );
+ }
+ elements.sarifModal.style.display = 'none';
+ } else {
+ const errorMessage =
+ result.message ||
+ result.detail ||
+ 'Failed to submit SARIF';
+ showNotification(`Error: ${errorMessage}`, 'error');
+ }
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ showNotification(
+ 'Invalid SARIF file: not valid JSON',
+ 'error',
+ );
+ } else {
+ console.error('Error submitting SARIF:', error);
+ showNotification('Network error occurred', 'error');
+ }
+ } finally {
+ submitBtn.disabled = false;
+ submitBtn.textContent = originalText;
+ }
+}
+
// Approve patch
async function approvePatch(taskId, patchId) {
try {
diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/styles.css b/orchestrator/src/buttercup/orchestrator/ui/static/styles.css
index 037005bf..63907967 100644
--- a/orchestrator/src/buttercup/orchestrator/ui/static/styles.css
+++ b/orchestrator/src/buttercup/orchestrator/ui/static/styles.css
@@ -466,6 +466,11 @@ body {
background-color: #c82333;
}
+.btn-sm {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.8rem;
+}
+
/* Modal styles */
.modal {
display: none;