mirror of
https://github.com/trailofbits/buttercup
synced 2026-06-21 14:11:39 +00:00
Add SARIF file submission button and form
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
<h1 class="nav-title">Buttercup CRS Dashboard</h1>
|
||||
<div class="nav-buttons">
|
||||
<button id="submit-task-btn" class="btn btn-primary">Submit Task</button>
|
||||
<button id="submit-sarif-btn" class="btn btn-primary">Submit SARIF</button>
|
||||
<button id="refresh-btn" class="btn btn-secondary">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,6 +180,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SARIF Upload Modal -->
|
||||
<div id="sarif-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Submit SARIF File</h2>
|
||||
<span class="close" id="close-sarif-modal">×</span>
|
||||
</div>
|
||||
<form id="sarif-form">
|
||||
<div class="form-group">
|
||||
<label for="sarif-task-select">Task:</label>
|
||||
<select id="sarif-task-select" required>
|
||||
<option value="">Select a task...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sarif-file-input">SARIF File (.sarif or .json):</label>
|
||||
<input type="file" id="sarif-file-input" accept=".sarif,.json" required>
|
||||
</div>
|
||||
<div id="sarif-preview" class="form-group" style="display: none;">
|
||||
<label>Preview:</label>
|
||||
<pre id="sarif-preview-content" style="max-height: 200px; overflow: auto; background: var(--bg-secondary); padding: 0.75rem; border-radius: 6px; font-size: 0.8rem;"></pre>
|
||||
</div>
|
||||
<div class="form-buttons">
|
||||
<button type="button" id="cancel-sarif-btn" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Submit SARIF</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications container -->
|
||||
<div id="notifications"></div>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
</div>
|
||||
|
||||
${task.status === 'active' ? `
|
||||
<div style="margin-top: 1rem;">
|
||||
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-primary" onclick="openSarifModal('${task.task_id}')">
|
||||
Submit SARIF
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="cancelTask('${task.task_id}')">
|
||||
Cancel Task
|
||||
</button>
|
||||
@@ -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 =
|
||||
'<option value="">Select a task...</option>';
|
||||
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 = '<span class="spinner"></span> 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 {
|
||||
|
||||
Reference in New Issue
Block a user