@@ -305,6 +305,23 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
|
|||||||
<button class="btn btn-outline-secondary btn-sm ms-2" id="testTelegramBtn"><i class="bi bi-send"></i> Test</button>
|
<button class="btn btn-outline-secondary btn-sm ms-2" id="testTelegramBtn"><i class="bi bi-send"></i> Test</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header"><i class="bi bi-x-circle me-1"></i>False Positives</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="small text-secondary">Lines matching these patterns will be ignored and not trigger any alert.</p>
|
||||||
|
<div class="table-responsive mb-2">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<thead><tr><th>Pattern</th><th>Description</th><th style="width:50px"></th></tr></thead>
|
||||||
|
<tbody id="falsePositivesBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control font-monospace" id="fpPattern" placeholder="/healthcheck/i">
|
||||||
|
<input type="text" class="form-control" id="fpDescription" placeholder="Description (optional)" style="max-width:200px">
|
||||||
|
<button class="btn btn-primary" id="addFpBtn"><i class="bi bi-plus-lg"></i> Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><i class="bi bi-info-circle me-1"></i>How to get your user token</div>
|
<div class="card-header"><i class="bi bi-info-circle me-1"></i>How to get your user token</div>
|
||||||
<div class="card-body small">
|
<div class="card-body small">
|
||||||
@@ -949,6 +966,10 @@ async function loadSettings() {
|
|||||||
document.getElementById('telegramBotToken').value = res.bot_token || '';
|
document.getElementById('telegramBotToken').value = res.bot_token || '';
|
||||||
document.getElementById('telegramChatId').value = res.chat_id || '';
|
document.getElementById('telegramChatId').value = res.chat_id || '';
|
||||||
} catch (e) { console.error('load telegram error', e); }
|
} catch (e) { console.error('load telegram error', e); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadFalsePositives();
|
||||||
|
} catch (e) { console.error('load false positives error', e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('saveTokensBtn').addEventListener('click', async () => {
|
document.getElementById('saveTokensBtn').addEventListener('click', async () => {
|
||||||
@@ -1010,6 +1031,54 @@ document.getElementById('testTelegramBtn').addEventListener('click', async () =>
|
|||||||
btn.innerHTML = '<i class="bi bi-send"></i> Test';
|
btn.innerHTML = '<i class="bi bi-send"></i> Test';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- FALSE POSITIVES ---
|
||||||
|
let fpItems = [];
|
||||||
|
|
||||||
|
async function loadFalsePositives() {
|
||||||
|
try {
|
||||||
|
fpItems = (await api('/false-positives')) || [];
|
||||||
|
renderFalsePositives();
|
||||||
|
} catch (e) { console.error('fp load error', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFalsePositives() {
|
||||||
|
const tbody = document.getElementById('falsePositivesBody');
|
||||||
|
if (!fpItems.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="text-secondary small text-center">No false positives configured</td></tr>';
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = fpItems.map(fp => `<tr>
|
||||||
|
<td><code>${esc(fp.pattern)}</code></td>
|
||||||
|
<td class="text-secondary">${esc(fp.description || '')}</td>
|
||||||
|
<td><button class="btn btn-outline-danger btn-sm py-0" onclick="deleteFp(${fp.id})"><i class="bi bi-trash"></i></button></td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFp(id) {
|
||||||
|
if (!confirm('Delete this false positive pattern?')) return;
|
||||||
|
try {
|
||||||
|
await api('/false-positives/' + id, { method: 'DELETE' });
|
||||||
|
toast('False positive deleted');
|
||||||
|
loadFalsePositives();
|
||||||
|
} catch (e) { toast('Delete failed', 'danger'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('addFpBtn').addEventListener('click', async () => {
|
||||||
|
const pattern = document.getElementById('fpPattern').value.trim();
|
||||||
|
const description = document.getElementById('fpDescription').value.trim();
|
||||||
|
if (!pattern) { toast('Enter a pattern', 'danger'); return; }
|
||||||
|
try {
|
||||||
|
await api('/false-positives', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ pattern, description }),
|
||||||
|
});
|
||||||
|
document.getElementById('fpPattern').value = '';
|
||||||
|
document.getElementById('fpDescription').value = '';
|
||||||
|
toast('False positive added');
|
||||||
|
loadFalsePositives();
|
||||||
|
} catch (e) { toast('Failed to add', 'danger'); }
|
||||||
|
});
|
||||||
|
|
||||||
// --- LOGS ---
|
// --- LOGS ---
|
||||||
async function loadLogs(query) {
|
async function loadLogs(query) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ class Router
|
|||||||
$path === '/config/telegram' && $method === 'PUT'
|
$path === '/config/telegram' && $method === 'PUT'
|
||||||
=> $this->updateTelegramConfig(),
|
=> $this->updateTelegramConfig(),
|
||||||
|
|
||||||
|
$path === '/false-positives' && $method === 'GET'
|
||||||
|
=> $this->repo->getFalsePositives(),
|
||||||
|
$path === '/false-positives' && $method === 'POST'
|
||||||
|
=> $this->createFalsePositive(),
|
||||||
|
preg_match('#^/false-positives/(\d+)$#', $path, $m) && $method === 'DELETE'
|
||||||
|
=> $this->deleteFalsePositive((int) $m[1]),
|
||||||
|
|
||||||
default => throw new \RuntimeException('Not found', 404),
|
default => throw new \RuntimeException('Not found', 404),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -315,4 +322,24 @@ class Router
|
|||||||
$this->repo->setConfig('telegram_chat_id', $body['chat_id'] ?? '');
|
$this->repo->setConfig('telegram_chat_id', $body['chat_id'] ?? '');
|
||||||
return $this->getTelegramConfig();
|
return $this->getTelegramConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createFalsePositive(): array
|
||||||
|
{
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$pattern = $body['pattern'] ?? '';
|
||||||
|
$description = $body['description'] ?? '';
|
||||||
|
|
||||||
|
if (empty($pattern)) {
|
||||||
|
http_response_code(400);
|
||||||
|
return ['error' => 'Missing "pattern" field'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repo->createFalsePositive($pattern, $description);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteFalsePositive(int $id): array
|
||||||
|
{
|
||||||
|
$this->repo->deleteFalsePositive($id);
|
||||||
|
return ['status' => 'deleted', 'id' => $id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,10 @@ class Engine
|
|||||||
|
|
||||||
public function evaluate(string $line, ?LogSource $source = null): ?Alert
|
public function evaluate(string $line, ?LogSource $source = null): ?Alert
|
||||||
{
|
{
|
||||||
|
if ($this->repo->isFalsePositive($line)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$rules = $this->repo->getActiveRules();
|
$rules = $this->repo->getActiveRules();
|
||||||
|
|
||||||
foreach ($rules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
|
|||||||
@@ -156,5 +156,14 @@ $this->pdo->exec("
|
|||||||
INSERT OR IGNORE INTO log_entries_fts(rowid, line, source_name)
|
INSERT OR IGNORE INTO log_entries_fts(rowid, line, source_name)
|
||||||
SELECT id, line, source_name FROM log_entries
|
SELECT id, line, source_name FROM log_entries
|
||||||
");
|
");
|
||||||
|
|
||||||
|
$this->pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS false_positives (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
pattern TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,4 +290,52 @@ class Repository
|
|||||||
|
|
||||||
return $row['count'] <= 1;
|
return $row['count'] <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- False Positives ---
|
||||||
|
|
||||||
|
public function getFalsePositives(): array
|
||||||
|
{
|
||||||
|
return $this->db->pdo()->query(
|
||||||
|
"SELECT id, pattern, description, created_at FROM false_positives ORDER BY id"
|
||||||
|
)->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createFalsePositive(string $pattern, string $description = ''): array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->pdo()->prepare(
|
||||||
|
"INSERT INTO false_positives (pattern, description) VALUES (?, ?)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$pattern, $description]);
|
||||||
|
$id = (int) $this->db->pdo()->lastInsertId();
|
||||||
|
return $this->getFalsePositive($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFalsePositive(int $id): ?array
|
||||||
|
{
|
||||||
|
$stmt = $this->db->pdo()->prepare(
|
||||||
|
"SELECT id, pattern, description, created_at FROM false_positives WHERE id = ?"
|
||||||
|
);
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
return $row ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteFalsePositive(int $id): void
|
||||||
|
{
|
||||||
|
$this->db->pdo()->prepare("DELETE FROM false_positives WHERE id = ?")->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFalsePositive(string $line): bool
|
||||||
|
{
|
||||||
|
$patterns = $this->db->pdo()->query(
|
||||||
|
"SELECT pattern FROM false_positives"
|
||||||
|
)->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $line)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user