Skip to content

Record client IP and user agent in the audit log with trusted-proxy unwrap - #58

Merged
maiphucgiang merged 5 commits into
mainfrom
feat/audit-client-meta
Oct 11, 2026
Merged

maiphucgiang merged 5 commits into
mainfrom
feat/audit-client-meta

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Request audit rows gain client_ip (real column, schema v3 additive migration, exact-match filter) and user_agent (payload). Only client-metadata headers are read (user-agent, X-Forwarded-For, X-Real-IP); bodies and all other headers stay unread.
  • Forwarded headers count only when the direct peer matches CODEBUDDY2API_TRUSTED_PROXIES (hot setting, IP/CIDR list, validated at save); otherwise the direct address wins so clients cannot spoof. Rightmost-untrusted XFF hop wins; X-Real-IP is the fallback; UA is sanitized (single-line printable, ≤200 chars, secret-shaped values dropped).
  • WebUI logs page: client IP column + IP filter, Chinese labels for event kind/action/outcome badges, and the new fields in the detail drawer.

Verification

  • python -B tests/test_*.py: all 70 files pass, including new migration (v2→v3), sanitize/filter, and proxy unwrap/spoof-rejection tests.
  • Web unit tests: 134 pass; vp check clean.

Summary by Sourcery

Enhance audit logging with secure client metadata capture, trusted-proxy support, searchable WebUI presentation, and bilingual interface text.

New Features:

  • Record sanitized client IP and user-agent metadata in request audit records.
  • Support trusted-proxy configuration for secure client-IP extraction from forwarded headers.
  • Add client-IP filtering and metadata display to the WebUI audit logs.
  • Add Chinese/English language switching across the WebUI.

Bug Fixes:

  • Prevent untrusted or malformed forwarded headers from spoofing the recorded client address.
  • Replace ambiguous unknown-value labels with a consistent em dash in the WebUI.

Enhancements:

  • Add an additive audit schema migration and indexed exact-match client-IP filtering.
  • Localize audit event kinds, actions, outcomes, dashboard labels, and common interface text.

Deployment:

  • Document trusted proxy configuration and the single-writer requirement for audit storage upgrades.

Documentation:

  • Document the trusted proxy environment variable in English and Chinese deployment guides.

Tests:

  • Add coverage for schema migration, metadata sanitization, client-IP filtering, trusted-proxy unwrapping, spoof rejection, and WebUI localization and log rendering.

…nwrap

Request rows gain client_ip (real column, schema v3 additive migration,
filterable in /admin/logs) and user_agent (payload). Headers are read only
for client metadata; forwarded headers count solely when the direct peer is
in CODEBUDDY2API_TRUSTED_PROXIES (hot setting, IP/CIDR list), otherwise the
direct address wins so clients cannot spoof. The WebUI logs page shows the
IP, an IP filter, Chinese labels for event kind/action/outcome and the new
fields.
@sourcery-ai

sourcery-ai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR captures sanitized request client metadata without reading bodies, resolves forwarded client IPs only through validated trusted proxies, stores and filters the new audit data via a v3 additive migration, and exposes it alongside localized event labels in the WebUI.

Sequence diagram for trusted-proxy client metadata capture

sequenceDiagram
    participant Client
    participant Proxy
    participant API
    participant AuditStore

    Client->>Proxy: HTTP request
    Proxy->>API: Request with client, X-Forwarded-For, X-Real-IP, User-Agent
    API->>API: client_meta(scope, trusted_proxies)
    alt direct peer matches trusted proxy
        API->>API: Select rightmost-untrusted XFF hop
        API->>API: Fallback to X-Real-IP
    else direct peer is untrusted
        API->>API: Use direct peer address
    end
    API->>API: safe_agent(User-Agent)
    API->>AuditStore: Record client_ip and user_agent
    AuditStore-->>API: Audit record committed
Loading

Entity relationship diagram for audit request client metadata

erDiagram
    REQUESTS {
        text id PK
        text client_ip
        text payload
    }
    REQUESTS ||--|| AUDIT_PAYLOAD : contains
    AUDIT_PAYLOAD {
        text user_agent
    }
Loading

Flow diagram for audit log client IP filtering

