Skip to content

Commit ac1b7cd

Browse files
chore(release): v9.6.1 — audit-search session_id filter fix (#433)
v9.6.1 is a patch on v9.6.0. It fixes a bug in the audit-search API where a session_id filter was silently ignored: the search endpoint accepted the request and returned 200 but dropped the filter, so drilling from a session-summary bucket into that session's individual records returned the tenant's records across every session in the time range instead of the one requested. Tenant isolation was never affected — the bug was confined to the session_id sub-filter, and the tenant scope every audit query enforces was always applied. It is a drop-in upgrade from v9.6.0, with no migration and no configuration change. Highlights: - The session_id filter on POST /api/v1/audit/search is now applied as a parameterized, tenant-scoped condition, so a search by session_id returns exactly that session's records. Aggregates from the session-summary endpoint itself were correct on v9.6.0 and are unchanged. Migration: none. Full notes: https://docs.getaxonflow.com/docs/releases/v9-6-1 Signed-off-by: AxonFlow Team <bot@getaxonflow.com> Co-authored-by: AxonFlow Team <bot@getaxonflow.com>
1 parent cc1a156 commit ac1b7cd

7 files changed

Lines changed: 87 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ community mirror, **Enterprise** changes are EE-only.
1010

1111
---
1212

13+
## [9.6.1] - 2026-07-08 (patch on 9.6.0: audit-search session_id drill-down)
14+
15+
**Patch.** Fixes a correctness bug in the audit-search API surfaced while documenting the v9.6.0 session-summary reporting: a `session_id` filter was silently dropped, so the per-session drill-down returned the tenant's rows across all sessions instead of the one requested. Within-tenant only; tenant isolation was never affected. No migration.
16+
17+
### Fixed
18+
19+
- **`session_id` filter on `POST /api/v1/audit/search` is now applied.** *(Community)* The audit-search request handler's body-decode struct was missing the `session_id` field the query layer already supported, so a `session_id` filter was silently dropped and the session-summary "drill into this session" flow returned all of the tenant's rows across every session with a `200`. The filter now reaches the query as a parameterized, tenant-scoped condition, so drilling into a session bucket returns exactly that session's rows. Tenant scoping (from the trusted tenant header) was never affected. Pinned by a handler-level regression test and a runtime-e2e drill-down leg.
20+
21+
---
22+
1323
## [9.6.0] - 2026-07-07 (session-summary reporting API + Claude Code usage dashboard)
1424

1525
**Additive minor.** v9.6.0 turns the Claude Code and Cowork activity v9.5.0 began ingesting into reporting. A new session-summary API rolls governed activity into per-session (or per-user-day) buckets, additively enriched with the developer and session usage metrics when the OTLP ingest is wired; a new operator Grafana dashboard visualizes that usage; and the bundled Grafana datasources are provisioned with stable uids, closing a pre-existing bug that broke provisioned dashboards. One idempotent migration (`core/141`) auto-applies on deploy.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9.6.0
1+
9.6.1

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ services:
8787
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
8888
AXONFLOW_INTEGRATIONS: ${AXONFLOW_INTEGRATIONS:-}
8989
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
90-
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.6.0}"
90+
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.6.1}"
9191

9292
# Anonymous platform startup telemetry — inherit the operator's
9393
# opt-out lever. CI / dev shells should export AXONFLOW_TELEMETRY=off
@@ -269,7 +269,7 @@ services:
269269
PORT: 8081
270270
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
271271
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
272-
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.6.0}"
272+
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.6.1}"
273273

274274
# Anonymous platform startup telemetry — inherit the operator's
275275
# opt-out lever. See agent service above for the full rationale.

platform/agent/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ARG EDITION=community
1111
# — the SAME source as the io.opencontainer.image.version label below, so the
1212
# baked binary version and the image label never drift. Re-declared here in the
1313
# builder stage (ARG scope is per-stage); the final stage declares it again.
14-
ARG AXONFLOW_VERSION=9.6.0
14+
ARG AXONFLOW_VERSION=9.6.1
1515

1616
# Install git for go modules
1717
RUN apk add --no-cache git ca-certificates tzdata
@@ -138,7 +138,7 @@ RUN set -e && \
138138
# Final stage - minimal runtime image
139139
FROM alpine:3.24
140140

141-
ARG AXONFLOW_VERSION=9.6.0
141+
ARG AXONFLOW_VERSION=9.6.1
142142
ENV AXONFLOW_VERSION=${AXONFLOW_VERSION}
143143

144144
# AWS Marketplace metadata

platform/orchestrator/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ARG EDITION=community
1111
# — the SAME source as the io.opencontainer.image.version label below, so the
1212
# baked binary version and the image label never drift. Re-declared here in the
1313
# builder stage (ARG scope is per-stage); the final stage declares it again.
14-
ARG AXONFLOW_VERSION=9.6.0
14+
ARG AXONFLOW_VERSION=9.6.1
1515

1616
# Install git for go modules
1717
RUN apk add --no-cache git ca-certificates tzdata
@@ -149,7 +149,7 @@ RUN set -e && \
149149
# Final stage - minimal runtime image
150150
FROM alpine:3.24
151151

152-
ARG AXONFLOW_VERSION=9.6.0
152+
ARG AXONFLOW_VERSION=9.6.1
153153
ENV AXONFLOW_VERSION=${AXONFLOW_VERSION}
154154

155155
# AWS Marketplace metadata

platform/orchestrator/run.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2583,6 +2583,7 @@ func auditSearchHandler(w http.ResponseWriter, r *http.Request) {
25832583
PolicyName string `json:"policy_name,omitempty"` // ADR-043: filter by policy_name
25842584
OverrideID string `json:"override_id,omitempty"` // ADR-044: filter by override_id in policy_details JSONB
25852585
Action string `json:"action,omitempty"` // filter by policy_decision (e.g. "blocked", "approved")
2586+
SessionID string `json:"session_id,omitempty"` // #2759: session-summary bucket drill-down against the first-class column
25862587
Limit int `json:"limit,omitempty"`
25872588
Offset int `json:"offset,omitempty"`
25882589
}

platform/orchestrator/run_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"axonflow/platform/orchestrator/rbi"
1818
"axonflow/platform/orchestrator/sebi"
1919

20+
"github.com/DATA-DOG/go-sqlmock"
2021
"github.com/gorilla/mux"
2122
)
2223

@@ -1783,6 +1784,74 @@ func TestAuditSearchHandler(t *testing.T) {
17831784
}
17841785
}
17851786

1787+
// TestAuditSearchHandler_SessionIDFilter pins the HTTP→SQL wiring of the
1788+
// #2759 session-summary drill-down: a session_id in the POST body must reach
1789+
// SearchAuditLogs as a criteria field and land in the SQL as a
1790+
// session_id = $n filter. Regression it guards: the backend filter shipped
1791+
// with #2759 (SearchAuditLogs + criteria adapter), but the handler's
1792+
// body-decode struct never carried the field, so an HTTP caller's session_id
1793+
// was silently dropped and the "drill into a session bucket" flow returned
1794+
// the tenant's rows across ALL sessions with a 200 (#2857). The criteria
1795+
// adapter reads whatever fields the caller's struct exposes, so this bug
1796+
// class is invisible to SearchAuditLogs-level tests — it must be pinned at
1797+
// the handler.
1798+
func TestAuditSearchHandler_SessionIDFilter(t *testing.T) {
1799+
oldAuditLogger := auditLogger
1800+
defer func() { auditLogger = oldAuditLogger }()
1801+
1802+
db, mock, err := sqlmock.New()
1803+
if err != nil {
1804+
t.Fatalf("Failed to create mock DB: %v", err)
1805+
}
1806+
defer func() { _ = db.Close() }()
1807+
auditLogger = &AuditLogger{db: db}
1808+
1809+
rows := sqlmock.NewRows([]string{
1810+
"id", "request_id", "timestamp", "user_id", "user_email", "user_role",
1811+
"client_id", "tenant_id", "request_type", "query", "policy_decision",
1812+
"policy_details", "provider", "model", "response_time_ms", "tokens_used",
1813+
"cost", "redacted_fields", "error_message", "compliance_flags",
1814+
"response_sample", "session_id",
1815+
"total_count",
1816+
}).AddRow(
1817+
"audit_sess_001", "req_sess_001", time.Now(), 5, "user@example.com", "user",
1818+
"client_001", "tenant_001", "query", "session query", "allowed",
1819+
[]byte(`{}`), "openai", "gpt-4", 100, 80,
1820+
0.004, []byte(`[]`), "", []byte(`[]`),
1821+
"", "sess-1",
1822+
1,
1823+
)
1824+
// The tenant comes from the trusted header ($1), the session filter from
1825+
// the body ($2). WithArgs is the real assertion: it fails unless the
1826+
// handler actually forwarded session_id into the query.
1827+
mock.ExpectQuery("SELECT (.+) FROM audit_logs WHERE (.+) tenant_id = (.+) session_id = (.+) ORDER BY timestamp DESC LIMIT").
1828+
WithArgs("tenant_001", "sess-1").
1829+
WillReturnRows(rows)
1830+
1831+
req := httptest.NewRequest("POST", "/api/v1/audit/search", strings.NewReader(`{"session_id":"sess-1","limit":50}`))
1832+
req.Header.Set("Content-Type", "application/json")
1833+
req.Header.Set("X-Tenant-ID", "tenant_001")
1834+
w := httptest.NewRecorder()
1835+
1836+
auditSearchHandler(w, req)
1837+
1838+
if w.Code != http.StatusOK {
1839+
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
1840+
}
1841+
var resp struct {
1842+
Entries []*AuditEntry `json:"entries"`
1843+
}
1844+
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
1845+
t.Fatalf("response not JSON: %v", err)
1846+
}
1847+
if len(resp.Entries) != 1 || resp.Entries[0].SessionID != "sess-1" {
1848+
t.Fatalf("want exactly the sess-1 entry back, got %+v", resp.Entries)
1849+
}
1850+
if err := mock.ExpectationsWereMet(); err != nil {
1851+
t.Errorf("Unfulfilled mock expectations: %v", err)
1852+
}
1853+
}
1854+
17861855
// TestTenantAuditLogsHandler tests tenant-specific audit logs
17871856
func TestTenantAuditLogsHandler(t *testing.T) {
17881857
tests := []struct {

0 commit comments

Comments
 (0)