db->pdo()->query("SELECT * FROM log_sources ORDER BY name")->fetchAll(); return array_map(fn(array $r) => LogSource::fromRow($r), $rows); } public function getActiveSources(): array { $rows = $this->db->pdo()->query("SELECT * FROM log_sources WHERE active = 1 ORDER BY name")->fetchAll(); return array_map(fn(array $r) => LogSource::fromRow($r), $rows); } public function getSource(int $id): ?LogSource { $stmt = $this->db->pdo()->prepare("SELECT * FROM log_sources WHERE id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ? LogSource::fromRow($row) : null; } public function createSource(string $name, LogSourceType $type, string $address, array $labels = []): LogSource { $stmt = $this->db->pdo()->prepare( "INSERT INTO log_sources (name, type, address, labels) VALUES (?, ?, ?, ?)" ); $stmt->execute([$name, $type->value, $address, json_encode($labels)]); return $this->getSource((int) $this->db->pdo()->lastInsertId()); } public function deleteSource(int $id): void { $this->db->pdo()->prepare("DELETE FROM log_sources WHERE id = ?")->execute([$id]); } // --- Rules --- public function getRules(): array { $rows = $this->db->pdo()->query("SELECT * FROM rules ORDER BY name")->fetchAll(); return array_map(fn(array $r) => Rule::fromRow($r), $rows); } public function getActiveRules(): array { $rows = $this->db->pdo()->query("SELECT * FROM rules WHERE active = 1 ORDER BY name")->fetchAll(); return array_map(fn(array $r) => Rule::fromRow($r), $rows); } public function getRule(int $id): ?Rule { $stmt = $this->db->pdo()->prepare("SELECT * FROM rules WHERE id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ? Rule::fromRow($row) : null; } public function createRule(string $name, string $pattern, string $severity, ?int $rateLimitSeconds = null): Rule { $stmt = $this->db->pdo()->prepare( "INSERT INTO rules (name, pattern, severity, rate_limit_seconds) VALUES (?, ?, ?, ?)" ); $stmt->execute([$name, $pattern, $severity, $rateLimitSeconds]); return $this->getRule((int) $this->db->pdo()->lastInsertId()); } public function deleteRule(int $id): void { $this->db->pdo()->prepare("DELETE FROM rules WHERE id = ?")->execute([$id]); } // --- Alerts --- public function createAlert(int $ruleId, string $ruleName, string $severity, string $message, string $rawLine, ?int $sourceId = null, ?string $sourceName = null): Alert { $stmt = $this->db->pdo()->prepare( "INSERT INTO alerts (rule_id, rule_name, severity, status, message, raw_line, source_id, source_name) VALUES (?, ?, ?, 'open', ?, ?, ?, ?)" ); $stmt->execute([$ruleId, $ruleName, $severity, $message, $rawLine, $sourceId, $sourceName]); $id = (int) $this->db->pdo()->lastInsertId(); return $this->getAlert($id); } public function getAlert(int $id): ?Alert { $stmt = $this->db->pdo()->prepare("SELECT * FROM alerts WHERE id = ?"); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ? Alert::fromRow($row) : null; } public function getAlerts(int $limit = 100, int $offset = 0, ?string $status = null, ?string $severity = null): array { $where = []; $params = []; if ($status) { $where[] = 'status = ?'; $params[] = $status; } if ($severity) { $where[] = 'severity = ?'; $params[] = $severity; } $sql = "SELECT * FROM alerts"; if ($where) { $sql .= ' WHERE ' . implode(' AND ', $where); } $sql .= " ORDER BY created_at DESC LIMIT ? OFFSET ?"; $params[] = $limit; $params[] = $offset; $stmt = $this->db->pdo()->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(); return array_map(fn(array $r) => Alert::fromRow($r), $rows); } public function updateAlertStatus(int $id, AlertStatus $status): void { $stmt = $this->db->pdo()->prepare("UPDATE alerts SET status = ? WHERE id = ?"); $stmt->execute([$status->value, $id]); } public function getAlertCounts(): array { return $this->db->pdo()->query( "SELECT status, severity, COUNT(*) as count FROM alerts GROUP BY status, severity" )->fetchAll(); } public function searchAlerts(string $query, int $limit = 100): array { $stmt = $this->db->pdo()->prepare( "SELECT a.* FROM alerts a JOIN alerts_fts fts ON a.id = fts.rowid WHERE alerts_fts MATCH ? ORDER BY rank LIMIT ?" ); $stmt->execute([$query, $limit]); $rows = $stmt->fetchAll(); return array_map(fn(array $r) => Alert::fromRow($r), $rows); } // --- Log Entries --- public function storeLogEntry(string $line, ?int $sourceId = null, ?string $sourceName = null, ?string $level = null): void { $stmt = $this->db->pdo()->prepare( "INSERT INTO log_entries (line, source_id, source_name, level) VALUES (?, ?, ?, ?)" ); $stmt->execute([$line, $sourceId, $sourceName, $level]); } public function searchLogEntries(string $query, int $limit = 200, int $offset = 0): array { $query = trim($query); if ($query === '') { return []; } try { $stmt = $this->db->pdo()->prepare( "SELECT e.* FROM log_entries e JOIN log_entries_fts fts ON e.id = fts.rowid WHERE log_entries_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ?" ); $stmt->execute([$query, $limit, $offset]); return $stmt->fetchAll(); } catch (\PDOException $e) { return $this->searchLogEntriesLike($query, $limit, $offset); } } private function searchLogEntriesLike(string $query, int $limit = 200, int $offset = 0): array { $like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $query) . '%'; $stmt = $this->db->pdo()->prepare( "SELECT e.* FROM log_entries e WHERE e.line LIKE ? ORDER BY e.created_at DESC LIMIT ? OFFSET ?" ); $stmt->execute([$like, $limit, $offset]); return $stmt->fetchAll(); } // --- Config --- public function getAllowedUserTokens(): array { $stmt = $this->db->pdo()->prepare("SELECT value FROM config WHERE key = ?"); $stmt->execute(['allowed_user_tokens']); $row = $stmt->fetch(); if (!$row || empty($row['value'])) { return []; } return json_decode($row['value'], true) ?? []; } public function setAllowedUserTokens(array $tokens): void { $stmt = $this->db->pdo()->prepare( "INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value" ); $stmt->execute(['allowed_user_tokens', json_encode(array_values($tokens))]); } // --- Rate Limiting --- public function checkRateLimit(int $ruleId, int $windowSeconds): bool { $now = time(); $window = intdiv($now, $windowSeconds) * $windowSeconds; $this->db->pdo()->prepare( "INSERT INTO rate_limiter (rule_id, window_start, count) VALUES (?, ?, 1) ON CONFLICT(rule_id, window_start) DO UPDATE SET count = count + 1" )->execute([$ruleId, $window]); $stmt = $this->db->pdo()->prepare( "SELECT count FROM rate_limiter WHERE rule_id = ? AND window_start = ?" ); $stmt->execute([$ruleId, $window]); $row = $stmt->fetch(); return $row['count'] <= 1; } }