flowchart LR
    WebUI[Logs page] -->|client_ip filter| AdminAPI[logs_get]
    AdminAPI -->|client_ip exact match| AuditStore[list_records]
    AuditStore --> Requests[(requests.client_ip)]
    Requests --> AdminAPI
    AdminAPI --> WebUI
Loading

File-Level Changes

Change Details Files
Adds bounded client metadata capture to request audit records with trusted-proxy-aware IP resolution.
  • Reads only User-Agent, X-Forwarded-For, and X-Real-IP from ASGI scope without consuming request bodies or forwarding headers upstream.
  • Uses the direct peer unless it matches configured trusted IP/CIDR networks, then selects the rightmost untrusted XFF hop with X-Real-IP fallback.
  • Sanitizes user agents to printable single-line values of at most 200 characters and drops secret-shaped values.
app/observability.py
app/audit_store.py
app/settings.py
Extends audit persistence and querying for client IP metadata through an additive schema migration.
  • Bumps the audit schema to v3 and adds requests.client_ip without losing existing rows.
  • Persists sanitized client IP and user agent fields in request payloads and supports exact client-IP filtering.
  • Validates and normalizes the hot trusted-proxies setting, including an IP/CIDR count limit and environment-variable configuration.
app/audit_store.py
app/admin_api.py
app/settings.py
docs/deployment.md
docs/deployment.zh-CN.md
Enhances the WebUI logs experience with client metadata and localized event presentation.
  • Adds a client IP column, exact-match filter, and client IP/user-agent detail labels.
  • Localizes known event kinds, actions, and outcomes while preserving raw values in tooltips or fallback rendering.
web/src/pages/Logs.tsx
web/src/values.tsx
Adds regression coverage for migration, metadata safety, proxy-chain handling, API filtering, and UI rendering.
  • Tests v2-to-v3 migration and persistence/accounting compatibility.
  • Tests spoof rejection, trusted-chain unwrapping, fallback behavior, and User-Agent sanitization.
  • Tests WebUI client-IP display/filtering and Chinese event labels.
tests/test_audit_store.py
tests/test_observability.py
web/src/logs.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/observability.py" line_range="53" />
<code_context>
+    headers = {}
+    for key, value in scope.get("headers") or []:
+        name = key.decode("latin-1").lower() if isinstance(key, (bytes, bytearray)) else str(key).lower()
+        if name in _META_HEADERS and name not in headers:
+            headers[name] = value.decode("latin-1", "replace").strip() if isinstance(value, (bytes, bytearray)) else str(value).strip()
+    client = scope.get("client")
</code_context>
<issue_to_address>
**Spoofed client addresses are recorded**

When a client supplies an X-Forwarded-For field and a trusted proxy appends its own value as a separate field, `client_meta` keeps only the first X-Forwarded-For field, so it ignores the proxy’s appended value and can record the client-supplied address as the requester.

Combine repeated X-Forwarded-For fields in order before parsing the chain.

Also at `app/observability.py:54`.
</issue_to_address>

### Comment 2
<location path="app/audit_store.py" line_range="168" />
<code_context>
+        # Additive v3 migration: record the audit-visible client address per request.
+        columns = [row[1] for row in self._db.execute("PRAGMA table_info(requests)").fetchall()]
+        if "client_ip" not in columns:
+            self._db.execute("ALTER TABLE requests ADD COLUMN client_ip TEXT")
+
     def _init_accounting(self):
</code_context>
<issue_to_address>
**Old workers stop recording audit rows**

When a new version migrates a shared database while an old process is still writing to it, the v3 migration adds a tenth `requests` column, so old workers’ positional nine-value inserts fail with a column-count error and their audit rows are dropped; old workers that reopen the database also reject schema version 3.

Use a migration and rollout strategy that prevents old workers from writing to or reopening the database after the schema changes.
</issue_to_address>

### Comment 3
<location path="app/settings.py" line_range="125-127" />
<code_context>
     "audit_max_bytes": _item(256 * 1024 * 1024, "integer", "审计明细预算", minimum=1024**2, maximum=1024**4),
     "audit_retention_days": _item(30, "integer", "审计明细保留天数", minimum=1, maximum=36500),
     "audit_diagnostic_bytes": _item(8192, "integer", "失败诊断最大字节", minimum=0, maximum=8192),
