webui (3)
This commit is contained in:
+11
-24
@@ -12,6 +12,11 @@ import sys
|
||||
import time
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
try:
|
||||
from .audit_policy import action_for, skip_user
|
||||
except ImportError:
|
||||
from audit_policy import action_for, skip_user
|
||||
|
||||
|
||||
SAMBA_LOG_GLOB = os.getenv("AUDIT_SOURCE_GLOB", "/var/log/samba/log.*")
|
||||
ARCHIVE_DIR = os.getenv("AUDIT_ARCHIVE_DIR", "/state/audit")
|
||||
@@ -69,28 +74,6 @@ def parse_samba_timestamp(raw_line: str, fallback: dt.datetime) -> str:
|
||||
return fallback.isoformat(timespec="milliseconds")
|
||||
|
||||
|
||||
def action_for(operation: str) -> str:
|
||||
operation = operation.lower()
|
||||
if operation in {
|
||||
"read", "pread", "pread_recv", "pread_send", "recvfile", "sendfile",
|
||||
"offload_read_recv", "offload_read_send",
|
||||
}:
|
||||
return "read"
|
||||
if operation in {
|
||||
"write", "pwrite", "pwrite_recv", "pwrite_send", "ftruncate",
|
||||
"fallocate", "create_file", "mkdirat", "mknodat", "renameat",
|
||||
"unlinkat", "symlinkat", "linkat", "offload_write_recv",
|
||||
"offload_write_send", "fsetxattr", "removexattr", "fremovexattr",
|
||||
"mkdir", "rmdir", "rename", "unlink",
|
||||
}:
|
||||
return "write"
|
||||
if operation in {"opendir", "fdopendir", "readdir", "freaddir_attr", "closedir"}:
|
||||
return "list"
|
||||
if operation in {"connect", "disconnect"}:
|
||||
return "session"
|
||||
return "metadata"
|
||||
|
||||
|
||||
def parse_audit_line(raw_line: str, source: str) -> Optional[Dict[str, object]]:
|
||||
match = AUDIT_MARKER_RE.search(raw_line)
|
||||
if match:
|
||||
@@ -104,16 +87,20 @@ def parse_audit_line(raw_line: str, source: str) -> Optional[Dict[str, object]]:
|
||||
return None
|
||||
observed_at = utc_now()
|
||||
operation = fields[5].strip()
|
||||
action = action_for(operation)
|
||||
user = fields[1].strip()
|
||||
if action is None or skip_user(user):
|
||||
return None
|
||||
result = fields[6].strip()
|
||||
return {
|
||||
"timestamp": parse_samba_timestamp(raw_line, observed_at),
|
||||
"ingestedAt": observed_at.isoformat(timespec="milliseconds"),
|
||||
"user": fields[1].strip(),
|
||||
"user": user,
|
||||
"clientIp": fields[2].strip(),
|
||||
"client": fields[3].strip(),
|
||||
"share": fields[4].strip(),
|
||||
"operation": operation,
|
||||
"action": action_for(operation),
|
||||
"action": action,
|
||||
"result": result,
|
||||
"success": result.upper() == "OK",
|
||||
"path": "|".join(fields[7:]).strip(),
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared policy for the small set of user-facing audit events."""
|
||||
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
AUDIT_ACTIONS = frozenset({"read", "write", "move", "delete"})
|
||||
|
||||
OPERATION_ACTIONS = {
|
||||
"read": "read",
|
||||
"pread": "read",
|
||||
"pread_recv": "read",
|
||||
"pread_send": "read",
|
||||
"sendfile": "read",
|
||||
"offload_read_recv": "read",
|
||||
"offload_read_send": "read",
|
||||
"write": "write",
|
||||
"pwrite": "write",
|
||||
"pwrite_recv": "write",
|
||||
"pwrite_send": "write",
|
||||
"recvfile": "write",
|
||||
"offload_write_recv": "write",
|
||||
"offload_write_send": "write",
|
||||
"renameat": "move",
|
||||
"rename": "move",
|
||||
"unlinkat": "delete",
|
||||
"unlink": "delete",
|
||||
"rmdir": "delete",
|
||||
}
|
||||
|
||||
|
||||
def action_for(operation: str) -> Optional[str]:
|
||||
return OPERATION_ACTIONS.get(operation.strip().casefold())
|
||||
|
||||
|
||||
def skipped_user_suffixes() -> Tuple[str, ...]:
|
||||
raw = os.getenv("AUDIT_SKIP_USER_SUFFIXES", "_svc,_ServiceAcc")
|
||||
return tuple(
|
||||
suffix.strip().casefold()
|
||||
for suffix in raw.split(",")
|
||||
if suffix.strip()
|
||||
)
|
||||
|
||||
|
||||
def account_name(user: str) -> str:
|
||||
account = user.strip().rsplit("\\", 1)[-1]
|
||||
return account.split("@", 1)[0]
|
||||
|
||||
|
||||
def skip_user(user: str) -> bool:
|
||||
account = account_name(user).casefold()
|
||||
return any(account.endswith(suffix) for suffix in skipped_user_suffixes())
|
||||
+6
-7
@@ -24,7 +24,7 @@ const utcTime = value => {
|
||||
const pad = number => String(number).padStart(2, "0");
|
||||
return `${pad(date.getUTCDate())}.${pad(date.getUTCMonth() + 1)}.${date.getUTCFullYear()} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC`;
|
||||
};
|
||||
const actionLabel = value => ({read: "Lesen", write: "Schreiben", list: "Auflisten", metadata: "Metadaten", session: "Sitzung"}[value] || value || "—");
|
||||
const actionLabel = value => ({read: "Lesen", write: "Schreiben", move: "Umbenennen/Verschieben", delete: "Löschen"}[value] || value || "—");
|
||||
const nodeTypeLabel = value => ({group: "Gruppe", user: "Benutzer", computer: "Computer", unknown: "Unbekannt"}[value] || value);
|
||||
const backupStateLabel = value => ({starting: "Startet", running: "Läuft", completed: "Abgeschlossen", failed: "Fehlgeschlagen", waiting: "Wartet", disabled: "Deaktiviert"}[value] || value || "Unbekannt");
|
||||
function backupMessage(data) {
|
||||
@@ -127,7 +127,7 @@ function eventRows(events) {
|
||||
return events.map(event => `<tr>
|
||||
<td class="timestamp">${esc(utcTime(event.timestamp))}</td>
|
||||
<td>${esc(event.user)}</td><td>${esc(event.clientIp)}</td><td>${esc(event.share)}</td>
|
||||
<td>${badge(actionLabel(event.action || event.operation))}<br><span class="muted">${esc(event.operation)}</span></td>
|
||||
<td>${badge(actionLabel(event.action || event.operation))}</td>
|
||||
<td class="path">${esc(event.path || "—")}</td>
|
||||
<td>${event.success ? badge("Erfolgreich") : badge(event.result || "Fehlgeschlagen", "error")}</td>
|
||||
</tr>`).join("");
|
||||
@@ -230,20 +230,19 @@ async function renderStorage(type) {
|
||||
async function renderActivity() {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(Date.now() - 86400000);
|
||||
content.innerHTML = pageHead("Aktivitätsprotokoll", "Aufgezeichnete Samba-Vorgänge nach Datum, Identität, Freigabe, Aktion, Ergebnis oder Pfad durchsuchen.") + `
|
||||
content.innerHTML = pageHead("Aktivitätsprotokoll", "Lese-, Schreib-, Umbenennungs-/Verschiebe- und Löschvorgänge nach Datum, Identität, Freigabe, Ergebnis oder Pfad durchsuchen.") + `
|
||||
<section class="panel">
|
||||
<form id="activity-filter" class="filters">
|
||||
<label>Von (UTC)<input name="from" type="date" value="${isoDay(yesterday)}" required></label>
|
||||
<label>Bis (UTC)<input name="to" type="date" value="${isoDay(today)}" required></label>
|
||||
<label>Benutzer<input name="user" list="users-list" placeholder="Alle Benutzer"></label>
|
||||
<label>Freigabe<input name="share" list="shares-list" placeholder="Alle Freigaben"></label>
|
||||
<label>Aktion<select name="action"><option value="">Alle Aktionen</option><option value="read">Lesen</option><option value="write">Schreiben</option><option value="list">Auflisten</option><option value="metadata">Metadaten</option><option value="session">Sitzung</option></select></label>
|
||||
<label>Aktion<select name="action"><option value="">Alle Aktionen</option><option value="read">Lesen</option><option value="write">Schreiben</option><option value="move">Umbenennen/Verschieben</option><option value="delete">Löschen</option></select></label>
|
||||
<label>Ergebnis<select name="result"><option value="">Alle Ergebnisse</option><option value="OK">Erfolgreich</option><option value="fail">Fehlgeschlagen</option></select></label>
|
||||
<label>Operation<input name="operation" list="operations-list" placeholder="z. B. pread"></label>
|
||||
<label class="wide">Pfad enthält<input name="path" placeholder="Ordner oder Dateiname"></label>
|
||||
<button type="submit">Filter anwenden</button>
|
||||
</form>
|
||||
<datalist id="users-list"></datalist><datalist id="shares-list"></datalist><datalist id="operations-list"></datalist>
|
||||
<datalist id="users-list"></datalist><datalist id="shares-list"></datalist>
|
||||
<p id="activity-summary" class="muted"></p>
|
||||
<div class="table-wrap"><table><thead><tr><th>Zeit (UTC)</th><th>Benutzer</th><th>Client</th><th>Freigabe</th><th>Aktion</th><th>Pfad</th><th>Ergebnis</th></tr></thead><tbody id="activity-rows"></tbody></table></div>
|
||||
<p><button id="load-more" hidden>Weitere laden</button></p>
|
||||
@@ -262,7 +261,7 @@ async function renderActivity() {
|
||||
const more = document.querySelector("#load-more");
|
||||
cursor = result.nextCursor || 0;
|
||||
more.hidden = !result.nextCursor;
|
||||
for (const [id, values] of [["users-list", result.facets.users], ["shares-list", result.facets.shares], ["operations-list", result.facets.operations]]) {
|
||||
for (const [id, values] of [["users-list", result.facets.users], ["shares-list", result.facets.shares]]) {
|
||||
document.querySelector(`#${id}`).innerHTML = values.filter(Boolean).map(value => `<option value="${esc(value)}">`).join("");
|
||||
}
|
||||
};
|
||||
|
||||
+22
-4
@@ -30,6 +30,11 @@ try:
|
||||
except ImportError: # Container execution uses /app as the import root.
|
||||
import reconcile_shares as directory
|
||||
|
||||
try:
|
||||
from app.audit_policy import AUDIT_ACTIONS, action_for, skip_user
|
||||
except ImportError: # Container execution uses /app as the import root.
|
||||
from audit_policy import AUDIT_ACTIONS, action_for, skip_user
|
||||
|
||||
|
||||
STATE_ROOT = os.getenv("WEB_STATE_DIR", "/state/web")
|
||||
AUDIT_ROOT = os.getenv("AUDIT_ARCHIVE_DIR", "/state/audit")
|
||||
@@ -87,7 +92,10 @@ def base64url(value: bytes) -> str:
|
||||
|
||||
|
||||
def base64url_decode(value: str) -> bytes:
|
||||
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
||||
decoded = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
||||
if not hmac.compare_digest(base64url(decoded), value):
|
||||
raise ValueError("non-canonical base64url")
|
||||
return decoded
|
||||
|
||||
|
||||
class TokenManager:
|
||||
@@ -589,10 +597,20 @@ def query_audit(params: Dict[str, List[str]]) -> Dict[str, object]:
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
for event in iter_audit_file_reverse(path):
|
||||
facets["users"].add(str(event.get("user", "")))
|
||||
user = str(event.get("user", ""))
|
||||
operation = str(event.get("operation", ""))
|
||||
action = action_for(operation)
|
||||
if action is None and not operation:
|
||||
stored_action = str(event.get("action", "")).casefold()
|
||||
action = stored_action if stored_action in AUDIT_ACTIONS else None
|
||||
if action is None or skip_user(user):
|
||||
continue
|
||||
if event.get("action") != action:
|
||||
event = {**event, "action": action}
|
||||
facets["users"].add(user)
|
||||
facets["shares"].add(str(event.get("share", "")))
|
||||
facets["operations"].add(str(event.get("operation", "")))
|
||||
facets["actions"].add(str(event.get("action", "")))
|
||||
facets["operations"].add(operation)
|
||||
facets["actions"].add(action)
|
||||
failed_filter = filters["result"] == "fail"
|
||||
if failed_filter and bool(event.get("success", False)):
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user