312 lines
13 KiB
JavaScript
312 lines
13 KiB
JavaScript
// app/static/js/results/core.js
|
|
// Entry point for the /results/<analysis_type>/<target> page.
|
|
// Wires up TabManager, PayloadManager, AnalysisTypeHandler, ModalHandler
|
|
// and the AnalysisCore poll loop.
|
|
|
|
import { TabManager, PayloadManager, AnalysisTypeHandler, ModalHandler } from './managers.js';
|
|
import { UI } from './renderers.js';
|
|
import { tools } from './tools.js';
|
|
|
|
// Analysis Core Logic
|
|
class AnalysisCore {
|
|
constructor() {
|
|
this.elements = {
|
|
analysisStatus: document.getElementById('analysisStatus'),
|
|
statusIcon: document.getElementById('statusIcon'),
|
|
analysisTimer: document.getElementById('analysisTimer'),
|
|
stageLine: document.getElementById('stageLine'),
|
|
analysisStage: document.getElementById('analysisStage')
|
|
};
|
|
this.startTime = Date.now();
|
|
this.timerInterval = null;
|
|
|
|
const pathParts = window.location.pathname.split('/');
|
|
this.analysisType = pathParts[2];
|
|
// Static/dynamic: /analyze/<type>/<hash> — hash at index 3.
|
|
// EDR: /analyze/edr/<profile>/<hash> — hash at index 4
|
|
// and profile at index 3 (used by the renderer to label the run).
|
|
this.fileHash = pathParts[pathParts.length - 1];
|
|
this.edrProfile = this.analysisType === 'edr' ? pathParts[3] : null;
|
|
}
|
|
|
|
updateTimer() {
|
|
const elapsed = Date.now() - this.startTime;
|
|
const minutes = Math.floor(elapsed / 60000);
|
|
const seconds = Math.floor((elapsed % 60000) / 1000);
|
|
const milliseconds = elapsed % 1000;
|
|
this.elements.analysisTimer.textContent =
|
|
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
|
|
// Update summary scan duration
|
|
document.getElementById('scanDuration').textContent = this.elements.analysisTimer.textContent;
|
|
}
|
|
startTimer() {
|
|
this.timerInterval = setInterval(() => this.updateTimer(), 1000);
|
|
}
|
|
|
|
stopTimer() {
|
|
clearInterval(this.timerInterval);
|
|
}
|
|
|
|
updateStatusIcon(status) {
|
|
this.elements.statusIcon.innerHTML = UI.icons[status] || '';
|
|
}
|
|
|
|
updateStageToComplete() {
|
|
// The new shell shows progress through the breadcrumb + statusbar,
|
|
// so the standalone stage indicator was dropped. Keep this method
|
|
// as a no-op when the legacy elements aren't in the DOM.
|
|
if (this.elements.stageLine) {
|
|
this.elements.stageLine.classList.remove('bg-gray-800');
|
|
this.elements.stageLine.classList.add('bg-green-500/15');
|
|
}
|
|
if (this.elements.analysisStage) {
|
|
this.elements.analysisStage.innerHTML = `
|
|
<div class="w-10 h-10 rounded-full bg-green-500/8 border border-green-500/35 flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-green-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<span class="text-gray-400">Analysis</span>`;
|
|
}
|
|
}
|
|
|
|
async startAnalysis() {
|
|
this.updateStatusIcon('running');
|
|
this.elements.analysisStatus.textContent = 'Running analysis...';
|
|
this.startTimer();
|
|
|
|
try {
|
|
// Retrieve the arguments from localStorage
|
|
const savedArgs = localStorage.getItem('analysisArgs');
|
|
const args = savedArgs ? JSON.parse(savedArgs) : []; // Default to an empty array if no args are saved
|
|
|
|
// POST to the same path the page was loaded from. This naturally
|
|
// handles the EDR case (/analyze/edr/<profile>/<hash>) without
|
|
// a special branch.
|
|
const response = await fetch(window.location.pathname, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
args // Dynamically include the arguments retrieved from storage
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
// Handle early termination
|
|
if (data.status === 'early_termination') {
|
|
this.updateTimer();
|
|
this.stopTimer();
|
|
this.updateStatusIcon('error');
|
|
this.elements.analysisStatus.textContent = data.error || 'Process terminated early';
|
|
|
|
// Create a minimal results object for summary
|
|
const results = {
|
|
status: 'early_termination',
|
|
analysis_metadata: {
|
|
early_termination: true,
|
|
total_duration: data.details?.termination_time || 0
|
|
}
|
|
};
|
|
|
|
if (tools.summary) {
|
|
tools.summary.render(results);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Normal completion flow.
|
|
//
|
|
// Special-case EDR runs that are still in their Phase-2 alert
|
|
// polling window: the page isn't actually done yet, so leave
|
|
// the duration timer running and don't flip the status to
|
|
// "Analysis completed". The EDR poll handler will stop the
|
|
// timer + flip status when Phase 2 lands a terminal result.
|
|
const edrPolling = !!(data.results && data.results.edr &&
|
|
data.results.edr.status === 'polling_alerts');
|
|
|
|
this.updateTimer();
|
|
if (!edrPolling) {
|
|
this.stopTimer();
|
|
this.updateStatusIcon('complete');
|
|
this.elements.analysisStatus.textContent = 'Analysis completed';
|
|
this.updateStageToComplete();
|
|
}
|
|
|
|
// First update the summary with all results
|
|
if (tools.summary && data.results) {
|
|
try {
|
|
tools.summary.render(data.results);
|
|
} catch (err) {
|
|
console.error('[results] summary render failed:', err);
|
|
}
|
|
}
|
|
|
|
// Then process individual tool results — isolate each so a
|
|
// single broken renderer doesn't suppress the rest.
|
|
Object.entries(data.results || {}).forEach(([toolKey, results]) => {
|
|
if (results && tools[toolKey] && toolKey !== 'summary') {
|
|
try {
|
|
tools[toolKey].render(results);
|
|
} catch (err) {
|
|
console.error(`[results] ${toolKey} render failed:`, err);
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
this.stopTimer();
|
|
this.updateStatusIcon('error');
|
|
this.elements.analysisStatus.textContent = `Error: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function handleUrlIdentifier() {
|
|
const urlPath = window.location.pathname;
|
|
const pathSegments = urlPath.split('/').filter(segment => segment.length > 0);
|
|
const identifier = pathSegments[pathSegments.length - 1];
|
|
|
|
if (isNumeric(identifier)) {
|
|
const staticButton = document.getElementById('staticAnalysisButton');
|
|
if (staticButton) {
|
|
staticButton.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function isNumeric(str) {
|
|
return /^\d+$/.test(str);
|
|
}
|
|
}
|
|
|
|
// HolyGrail scan function (add this anywhere in the file)
|
|
window.startHolyGrailScan = function() {
|
|
const pathParts = window.location.pathname.split('/');
|
|
const fileHash = pathParts[pathParts.length - 1];
|
|
|
|
if (!fileHash) {
|
|
console.error('No file hash found');
|
|
return;
|
|
}
|
|
|
|
// Show loading message
|
|
const button = document.getElementById('holygrailAnalysisButton');
|
|
if (button) {
|
|
button.innerHTML = `
|
|
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span>Starting HolyGrail Analysis...</span>
|
|
`;
|
|
button.disabled = true;
|
|
}
|
|
|
|
// Call HolyGrail analysis endpoint
|
|
fetch(`/holygrail?hash=${fileHash}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
// Redirect to results page
|
|
window.location.href = `/results/byovd/${fileHash}`;
|
|
} else {
|
|
// Handle error and restore button
|
|
console.error('HolyGrail analysis failed:', data.error || data.message);
|
|
restoreHolyGrailButton();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('HolyGrail analysis error:', error);
|
|
restoreHolyGrailButton();
|
|
});
|
|
|
|
function restoreHolyGrailButton() {
|
|
if (button) {
|
|
button.innerHTML = `
|
|
<svg class="w-5 h-5 text-yellow-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 9v2a5 5 0 0010 0V9"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 9h12"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 16v3"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19h6"/>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 9h-1a1 1 0 000 2h1M18 9h1a1 1 0 010 2h-1"/>
|
|
</svg>
|
|
<span>HolyGrail BYOVD Scan</span>
|
|
`;
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Initialize Everything
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Initialize tab navigation
|
|
const tabManager = new TabManager();
|
|
|
|
// Process URL identifier logic
|
|
handleUrlIdentifier();
|
|
|
|
// Initialize modal and analysis handlers
|
|
const modal = new ModalHandler();
|
|
const analysis = new AnalysisCore();
|
|
// Expose so EDR's Phase-2 poll handler can stop the timer + flip the
|
|
// status when correlation finishes — the duration chip should keep
|
|
// ticking through Phase 2, not freeze when Phase 1 returns.
|
|
window.__analysisCore = analysis;
|
|
|
|
// Initialize PayloadManager
|
|
const payloadManager = new PayloadManager();
|
|
// Check file extension and show appropriate button
|
|
const fileExtension = localStorage.getItem('currentFileExtension');
|
|
const dynamicButton = document.getElementById('dynamicAnalysisButton');
|
|
const holygrailButton = document.getElementById('holygrailAnalysisButton');
|
|
|
|
if (fileExtension && fileExtension.toLowerCase() === 'sys') {
|
|
// Show HolyGrail button for .sys files
|
|
if (dynamicButton) dynamicButton.style.display = 'none';
|
|
if (holygrailButton) holygrailButton.style.display = 'flex';
|
|
} else {
|
|
// Show Dynamic Analysis button for other files
|
|
if (holygrailButton) holygrailButton.style.display = 'none';
|
|
if (dynamicButton) dynamicButton.style.display = 'flex';
|
|
}
|
|
// Make modal functions globally accessible
|
|
window.showDynamicWarning = () => {
|
|
// Pre-populate the args input with whatever was last used so the user
|
|
// can re-run with the same args or tweak them.
|
|
const argsInput = document.getElementById('dynamicAnalysisArgs');
|
|
if (argsInput) {
|
|
try {
|
|
const saved = JSON.parse(localStorage.getItem('analysisArgs') || '[]');
|
|
argsInput.value = Array.isArray(saved) ? saved.join(' ') : '';
|
|
} catch {
|
|
argsInput.value = '';
|
|
}
|
|
}
|
|
modal.show();
|
|
};
|
|
window.hideDynamicWarning = () => modal.hide();
|
|
window.proceedWithDynamicAnalysis = (fileHash) => {
|
|
const argsInput = document.getElementById('dynamicAnalysisArgs');
|
|
const argsValue = argsInput ? argsInput.value : '';
|
|
const args = argsValue.split(' ').filter(arg => arg.trim() !== '');
|
|
localStorage.setItem('analysisArgs', JSON.stringify(args));
|
|
window.location.href = `/analyze/dynamic/${fileHash}`;
|
|
};
|
|
|
|
// Start analysis if parameters exist
|
|
if (analysis.analysisType && analysis.fileHash) {
|
|
analysis.startAnalysis();
|
|
}
|
|
|
|
// Use PayloadManager methods
|
|
window.updatePayloadOutput = (results) => payloadManager.updatePayloadOutput(results);
|
|
});
|
|
|