mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 21:53:13 +08:00
Drop HTTP polling noise from operation audit by default.
Persist only business events and mutating/failed HTTP calls, and default the audit UI to a business view with an explicit HTTP filter. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8915455b52
commit
c181158209
8 changed files with 196 additions and 24 deletions
|
|
@ -18,9 +18,32 @@ _lock = threading.Lock()
|
|||
_counter = 0
|
||||
_dropped = 0
|
||||
|
||||
# Middleware-tagged HTTP wrappers that duplicate semantic business audits.
|
||||
_MIDDLEWARE_NOISE_ACTIONS = frozenset(
|
||||
{
|
||||
"audit.list",
|
||||
"webcrt.get",
|
||||
"webcrt.post",
|
||||
"webcrt.put",
|
||||
"webcrt.patch",
|
||||
"webcrt.delete",
|
||||
"users.get",
|
||||
"api_tokens.get",
|
||||
}
|
||||
)
|
||||
|
||||
_ALWAYS_KEEP_PREFIXES = (
|
||||
"auth.",
|
||||
"users.",
|
||||
"api_tokens.",
|
||||
"ne.",
|
||||
"port_traffic.",
|
||||
"config_sync.",
|
||||
)
|
||||
|
||||
|
||||
def _sample_ok() -> bool:
|
||||
"""When sample_n > 1, keep 1/N of http.* audits; always keep auth/security actions."""
|
||||
"""When sample_n > 1, keep 1/N of leftover generic events."""
|
||||
global _counter
|
||||
n = int(getattr(settings, "audit_sample_n", 1) or 1)
|
||||
if n <= 1:
|
||||
|
|
@ -29,6 +52,60 @@ def _sample_ok() -> bool:
|
|||
return (_counter % n) == 0
|
||||
|
||||
|
||||
def audit_should_persist(
|
||||
*,
|
||||
action: str,
|
||||
method: str = "",
|
||||
status_code: int = 0,
|
||||
path: str = "",
|
||||
) -> bool:
|
||||
"""Decide whether a candidate audit event is worth writing.
|
||||
|
||||
Policy:
|
||||
- Always keep auth / users / tokens / NE / port_traffic / config_sync / semantic webcrt.
|
||||
- Always keep HTTP failures (status >= 400), except pure list noise.
|
||||
- Drop successful GET/HEAD/OPTIONS ``http.*`` (page polling).
|
||||
- Always keep mutating ``http.*`` (POST/PUT/PATCH/DELETE) — no sampling.
|
||||
- Drop middleware ``webcrt.{method}`` / ``audit.list`` (covered by business events).
|
||||
"""
|
||||
del path # reserved for future path allow/deny lists
|
||||
act = str(action or "")
|
||||
method_u = str(method or "").upper()
|
||||
code = int(status_code or 0)
|
||||
|
||||
if act in _MIDDLEWARE_NOISE_ACTIONS:
|
||||
return False
|
||||
|
||||
if act.startswith("webcrt.session_") or act == "webcrt.command":
|
||||
return True
|
||||
|
||||
if any(act.startswith(p) for p in _ALWAYS_KEEP_PREFIXES):
|
||||
return True
|
||||
|
||||
if code >= 400:
|
||||
return True
|
||||
|
||||
if act.startswith("http."):
|
||||
if method_u in ("GET", "HEAD", "OPTIONS") or act in ("http.get", "http.head", "http.options"):
|
||||
return False
|
||||
if method_u in ("POST", "PUT", "PATCH", "DELETE") or act in (
|
||||
"http.post",
|
||||
"http.put",
|
||||
"http.patch",
|
||||
"http.delete",
|
||||
):
|
||||
return True
|
||||
return _sample_ok()
|
||||
|
||||
# e.g. ume.token.* — keep writes, drop successful reads
|
||||
if act.startswith("ume."):
|
||||
if method_u in ("GET", "HEAD", "OPTIONS"):
|
||||
return False
|
||||
return True
|
||||
|
||||
return _sample_ok()
|
||||
|
||||
|
||||
def audit_queue_status() -> dict[str, int]:
|
||||
q = _q
|
||||
return {
|
||||
|
|
@ -99,16 +176,7 @@ def enqueue_audit(
|
|||
) -> None:
|
||||
global _dropped
|
||||
act = str(action or "")
|
||||
# Always persist auth / security / device-op events.
|
||||
if (
|
||||
act.startswith("auth.")
|
||||
or act.startswith("users.")
|
||||
or act.startswith("api_tokens.")
|
||||
or act.startswith("webcrt.")
|
||||
or act.startswith("ne.")
|
||||
):
|
||||
pass
|
||||
elif act.startswith("http.") and not _sample_ok():
|
||||
if not audit_should_persist(action=act, method=method, status_code=status_code, path=path):
|
||||
return
|
||||
payload = {
|
||||
"action": act,
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ def api_audit_logs(
|
|||
page_size: int = Query(default=50, ge=1, le=200),
|
||||
username: str = Query(default=""),
|
||||
action: str = Query(default=""),
|
||||
exclude_noise: bool = Query(default=True),
|
||||
) -> dict[str, Any]:
|
||||
return list_audit_logs(
|
||||
db,
|
||||
|
|
@ -402,6 +403,7 @@ def api_audit_logs(
|
|||
page_size=page_size,
|
||||
username=username,
|
||||
action=action,
|
||||
exclude_noise=exclude_noise,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -723,6 +723,7 @@ def list_audit_logs(
|
|||
page_size: int = 50,
|
||||
username: str = "",
|
||||
action: str = "",
|
||||
exclude_noise: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
page = max(1, int(page or 1))
|
||||
page_size = max(1, min(200, int(page_size or 50)))
|
||||
|
|
@ -733,6 +734,20 @@ def list_audit_logs(
|
|||
q = q.filter(AuditLog.actor_username == username.strip())
|
||||
if action.strip():
|
||||
q = q.filter(AuditLog.action.ilike(f"%{action.strip()}%"))
|
||||
if exclude_noise:
|
||||
# Hide historical HTTP polling + middleware wrappers; keep semantic webcrt.*.
|
||||
noise_actions = (
|
||||
"audit.list",
|
||||
"webcrt.get",
|
||||
"webcrt.post",
|
||||
"webcrt.put",
|
||||
"webcrt.patch",
|
||||
"webcrt.delete",
|
||||
"users.get",
|
||||
"api_tokens.get",
|
||||
)
|
||||
q = q.filter(~AuditLog.action.like("http.%"))
|
||||
q = q.filter(~AuditLog.action.in_(noise_actions))
|
||||
total = int(q.count())
|
||||
rows = (
|
||||
q.order_by(AuditLog.ts.desc())
|
||||
|
|
@ -761,4 +776,5 @@ def list_audit_logs(
|
|||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": items,
|
||||
"exclude_noise": bool(exclude_noise),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,8 @@ class Settings(BaseSettings):
|
|||
docs_enabled: bool = False
|
||||
# Refuse start when bind host is non-loopback and insecure defaults remain.
|
||||
allow_insecure_defaults: bool = False
|
||||
# Async audit writer; sample_n>1 keeps 1/N of generic http.* events.
|
||||
# Async audit writer. Successful GET http.* are dropped; mutating http.* always kept.
|
||||
# sample_n only applies to leftover unclassified events.
|
||||
audit_async: bool = True
|
||||
audit_sample_n: int = 5
|
||||
# Prefer Alembic on API start; brownfield patches live in schema_patches + revisions.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue