Load a report first
@@ -260,6 +284,9 @@
const huntsEl = document.getElementById('hunts');
const huntTag = document.getElementById('huntTag');
const viewReportBtn = document.getElementById('viewReportBtn');
+ const reportNavBtn = document.getElementById('reportNavBtn');
+ const activeReportIndicator = document.getElementById('activeReportIndicator');
+ const activeReportName = document.getElementById('activeReportName');
const reportSection = document.getElementById('reportSection');
const reportHeader = document.getElementById('reportHeader');
const reportBody = document.getElementById('reportBody');
@@ -314,12 +341,29 @@
const r = await fetch('/api/report?file='+encodeURIComponent(file));
const data = await r.json();
renderReport(data);
+
+ // Set active report indicator
+ setActiveReport(file);
+
location.hash = '#report';
}
+ function setActiveReport(reportFileName) {
+ if (reportFileName) {
+ activeReportIndicator.classList.remove('hidden');
+ activeReportName.textContent = reportFileName;
+ reportNavBtn.classList.remove('hidden');
+ } else {
+ activeReportIndicator.classList.add('hidden');
+ activeReportName.textContent = 'No report loaded';
+ reportNavBtn.classList.add('hidden');
+ }
+ }
+
function renderReport(rep){
reportHeader.innerHTML = '';
reportBody.innerHTML = '';
+
const risk = rep.riskRating||{};
reportHeader.innerHTML = `
Risk: ${risk.rating||'N/A'} (${(risk.score??'-')}/10)
${escapeHTML(rep.riskRating?.summary||'')}
`;
@@ -342,7 +386,12 @@
const wrap = document.createElement('div');
wrap.className='card';
const count = Array.isArray(b.arr)?b.arr.length:0;
- wrap.innerHTML = `
${b.title} ${count} ⓘ
`;
+ wrap.innerHTML = `
+
+ ${b.title}
+ (${count})
+ ?
+
`;
if(count===0){
wrap.innerHTML += '
No items found.
';
} else {
@@ -358,25 +407,141 @@
}
function renderItem(kind, item){
- const d = document.createElement('div'); d.className='card'; d.style.margin='0';
+ const d = document.createElement('div');
+ d.className='card expandable-item';
+ d.style.margin='0';
+ d.style.cursor='pointer';
+
const esc = escapeHTML;
+ let summary = '';
+ let details = '';
+
if(kind==='processes'){
- d.innerHTML = `
${esc(item.name||'')}PID: ${item.pid} • Path: ${esc(item.path||'')}
`;
+ summary = `
${esc(item.name||'')}PID: ${item.pid} • ${esc(item.path||'')}
`;
+ details = `
+
+
+
Name: ${esc(item.name||'N/A')}
+
PID: ${item.pid||'N/A'}
+
PPID: ${item.ppid||'N/A'}
+
Parent: ${esc(item.parent||'N/A')}
+
Path: ${esc(item.path||'N/A')}
+
Arguments: ${esc(item.args||'N/A')}
+
Created: ${esc(item.created||'N/A')}
+ ${item.eliminated ? '
✓ Eliminated
' : ''}
+
+
`;
} else if(kind==='services'){
- d.innerHTML = `
${esc(item.displayName||item.name||'')}Start: ${esc(item.startType||'')} • Bin: ${esc(item.binaryPathName||'')}
`;
+ summary = `
${esc(item.displayName||item.name||'')}${esc(item.startType||'')} • ${esc(item.binaryPathName||'')}
`;
+ details = `
+
+
+
Display Name: ${esc(item.displayName||'N/A')}
+
Service Name: ${esc(item.name||'N/A')}
+
Service Type: ${esc(item.serviceType||'N/A')}
+
Start Type: ${esc(item.startType||'N/A')}
+
Binary Path: ${esc(item.binaryPathName||'N/A')}
+
Description: ${esc(item.description||'N/A')}
+
Error Control: ${esc(item.errorControl||'N/A')}
+
Load Order Group: ${esc(item.loadOrderGroup||'N/A')}
+
Service Start Name: ${esc(item.serviceStartName||'N/A')}
+
Delayed Auto Start: ${item.delayedAutoStart ? 'Yes' : 'No'}
+
Dependencies: ${item.dependencies && item.dependencies.length > 0 ? esc(item.dependencies.join(', ')) : 'None'}
+ ${item.eliminated ? '
✓ Eliminated
' : ''}
+
+
`;
} else if(kind==='connections'){
- d.innerHTML = `
${esc(item.process||'')}${esc(item.localAddr||'')} → ${esc(item.remoteAddr||'')} (${esc(item.remoteHost||'')})
`;
+ summary = `
${esc(item.process||'')}${esc(item.localAddr||'')} → ${esc(item.remoteAddr||'')} (${esc(item.remoteHost||'')})
`;
+ details = `
+
+
+
Process: ${esc(item.process||'N/A')}
+
PID: ${item.pid||'N/A'}
+
Local Address: ${esc(item.localAddr||'N/A')}
+
Remote Address: ${esc(item.remoteAddr||'N/A')}
+
Remote Host: ${esc(item.remoteHost||'N/A')}
+
State: ${esc(item.state||'N/A')}
+ ${item.eliminated ? '
✓ Eliminated (Firewall Rule)
' : ''}
+
+
`;
} else if(kind==='autoruns'){
- d.innerHTML = `
${esc(item.name||item.imageName||item.entry||'')}${esc(item.location||item.type||'')} • ${esc(item.command||item.imagePath||item.launchString||item.launch_string||'')}
`;
+ const displayName = item.image_name || item.imageName || item.entry || 'Unknown AutoRun';
+ const launchStr = item.launch_string || item.launchString || item.imagePath || item.image_path || '';
+ summary = `
${esc(displayName)}${esc(item.type||'')} • ${esc(item.location||'')}
`;
+ details = `
+
+
+
Image Name: ${esc(item.image_name||item.imageName||'N/A')}
+
Entry: ${esc(item.entry||'N/A')}
+
Type: ${esc(item.type||'N/A')}
+
Location: ${esc(item.location||'N/A')}
+
Image Path: ${esc(item.image_path||item.imagePath||'N/A')}
+
Launch String: ${esc(launchStr||'N/A')}
+
Arguments: ${esc(item.arguments||'N/A')}
+
MD5: ${esc(item.md5||'N/A')}
+
SHA1: ${esc(item.sha1||'N/A')}
+
SHA256: ${esc(item.sha256||'N/A')}
+ ${item.eliminated ? '
✓ Eliminated
' : ''}
+
+
`;
} else if(kind==='tasks'){
- d.innerHTML = `
${esc(item.name||'')}State: ${esc(item.state||'')} • Next: ${esc(item.nextRun||'')}
`;
+ summary = `
${esc(item.name||'')}${esc(item.state||'')} • ${esc(item.path||'')}
`;
+ details = `
+
+
+
Name: ${esc(item.name||'N/A')}
+
Path: ${esc(item.path||'N/A')}
+
State: ${esc(item.state||'N/A')}
+
Enabled: ${item.enabled ? 'Yes' : 'No'}
+
Author: ${esc(item.author||'N/A')}
+
Description: ${esc(item.description||'N/A')}
+
Next Run: ${esc(item.nextRun||'N/A')}
+
Last Run: ${esc(item.lastRun||'N/A')}
+
Last Result: ${esc(item.lastResult||'N/A')}
+
Created: ${esc(item.createdDate||'N/A')}
+
Modified: ${esc(item.modifiedDate||'N/A')}
+ ${item.eliminated ? '
✓ Eliminated
' : ''}
+
+
`;
} else if(kind==='binaries'){
const path = item.path || item.Path || (typeof item === 'string' ? item : '');
- d.innerHTML = `
📄 Binary
${esc(path)}`;
+ summary = `
📄 Binary
${esc(path)}`;
+ details = `
+
+
+
Path: ${esc(path)}
+ ${item.eliminated ? '
✓ Eliminated
' : ''}
+
+
`;
} else if(kind==='directories'){
const path = item.path || item.Path || (typeof item === 'string' ? item : '');
- d.innerHTML = `
📁 Directory
${esc(path)}`;
+ summary = `
📁 Directory
${esc(path)}`;
+ details = `
+
+
+
Path: ${esc(path)}
+ ${item.eliminated ? '
✓ Eliminated
' : ''}
+
+
`;
}
+
+ d.innerHTML = `
+
${summary}
+
${details}
+ `;
+
+ // Toggle expand/collapse on click
+ d.addEventListener('click', () => {
+ const detailsEl = d.querySelector('.item-details');
+ const isExpanded = detailsEl.style.display !== 'none';
+
+ if (isExpanded) {
+ detailsEl.style.display = 'none';
+ } else {
+ detailsEl.style.display = 'block';
+ }
+ });
+
return d;
}
@@ -454,10 +619,14 @@
await typeText(huntTag, 'Report: '+data.reportName+'.json', 30);
// Load report
- const rep = await (await fetch('/api/report?file='+encodeURIComponent(data.reportName+'.json'))).json();
+ const reportFileName = data.reportName+'.json';
+ const rep = await (await fetch('/api/report?file='+encodeURIComponent(reportFileName))).json();
lastReportData = rep;
renderReport(rep);
+ // Set active report indicator
+ setActiveReport(reportFileName);
+
// Show View Report button AFTER everything is loaded
viewReportBtn.classList.remove('hidden');
}
@@ -497,9 +666,19 @@
// Elimination interface
let currentReport = null;
+ let showEliminatedItems = false;
const elimTree = document.getElementById('elimTree');
const elimCenter = document.getElementById('elimCenter');
const elimWiki = document.getElementById('elimWiki');
+ const toggleEliminatedBtn = document.getElementById('toggleEliminatedBtn');
+ const toggleEliminatedText = document.getElementById('toggleEliminatedText');
+
+ // Toggle eliminated items visibility
+ toggleEliminatedBtn.addEventListener('click', () => {
+ showEliminatedItems = !showEliminatedItems;
+ elimTree.classList.toggle('show-eliminated', showEliminatedItems);
+ toggleEliminatedText.textContent = showEliminatedItems ? 'Hide Eliminated Items' : 'Show Eliminated Items';
+ });
// Order of operations for safe elimination
const eliminationOrder = {
@@ -621,6 +800,7 @@
function loadEliminationData(data) {
currentReport = data;
const findings = data.findings || data;
+ console.log('Loading elimination data, findings:', findings);
// Build tree structure using order of operations
const categories = [
@@ -632,6 +812,7 @@
{ key: 'binaries', title: 'Binaries', arr: findings.binaries },
{ key: 'directories', title: 'Directories', arr: findings.directories }
];
+ console.log('Categories:', categories);
elimTree.innerHTML = '';
elimCenter.innerHTML = '
Select an item from the tree
';
@@ -648,11 +829,14 @@
if (!Array.isArray(cat.arr) || cat.arr.length === 0) return;
const orderInfo = eliminationOrder[cat.key];
+ const activeCount = cat.arr.filter(item => !item.eliminated).length;
+ const totalCount = cat.arr.length;
+
const node = document.createElement('div');
node.className = 'tree-node';
node.innerHTML = `
▶ ${cat.title}
- ${cat.arr.length}
+ ${activeCount}${activeCount !== totalCount ? ` / ${totalCount}` : ''}
`;
const childContainer = document.createElement('div');
@@ -661,6 +845,14 @@
cat.arr.forEach((item, idx) => {
const child = document.createElement('div');
child.className = 'tree-child';
+ child.dataset.type = cat.key;
+ child.dataset.index = idx;
+
+ // Mark as eliminated if the item has eliminated flag
+ if (item.eliminated) {
+ child.classList.add('eliminated');
+ }
+
child.textContent = getItemLabel(cat.key, item, idx);
child.onclick = (e) => {
e.stopPropagation();
@@ -688,12 +880,44 @@
if (type === 'processes') return `${item.name} (PID ${item.pid})`;
if (type === 'services') return item.displayName || item.name;
if (type === 'tasks') return item.name;
- if (type === 'autoruns') return item.imageName || item.entry || `Entry ${idx + 1}`;
+ if (type === 'autoruns') return item.image_name || item.imageName || item.entry || 'Unknown AutoRun';
if (type === 'binaries') return item.path || item;
if (type === 'directories') return item.path || item;
return `Item ${idx + 1}`;
}
+ function updateCategoryCount(type) {
+ // Find the category node and update its count
+ const categoryTitles = {
+ 'connections': 'Outbound Connections',
+ 'processes': 'Processes',
+ 'services': 'Services',
+ 'tasks': 'Scheduled Tasks',
+ 'autoruns': 'AutoRuns',
+ 'binaries': 'Binaries',
+ 'directories': 'Directories'
+ };
+
+ const categoryTitle = categoryTitles[type];
+ const treeNodes = elimTree.querySelectorAll('.tree-node');
+
+ treeNodes.forEach(node => {
+ if (node.textContent.includes(categoryTitle)) {
+ const childContainer = node.nextElementSibling;
+ if (childContainer) {
+ const allChildren = Array.from(childContainer.querySelectorAll('.tree-child'));
+ const activeCount = allChildren.filter(child => !child.classList.contains('eliminated')).length;
+ const totalCount = allChildren.length;
+
+ const tagElement = node.querySelector('.tag');
+ if (tagElement) {
+ tagElement.textContent = `${activeCount}${activeCount !== totalCount ? ` / ${totalCount}` : ''}`;
+ }
+ }
+ }
+ });
+ }
+
function showItemDetails(type, item, idx) {
const orderInfo = eliminationOrder[type];
const wiki = wikiContent[type];
@@ -737,18 +961,53 @@
}
function renderItemFields(type, item, wiki) {
- const fields = Object.keys(wiki.fields);
- return fields.map(field => {
+ // Get all fields from the item, not just wiki fields
+ const allFields = new Set();
+
+ // Add wiki fields first (for ordering)
+ Object.keys(wiki.fields).forEach(f => allFields.add(f));
+
+ // Add all actual item fields (handle snake_case variants)
+ Object.keys(item).forEach(key => {
+ if (key !== 'eliminated') { // Skip eliminated flag
+ allFields.add(key);
+ // Also add camelCase variant if this is snake_case
+ if (key.includes('_')) {
+ const camelCase = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
+ allFields.add(camelCase);
+ }
+ }
+ });
+
+ return Array.from(allFields).map(field => {
+ // Try both snake_case and camelCase
let value = item[field];
+ if (value === undefined || value === null) {
+ const snakeCase = field.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
+ value = item[snakeCase];
+ }
+
if (value === undefined || value === null || value === '') return '';
+
+ // Handle arrays
+ if (Array.isArray(value)) {
+ value = value.length > 0 ? value.join(', ') : 'None';
+ }
+
+ // Handle booleans
if (typeof value === 'boolean') value = value ? 'Yes' : 'No';
+
+ // Format field name for display
+ const displayName = field.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
+ .split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
+
return `
-
${field}
+
${displayName}
${escapeHTML(String(value))}
`;
- }).join('');
+ }).filter(x => x).join('');
}
// Modal functions
@@ -756,63 +1015,90 @@
const modalIcon = document.getElementById('modalIcon');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
- const modalBtn = document.getElementById('modalBtn');
+ const modalButtons = document.getElementById('modalButtons');
- function showModal(type, title, message) {
+ function showModal(type, title, message, onConfirm = null) {
modal.classList.add('active');
modalIcon.className = `modal-icon ${type}`;
modalTitle.className = `modal-title ${type}`;
modalTitle.textContent = title;
modalMessage.textContent = message;
- modalBtn.className = `modal-btn ${type}`;
- modalBtn.textContent = 'OK';
if (type === 'success') {
modalIcon.textContent = '✓';
- } else {
+ } else if (type === 'error') {
modalIcon.textContent = '✕';
+ } else if (type === 'confirm') {
+ modalIcon.textContent = '⚠';
}
- modalBtn.onclick = () => {
- modal.classList.remove('active');
- };
+ // Clear existing buttons
+ modalButtons.innerHTML = '';
+
+ if (type === 'confirm' && onConfirm) {
+ // Confirmation modal with Cancel and Eliminate buttons
+ const cancelBtn = document.createElement('button');
+ cancelBtn.className = 'modal-btn';
+ cancelBtn.textContent = 'Cancel';
+ cancelBtn.onclick = () => {
+ modal.classList.remove('active');
+ };
+
+ const confirmBtn = document.createElement('button');
+ confirmBtn.className = 'modal-btn danger';
+ confirmBtn.textContent = 'Yes, Eliminate This Item';
+ confirmBtn.onclick = () => {
+ modal.classList.remove('active');
+ onConfirm();
+ };
+
+ modalButtons.appendChild(cancelBtn);
+ modalButtons.appendChild(confirmBtn);
+ } else {
+ // Regular modal with just OK button
+ const okBtn = document.createElement('button');
+ okBtn.className = `modal-btn primary`;
+ okBtn.textContent = 'OK';
+ okBtn.onclick = () => {
+ modal.classList.remove('active');
+ };
+ modalButtons.appendChild(okBtn);
+ }
}
window.eliminateItem = async function(type, idx) {
- if (!confirm(`Are you sure you want to eliminate this ${type.slice(0, -1)}?\n\nThis action cannot be undone.`)) return;
-
if (!currentReport) {
showModal('error', 'Error', 'No report loaded');
return;
}
- // Get the report filename from the current report
- const reportFile = currentReport.reportName || currentReport.name || 'rmm-hunter-report.json';
+ // Show custom confirmation modal
+ const typeName = type.slice(0, -1);
+ showModal('confirm', 'Confirm Elimination',
+ `Are you sure you want to eliminate this ${typeName}?\n\nThis action cannot be undone and will permanently remove it from your system.`,
+ async () => {
+ // User confirmed, proceed with elimination
+ await performElimination(type, idx);
+ }
+ );
+ };
- // Find the tree child element for this item
+ async function performElimination(type, idx) {
+ // Get the report filename from the current report
+ let reportFile = currentReport.reportName || currentReport.name || 'rmm-hunter-report.json';
+ // Ensure .json extension
+ if (!reportFile.endsWith('.json')) {
+ reportFile += '.json';
+ }
+
+ // Find the tree child element for this item using data attributes
const treeChildren = document.querySelectorAll('.tree-child');
let targetElement = null;
- // Find the element by matching the index and type
- treeChildren.forEach((child, childIdx) => {
- const parentNode = child.previousElementSibling;
- if (parentNode && parentNode.classList.contains('tree-node')) {
- // Check if this is the right category
- const categoryText = parentNode.textContent.toLowerCase();
- if ((type === 'connections' && categoryText.includes('outbound')) ||
- (type === 'processes' && categoryText.includes('processes')) ||
- (type === 'services' && categoryText.includes('services')) ||
- (type === 'tasks' && categoryText.includes('scheduled')) ||
- (type === 'autoruns' && categoryText.includes('autorun')) ||
- (type === 'binaries' && categoryText.includes('binaries')) ||
- (type === 'directories' && categoryText.includes('directories'))) {
- // This is the right category, now check index
- const siblings = Array.from(child.parentElement.children).filter(c => c.classList.contains('tree-child'));
- const itemIdx = siblings.indexOf(child);
- if (itemIdx === idx) {
- targetElement = child;
- }
- }
+ // Find the element by matching the data attributes
+ treeChildren.forEach((child) => {
+ if (child.dataset.type === type && parseInt(child.dataset.index) === idx) {
+ targetElement = child;
}
});
@@ -834,33 +1120,47 @@
return;
}
- // Success! Trigger slide-out animation
+ // Success! Trigger slide-out animation and mark as eliminated
if (targetElement) {
targetElement.classList.add('eliminating');
- // Wait for animation to complete before updating tree
- setTimeout(async () => {
+ // Wait for animation to complete before hiding
+ setTimeout(() => {
+ // Mark as eliminated (hidden by default, shown in greyscale if toggle is on)
+ targetElement.classList.remove('eliminating');
+ targetElement.classList.add('eliminated');
+
+ // Update the item in currentReport to mark it as eliminated
+ if (currentReport && currentReport.findings) {
+ const findings = currentReport.findings;
+ const typeMap = {
+ 'connections': 'outboundConnections',
+ 'processes': 'processes',
+ 'services': 'services',
+ 'tasks': 'scheduledTasks',
+ 'autoruns': 'autoRuns',
+ 'binaries': 'binaries',
+ 'directories': 'directories'
+ };
+ const arrayKey = typeMap[type];
+ if (findings[arrayKey] && findings[arrayKey][idx]) {
+ findings[arrayKey][idx].eliminated = true;
+ }
+ }
+
+ // Update the category count badge
+ updateCategoryCount(type);
+
// Show success modal
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
- // Reload the report to get updated data
- const updatedReport = await (await fetch('/api/report?file=' + encodeURIComponent(reportFile))).json();
- currentReport = updatedReport;
-
- // Reload elimination tree with updated data
- loadEliminationData(updatedReport);
-
// Clear the center and wiki panels
elimCenter.innerHTML = '
Select an item from the tree
';
elimWiki.innerHTML = '
Item details will appear here
';
}, 600); // Match animation duration
} else {
- // Fallback if element not found
+ // Fallback if element not found - just show success
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
-
- const updatedReport = await (await fetch('/api/report?file=' + encodeURIComponent(reportFile))).json();
- currentReport = updatedReport;
- loadEliminationData(updatedReport);
elimCenter.innerHTML = '
Select an item from the tree
';
elimWiki.innerHTML = '
Item details will appear here
';
}
@@ -868,7 +1168,7 @@
} catch (error) {
showModal('error', 'Error', `Failed to eliminate: ${error.message}`);
}
- };
+ }
// Update renderReport to also load elimination data
const originalRenderReport = renderReport;
diff --git a/internal/web/webserver.go b/internal/web/webserver.go
index 15ad33a..0cca693 100644
--- a/internal/web/webserver.go
+++ b/internal/web/webserver.go
@@ -216,12 +216,16 @@ func (s *server) handleListHunts(w http.ResponseWriter, r *http.Request) {
func (s *server) handleGetReport(w http.ResponseWriter, r *http.Request) {
f := r.URL.Query().Get("file")
if f == "" || strings.Contains(f, "..") {
- http.Error(w, "bad file", 400)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": "bad file"})
return
}
b, err := os.ReadFile(f)
if err != nil {
- http.Error(w, "not found", 404)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
return
}
w.Header().Set("Content-Type", "application/json")
@@ -230,7 +234,9 @@ func (s *server) handleGetReport(w http.ResponseWriter, r *http.Request) {
func (s *server) handleStartHunt(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
- http.Error(w, "use POST", 405)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ json.NewEncoder(w).Encode(map[string]string{"error": "use POST"})
return
}
name := fmt.Sprintf("hunt-%s", time.Now().Format("20060102-150405"))
@@ -296,7 +302,9 @@ func (s *server) handleWS(w http.ResponseWriter, r *http.Request) {
func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
return
}
@@ -307,26 +315,42 @@ func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid request", http.StatusBadRequest)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": "invalid request"})
return
}
// Load the report file
- reportPath := filepath.Join(".", req.ReportFile)
+ reportFile := req.ReportFile
+ if !strings.HasSuffix(reportFile, ".json") {
+ reportFile += ".json"
+ }
+ reportPath := filepath.Join(".", reportFile)
data, err := os.ReadFile(reportPath)
if err != nil {
- http.Error(w, fmt.Sprintf("failed to read report: %v", err), http.StatusInternalServerError)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to read report: %v", err)})
return
}
- var report suspicious.Suspicious
- if err := json.Unmarshal(data, &report); err != nil {
- http.Error(w, fmt.Sprintf("failed to parse report: %v", err), http.StatusInternalServerError)
+ // Parse the full report structure with findings wrapper
+ var fullReport struct {
+ ReportName string `json:"reportName"`
+ GeneratedAt string `json:"generatedAt"`
+ RiskRating interface{} `json:"riskRating"`
+ Findings suspicious.Suspicious `json:"findings"`
+ }
+ if err := json.Unmarshal(data, &fullReport); err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to parse report: %v", err)})
return
}
// Perform elimination based on type
- if err := performElimination(&report, req.Type, req.Index); err != nil {
+ if err := performElimination(&fullReport.Findings, req.Type, req.Index); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
@@ -334,14 +358,18 @@ func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
}
// Save updated report
- updatedData, err := json.MarshalIndent(report, "", " ")
+ updatedData, err := json.MarshalIndent(fullReport, "", " ")
if err != nil {
- http.Error(w, fmt.Sprintf("failed to marshal report: %v", err), http.StatusInternalServerError)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to marshal report: %v", err)})
return
}
if err := os.WriteFile(reportPath, updatedData, 0644); err != nil {
- http.Error(w, fmt.Sprintf("failed to save report: %v", err), http.StatusInternalServerError)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to save report: %v", err)})
return
}
@@ -351,7 +379,9 @@ func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
- http.Error(w, "use POST", 405)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ json.NewEncoder(w).Encode(map[string]string{"error": "use POST"})
return
}
w.Header().Set("Content-Type", "application/json")
@@ -363,8 +393,8 @@ func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
func performElimination(report *suspicious.Suspicious, typeKey string, idx int) error {
switch typeKey {
case "connections":
- if idx >= len(report.OutboundConnections) {
- return fmt.Errorf("invalid index")
+ if idx < 0 || idx >= len(report.OutboundConnections) {
+ return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.OutboundConnections))
}
conn := report.OutboundConnections[idx]
if err := eliminate.EliminateConnection(conn.RemoteHost); err != nil {
@@ -373,8 +403,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.OutboundConnections[idx].Eliminated = true
case "processes":
- if idx >= len(report.Processes) {
- return fmt.Errorf("invalid index")
+ if idx < 0 || idx >= len(report.Processes) {
+ return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Processes))
}
proc := report.Processes[idx]
if err := eliminate.EliminateProcess(proc); err != nil {
@@ -383,8 +413,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.Processes[idx].Eliminated = true
case "services":
- if idx >= len(report.Services) {
- return fmt.Errorf("invalid index")
+ if idx < 0 || idx >= len(report.Services) {
+ return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Services))
}
svc := report.Services[idx]
if svc == nil {
@@ -396,8 +426,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.Services[idx].Eliminated = true
case "tasks":
- if idx >= len(report.ScheduledTasks) {
- return fmt.Errorf("invalid index")
+ if idx < 0 || idx >= len(report.ScheduledTasks) {
+ return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.ScheduledTasks))
}
task := report.ScheduledTasks[idx]
if task == nil {
@@ -409,8 +439,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.ScheduledTasks[idx].Eliminated = true
case "autoruns":
- if idx >= len(report.AutoRuns) {
- return fmt.Errorf("invalid index")
+ if idx < 0 || idx >= len(report.AutoRuns) {
+ return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.AutoRuns))
}
ar := report.AutoRuns[idx]
if err := eliminate.EliminateAutoRun(ar); err != nil {
@@ -419,8 +449,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.AutoRuns[idx].Eliminated = true
case "binaries":
- if idx >= len(report.Binaries) {
- return fmt.Errorf("invalid index")
+ if idx < 0 || idx >= len(report.Binaries) {
+ return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Binaries))
}
bin := report.Binaries[idx]
// Check if binary is blocked by active processes/services
@@ -433,8 +463,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.Binaries[idx].Eliminated = true
case "directories":
- if idx >= len(report.Directories) {
- return fmt.Errorf("invalid index")
+ if idx < 0 || idx >= len(report.Directories) {
+ return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Directories))
}
dir := report.Directories[idx]
// Check if directory is blocked by active processes/services