+    "trusted_proxies": _item("", "string", "可信代理地址(IP/CIDR,逗号分隔)",
+                             env="CODEBUDDY2API_TRUSTED_PROXIES", allow_empty=True,
+                             max_length=2000, validator=normalize_trusted_proxies),
 }

</code_context>
<issue_to_address>
**Compose ignores trusted proxies**

When the service is deployed with Docker Compose and `CODEBUDDY2API_TRUSTED_PROXIES` is set in the host environment or `.env` file, compose does not pass the variable into the container, so `apply_persisted_settings` leaves `trusted_proxies` empty and `AuditMiddleware` records the proxy address instead of the client address.

Add `CODEBUDDY2API_TRUSTED_PROXIES` to the Compose environment mapping.

Also at `docs/deployment.md:16`.
</issue_to_address>

### Comment 4
<location path="app/observability.py" line_range="76" />
<code_context>
+            if not any(hop_ip in network for network in networks):
+                break
+        if chosen is None:
+            chosen = headers.get("x-real-ip") or (hops[0] if hops else None)
+        ip = chosen or remote
+    return ip, safe_agent(headers.get("user-agent"))
</code_context>
<issue_to_address>
**Malformed values are stored as client IPs**

When a trusted proxy supplies a malformed `X-Real-IP` or XFF fallback and no valid XFF hop is selected, `client_meta` copies the fallback without IP validation, and `safe_label` accepts ordinary identifier strings, so malformed values persist and appear as client addresses.

Parse fallback values with `ipaddress.ip_address` and ignore them when invalid.

Also at `app/audit_store.py:285`.
</issue_to_address>

### Comment 5
<location path="app/audit_store.py" line_range="459" />
<code_context>
             params.extend((stamp, stamp, record_id))
         if kind == "request":
-            for key in ("model", "profile", "credential", "outcome", "status_code"):
+            for key in ("model", "profile", "credential", "outcome", "status_code", "client_ip"):
                 value = filters.get(key)
                 if value is not None and value != "":
</code_context>
<issue_to_address>
**Client IP filters time out**

When the audit database has many retained requests and an exact client-IP filter has few or no matches, `list_records` scans retained request rows because `client_ip` has no supporting index; `_run` interrupts the query after one second and the logs endpoint returns a degraded result instead of the matching records.

Add an index that supports filtering request rows by `client_ip`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and if the trusted-proxy configuration or forwarded-address parsing is wrong, audit records can persist incorrect client IPs or user agents, including spoofed proxy-chain identities. Reverting stops new records but does not remove the bounded set of historical metadata already written; the audit data can otherwise be cleared or regenerated through normal operational handling.

Blocking findings: app/observability.py:53, app/audit_store.py:168, app/settings.py:127, app/observability.py:76, app/audit_store.py:459


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread app/observability.py Outdated
Comment thread app/audit_store.py
Comment thread app/settings.py
Comment thread app/observability.py Outdated
Comment thread app/audit_store.py
Variable-key i18n (zh/en dictionaries both loaded, zh default, persisted)
with a top-left toggle in the sidebar and login pages. The audit logs page
is fully translated (tabs, filters, columns, drawer, clear dialog), event
kind/action and outcome render from label keys, and empty values show a
neutral dash instead of asserting unknown. Dashboard, Trend and the shared
chrome are translated; Models/Credentials/Settings keep zh strings until
their keys land.
The provider no longer writes localStorage on mount, restoring the
anonymous-visit zero-storage guarantee the e2e admin test asserts. The
audit detail drawer renders known enum values (outcomes, error codes,
stream modes, usage sources, stages, event codes) in the UI language
with the raw value on hover.
Review fixes: combine repeated X-Forwarded-For fields in order before
walking the chain (a proxy appending its hop as a separate field is no
longer shadowed by a spoofed first field); validate every XFF hop and the
X-Real-IP fallback with ipaddress and record the direct peer when a chain
is broken or the fallback is not an address; create requests_client on
migration so exact client-IP filters stay indexed; pass
CODEBUDDY2API_TRUSTED_PROXIES through Compose; document the single-writer
audit-schema rollout constraint.
@maiphucgiang
maiphucgiang merged commit 9a7bbcf into main Oct 11, 2026
8 checks passed
@maiphucgiang
maiphucgiang deleted the feat/audit-client-meta branch October 11, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant