7 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
quiet-professional ead3c9043d updated branding
Build and Release Core / Test and Build (push) Successful in 4m44s
2026-05-01 09:59:58 -04:00
quiet-professional 317dd9aedf patched bug
Build and Release Core / Test and Build (push) Successful in 4m55s
2026-04-30 13:24:41 -04:00
quiet-professional 7f22302859 patched sla bug
Build and Release Core / Test and Build (push) Successful in 6m4s
2026-04-30 12:49:51 -04:00
11 changed files with 141 additions and 93 deletions
+21 -5
View File
@@ -28,8 +28,13 @@ CREATE TABLE IF NOT EXISTS sla_policies (
);
INSERT OR IGNORE INTO sla_policies (domain, severity, days_to_triage, days_to_remediate, max_extensions) VALUES
('Vulnerability', 'Critical', 3, 14, 1), ('Vulnerability', 'High', 3, 30, 2),
('Privacy', 'Critical', 3, 3, 0), ('Privacy', 'High', 3, 7, 1),
('Vulnerability', 'Critical', 3, 14, 1),
('Vulnerability', 'High', 3, 30, 2),
('Vulnerability', 'Medium', 7, 60, 2),
('Vulnerability', 'Low', 14, 90, 3),
('Vulnerability', 'Info', 30, 180, 5),
('Privacy', 'Critical', 3, 3, 0),
('Privacy', 'High', 3, 7, 1),
('Incident', 'Critical', 3, 1, 0);
CREATE TABLE IF NOT EXISTS users (
@@ -50,7 +55,6 @@ CREATE TABLE IF NOT EXISTS sessions (
expires_at DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL DEFAULT 'Vulnerability',
@@ -70,7 +74,10 @@ CREATE TABLE IF NOT EXISTS tickets (
'Triaged',
'Assigned Out',
'Patched',
'False Positive'
'False Positive',
'Pending Risk Approval',
'Risk Accepted',
'Pending Verification'
)),
dedupe_hash TEXT UNIQUE NOT NULL,
patch_evidence TEXT,
@@ -78,6 +85,16 @@ CREATE TABLE IF NOT EXISTS tickets (
assignee TEXT DEFAULT 'Unassigned',
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,
owner_viewed_at DATETIME,
triage_due_date DATETIME,
@@ -87,7 +104,6 @@ CREATE TABLE IF NOT EXISTS tickets (
patched_at DATETIME,
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_severity ON tickets(severity);
CREATE INDEX IF NOT EXISTS idx_tickets_domain ON tickets(domain);
+6 -6
View File
@@ -7,7 +7,6 @@ import (
"encoding/json"
"log"
"net/http"
"strconv"
"code.riskrancher.com/RiskRancher/core/pkg/domain"
)
@@ -76,14 +75,15 @@ func (h *Handler) HandleCSVIngest(w http.ResponseWriter, r *http.Request) {
return
}
adapterIDStr := r.FormValue("adapter_id")
adapterID, err := strconv.Atoi(adapterIDStr)
if err != nil {
http.Error(w, "Invalid adapter_id", http.StatusBadRequest)
// 1. Grab the adapter_name sent by the frontend JS
adapterName := r.FormValue("adapter_name")
if adapterName == "" {
http.Error(w, "Missing adapter_name", http.StatusBadRequest)
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 {
http.Error(w, "Adapter mapping not found", http.StatusNotFound)
return
+2 -3
View File
@@ -66,6 +66,8 @@ func RegisterRoutes(app *App) {
// Adapters & Configuration
app.Router.Handle("GET /api/adapters", protected(adapterH.HandleGetAdapters))
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
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("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/check-updates", sheriffOnly(adminH.HandleCheckUpdates))
app.Router.Handle("POST /api/admin/shutdown", sheriffOnly(adminH.HandleShutdown))
+26 -12
View File
@@ -62,7 +62,6 @@ function autoDetectArrayPath(obj) {
return bestPath || ".";
}
function processPreview() {
let headers = [];
let rows = [];
@@ -72,7 +71,6 @@ function processPreview() {
const parsed = JSON.parse(currentRawData);
const findings = getNestedValue(parsed, pathInput.value);
if (!Array.isArray(findings) || findings.length === 0) {
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) => {
e.preventDefault();
// Reusable save function mapped to the API
async function saveAdapterToAPI() {
const data = {
name: document.getElementById('name').value,
source_name: document.getElementById('source_name').value,
@@ -165,12 +163,28 @@ document.getElementById('adapter-form').onsubmit = async (e) => {
} else {
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() {
const name = document.getElementById("adapterName").value.trim();
const sourceName = document.getElementById("sourceName").value.trim();
const rootPath = document.getElementById("rootPath").value.trim();
// Fixed mismatched Element IDs
const name = document.getElementById("name").value.trim();
const sourceName = document.getElementById("source_name").value.trim();
const rootPath = document.getElementById("findings_path").value.trim();
if (!name || !sourceName) {
return alert("Adapter Name and Source Name are required to export.");
@@ -180,11 +194,11 @@ window.exportAdapterJSON = function() {
name: name,
source_name: sourceName,
findings_path: rootPath,
mapping_title: document.getElementById("mapTitle").value.trim(),
mapping_asset: document.getElementById("mapAsset").value.trim(),
mapping_severity: document.getElementById("mapSeverity").value.trim(),
mapping_description: document.getElementById("mapDesc").value.trim(),
mapping_remediation: document.getElementById("mapRem").value.trim()
mapping_title: document.getElementById("mapping_title").value.trim(),
mapping_asset: document.getElementById("mapping_asset").value.trim(),
mapping_severity: document.getElementById("mapping_severity").value.trim(),
mapping_description: document.getElementById("mapping_description").value.trim(),
mapping_remediation: document.getElementById("mapping_remediation").value.trim()
};
// Create a downloadable JSON blob
+11 -12
View File
@@ -1,4 +1,3 @@
window.showUpsell = function(featureName) {
const featureNameEl = document.getElementById('upsellFeatureName');
const modalEl = document.getElementById('upsellModal');
@@ -10,7 +9,6 @@ window.showUpsell = function(featureName) {
}
};
window.renderMarkdown = function(text) {
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>');
@@ -18,13 +16,11 @@ window.renderMarkdown = function(text) {
return html;
};
window.updateDrawerPreview = function() {
const rawDesc = document.getElementById('drawerDescEdit').value;
document.getElementById('drawerDescPreview').innerHTML = renderMarkdown(rawDesc);
};
window.openDrawer = function(id, title, asset, severity) {
document.getElementById('drawerTicketID').value = id;
document.getElementById('drawerTitle').innerText = title;
@@ -34,9 +30,7 @@ window.openDrawer = function(id, title, asset, severity) {
badge.innerText = severity;
badge.className = `badge ${severity.toLowerCase()}`;
document.getElementById('drawerSeverity').value = severity;
document.getElementById('drawerComment').value = "";
// Read hidden inputs from the table row
const rawDesc = document.getElementById('desc-' + id) ? document.getElementById('desc-' + id).value : "";
const rawRem = document.getElementById('rem-' + id) ? document.getElementById('rem-' + 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 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('drawerRemEdit').value = rawRem;
@@ -307,13 +305,14 @@ document.addEventListener("DOMContentLoaded", function() {
const assigneeInput = document.getElementById("drawerAssignee");
const newAssignee = assigneeInput ? assigneeInput.value.trim() : "";
const currentStatus = document.getElementById("status-" + id).value;
let newStatus = currentStatus;
if (newAssignee !== "" && newAssignee !== "Unassigned") {
// Explicitly grab the selected status from the new dropdown
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";
} 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.");
+45 -33
View File
@@ -1,50 +1,47 @@
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
async function uploadScan() {
const fileInput = document.getElementById('scanFile');
const adapterSelect = document.getElementById('adapterSelect');
const resultDiv = document.getElementById('ingestResult');
async function processFile(file) {
const statusText = document.getElementById('status-text');
document.getElementById('status-area').classList.remove('d-none');
const file = fileInput.files[0];
const adapterName = adapterSelect.value;
const adapterSelect = document.getElementById('adapter-select');
const adapterId = adapterSelect.value;
let adapterName = "";
if (adapterSelect.selectedIndex > 0) {
adapterName = adapterSelect.options[adapterSelect.selectedIndex].getAttribute('data-name');
if (!file) {
showResult("Please select a file to upload.", false);
return;
}
if (!adapterName) {
showResult("Please select an adapter.", false);
return;
}
// Show processing state
showResult("Processing...", true, true);
try {
let response;
// Route appropriately based on file extension
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();
response = await fetch(`/api/ingest/${encodeURIComponent(adapterName)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: rawText
});
}
else {
} else {
let formData = new FormData();
formData.append('file', file);
if (adapterId) {
formData.append('adapter_id', adapterId);
}
formData.append('adapter_name', adapterName); // Pass adapter name for CSVs
response = await fetch('/api/ingest/csv', { method: 'POST', body: formData });
}
if (!response.ok) {
// Auto-redirect to builder if the adapter doesn't match the file structure
if (response.status === 404) {
statusText.innerText = "Format not recognized. Redirecting to Adapter Builder...";
showResult("Format not recognized. Redirecting to Adapter Builder...", false);
setTimeout(() => {
window.location.href = `/admin/adapters/new?filename=${encodeURIComponent(file.name)}`;
}, 1200);
@@ -53,16 +50,31 @@ async function processFile(file) {
throw new Error(errText);
}
} else {
statusText.innerText = "Yeehaw! Tickets corralled successfully.";
setTimeout(() => window.location.href = "/dashboard", 800);
showResult("Yeehaw! Tickets corralled successfully.", true);
setTimeout(() => window.location.href = "/dashboard", 1000);
}
} catch (err) {
statusText.innerText = "Stampede! Error: " + err.message;
showResult("Stampede! Error: " + err.message, false);
}
}
dropZone.onclick = () => fileInput.click();
fileInput.onchange = (e) => processFile(e.target.files[0]);
dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.background = "#e1f5fe"; };
dropZone.ondragleave = () => dropZone.style.background = "#f8f9fa";
dropZone.ondrop = (e) => { e.preventDefault(); processFile(e.dataTransfer.files[0]); };
// Helper function to handle status messages nicely
function showResult(msg, isSuccess, isInfo = false) {
const div = document.getElementById('ingestResult');
div.style.display = 'block';
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>
</div>
<div style="display: flex; gap: 15px; margin-top: 20px;">
<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" style="background: #2563eb; color: white;" onclick="saveAdapter()">💾 Save to Database</button>
<button type="button" class="btn btn-secondary" onclick="exportAdapterJSON()">⬇️ Export JSON Profile</button>
</div>
</form>
</div>
+10 -4
View File
@@ -3,16 +3,22 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RiskRancher OSS</title>
<title>RiskRancher {{if isProActive}}PRO{{else}}OSS{{end}}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header style="display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background: white; border-bottom: 1px solid #e2e8f0;">
<div class="logo">
<h2 style="margin: 0;"><a href="/dashboard" style="color: #0f172a; text-decoration: none;">🐴 RiskRancher</a></h2>
<h2 style="margin: 0;">
<a href="/dashboard" style="color: #0f172a; text-decoration: none;">
🐴 RiskRancher{{if isProActive}}{{with getCompanyName}} | {{.}}{{end}}{{end}}
</a>
</h2>
</div>
<nav style="display: flex; align-items: center; gap: 15px;">
<span style="color: #475569; font-size: 0.9rem; font-family: monospace; padding-left: 15px; margin-left: 5px;">Community Edition</span>
<span style="color: {{if isProActive}}#059669{{else}}#475569{{end}}; font-size: 0.9rem; font-family: monospace; padding-left: 15px; margin-left: 5px; font-weight: {{if isProActive}}bold{{else}}normal{{end}};">
{{if isProActive}}PRO Edition{{else}}Community Edition{{end}}
</span>
<button onclick="logout()" style="background: #f1f5f9; color: #dc2626; border: 1px solid #e2e8f0; padding: 6px 12px; border-radius: 4px; font-weight: bold; cursor: pointer; font-size: 0.85rem;">Log Out</button>
</nav>
</header>
@@ -29,7 +35,7 @@
{{template "content" .}}
</main>
<footer style="text-align: center; padding: 20px; margin-top: 40px; border-top: 1px solid #e2e8f0; color: #94a3b8; font-size: 0.85rem;">
🐴 RiskRancher Core Edition | Version: <strong>{{.Version}}</strong> | Build: <strong>{{.Commit}}</strong>
🐴 RiskRancher {{if isProActive}}PRO Edition{{else}}Core Edition{{end}} | Version: <strong>{{.Version}}</strong> | Build: <strong>{{.Commit}}</strong>
</footer>
</body>
</html>
@@ -83,20 +83,6 @@
<h3 style="margin: 0 0 15px 0;">⚙️ Operations</h3>
{{block "pro_backups" .}}
<div style="margin-bottom: 20px;">
<label style="font-weight: bold; display: flex; justify-content: space-between;">
Automated Backups
<span style="font-size: 0.75rem; color: #8b5cf6; font-weight: normal;">Pro Feature</span>
</label>
<div style="display: flex; gap: 10px; margin-top: 5px;">
<select disabled style="flex: 1; padding: 6px; background: #f1f5f9; color: #94a3b8; cursor: not-allowed; border: 1px solid #cbd5e1;">
<option>Manual Only (Free Core)</option>
<option>Daily Automated</option>
<option>Weekly Automated</option>
</select>
<button class="btn btn-secondary" style="color: #94a3b8; border-color: #cbd5e1;" onclick="showUpsell('Automated DB Backups')">🔒 Apply</button>
</div>
</div>
{{end}}
<div style="margin-bottom: 20px;">
+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>
</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>
<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;">
@@ -101,6 +101,18 @@
<option value="Info">Info</option>
</select>
</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>
<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;">
+5 -1
View File
@@ -36,7 +36,11 @@ func SetVersionInfo(version, commit string) {
}
func init() {
funcMap := template.FuncMap{"lower": strings.ToLower}
funcMap := template.FuncMap{
"lower": strings.ToLower,
"isProActive": func() bool { return false },
"getCompanyName": func() string { return "" },
}
Pages = make(map[string]*template.Template)
var err error