Files
DECNET/tests/correlation/test_parser_double_wrap.py
anti d80e6aa6d1 fix(security): close MEDIUM ASVS findings — JWT pinning, SSE tickets, SSRF, mTLS pin, rate limits + correctness bugs
Auth (V2.1.1/V3.1.2, V2.1.3, V3.1.1):
- Pin JWT iss/aud/typ at mint and require+verify them at decode; revocation
  (jti denylist + tokens_valid_from) still enforced.
- Change-password now requires min_length=12.
- SSE auth moves off JWT-in-URL to a single-use 60s opaque ticket
  (POST /auth/sse-ticket); raw JWT in query no longer authenticates a stream.
  Removed dead fail-open get_stream_user helper.

Egress (V5.1.1, V9.1.1/V14.1.3):
- Webhook delivery + CRUD reject SSRF destinations (private/loopback/link-local/
  metadata, IPv4-mapped, multi-A-record) via resolved-IP validation, pin to the
  vetted IP, and never auto-follow redirects. Opt-out via DECNET_WEBHOOK_ALLOW_PRIVATE.
- UpdaterClient pins the worker leaf cert SHA-256 against the stored per-host
  fingerprint (fail closed on missing/mismatch); DECNET_VERIFY_HOSTNAME now
  defaults True.

Hardening (V13.1.3, V4.1.4, V13.1.2):
- Rate-limit change-password (5/min), enroll-bundle (10/min), webhook-create
  (20/min), host-delete (20/min) via the existing slowapi limiter.
- Correct false 'global auth middleware' comment; document enroll-bundle proxy
  trust.

Correctness (BUG-7..11):
- BUG-7 unbound bus in finally; BUG-8 apply_ceiling clamps to min(base,ceiling);
  BUG-9 commit before emit; BUG-10 multi-actor rearm for sub-threshold identities;
  BUG-11 normalize naive timestamps to UTC.

Already-closed (no change): V14.1.1, V2.1.2/V3.1.3, V5.1.2. Tests added for
every fix; unanimous adversarial review.
2026-06-10 12:32:15 -04:00

114 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Correlation parser unwraps double-wrapped RFC5424 lines.
Mirrors ``tests/collector/test_parse_rfc5424_double_wrap.py``. Both
parsers read the same on-wire format; the profiler's ``parse_line``
must agree with the collector's ``parse_rfc5424`` so that
``Attacker.commands`` rows carry the real ``command`` event_type
(not the outer Docker envelope's NIL MSGID).
"""
from __future__ import annotations
from datetime import timezone
from decnet.correlation.parser import parse_line
_DOUBLE_WRAPPED_CMD = (
"<14>1 2026-05-02T06:22:48.089309+00:00 omega-decky 1 - - - "
" 2026-05-02T06:22:48.089286+00:00 SRV-DELTA-77 bash - command "
"[timeQuality tzKnown=\"1\" isSynced=\"1\" syncAccuracy=\"326228\"] "
"CMD uid=0 user=root src=192.168.1.5 pwd=/root cmd=ls /var/www/html"
)
def test_double_wrapped_bash_cmd_event_type_is_command() -> None:
e = parse_line(_DOUBLE_WRAPPED_CMD)
assert e is not None
assert e.event_type == "command"
def test_double_wrapped_bash_cmd_uses_inner_decky_and_service() -> None:
e = parse_line(_DOUBLE_WRAPPED_CMD)
assert e is not None
assert e.decky == "SRV-DELTA-77"
assert e.service == "bash"
def test_double_wrapped_bash_cmd_extracts_attacker_ip() -> None:
e = parse_line(_DOUBLE_WRAPPED_CMD)
assert e is not None
assert e.attacker_ip == "192.168.1.5"
def test_double_wrapped_bash_cmd_extracts_command_field() -> None:
"""The behavioral profiler reads ``fields['command']`` for shell
rules and the per-attacker ``commands`` rollup. Without it the
R0001R0030 pattern rules have no haystack."""
e = parse_line(_DOUBLE_WRAPPED_CMD)
assert e is not None
assert e.fields.get("command") == "ls /var/www/html"
def test_single_wrapped_line_unchanged() -> None:
line = (
"<134>1 2026-05-02T06:00:25.453826+00:00 omega-decky smtp - "
"disconnect [relay@55555 src_ip=\"192.168.1.5\"]"
)
e = parse_line(line)
assert e is not None
assert e.event_type == "disconnect"
assert e.decky == "omega-decky"
assert e.service == "smtp"
assert e.attacker_ip == "192.168.1.5"
def test_outer_msgid_set_does_not_recurse() -> None:
line = (
"<134>1 2026-05-02T06:22:48.089309+00:00 omega-decky auth-helper - "
"auth_attempt [relay@55555 username=\"root\" src_ip=\"192.168.1.5\"]"
)
e = parse_line(line)
assert e is not None
assert e.event_type == "auth_attempt"
assert e.decky == "omega-decky"
assert e.service == "auth-helper"
# ---------------------------------------------------------------------------
# BUG-11 regression: naive datetime normalization
# ---------------------------------------------------------------------------
_NAIVE_TS_LINE = (
"<14>1 2026-05-02T06:22:48.089309 omega-decky smtp - disconnect "
"[relay@55555 src_ip=\"10.0.0.1\"]"
)
_AWARE_TS_LINE = (
"<14>1 2026-05-02T06:22:48.089309+00:00 omega-decky smtp - disconnect "
"[relay@55555 src_ip=\"10.0.0.2\"]"
)
def test_naive_timestamp_normalized_to_utc() -> None:
"""BUG-11 regression: a log line with a naïve ISO timestamp (no tz offset)
must parse to a tz-aware UTC datetime so it sorts alongside aware ones
without TypeError. Before fix, fromisoformat returned a naïve datetime
which crashed min/max/sort with aware datetimes downstream."""
e = parse_line(_NAIVE_TS_LINE)
assert e is not None
assert e.timestamp.tzinfo is not None
assert e.timestamp.tzinfo == timezone.utc
def test_naive_and_aware_timestamps_sortable_together() -> None:
"""A naïve-source entry and an aware-source entry must compare
without raising TypeError."""
naive_entry = parse_line(_NAIVE_TS_LINE)
aware_entry = parse_line(_AWARE_TS_LINE)
assert naive_entry is not None
assert aware_entry is not None
# min/max would raise TypeError pre-fix
earliest = min(naive_entry.timestamp, aware_entry.timestamp)
assert earliest is not None