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 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), ('Vulnerability', 'Critical', 3, 14, 1),
('Privacy', 'Critical', 3, 3, 0), ('Privacy', 'High', 3, 7, 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); ('Incident', 'Critical', 3, 1, 0);
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
@@ -50,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',
@@ -70,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,
@@ -78,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,
@@ -87,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>
+10 -4
View File
@@ -3,16 +3,22 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
<body> <body>
<header style="display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background: white; border-bottom: 1px solid #e2e8f0;"> <header style="display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background: white; border-bottom: 1px solid #e2e8f0;">
<div class="logo"> <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> </div>
<nav style="display: flex; align-items: center; gap: 15px;"> <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> <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> </nav>
</header> </header>
@@ -29,7 +35,7 @@
{{template "content" .}} {{template "content" .}}
</main> </main>
<footer style="text-align: center; padding: 20px; margin-top: 40px; border-top: 1px solid #e2e8f0; color: #94a3b8; font-size: 0.85rem;"> <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> </footer>
</body> </body>
</html> </html>
@@ -83,20 +83,6 @@
<h3 style="margin: 0 0 15px 0;">⚙️ Operations</h3> <h3 style="margin: 0 0 15px 0;">⚙️ Operations</h3>
{{block "pro_backups" .}} {{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}} {{end}}
<div style="margin-bottom: 20px;"> <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> <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;">
+5 -1
View File
@@ -36,7 +36,11 @@ func SetVersionInfo(version, commit string) {
} }
func init() { 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) Pages = make(map[string]*template.Template)
var err error var err error