4 Commits

Author SHA1 Message Date
quiet-professional 5184ad2a35 patched status change bug
Build and Release Core / Test and Build (push) Failing after 5m19s
2026-05-08 09:53:34 -04:00
quiet-professional bda39c40f6 patched adapter bug
Build and Release Core / Test and Build (push) Failing after 5m41s
2026-05-07 13:24:30 -04:00
quiet-professional 7fecb4905e patched table
Build and Release Core / Test and Build (push) Successful in 4m50s
2026-05-02 13:28:02 -04:00
quiet-professional 2a01e0fc08 patched bug
Build and Release Core / Test and Build (push) Successful in 5m3s
2026-05-01 13:02:30 -04:00
8 changed files with 119 additions and 72 deletions
+14 -3
View File
@@ -55,7 +55,6 @@ CREATE TABLE IF NOT EXISTS sessions (
expires_at DATETIME NOT NULL, expires_at DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
); );
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL DEFAULT 'Vulnerability', domain TEXT NOT NULL DEFAULT 'Vulnerability',
@@ -75,7 +74,10 @@ CREATE TABLE IF NOT EXISTS tickets (
'Triaged', 'Triaged',
'Assigned Out', 'Assigned Out',
'Patched', 'Patched',
'False Positive' 'False Positive',
'Pending Risk Approval',
'Risk Accepted',
'Pending Verification'
)), )),
dedupe_hash TEXT UNIQUE NOT NULL, dedupe_hash TEXT UNIQUE NOT NULL,
patch_evidence TEXT, patch_evidence TEXT,
@@ -83,6 +85,16 @@ CREATE TABLE IF NOT EXISTS tickets (
assignee TEXT DEFAULT 'Unassigned', assignee TEXT DEFAULT 'Unassigned',
latest_comment TEXT DEFAULT '', latest_comment TEXT DEFAULT '',
-- 🚀 RE-ADDED: The missing Enterprise Risk & CISA tracking fields!
is_cisa_kev BOOLEAN DEFAULT 0,
verification_requested_at DATETIME,
extension_count INTEGER DEFAULT 0,
risk_rationale TEXT,
risk_evidence TEXT,
risk_approved_by TEXT,
risk_approved_at DATETIME,
exception_expires_at DATETIME,
assigned_at DATETIME, assigned_at DATETIME,
owner_viewed_at DATETIME, owner_viewed_at DATETIME,
triage_due_date DATETIME, triage_due_date DATETIME,
@@ -92,7 +104,6 @@ CREATE TABLE IF NOT EXISTS tickets (
patched_at DATETIME, patched_at DATETIME,
FOREIGN KEY(domain) REFERENCES domains(name) ON DELETE SET DEFAULT FOREIGN KEY(domain) REFERENCES domains(name) ON DELETE SET DEFAULT
); );
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
CREATE INDEX IF NOT EXISTS idx_tickets_severity ON tickets(severity); CREATE INDEX IF NOT EXISTS idx_tickets_severity ON tickets(severity);
CREATE INDEX IF NOT EXISTS idx_tickets_domain ON tickets(domain); CREATE INDEX IF NOT EXISTS idx_tickets_domain ON tickets(domain);
+6 -6
View File
@@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"strconv"
"code.riskrancher.com/RiskRancher/core/pkg/domain" "code.riskrancher.com/RiskRancher/core/pkg/domain"
) )
@@ -76,14 +75,15 @@ func (h *Handler) HandleCSVIngest(w http.ResponseWriter, r *http.Request) {
return return
} }
adapterIDStr := r.FormValue("adapter_id") // 1. Grab the adapter_name sent by the frontend JS
adapterID, err := strconv.Atoi(adapterIDStr) adapterName := r.FormValue("adapter_name")
if err != nil { if adapterName == "" {
http.Error(w, "Invalid adapter_id", http.StatusBadRequest) http.Error(w, "Missing adapter_name", http.StatusBadRequest)
return return
} }
adapter, err := h.Store.GetAdapterByID(r.Context(), adapterID) // 2. Look up the adapter by Name instead of ID
adapter, err := h.Store.GetAdapterByName(r.Context(), adapterName)
if err != nil { if err != nil {
http.Error(w, "Adapter mapping not found", http.StatusNotFound) http.Error(w, "Adapter mapping not found", http.StatusNotFound)
return return
+2 -3
View File
@@ -66,6 +66,8 @@ func RegisterRoutes(app *App) {
// Adapters & Configuration // Adapters & Configuration
app.Router.Handle("GET /api/adapters", protected(adapterH.HandleGetAdapters)) app.Router.Handle("GET /api/adapters", protected(adapterH.HandleGetAdapters))
app.Router.Handle("GET /api/config", protected(adminH.HandleGetConfig)) app.Router.Handle("GET /api/config", protected(adminH.HandleGetConfig))
app.Router.Handle("POST /api/adapters", protected(adapterH.HandleCreateAdapter))
app.Router.Handle("DELETE /api/adapters/{id}", protected(adapterH.HandleDeleteAdapter))
// Analytics // Analytics
app.Router.Handle("GET /api/analytics/summary", protected(analyticsH.HandleGetAnalyticsSummary)) app.Router.Handle("GET /api/analytics/summary", protected(analyticsH.HandleGetAnalyticsSummary))
@@ -83,9 +85,6 @@ func RegisterRoutes(app *App) {
app.Router.Handle("GET /admin", sheriffOnly(ui.HandleAdminDashboard(app.Store))) app.Router.Handle("GET /admin", sheriffOnly(ui.HandleAdminDashboard(app.Store)))
app.Router.Handle("POST /api/adapters", adminOnly(adapterH.HandleCreateAdapter))
app.Router.Handle("DELETE /api/adapters/{id}", adminOnly(adapterH.HandleDeleteAdapter))
app.Router.Handle("GET /api/admin/export", sheriffOnly(adminH.HandleExportState)) app.Router.Handle("GET /api/admin/export", sheriffOnly(adminH.HandleExportState))
app.Router.Handle("GET /api/admin/check-updates", sheriffOnly(adminH.HandleCheckUpdates)) app.Router.Handle("GET /api/admin/check-updates", sheriffOnly(adminH.HandleCheckUpdates))
app.Router.Handle("POST /api/admin/shutdown", sheriffOnly(adminH.HandleShutdown)) app.Router.Handle("POST /api/admin/shutdown", sheriffOnly(adminH.HandleShutdown))
+26 -12
View File
@@ -62,7 +62,6 @@ function autoDetectArrayPath(obj) {
return bestPath || "."; return bestPath || ".";
} }
function processPreview() { function processPreview() {
let headers = []; let headers = [];
let rows = []; let rows = [];
@@ -72,7 +71,6 @@ function processPreview() {
const parsed = JSON.parse(currentRawData); const parsed = JSON.parse(currentRawData);
const findings = getNestedValue(parsed, pathInput.value); const findings = getNestedValue(parsed, pathInput.value);
if (!Array.isArray(findings) || findings.length === 0) { if (!Array.isArray(findings) || findings.length === 0) {
const rawPreview = JSON.stringify(parsed, null, 2).substring(0, 1500) + "\n\n... (file truncated for preview)"; const rawPreview = JSON.stringify(parsed, null, 2).substring(0, 1500) + "\n\n... (file truncated for preview)";
@@ -140,8 +138,8 @@ function populateDropdowns(headers) {
}); });
} }
document.getElementById('adapter-form').onsubmit = async (e) => { // Reusable save function mapped to the API
e.preventDefault(); async function saveAdapterToAPI() {
const data = { const data = {
name: document.getElementById('name').value, name: document.getElementById('name').value,
source_name: document.getElementById('source_name').value, source_name: document.getElementById('source_name').value,
@@ -165,12 +163,28 @@ document.getElementById('adapter-form').onsubmit = async (e) => {
} else { } else {
alert("Failed to save adapter: " + await resp.text()); alert("Failed to save adapter: " + await resp.text());
} }
}
// Bound to the primary "Save & Enable Adapter" submit button
document.getElementById('adapter-form').onsubmit = async (e) => {
e.preventDefault();
await saveAdapterToAPI();
};
// Bound to the secondary "Save to Database" button
window.saveAdapter = async function() {
const form = document.getElementById('adapter-form');
// Ensure HTML validations (like 'required') are checked before saving
if (form.reportValidity()) {
await saveAdapterToAPI();
}
}; };
window.exportAdapterJSON = function() { window.exportAdapterJSON = function() {
const name = document.getElementById("adapterName").value.trim(); // Fixed mismatched Element IDs
const sourceName = document.getElementById("sourceName").value.trim(); const name = document.getElementById("name").value.trim();
const rootPath = document.getElementById("rootPath").value.trim(); const sourceName = document.getElementById("source_name").value.trim();
const rootPath = document.getElementById("findings_path").value.trim();
if (!name || !sourceName) { if (!name || !sourceName) {
return alert("Adapter Name and Source Name are required to export."); return alert("Adapter Name and Source Name are required to export.");
@@ -180,11 +194,11 @@ window.exportAdapterJSON = function() {
name: name, name: name,
source_name: sourceName, source_name: sourceName,
findings_path: rootPath, findings_path: rootPath,
mapping_title: document.getElementById("mapTitle").value.trim(), mapping_title: document.getElementById("mapping_title").value.trim(),
mapping_asset: document.getElementById("mapAsset").value.trim(), mapping_asset: document.getElementById("mapping_asset").value.trim(),
mapping_severity: document.getElementById("mapSeverity").value.trim(), mapping_severity: document.getElementById("mapping_severity").value.trim(),
mapping_description: document.getElementById("mapDesc").value.trim(), mapping_description: document.getElementById("mapping_description").value.trim(),
mapping_remediation: document.getElementById("mapRem").value.trim() mapping_remediation: document.getElementById("mapping_remediation").value.trim()
}; };
// Create a downloadable JSON blob // Create a downloadable JSON blob
+11 -12
View File
@@ -1,4 +1,3 @@
window.showUpsell = function(featureName) { window.showUpsell = function(featureName) {
const featureNameEl = document.getElementById('upsellFeatureName'); const featureNameEl = document.getElementById('upsellFeatureName');
const modalEl = document.getElementById('upsellModal'); const modalEl = document.getElementById('upsellModal');
@@ -10,7 +9,6 @@ window.showUpsell = function(featureName) {
} }
}; };
window.renderMarkdown = function(text) { window.renderMarkdown = function(text) {
if (!text) return "<i style='color:#94a3b8;'>No description provided.</i>"; if (!text) return "<i style='color:#94a3b8;'>No description provided.</i>";
let html = text.replace(/!\[.*?\]\((.*?)\)/g, '<br><img src="$1" style="max-width: 100%; max-height: 400px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; margin: 10px 0; display: block; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);"><br>'); let html = text.replace(/!\[.*?\]\((.*?)\)/g, '<br><img src="$1" style="max-width: 100%; max-height: 400px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; margin: 10px 0; display: block; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);"><br>');
@@ -18,13 +16,11 @@ window.renderMarkdown = function(text) {
return html; return html;
}; };
window.updateDrawerPreview = function() { window.updateDrawerPreview = function() {
const rawDesc = document.getElementById('drawerDescEdit').value; const rawDesc = document.getElementById('drawerDescEdit').value;
document.getElementById('drawerDescPreview').innerHTML = renderMarkdown(rawDesc); document.getElementById('drawerDescPreview').innerHTML = renderMarkdown(rawDesc);
}; };
window.openDrawer = function(id, title, asset, severity) { window.openDrawer = function(id, title, asset, severity) {
document.getElementById('drawerTicketID').value = id; document.getElementById('drawerTicketID').value = id;
document.getElementById('drawerTitle').innerText = title; document.getElementById('drawerTitle').innerText = title;
@@ -34,9 +30,7 @@ window.openDrawer = function(id, title, asset, severity) {
badge.innerText = severity; badge.innerText = severity;
badge.className = `badge ${severity.toLowerCase()}`; badge.className = `badge ${severity.toLowerCase()}`;
document.getElementById('drawerSeverity').value = severity; // Read hidden inputs from the table row
document.getElementById('drawerComment').value = "";
const rawDesc = document.getElementById('desc-' + id) ? document.getElementById('desc-' + id).value : ""; const rawDesc = document.getElementById('desc-' + id) ? document.getElementById('desc-' + id).value : "";
const rawRem = document.getElementById('rem-' + id) ? document.getElementById('rem-' + id).value : ""; const rawRem = document.getElementById('rem-' + id) ? document.getElementById('rem-' + id).value : "";
const rawEv = document.getElementById('ev-' + id) ? document.getElementById('ev-' + id).value : ""; const rawEv = document.getElementById('ev-' + id) ? document.getElementById('ev-' + id).value : "";
@@ -44,6 +38,10 @@ window.openDrawer = function(id, title, asset, severity) {
const rawComment = document.getElementById('comment-' + id) ? document.getElementById('comment-' + id).value : ""; const rawComment = document.getElementById('comment-' + id) ? document.getElementById('comment-' + id).value : "";
const assignee = document.getElementById('assignee-' + id) ? document.getElementById('assignee-' + id).value : ""; const assignee = document.getElementById('assignee-' + id) ? document.getElementById('assignee-' + id).value : "";
// Set initial values in the drawer
document.getElementById('drawerSeverity').value = severity;
document.getElementById('drawerStatus').value = status; // Pre-select current status
document.getElementById('drawerComment').value = "";
document.getElementById('drawerDescEdit').value = rawDesc; document.getElementById('drawerDescEdit').value = rawDesc;
document.getElementById('drawerRemEdit').value = rawRem; document.getElementById('drawerRemEdit').value = rawRem;
@@ -307,13 +305,14 @@ document.addEventListener("DOMContentLoaded", function() {
const assigneeInput = document.getElementById("drawerAssignee"); const assigneeInput = document.getElementById("drawerAssignee");
const newAssignee = assigneeInput ? assigneeInput.value.trim() : ""; const newAssignee = assigneeInput ? assigneeInput.value.trim() : "";
const currentStatus = document.getElementById("status-" + id).value;
let newStatus = currentStatus; // Explicitly grab the selected status from the new dropdown
if (newAssignee !== "" && newAssignee !== "Unassigned") { let explicitStatus = document.getElementById("drawerStatus").value;
let newStatus = explicitStatus;
// Helpful UX: If they typed an email but left the status as "Waiting", auto-assign it
if (newAssignee !== "" && newAssignee !== "Unassigned" && explicitStatus === "Waiting to be Triaged") {
newStatus = "Assigned Out"; newStatus = "Assigned Out";
} else if (currentStatus === "Returned to Security") {
newStatus = "Waiting to be Triaged";
} }
if (!comment.trim()) return alert("An audit trail comment is strictly required when modifying a finding."); if (!comment.trim()) return alert("An audit trail comment is strictly required when modifying a finding.");
+45 -33
View File
@@ -1,50 +1,47 @@
const dropZone = document.getElementById('drop-zone'); async function uploadScan() {
const fileInput = document.getElementById('file-input'); const fileInput = document.getElementById('scanFile');
const adapterSelect = document.getElementById('adapterSelect');
const resultDiv = document.getElementById('ingestResult');
async function processFile(file) { const file = fileInput.files[0];
const statusText = document.getElementById('status-text'); const adapterName = adapterSelect.value;
document.getElementById('status-area').classList.remove('d-none');
const adapterSelect = document.getElementById('adapter-select'); if (!file) {
const adapterId = adapterSelect.value; showResult("Please select a file to upload.", false);
return;
let adapterName = "";
if (adapterSelect.selectedIndex > 0) {
adapterName = adapterSelect.options[adapterSelect.selectedIndex].getAttribute('data-name');
} }
if (!adapterName) {
showResult("Please select an adapter.", false);
return;
}
// Show processing state
showResult("Processing...", true, true);
try { try {
let response; let response;
// Route appropriately based on file extension
if (file.name.toLowerCase().endsWith('.json')) { if (file.name.toLowerCase().endsWith('.json')) {
if (!adapterName) {
statusText.innerText = "Unknown JSON format. Redirecting to Adapter Builder...";
setTimeout(() => {
window.location.href = `/admin/adapters/new?filename=${encodeURIComponent(file.name)}`;
}, 1200);
return;
}
const rawText = await file.text(); const rawText = await file.text();
response = await fetch(`/api/ingest/${encodeURIComponent(adapterName)}`, { response = await fetch(`/api/ingest/${encodeURIComponent(adapterName)}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: rawText body: rawText
}); });
} } else {
else {
let formData = new FormData(); let formData = new FormData();
formData.append('file', file); formData.append('file', file);
if (adapterId) { formData.append('adapter_name', adapterName); // Pass adapter name for CSVs
formData.append('adapter_id', adapterId);
}
response = await fetch('/api/ingest/csv', { method: 'POST', body: formData }); response = await fetch('/api/ingest/csv', { method: 'POST', body: formData });
} }
if (!response.ok) { if (!response.ok) {
// Auto-redirect to builder if the adapter doesn't match the file structure
if (response.status === 404) { if (response.status === 404) {
statusText.innerText = "Format not recognized. Redirecting to Adapter Builder..."; showResult("Format not recognized. Redirecting to Adapter Builder...", false);
setTimeout(() => { setTimeout(() => {
window.location.href = `/admin/adapters/new?filename=${encodeURIComponent(file.name)}`; window.location.href = `/admin/adapters/new?filename=${encodeURIComponent(file.name)}`;
}, 1200); }, 1200);
@@ -53,16 +50,31 @@ async function processFile(file) {
throw new Error(errText); throw new Error(errText);
} }
} else { } else {
statusText.innerText = "Yeehaw! Tickets corralled successfully."; showResult("Yeehaw! Tickets corralled successfully.", true);
setTimeout(() => window.location.href = "/dashboard", 800); setTimeout(() => window.location.href = "/dashboard", 1000);
} }
} catch (err) { } catch (err) {
statusText.innerText = "Stampede! Error: " + err.message; showResult("Stampede! Error: " + err.message, false);
} }
} }
dropZone.onclick = () => fileInput.click(); // Helper function to handle status messages nicely
fileInput.onchange = (e) => processFile(e.target.files[0]); function showResult(msg, isSuccess, isInfo = false) {
dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.background = "#e1f5fe"; }; const div = document.getElementById('ingestResult');
dropZone.ondragleave = () => dropZone.style.background = "#f8f9fa"; div.style.display = 'block';
dropZone.ondrop = (e) => { e.preventDefault(); processFile(e.dataTransfer.files[0]); }; div.innerText = msg;
if (isInfo) {
div.style.backgroundColor = '#e0f2fe';
div.style.color = '#0369a1';
div.style.border = '1px solid #bae6fd';
} else if (isSuccess) {
div.style.backgroundColor = '#dcfce7';
div.style.color = '#166534';
div.style.border = '1px solid #bbf7d0';
} else {
div.style.backgroundColor = '#fee2e2';
div.style.color = '#991b1b';
div.style.border = '1px solid #fecaca';
}
}
+2 -2
View File
@@ -90,8 +90,8 @@
<button type="submit" class="btn disabled" id="save-btn" style="width: 100%; background: var(--primary); color: white; border: none; padding: 12px;">Save & Enable Adapter</button> <button type="submit" class="btn disabled" id="save-btn" style="width: 100%; background: var(--primary); color: white; border: none; padding: 12px;">Save & Enable Adapter</button>
</div> </div>
<div style="display: flex; gap: 15px; margin-top: 20px;"> <div style="display: flex; gap: 15px; margin-top: 20px;">
<button class="btn" style="background: #2563eb; color: white;" onclick="saveAdapter()">💾 Save to Database</button> <button type="button" class="btn" style="background: #2563eb; color: white;" onclick="saveAdapter()">💾 Save to Database</button>
<button class="btn btn-secondary" onclick="exportAdapterJSON()">⬇️ Export JSON Profile</button> <button type="button" class="btn btn-secondary" onclick="exportAdapterJSON()">⬇️ Export JSON Profile</button>
</div> </div>
</form> </form>
</div> </div>
+13 -1
View File
@@ -90,7 +90,7 @@
<textarea id="drawerRemEdit" style="width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.9rem; resize: vertical; min-height: 80px;"></textarea> <textarea id="drawerRemEdit" style="width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.9rem; resize: vertical; min-height: 80px;"></textarea>
</div> </div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;"> <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-bottom: 20px;">
<div> <div>
<label style="font-weight: bold; color: #334155; display: block; margin-bottom: 5px;">Adjust Severity:</label> <label style="font-weight: bold; color: #334155; display: block; margin-bottom: 5px;">Adjust Severity:</label>
<select id="drawerSeverity" style="width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.95rem;"> <select id="drawerSeverity" style="width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.95rem;">
@@ -101,6 +101,18 @@
<option value="Info">Info</option> <option value="Info">Info</option>
</select> </select>
</div> </div>
<div>
<label style="font-weight: bold; color: #334155; display: block; margin-bottom: 5px;">Adjust Status:</label>
<select id="drawerStatus" style="width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.95rem;">
<option value="Waiting to be Triaged">Waiting to be Triaged</option>
<option value="Assigned Out">Assigned Out</option>
<option value="Returned to Security">Returned to Security</option>
<option value="Patched">Remediated</option>
<option value="False Positive">False Positive</option>
</select>
</div>
<div> <div>
<label style="font-weight: bold; color: #334155; display: block; margin-bottom: 5px;">Assign to IT (Email):</label> <label style="font-weight: bold; color: #334155; display: block; margin-bottom: 5px;">Assign to IT (Email):</label>
<input type="text" id="drawerAssignee" placeholder="e.g. sysadmin@company.com" style="width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box;"> <input type="text" id="drawerAssignee" placeholder="e.g. sysadmin@company.com" style="width: 100%; padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box;">