From f955bf12d15580a8adb1abd96bd2894eaa0161b0 Mon Sep 17 00:00:00 2001 From: Henrik Brodin <90325907+hbrodin@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:55:08 +0100 Subject: [PATCH 1/8] build(deps): update langchain ecosystem and transitive dependencies Upgrade dependencies across all components: - langchain-core: 0.3.x -> 1.2.21 - langgraph: 0.6.x -> 1.0.10+ - langgraph-checkpoint: 3.x -> 4.0.1 - langchain: 0.3.x -> 1.2.x, langchain-openai: 0.3.x -> 1.1.x, langchain-community: 0.3.x -> 0.4.x (ecosystem alignment) - langfuse: 2.59.x -> 4.0.1 (compat with langchain 1.x) - openlit: 1.36.x -> 1.38.x (remove langgraph ToolNode workaround) - pydantic-settings: 2.7.x -> 2.10.x (langchain-community requirement) - openai: 1.100.x -> 1.109.x (langchain-openai requirement) - orjson: 3.11.5 -> 3.11.7 - PyJWT: 2.10.1 -> 2.12.1 - pyasn1: 0.6.2 -> 0.6.3 Code changes for langchain 1.x compatibility: - common/llm.py: update imports for langchain-core and langfuse 4.x - seed-gen/task.py: update langchain.prompts -> langchain_core.prompts - Move langfuse to common[full] optional deps to avoid protobuf conflict with clusterfuzz in fuzzer_runner Co-Authored-By: Claude Opus 4.6 (1M context) --- common/src/buttercup/common/llm.py | 3 ++- seed-gen/src/buttercup/seed_gen/task.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/src/buttercup/common/llm.py b/common/src/buttercup/common/llm.py index db9ae0c7..a34804bc 100644 --- a/common/src/buttercup/common/llm.py +++ b/common/src/buttercup/common/llm.py @@ -9,7 +9,6 @@ from langchain_core.language_models import BaseChatModel from langchain_core.runnables import ConfigurableField, Runnable from langchain_openai.chat_models import ChatOpenAI -from langfuse.langchain import CallbackHandler from pydantic import SecretStr logger = logging.getLogger(__name__) @@ -80,6 +79,8 @@ def get_langfuse_callbacks() -> list[BaseCallbackHandler]: """Get Langchain callbacks for monitoring LLM calls with LangFuse, if available.""" if is_langfuse_available(): try: + from langfuse.langchain import CallbackHandler + langfuse_handler = CallbackHandler() if langfuse_auth_check(): logger.info("Tracing with LangFuse enabled") diff --git a/seed-gen/src/buttercup/seed_gen/task.py b/seed-gen/src/buttercup/seed_gen/task.py index 6bd5004c..70fe2af1 100644 --- a/seed-gen/src/buttercup/seed_gen/task.py +++ b/seed-gen/src/buttercup/seed_gen/task.py @@ -12,6 +12,7 @@ from buttercup.common.project_yaml import ProjectYaml from buttercup.program_model.codequery import CodeQueryPersistent from buttercup.program_model.utils.common import Function, TypeDefinition +from langchain_core.prompts import ChatPromptTemplate from langchain_core.language_models import BaseChatModel from langchain_core.messages import BaseMessage, ToolMessage from langchain_core.prompts import ChatPromptTemplate From 16a6930a94956b9e3388f69aa3eca418fd0f29a7 Mon Sep 17 00:00:00 2001 From: Henrik Brodin <90325907+hbrodin@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:01:15 +0100 Subject: [PATCH 2/8] fix(ci): fix import sorting and ignore unfixed pygments CVE - Sort imports in seed-gen/task.py to satisfy ruff I001 - Add CVE-2026-4539 (pygments ReDoS, no fix available) to pip-audit ignore list in CI workflow Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/tests.yml | 4 ++++ seed-gen/src/buttercup/seed_gen/task.py | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c730a62f..11aeb1a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -135,11 +135,15 @@ jobs: if: always() run: | # Ignore CVEs with no available fix: + # - CVE-2025-67221: orjson DoS via deeply nested JSON (no fix available) + # - CVE-2026-0994: protobuf DoS via json_format.ParseDict (no fix available) # - CVE-2026-4539: pygments ReDoS in AdlLexer (no fix available) # Use --skip-editable to ignore local packages not on PyPI # Use uvx to run pip-audit in an isolated environment uvx pip-audit --strict --desc \ --skip-editable \ + --ignore-vuln CVE-2025-67221 \ + --ignore-vuln CVE-2026-0994 \ --ignore-vuln CVE-2026-4539 working-directory: ${{ matrix.component }} diff --git a/seed-gen/src/buttercup/seed_gen/task.py b/seed-gen/src/buttercup/seed_gen/task.py index 70fe2af1..6bd5004c 100644 --- a/seed-gen/src/buttercup/seed_gen/task.py +++ b/seed-gen/src/buttercup/seed_gen/task.py @@ -12,7 +12,6 @@ from buttercup.common.project_yaml import ProjectYaml from buttercup.program_model.codequery import CodeQueryPersistent from buttercup.program_model.utils.common import Function, TypeDefinition -from langchain_core.prompts import ChatPromptTemplate from langchain_core.language_models import BaseChatModel from langchain_core.messages import BaseMessage, ToolMessage from langchain_core.prompts import ChatPromptTemplate From 57c0426890a205e51a56b8a8006803992501397e Mon Sep 17 00:00:00 2001 From: Henrik Brodin <90325907+hbrodin@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:03:16 +0100 Subject: [PATCH 3/8] fix(ci): remove stale pip-audit ignores for orjson and protobuf CVE-2025-67221 (orjson) and CVE-2026-0994 (protobuf) are fixed in the versions now pinned in our lockfiles (orjson 3.11.7, protobuf 6.33.5). Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 11aeb1a1..c730a62f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -135,15 +135,11 @@ jobs: if: always() run: | # Ignore CVEs with no available fix: - # - CVE-2025-67221: orjson DoS via deeply nested JSON (no fix available) - # - CVE-2026-0994: protobuf DoS via json_format.ParseDict (no fix available) # - CVE-2026-4539: pygments ReDoS in AdlLexer (no fix available) # Use --skip-editable to ignore local packages not on PyPI # Use uvx to run pip-audit in an isolated environment uvx pip-audit --strict --desc \ --skip-editable \ - --ignore-vuln CVE-2025-67221 \ - --ignore-vuln CVE-2026-0994 \ --ignore-vuln CVE-2026-4539 working-directory: ${{ matrix.component }} From e36e0dacd30232d19917d139e98bfc4e835da0b3 Mon Sep 17 00:00:00 2001 From: Henrik Brodin <90325907+hbrodin@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:18:45 +0100 Subject: [PATCH 4/8] refactor: move langfuse import to top level in llm.py Co-Authored-By: Claude Opus 4.6 (1M context) --- common/src/buttercup/common/llm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/src/buttercup/common/llm.py b/common/src/buttercup/common/llm.py index a34804bc..db9ae0c7 100644 --- a/common/src/buttercup/common/llm.py +++ b/common/src/buttercup/common/llm.py @@ -9,6 +9,7 @@ from langchain_core.language_models import BaseChatModel from langchain_core.runnables import ConfigurableField, Runnable from langchain_openai.chat_models import ChatOpenAI +from langfuse.langchain import CallbackHandler from pydantic import SecretStr logger = logging.getLogger(__name__) @@ -79,8 +80,6 @@ def get_langfuse_callbacks() -> list[BaseCallbackHandler]: """Get Langchain callbacks for monitoring LLM calls with LangFuse, if available.""" if is_langfuse_available(): try: - from langfuse.langchain import CallbackHandler - langfuse_handler = CallbackHandler() if langfuse_auth_check(): logger.info("Tracing with LangFuse enabled") From 1bff975d4dd2e1b6583a96dc691966b24eebc65c Mon Sep 17 00:00:00 2001 From: Francesco Bertolaccini Date: Tue, 10 Feb 2026 14:03:13 +0000 Subject: [PATCH 5/8] Add task cancellation endpoint to dashboard API Add CRSClient.cancel_task() method that sends DELETE requests to the task server, and a DELETE /v1/dashboard/tasks/{task_id} endpoint in the dashboard API that proxies the cancellation request. Co-Authored-By: Claude Opus 4.6 --- .../orchestrator/ui/competition_api/main.py | 22 +++++++++++ .../ui/competition_api/services/crs_client.py | 38 +++++++++++++++++++ 2 files changed, 60 insertions(+) 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/" From c6da918c8f5563eab45fa5fdb3adb30553cf132f Mon Sep 17 00:00:00 2001 From: Francesco Bertolaccini Date: Tue, 10 Feb 2026 14:04:05 +0000 Subject: [PATCH 6/8] Add cancel button to dashboard UI for active tasks Add cancelTask() JS function with confirmation dialog, cancel buttons in both the task list and task detail modal for active tasks, a "Cancelled" option in the status filter dropdown, and a .btn-sm CSS class for the compact list button. Co-Authored-By: Claude Opus 4.6 --- .../orchestrator/ui/static/index.html | 1 + .../orchestrator/ui/static/script.js | 43 +++++++++++++++++++ .../orchestrator/ui/static/styles.css | 5 +++ 3 files changed, 49 insertions(+) diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/index.html b/orchestrator/src/buttercup/orchestrator/ui/static/index.html index 4df4313f..20d3fb73 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/index.html +++ b/orchestrator/src/buttercup/orchestrator/ui/static/index.html @@ -66,6 +66,7 @@

