Release / Plan (reuse the published image?) (push) Successful in 3s
CI / Docker image (build and smoke test) (push) Skipped
CI / Format and clippy (push) Successful in 4m32s
CI / Dependency audit (push) Successful in 11s
CI / Tests (Linux) (push) Successful in 9m15s
Release / ci (push) Successful in 9m18s
Release / Build image (arm64) (push) Failing after 7m7s
Release / Build image (amd64) (push) Failing after 13m53s
Release / Publish tags (push) Skipped
Release / Gitea release (push) Skipped
Acaua is a collective SOC daemon consuming only Zeek log files, not a single-user tool needing local hardware like its siblings. Drop the Tauri desktop shell, installers and self-update entirely; the server and its web dashboard ship as a container, updated by pulling a new image tag. Link against the official prebuilt libduckdb instead of compiling it from source via the bundled feature, cutting image build time from 15+ minutes to about 3. Fix a real bug found along the way: as PID 1 in a container, the daemon ignored SIGTERM and was always killed after the stop timeout; it now shuts down gracefully. Also corrects the Pillar 1 beaconing score: the period estimate had a harmonic alias (a perfectly periodic 4h series resolved to 600s), and the old score formula flagged pure random traffic as suspicious (p95 = 0.839 on Poisson noise). The new formula was validated against 300 samples per scenario. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
221 lines
6.9 KiB
JavaScript
221 lines
6.9 KiB
JavaScript
import { setLang, tr } from './i18n.js';
|
|
|
|
const $ = (selector) => document.querySelector(selector);
|
|
|
|
const connection = $('#connection');
|
|
const connectionText = $('#connectionText');
|
|
const version = $('#version');
|
|
const updateBtn = $('#updateBtn');
|
|
const datasetPath = $('#datasetPath');
|
|
const analyzeBtn = $('#analyzeBtn');
|
|
const refreshBtn = $('#refreshBtn');
|
|
const output = $('#output');
|
|
const alertList = $('#alertList');
|
|
const settingsDlg = $('#settingsDlg');
|
|
const settingsBtn = $('#settingsBtn');
|
|
const closeSettings = $('#closeSettings');
|
|
const doneSettings = $('#doneSettings');
|
|
const langSel = $('#langSel');
|
|
const themePick = $('#themePick');
|
|
|
|
let lastAlerts = [];
|
|
let themePreference = localStorage.getItem('acaua-theme') || 'auto';
|
|
|
|
async function fetchJson(path, options = {}) {
|
|
const response = await fetch(path, options);
|
|
const body = await response.json().catch(() => ({}));
|
|
if (!response.ok) throw new Error(body.error || response.statusText);
|
|
return body;
|
|
}
|
|
|
|
async function call(command, args = {}) {
|
|
switch (command) {
|
|
case 'app_info':
|
|
return fetchJson('/api/status');
|
|
case 'analyze_dataset':
|
|
return fetchJson('/api/analyze', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(args),
|
|
});
|
|
case 'list_alerts':
|
|
return fetchJson('/api/alerts');
|
|
case 'update_check':
|
|
return fetchJson('/api/update');
|
|
case 'doctor_report':
|
|
return fetchJson('/api/health');
|
|
default:
|
|
throw new Error(`unsupported command: ${command}`);
|
|
}
|
|
}
|
|
|
|
function message(template, ...values) {
|
|
let index = 0;
|
|
return tr(template).replace(/\{n\}/g, () => values[index++] ?? '');
|
|
}
|
|
|
|
function renderAlerts(alerts) {
|
|
lastAlerts = Array.isArray(alerts) ? alerts : [];
|
|
alertList.replaceChildren();
|
|
if (!lastAlerts.length) {
|
|
const empty = document.createElement('li');
|
|
empty.className = 'empty';
|
|
empty.textContent = tr('Nenhum alerta ainda.');
|
|
alertList.append(empty);
|
|
return;
|
|
}
|
|
for (const alert of lastAlerts) {
|
|
const item = document.createElement('li');
|
|
const meta = document.createElement('div');
|
|
meta.className = 'alert-meta';
|
|
const pillar = document.createElement('strong');
|
|
pillar.textContent = message('Pilar {n}', alert.pillar);
|
|
const score = document.createElement('span');
|
|
score.className = 'mono';
|
|
score.textContent = Number(alert.score).toFixed(3);
|
|
meta.append(pillar, score);
|
|
const summary = document.createElement('div');
|
|
summary.className = 'alert-summary';
|
|
summary.setAttribute('data-i18n-skip', '');
|
|
summary.textContent = alert.summary;
|
|
item.append(meta, summary);
|
|
alertList.append(item);
|
|
}
|
|
}
|
|
|
|
async function refreshStatus() {
|
|
try {
|
|
const info = await call('app_info');
|
|
version.textContent = `v${info.version}`;
|
|
connectionText.textContent = tr('Serviço local ativo');
|
|
connection.classList.remove('offline');
|
|
} catch (error) {
|
|
connectionText.textContent = tr('Serviço indisponível');
|
|
connection.classList.add('offline');
|
|
version.textContent = '—';
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
async function refreshAlerts() {
|
|
try {
|
|
renderAlerts(await call('list_alerts'));
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
async function analyzeDataset() {
|
|
const path = datasetPath.value.trim();
|
|
if (!path) {
|
|
output.textContent = tr('Nenhum dataset selecionado.');
|
|
return;
|
|
}
|
|
analyzeBtn.disabled = true;
|
|
output.textContent = tr('Analisando…');
|
|
try {
|
|
const report = await call('analyze_dataset', { path });
|
|
output.textContent = message(
|
|
'Análise concluída: {n} registros e {n} alertas.',
|
|
report.records,
|
|
report.alerts.length,
|
|
);
|
|
await refreshAlerts();
|
|
} catch (error) {
|
|
output.textContent = message('Falha na análise: {n}', error.message);
|
|
} finally {
|
|
analyzeBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function checkUpdate() {
|
|
updateBtn.disabled = true;
|
|
updateBtn.textContent = tr('Verificando atualização…');
|
|
try {
|
|
const status = await call('update_check');
|
|
updateBtn.textContent = status.update_available
|
|
? message('Atualização {n} disponível.', status.latest_version)
|
|
: message('Versão {n} é a mais recente.', status.current_version);
|
|
} catch (error) {
|
|
updateBtn.title = message('Não foi possível verificar a atualização: {n}', error.message);
|
|
updateBtn.textContent = tr('checar atualização');
|
|
} finally {
|
|
updateBtn.disabled = false;
|
|
window.setTimeout(() => {
|
|
updateBtn.textContent = tr('checar atualização');
|
|
updateBtn.title = '';
|
|
}, 6000);
|
|
}
|
|
}
|
|
|
|
function applyTheme(preference) {
|
|
themePreference = preference;
|
|
localStorage.setItem('acaua-theme', preference);
|
|
const light = window.matchMedia('(prefers-color-scheme: light)').matches;
|
|
const effective = preference === 'auto' ? (light ? 'light' : 'dark') : preference;
|
|
document.documentElement.dataset.theme = effective;
|
|
themePick.querySelectorAll('[data-theme-pick]').forEach((button) => {
|
|
button.classList.toggle('active', button.dataset.themePick === preference);
|
|
});
|
|
}
|
|
|
|
function openSettings() {
|
|
langSel.value = localStorage.getItem('acaua-lang') || 'auto';
|
|
settingsDlg.showModal();
|
|
}
|
|
|
|
function closeDialog() {
|
|
settingsDlg.close();
|
|
}
|
|
|
|
function setActiveNav(target) {
|
|
document.querySelectorAll('.nav-item').forEach((button) => {
|
|
button.classList.toggle('active', button.dataset.target === target);
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
applyTheme(themePreference);
|
|
langSel.value = localStorage.getItem('acaua-lang') || 'auto';
|
|
setLang(langSel.value);
|
|
refreshStatus();
|
|
refreshAlerts();
|
|
|
|
analyzeBtn.addEventListener('click', analyzeDataset);
|
|
datasetPath.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') analyzeDataset();
|
|
});
|
|
refreshBtn.addEventListener('click', async () => {
|
|
await refreshStatus();
|
|
await refreshAlerts();
|
|
});
|
|
updateBtn.addEventListener('click', checkUpdate);
|
|
settingsBtn.addEventListener('click', openSettings);
|
|
closeSettings.addEventListener('click', closeDialog);
|
|
doneSettings.addEventListener('click', closeDialog);
|
|
langSel.addEventListener('change', () => {
|
|
localStorage.setItem('acaua-lang', langSel.value);
|
|
setLang(langSel.value);
|
|
refreshStatus();
|
|
renderAlerts(lastAlerts);
|
|
updateBtn.textContent = tr('checar atualização');
|
|
});
|
|
themePick.addEventListener('click', (event) => {
|
|
const button = event.target.closest('[data-theme-pick]');
|
|
if (button) applyTheme(button.dataset.themePick);
|
|
});
|
|
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
|
|
if (themePreference === 'auto') applyTheme('auto');
|
|
});
|
|
document.querySelectorAll('.nav-item').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const target = button.dataset.target;
|
|
setActiveNav(target);
|
|
if (target === 'settings') openSettings();
|
|
else document.getElementById(target)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
});
|
|
}
|
|
|
|
init();
|