409 lines
18 KiB
HTML
409 lines
18 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% block title %}AUTARCH — Malware Sandbox{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="page-header">
|
||
|
|
<h1>Malware Sandbox</h1>
|
||
|
|
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||
|
|
Submit, analyze, and report on suspicious files using static and dynamic analysis.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tab Bar -->
|
||
|
|
<div class="tab-bar">
|
||
|
|
<button class="tab active" data-tab-group="sandbox" data-tab="submit" onclick="showTab('sandbox','submit')">Submit</button>
|
||
|
|
<button class="tab" data-tab-group="sandbox" data-tab="analyze" onclick="showTab('sandbox','analyze')">Analyze</button>
|
||
|
|
<button class="tab" data-tab-group="sandbox" data-tab="reports" onclick="showTab('sandbox','reports')">Reports</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== SUBMIT TAB ==================== -->
|
||
|
|
<div class="tab-content active" data-tab-group="sandbox" data-tab="submit">
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Submit Sample</h2>
|
||
|
|
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:12px">
|
||
|
|
Upload a file or specify a path on the server to submit for analysis.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<div class="form-row" style="margin-bottom:12px">
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Upload File</label>
|
||
|
|
<input type="file" id="sandbox-upload-file">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div style="font-size:0.8rem;color:var(--text-muted);text-align:center;margin-bottom:12px">-- OR --</div>
|
||
|
|
<div class="form-row" style="margin-bottom:12px">
|
||
|
|
<div class="form-group">
|
||
|
|
<label>Server File Path</label>
|
||
|
|
<input type="text" id="sandbox-file-path" placeholder="/path/to/suspicious/file">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="tool-actions" style="margin-bottom:12px">
|
||
|
|
<button id="btn-sandbox-submit" class="btn btn-primary" onclick="sandboxSubmit()">Submit Sample</button>
|
||
|
|
</div>
|
||
|
|
<pre class="output-panel" id="sandbox-submit-output" style="min-height:0"></pre>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Submitted Samples</h2>
|
||
|
|
<div class="tool-actions" style="margin-bottom:12px">
|
||
|
|
<button class="btn btn-small" onclick="sandboxLoadSamples()">Refresh</button>
|
||
|
|
</div>
|
||
|
|
<table class="data-table">
|
||
|
|
<thead><tr><th>Name</th><th>Size</th><th>SHA256</th><th>Submitted</th><th>Status</th></tr></thead>
|
||
|
|
<tbody id="sandbox-samples-table">
|
||
|
|
<tr><td colspan="5" class="empty-state">No samples submitted yet.</td></tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== ANALYZE TAB ==================== -->
|
||
|
|
<div class="tab-content" data-tab-group="sandbox" data-tab="analyze">
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Select Sample</h2>
|
||
|
|
<div class="input-row">
|
||
|
|
<select id="sandbox-sample-select" style="flex:2">
|
||
|
|
<option value="">-- Select a sample --</option>
|
||
|
|
</select>
|
||
|
|
<button class="btn btn-small" onclick="sandboxRefreshSelect()">Refresh List</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Static Analysis</h2>
|
||
|
|
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:8px">
|
||
|
|
Inspect file headers, strings, imports, and calculate risk score without execution.
|
||
|
|
</p>
|
||
|
|
<div class="tool-actions" style="margin-bottom:12px">
|
||
|
|
<button id="btn-static" class="btn btn-primary" onclick="sandboxStatic()">Run Static Analysis</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Risk Score Gauge -->
|
||
|
|
<div id="sandbox-static-results" style="display:none">
|
||
|
|
<div style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap;margin-bottom:16px">
|
||
|
|
<div class="score-display">
|
||
|
|
<div class="score-value" id="sandbox-risk-score">--</div>
|
||
|
|
<div class="score-label">Risk Score</div>
|
||
|
|
</div>
|
||
|
|
<div style="flex:1;min-width:250px">
|
||
|
|
<table class="data-table" style="font-size:0.85rem">
|
||
|
|
<tbody>
|
||
|
|
<tr><td>File Type</td><td id="sandbox-file-type">--</td></tr>
|
||
|
|
<tr><td>File Size</td><td id="sandbox-file-size">--</td></tr>
|
||
|
|
<tr><td>MD5</td><td id="sandbox-md5" style="font-family:monospace;font-size:0.8rem">--</td></tr>
|
||
|
|
<tr><td>SHA256</td><td id="sandbox-sha256" style="font-family:monospace;font-size:0.8rem;word-break:break-all">--</td></tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h3>Indicators by Category</h3>
|
||
|
|
<div id="sandbox-indicators" style="margin-bottom:16px"></div>
|
||
|
|
|
||
|
|
<h3>Interesting Strings</h3>
|
||
|
|
<pre class="output-panel scrollable" id="sandbox-strings" style="max-height:200px"></pre>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Dynamic Analysis</h2>
|
||
|
|
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:8px">
|
||
|
|
Execute sample in an isolated Docker container and monitor behavior.
|
||
|
|
</p>
|
||
|
|
<div class="tool-actions" style="margin-bottom:12px">
|
||
|
|
<button id="btn-dynamic" class="btn btn-danger" onclick="sandboxDynamic()">Run Dynamic Analysis</button>
|
||
|
|
<span id="sandbox-dynamic-status" style="font-size:0.8rem;color:var(--text-muted);margin-left:12px"></span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="sandbox-dynamic-results" style="display:none">
|
||
|
|
<h3>Syscalls</h3>
|
||
|
|
<pre class="output-panel scrollable" id="sandbox-syscalls" style="max-height:200px"></pre>
|
||
|
|
|
||
|
|
<h3 style="margin-top:12px">Files Accessed</h3>
|
||
|
|
<pre class="output-panel scrollable" id="sandbox-files" style="max-height:200px"></pre>
|
||
|
|
|
||
|
|
<h3 style="margin-top:12px">Network Calls</h3>
|
||
|
|
<pre class="output-panel scrollable" id="sandbox-network" style="max-height:200px"></pre>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== REPORTS TAB ==================== -->
|
||
|
|
<div class="tab-content" data-tab-group="sandbox" data-tab="reports">
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Analysis Reports</h2>
|
||
|
|
<div class="tool-actions" style="margin-bottom:12px">
|
||
|
|
<button class="btn btn-small" onclick="sandboxLoadReports()">Refresh</button>
|
||
|
|
</div>
|
||
|
|
<table class="data-table">
|
||
|
|
<thead><tr><th>Sample</th><th>Risk Level</th><th>Date</th><th>Action</th></tr></thead>
|
||
|
|
<tbody id="sandbox-reports-table">
|
||
|
|
<tr><td colspan="4" class="empty-state">No reports generated yet.</td></tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section">
|
||
|
|
<h2>Generate Report</h2>
|
||
|
|
<div class="input-row">
|
||
|
|
<select id="sandbox-report-sample" style="flex:2">
|
||
|
|
<option value="">-- Select a sample --</option>
|
||
|
|
</select>
|
||
|
|
<button id="btn-gen-report" class="btn btn-primary" onclick="sandboxGenReport()">Generate Report</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="section" id="sandbox-report-viewer" style="display:none">
|
||
|
|
<h2>Report</h2>
|
||
|
|
<div id="sandbox-report-content"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<'); }
|
||
|
|
|
||
|
|
var _dynamicPoll = null;
|
||
|
|
|
||
|
|
/* ── Submit ── */
|
||
|
|
function sandboxSubmit() {
|
||
|
|
var btn = document.getElementById('btn-sandbox-submit');
|
||
|
|
var fileInput = document.getElementById('sandbox-upload-file');
|
||
|
|
var pathInput = document.getElementById('sandbox-file-path').value.trim();
|
||
|
|
|
||
|
|
if (fileInput.files.length > 0) {
|
||
|
|
setLoading(btn, true);
|
||
|
|
var fd = new FormData();
|
||
|
|
fd.append('file', fileInput.files[0]);
|
||
|
|
fetch('/sandbox/submit', {method: 'POST', body: fd}).then(function(r) { return r.json(); }).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
renderOutput('sandbox-submit-output', data.message || data.error || 'Submitted');
|
||
|
|
if (data.success) { sandboxLoadSamples(); sandboxRefreshSelect(); }
|
||
|
|
}).catch(function() { setLoading(btn, false); renderOutput('sandbox-submit-output', 'Upload failed'); });
|
||
|
|
} else if (pathInput) {
|
||
|
|
setLoading(btn, true);
|
||
|
|
postJSON('/sandbox/submit', {path: pathInput}).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
renderOutput('sandbox-submit-output', data.message || data.error || 'Submitted');
|
||
|
|
if (data.success) { sandboxLoadSamples(); sandboxRefreshSelect(); }
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
} else {
|
||
|
|
renderOutput('sandbox-submit-output', 'Select a file or enter a path.');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function sandboxLoadSamples() {
|
||
|
|
fetchJSON('/sandbox/samples').then(function(data) {
|
||
|
|
var tb = document.getElementById('sandbox-samples-table');
|
||
|
|
var samples = data.samples || [];
|
||
|
|
if (!samples.length) {
|
||
|
|
tb.innerHTML = '<tr><td colspan="5" class="empty-state">No samples submitted yet.</td></tr>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var html = '';
|
||
|
|
samples.forEach(function(s) {
|
||
|
|
var statusBadge = s.status === 'analyzed' ? '<span class="badge badge-pass">Analyzed</span>'
|
||
|
|
: s.status === 'pending' ? '<span class="badge badge-medium">Pending</span>'
|
||
|
|
: '<span class="badge badge-info">' + esc(s.status) + '</span>';
|
||
|
|
html += '<tr><td>' + esc(s.name) + '</td><td>' + esc(s.size) + '</td>'
|
||
|
|
+ '<td style="font-family:monospace;font-size:0.75rem;word-break:break-all">' + esc(s.sha256 || '--') + '</td>'
|
||
|
|
+ '<td>' + esc(s.date) + '</td><td>' + statusBadge + '</td></tr>';
|
||
|
|
});
|
||
|
|
tb.innerHTML = html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function sandboxRefreshSelect() {
|
||
|
|
fetchJSON('/sandbox/samples').then(function(data) {
|
||
|
|
var samples = data.samples || [];
|
||
|
|
var opts = '<option value="">-- Select a sample --</option>';
|
||
|
|
samples.forEach(function(s) {
|
||
|
|
opts += '<option value="' + esc(s.id || s.sha256) + '">' + esc(s.name) + ' (' + esc(s.size) + ')</option>';
|
||
|
|
});
|
||
|
|
document.getElementById('sandbox-sample-select').innerHTML = opts;
|
||
|
|
document.getElementById('sandbox-report-sample').innerHTML = opts;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Static Analysis ── */
|
||
|
|
function sandboxStatic() {
|
||
|
|
var sampleId = document.getElementById('sandbox-sample-select').value;
|
||
|
|
if (!sampleId) { alert('Select a sample first.'); return; }
|
||
|
|
var btn = document.getElementById('btn-static');
|
||
|
|
setLoading(btn, true);
|
||
|
|
document.getElementById('sandbox-static-results').style.display = 'none';
|
||
|
|
|
||
|
|
postJSON('/sandbox/analyze/static', {sample_id: sampleId}).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||
|
|
document.getElementById('sandbox-static-results').style.display = 'block';
|
||
|
|
|
||
|
|
var score = data.risk_score || 0;
|
||
|
|
var scoreEl = document.getElementById('sandbox-risk-score');
|
||
|
|
scoreEl.textContent = score + '/100';
|
||
|
|
scoreEl.style.color = score >= 70 ? 'var(--danger)' : score >= 40 ? 'var(--warning)' : 'var(--success)';
|
||
|
|
|
||
|
|
document.getElementById('sandbox-file-type').textContent = data.file_type || '--';
|
||
|
|
document.getElementById('sandbox-file-size').textContent = data.file_size || '--';
|
||
|
|
document.getElementById('sandbox-md5').textContent = data.md5 || '--';
|
||
|
|
document.getElementById('sandbox-sha256').textContent = data.sha256 || '--';
|
||
|
|
|
||
|
|
var indHtml = '';
|
||
|
|
var indicators = data.indicators || {};
|
||
|
|
Object.keys(indicators).forEach(function(cat) {
|
||
|
|
indHtml += '<div style="margin-bottom:8px"><strong style="color:var(--text-secondary);font-size:0.85rem">' + esc(cat) + '</strong><ul style="margin:4px 0 0 16px;font-size:0.85rem">';
|
||
|
|
(indicators[cat] || []).forEach(function(item) {
|
||
|
|
indHtml += '<li>' + esc(item) + '</li>';
|
||
|
|
});
|
||
|
|
indHtml += '</ul></div>';
|
||
|
|
});
|
||
|
|
document.getElementById('sandbox-indicators').innerHTML = indHtml || '<span class="empty-state">No indicators found.</span>';
|
||
|
|
|
||
|
|
var strings = (data.strings || []).join('\n');
|
||
|
|
renderOutput('sandbox-strings', strings || 'No interesting strings found.');
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Dynamic Analysis ── */
|
||
|
|
function sandboxDynamic() {
|
||
|
|
var sampleId = document.getElementById('sandbox-sample-select').value;
|
||
|
|
if (!sampleId) { alert('Select a sample first.'); return; }
|
||
|
|
var btn = document.getElementById('btn-dynamic');
|
||
|
|
setLoading(btn, true);
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Submitting for dynamic analysis...';
|
||
|
|
document.getElementById('sandbox-dynamic-results').style.display = 'none';
|
||
|
|
|
||
|
|
postJSON('/sandbox/analyze/dynamic', {sample_id: sampleId}).then(function(data) {
|
||
|
|
if (data.error) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Error: ' + data.error;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (data.job_id) {
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Running in Docker sandbox...';
|
||
|
|
sandboxPollDynamic(data.job_id);
|
||
|
|
} else {
|
||
|
|
setLoading(btn, false);
|
||
|
|
sandboxRenderDynamic(data);
|
||
|
|
}
|
||
|
|
}).catch(function() {
|
||
|
|
setLoading(btn, false);
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Request failed';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function sandboxPollDynamic(jobId) {
|
||
|
|
if (_dynamicPoll) clearInterval(_dynamicPoll);
|
||
|
|
_dynamicPoll = setInterval(function() {
|
||
|
|
fetchJSON('/sandbox/analyze/dynamic/status/' + jobId).then(function(data) {
|
||
|
|
if (data.status === 'running') {
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Running... (' + (data.elapsed || '0') + 's)';
|
||
|
|
} else {
|
||
|
|
clearInterval(_dynamicPoll);
|
||
|
|
_dynamicPoll = null;
|
||
|
|
setLoading(document.getElementById('btn-dynamic'), false);
|
||
|
|
if (data.status === 'complete') {
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Analysis complete';
|
||
|
|
sandboxRenderDynamic(data);
|
||
|
|
} else {
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Failed: ' + (data.error || 'unknown');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}).catch(function() {
|
||
|
|
clearInterval(_dynamicPoll);
|
||
|
|
_dynamicPoll = null;
|
||
|
|
setLoading(document.getElementById('btn-dynamic'), false);
|
||
|
|
document.getElementById('sandbox-dynamic-status').textContent = 'Poll error';
|
||
|
|
});
|
||
|
|
}, 3000);
|
||
|
|
}
|
||
|
|
|
||
|
|
function sandboxRenderDynamic(data) {
|
||
|
|
document.getElementById('sandbox-dynamic-results').style.display = 'block';
|
||
|
|
renderOutput('sandbox-syscalls', (data.syscalls || []).join('\n') || 'No syscalls captured.');
|
||
|
|
renderOutput('sandbox-files', (data.files_accessed || []).join('\n') || 'No file access recorded.');
|
||
|
|
renderOutput('sandbox-network', (data.network_calls || []).join('\n') || 'No network activity recorded.');
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Reports ── */
|
||
|
|
function sandboxLoadReports() {
|
||
|
|
fetchJSON('/sandbox/reports').then(function(data) {
|
||
|
|
var tb = document.getElementById('sandbox-reports-table');
|
||
|
|
var reports = data.reports || [];
|
||
|
|
if (!reports.length) {
|
||
|
|
tb.innerHTML = '<tr><td colspan="4" class="empty-state">No reports generated yet.</td></tr>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
var html = '';
|
||
|
|
reports.forEach(function(r) {
|
||
|
|
var lvl = (r.risk_level || 'unknown').toLowerCase();
|
||
|
|
var badgeClass = lvl === 'critical' || lvl === 'high' ? 'badge-fail'
|
||
|
|
: lvl === 'medium' ? 'badge-medium'
|
||
|
|
: lvl === 'low' ? 'badge-low' : 'badge-info';
|
||
|
|
html += '<tr><td>' + esc(r.sample_name) + '</td>'
|
||
|
|
+ '<td><span class="badge ' + badgeClass + '">' + esc(r.risk_level) + '</span></td>'
|
||
|
|
+ '<td>' + esc(r.date) + '</td>'
|
||
|
|
+ '<td><button class="btn btn-small" onclick="sandboxViewReport(\'' + esc(r.id) + '\')">View</button></td></tr>';
|
||
|
|
});
|
||
|
|
tb.innerHTML = html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function sandboxGenReport() {
|
||
|
|
var sampleId = document.getElementById('sandbox-report-sample').value;
|
||
|
|
if (!sampleId) { alert('Select a sample first.'); return; }
|
||
|
|
var btn = document.getElementById('btn-gen-report');
|
||
|
|
setLoading(btn, true);
|
||
|
|
postJSON('/sandbox/reports/generate', {sample_id: sampleId}).then(function(data) {
|
||
|
|
setLoading(btn, false);
|
||
|
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||
|
|
sandboxLoadReports();
|
||
|
|
if (data.report_id) sandboxViewReport(data.report_id);
|
||
|
|
}).catch(function() { setLoading(btn, false); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function sandboxViewReport(reportId) {
|
||
|
|
fetchJSON('/sandbox/reports/' + reportId).then(function(data) {
|
||
|
|
if (data.error) { alert('Error: ' + data.error); return; }
|
||
|
|
document.getElementById('sandbox-report-viewer').style.display = 'block';
|
||
|
|
var html = '<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px">';
|
||
|
|
|
||
|
|
var lvl = (data.risk_level || 'unknown').toLowerCase();
|
||
|
|
var badgeClass = lvl === 'critical' || lvl === 'high' ? 'badge-fail'
|
||
|
|
: lvl === 'medium' ? 'badge-medium'
|
||
|
|
: lvl === 'low' ? 'badge-low' : 'badge-info';
|
||
|
|
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">'
|
||
|
|
+ '<strong>' + esc(data.sample_name || 'Unknown') + '</strong>'
|
||
|
|
+ '<span class="badge ' + badgeClass + '">' + esc(data.risk_level || 'N/A') + '</span></div>';
|
||
|
|
|
||
|
|
html += '<table class="data-table" style="font-size:0.85rem;margin-bottom:12px"><tbody>';
|
||
|
|
html += '<tr><td>Risk Score</td><td>' + (data.risk_score || '--') + '/100</td></tr>';
|
||
|
|
html += '<tr><td>File Type</td><td>' + esc(data.file_type || '--') + '</td></tr>';
|
||
|
|
html += '<tr><td>SHA256</td><td style="font-family:monospace;font-size:0.8rem;word-break:break-all">' + esc(data.sha256 || '--') + '</td></tr>';
|
||
|
|
html += '<tr><td>Date</td><td>' + esc(data.date || '--') + '</td></tr>';
|
||
|
|
html += '</tbody></table>';
|
||
|
|
|
||
|
|
if (data.summary) {
|
||
|
|
html += '<h4 style="margin-bottom:6px;font-size:0.85rem;color:var(--text-secondary)">Summary</h4>';
|
||
|
|
html += '<p style="font-size:0.85rem;margin-bottom:12px">' + esc(data.summary) + '</p>';
|
||
|
|
}
|
||
|
|
|
||
|
|
html += '</div>';
|
||
|
|
document.getElementById('sandbox-report-content').innerHTML = html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Init ── */
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
sandboxLoadSamples();
|
||
|
|
sandboxRefreshSelect();
|
||
|
|
sandboxLoadReports();
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|