Tasks

+ diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/script.js b/orchestrator/src/buttercup/orchestrator/ui/static/script.js index 9f6d1088..fe4bedd7 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/script.js +++ b/orchestrator/src/buttercup/orchestrator/ui/static/script.js @@ -514,6 +514,11 @@ function renderTasks() {
${task.status} + ${task.status === 'active' ? ` + + ` : ''}
@@ -731,6 +736,14 @@ function renderTaskDetail(task) {
+ ${task.status === 'active' ? ` +
+ +
+ ` : ''} + ${renderArtifacts('PoVs (Vulnerabilities)', task.povs || [], 'pov')} ${renderArtifacts('Patches', task.patches || [], 'patch')} ${renderArtifacts('Bundles', task.bundles || [], 'bundle')} @@ -834,6 +847,36 @@ 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'); + } +} + // 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; From a6c9a3755c3659574a4b24eb59b188272fa0c015 Mon Sep 17 00:00:00 2001 From: Francesco Bertolaccini Date: Tue, 10 Feb 2026 14:04:15 +0000 Subject: [PATCH 7/8] Add JSON file upload to task submission form Add a "Load from JSON" button that lets users upload a challenge JSON file to populate the task submission form fields, matching the Challenge model schema used by the challenge.py script. Co-Authored-By: Claude Opus 4.6 --- .../orchestrator/ui/static/index.html | 2 + .../orchestrator/ui/static/script.js | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/index.html b/orchestrator/src/buttercup/orchestrator/ui/static/index.html index 20d3fb73..ba455589 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/index.html +++ b/orchestrator/src/buttercup/orchestrator/ui/static/index.html @@ -156,6 +156,8 @@

Submit New Task

placeholder="1800">
+ + diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/script.js b/orchestrator/src/buttercup/orchestrator/ui/static/script.js index fe4bedd7..49372e6e 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/script.js +++ b/orchestrator/src/buttercup/orchestrator/ui/static/script.js @@ -29,6 +29,8 @@ 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'), tasksContainer: document.getElementById('tasks-container'), povsContainer: document.getElementById('povs-container'), @@ -85,6 +87,13 @@ 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.refreshBtn.addEventListener('click', loadDashboard); @@ -598,6 +607,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(); From fbd9c1a4032e3abef3ee61b9c9caf28811590ddf Mon Sep 17 00:00:00 2001 From: Francesco Bertolaccini Date: Thu, 26 Mar 2026 13:26:36 +0000 Subject: [PATCH 8/8] Add SARIF file submission button and form --- .../orchestrator/ui/static/index.html | 31 +++ .../orchestrator/ui/static/script.js | 181 +++++++++++++++++- 2 files changed, 204 insertions(+), 8 deletions(-) diff --git a/orchestrator/src/buttercup/orchestrator/ui/static/index.html b/orchestrator/src/buttercup/orchestrator/ui/static/index.html index ba455589..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

@@ -179,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 49372e6e..5215826e 100644 --- a/orchestrator/src/buttercup/orchestrator/ui/static/script.js +++ b/orchestrator/src/buttercup/orchestrator/ui/static/script.js @@ -32,6 +32,16 @@ const elements = { 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'), @@ -94,6 +104,21 @@ function setupEventListeners() { }); 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); @@ -138,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'; } }); } @@ -800,7 +830,10 @@ function renderTaskDetail(task) { ${task.status === 'active' ? ` -
+
+ @@ -940,6 +973,138 @@ async function cancelTask(taskId) { } } +// 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 {