From 8e65349bd74cb82ead68d6ad9f6479e349a988df Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Mon, 21 Sep 2026 13:06:35 -0400 Subject: [PATCH 1/4] style(frontend): format three files prettier flags on main Format-only. The pre-commit prettier hook checks the whole frontend directory, so any commit that touches a frontend file is blocked by drift that landed earlier on main (24057cb3, 1507b799). No CI job runs this check. No logic changes. --- frontend/src/api/host-view-model.ts | 7 +------ frontend/src/pages/HostDetailPage.tsx | 3 +-- frontend/src/pages/settings/ScanningPage.tsx | 7 +------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/frontend/src/api/host-view-model.ts b/frontend/src/api/host-view-model.ts index d0b11ddcc..9b638f179 100644 --- a/frontend/src/api/host-view-model.ts +++ b/frontend/src/api/host-view-model.ts @@ -5,12 +5,7 @@ // monitoring_state distinguishes WHICH layer is failing (sudo broken vs ssh // down vs network outage). 'status' stays as the coarse online/down view. export type MonitoringBand = - | 'online' - | 'degraded' - | 'critical' - | 'down' - | 'maintenance' - | 'unknown'; + 'online' | 'degraded' | 'critical' | 'down' | 'maintenance' | 'unknown'; export interface DevHost { id: string; diff --git a/frontend/src/pages/HostDetailPage.tsx b/frontend/src/pages/HostDetailPage.tsx index 400e8d3b5..503959d88 100644 --- a/frontend/src/pages/HostDetailPage.tsx +++ b/frontend/src/pages/HostDetailPage.tsx @@ -1543,8 +1543,7 @@ function RemediationRowAction({ const review = useMutation({ mutationFn: async (action: 'approve' | 'reject') => { const path = `/api/v1/remediation/requests/{rid}:${action}` as - | '/api/v1/remediation/requests/{rid}:approve' - | '/api/v1/remediation/requests/{rid}:reject'; + '/api/v1/remediation/requests/{rid}:approve' | '/api/v1/remediation/requests/{rid}:reject'; const { error, response } = await api.POST(path, { params: { path: { rid: request.id } }, body: {}, diff --git a/frontend/src/pages/settings/ScanningPage.tsx b/frontend/src/pages/settings/ScanningPage.tsx index d132aa8d2..626f84cb7 100644 --- a/frontend/src/pages/settings/ScanningPage.tsx +++ b/frontend/src/pages/settings/ScanningPage.tsx @@ -69,12 +69,7 @@ interface StateRowConfig { } type ScanStateId = - | 'critical' - | 'non_compliant' - | 'partial' - | 'mostly_compliant' - | 'compliant' - | 'unknown'; + 'critical' | 'non_compliant' | 'partial' | 'mostly_compliant' | 'compliant' | 'unknown'; interface ComplianceRowSeed { id: ScanStateId; From 44a79b58895364dbca4558bdeca490fdafcc278c Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Mon, 21 Sep 2026 13:07:00 -0400 Subject: [PATCH 2/4] fix(audit): export takes correlation_id and refuses a filter it does not declare The export accepted six of the list's seven filters; correlation_id was missing from the contract and the handler. Because the generated router drops undeclared query parameters, an export narrowed by correlation_id (or any misspelled filter) returned the caller's whole trail up to the 10,000-row cap with no signal, and an incident package built from it would be wrong without anyone noticing (CP bugs/OW-064). Contract first: api-audit-events-query 1.5.0 amends C-08 (the export accepts the same filters as the list, correlation_id included, and rejects an undeclared query parameter with 400 request.unknown_parameter naming it; the strictness is confined to the export route by founder decision) and adds AC-17. The export operation in api/openapi.yaml declares correlation_id and the 400 response; generated code follows. The handler copies correlation_id into the list query and checks the raw query string against the set of declared parameters before running anything, in the request's own order, so the first unknown key is named. The list endpoint is unchanged and stays lenient. Tests: AC-17 seeds events under two correlation ids and asserts the narrowed export in both formats, the 400 envelope for a misspelled filter with no Content-Disposition, and the list's 200 for the same misspelling. A second test reads the export operation from api/openapi.yaml and requires the guard's set to equal the declared parameters, so the two cannot drift; removing correlation_id from the guard turns both tests red. The API guide's audit section documents the export beside the list. The detect-secrets baseline is the hook's own line-number refresh. CP: bugs/doing/OW-064 --- .secrets.baseline | 6 +- api/openapi.yaml | 8 ++ docs/guides/API_GUIDE.md | 16 ++- frontend/src/api/schema.d.ts | 10 ++ internal/server/api/server.gen.go | 28 ++++-- internal/server/api_audit_query_test.go | 125 ++++++++++++++++++++++++ internal/server/audit_export_handler.go | 56 +++++++++-- specs/api/audit-events-query.spec.yaml | 17 +++- 8 files changed, 245 insertions(+), 21 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index ebd9373f1..fb160ab7a 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -558,14 +558,14 @@ "filename": "internal/server/api/server.gen.go", "hashed_secret": "9fd0aaae1a3d0bc789d081307161ea9a821f9dee", "is_verified": false, - "line_number": 4593 + "line_number": 4594 }, { "type": "Secret Keyword", "filename": "internal/server/api/server.gen.go", "hashed_secret": "eca525ee60b3564d9633eb140726685271d52341", "is_verified": false, - "line_number": 4731 + "line_number": 4732 } ], "internal/server/api_scans_test.go": [ @@ -818,5 +818,5 @@ } ] }, - "generated_at": "2026-09-19T22:42:43Z" + "generated_at": "2026-09-21T17:06:19Z" } diff --git a/api/openapi.yaml b/api/openapi.yaml index 17a76334e..989cf3526 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1306,6 +1306,9 @@ paths: - name: action in: query schema: {type: string} + - name: correlation_id + in: query + schema: {type: string} - name: actor_type in: query schema: {type: string} @@ -1322,6 +1325,11 @@ paths: in: query schema: {type: string, format: date-time} responses: + '400': + description: A query parameter the export does not declare (request.unknown_parameter), or one that fails to parse. + content: + application/json: + schema: {$ref: '#/components/schemas/ErrorEnvelope'} '200': description: Audit export file (CSV or JSON attachment) content: diff --git a/docs/guides/API_GUIDE.md b/docs/guides/API_GUIDE.md index 51dacddf5..ae650f488 100644 --- a/docs/guides/API_GUIDE.md +++ b/docs/guides/API_GUIDE.md @@ -309,9 +309,19 @@ cursor-paginated, newest first. |--------|------|---------| | `GET` | `/api/v1/audit/events` | List audit events. | -Query parameters: `action`, `correlation_id`, `actor_type`, `resource_type`, -`resource_id`, `since`, `until` (both RFC 3339), `cursor`, and `limit` (1–200, -default 50). Follow the `cursor` field in each page to paginate. +| `GET` | `/api/v1/audit/events/export` | Download the filtered trail as CSV (default) or JSON (`format=json`). Requires `audit:export`. | + +List query parameters: `action`, `correlation_id`, `actor_type`, +`resource_type`, `resource_id`, `since`, `until` (both RFC 3339), `cursor`, +and `limit` (1 to 200, default 50). Each page carries `next_cursor`; pass it +as the next request's `cursor`. + +The export takes the same seven filters, returns the whole filtered set +newest first, and stops at 10,000 rows; a capped export carries an +`X-OpenWatch-Export-Truncated` header. A query parameter the export does +not declare is refused with `400` `request.unknown_parameter` naming it, so +a misspelled filter cannot silently widen an export you will file; the list +endpoint ignores unknown parameters as before. --- diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts index ea7ce9cd3..ee4adfa5a 100644 --- a/frontend/src/api/schema.d.ts +++ b/frontend/src/api/schema.d.ts @@ -6781,6 +6781,7 @@ export interface operations { /** @description Output format. Defaults to csv. */ format?: "csv" | "json"; action?: string; + correlation_id?: string; actor_type?: string; resource_type?: string; resource_id?: string; @@ -6803,6 +6804,15 @@ export interface operations { "application/json": components["schemas"]["AuditEvent"][]; }; }; + /** @description A query parameter the export does not declare (request.unknown_parameter), or one that fails to parse. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorEnvelope"]; + }; + }; /** @description Caller is not authenticated */ 401: { headers: { diff --git a/internal/server/api/server.gen.go b/internal/server/api/server.gen.go index 3b620a8fa..836965b76 100644 --- a/internal/server/api/server.gen.go +++ b/internal/server/api/server.gen.go @@ -4348,13 +4348,14 @@ type GetAuditEventsParams struct { // GetAuditEventsExportParams defines parameters for GetAuditEventsExport. type GetAuditEventsExportParams struct { // Format Output format. Defaults to csv. - Format *GetAuditEventsExportParamsFormat `form:"format,omitempty" json:"format,omitempty"` - Action *string `form:"action,omitempty" json:"action,omitempty"` - ActorType *string `form:"actor_type,omitempty" json:"actor_type,omitempty"` - ResourceType *string `form:"resource_type,omitempty" json:"resource_type,omitempty"` - ResourceId *string `form:"resource_id,omitempty" json:"resource_id,omitempty"` - Since *time.Time `form:"since,omitempty" json:"since,omitempty"` - Until *time.Time `form:"until,omitempty" json:"until,omitempty"` + Format *GetAuditEventsExportParamsFormat `form:"format,omitempty" json:"format,omitempty"` + Action *string `form:"action,omitempty" json:"action,omitempty"` + CorrelationId *string `form:"correlation_id,omitempty" json:"correlation_id,omitempty"` + ActorType *string `form:"actor_type,omitempty" json:"actor_type,omitempty"` + ResourceType *string `form:"resource_type,omitempty" json:"resource_type,omitempty"` + ResourceId *string `form:"resource_id,omitempty" json:"resource_id,omitempty"` + Since *time.Time `form:"since,omitempty" json:"since,omitempty"` + Until *time.Time `form:"until,omitempty" json:"until,omitempty"` } // GetAuditEventsExportParamsFormat defines parameters for GetAuditEventsExport. @@ -6730,6 +6731,19 @@ func (siw *ServerInterfaceWrapper) GetAuditEventsExport(w http.ResponseWriter, r return } + // ------------- Optional query parameter "correlation_id" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "correlation_id", r.URL.Query(), ¶ms.CorrelationId, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "correlation_id"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "correlation_id", Err: err}) + } + return + } + // ------------- Optional query parameter "actor_type" ------------- err = runtime.BindQueryParameterWithOptions("form", true, false, "actor_type", r.URL.Query(), ¶ms.ActorType, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) diff --git a/internal/server/api_audit_query_test.go b/internal/server/api_audit_query_test.go index 95b9ef7d9..27838257d 100644 --- a/internal/server/api_audit_query_test.go +++ b/internal/server/api_audit_query_test.go @@ -10,6 +10,8 @@ import ( "io" "net/http" neturl "net/url" + "os" + "path/filepath" "strings" "testing" "time" @@ -647,3 +649,126 @@ func TestAPI_AuditEvents_ExportRequiresAuditExport(t *testing.T) { } }) } + +// @ac AC-17 +// api-audit-events-query/AC-17 (v1.5.0): the export takes every filter the +// list takes, correlation_id included, and refuses a filter it does not +// declare instead of silently exporting everything. The list endpoint keeps +// its lenient behavior, so the strictness is confined to the export. +func TestAPI_AuditEvents_ExportFilterParityAndNoSilentWidening(t *testing.T) { + t.Run("api-audit-events-query/AC-17", func(t *testing.T) { + url, pool := freshAPIServer(t) + ctx := context.Background() + seed := func(corr string) { + t.Helper() + id := uuid.Must(uuid.NewV7()) + if _, err := pool.Exec(ctx, + `INSERT INTO audit_events + (id, correlation_id, actor_type, actor_label, action, severity, occurred_at) + VALUES ($1,$2,'user','alice@example.com','host.created','info',now())`, + id, corr); err != nil { + t.Fatalf("seed audit event: %v", err) + } + } + seed("corr-a") + seed("corr-a") + seed("corr-b") + + // correlation_id narrows the export in both formats. + resp := doReq(t, asRole(t, "GET", url+"/api/v1/audit/events/export?format=json&correlation_id=corr-a", auth.RoleAuditor, nil)) + if resp.StatusCode != http.StatusOK { + t.Fatalf("json export status = %d, want 200", resp.StatusCode) + } + var events []map[string]any + if err := json.NewDecoder(resp.Body).Decode(&events); err != nil { + t.Fatalf("decode json export: %v", err) + } + resp.Body.Close() + if len(events) != 2 { + t.Fatalf("json export rows = %d, want 2 (corr-a only)", len(events)) + } + for _, ev := range events { + if ev["correlation_id"] != "corr-a" { + t.Errorf("json export leaked correlation_id %v", ev["correlation_id"]) + } + } + resp = doReq(t, asRole(t, "GET", url+"/api/v1/audit/events/export?correlation_id=corr-b", auth.RoleAuditor, nil)) + raw, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("csv export status = %d, want 200", resp.StatusCode) + } + if lines := strings.Count(strings.TrimSpace(string(raw)), "\n"); lines != 1 { + t.Errorf("csv export data rows = %d, want 1 (corr-b only); body=%q", lines, raw) + } + + // A misspelled filter is refused, and nothing is exported. + resp = doReq(t, asRole(t, "GET", url+"/api/v1/audit/events/export?correlation_id=corr-a&actr_type=user", auth.RoleAuditor, nil)) + raw, _ = io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("misspelled filter status = %d, want 400; body=%q", resp.StatusCode, raw) + } + if resp.Header.Get("Content-Disposition") != "" { + t.Errorf("a refused export must not set Content-Disposition") + } + var env struct { + Error struct { + Code string `json:"code"` + HumanMessage string `json:"human_message"` + } `json:"error"` + } + if err := json.Unmarshal(raw, &env); err != nil { + t.Fatalf("400 body is not the envelope: %v; body=%q", err, raw) + } + if env.Error.Code != "request.unknown_parameter" || !strings.Contains(env.Error.HumanMessage, "actr_type") { + t.Errorf("envelope = %+v, want request.unknown_parameter naming actr_type", env.Error) + } + + // The list endpoint keeps ignoring an unknown parameter. + resp = doReq(t, asRole(t, "GET", url+"/api/v1/audit/events?actr_type=user", auth.RoleAuditor, nil)) + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("list with unknown parameter status = %d, want 200 (lenient)", resp.StatusCode) + } + }) +} + +// auditExportParams must equal the query parameters getAuditEventsExport +// declares, or the unknown-parameter guard would reject a declared filter +// or admit an undeclared one. Read from the contract, not remembered. +func TestAPI_AuditEvents_ExportParamGuardMatchesContract(t *testing.T) { + t.Run("api-audit-events-query/AC-17", func(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("..", "..", "api", "openapi.yaml")) + if err != nil { + t.Fatal(err) + } + doc := string(raw) + start := strings.Index(doc, " /api/v1/audit/events/export:") + if start < 0 { + t.Fatal("export path not found in api/openapi.yaml") + } + end := strings.Index(doc[start:], " responses:") + block := doc[start : start+end] + declared := map[string]struct{}{} + for _, line := range strings.Split(block, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "- name: ") { + declared[strings.TrimPrefix(line, "- name: ")] = struct{}{} + } + } + if len(declared) == 0 { + t.Fatal("no query parameters parsed from the export operation") + } + for name := range declared { + if _, ok := auditExportParams[name]; !ok { + t.Errorf("contract declares %q but auditExportParams would reject it", name) + } + } + for name := range auditExportParams { + if _, ok := declared[name]; !ok { + t.Errorf("auditExportParams admits %q which the contract does not declare", name) + } + } + }) +} diff --git a/internal/server/audit_export_handler.go b/internal/server/audit_export_handler.go index 61ddee61b..f026d227f 100644 --- a/internal/server/audit_export_handler.go +++ b/internal/server/audit_export_handler.go @@ -12,6 +12,8 @@ import ( "fmt" "log/slog" "net/http" + "net/url" + "strings" "time" "github.com/Hanalyx/openwatch/internal/auth" @@ -23,6 +25,35 @@ import ( // cap is logged + flagged (X-OpenWatch-Export-Truncated) so a truncated export is never silently mistaken for "all". const auditExportCap = 10000 +// auditExportParams is the query surface the export declares in +// api/openapi.yaml, one entry per parameter of getAuditEventsExport. The +// contract-coverage test keeps it equal to the declaration. +var auditExportParams = map[string]struct{}{ + "format": {}, "action": {}, "correlation_id": {}, "actor_type": {}, + "resource_type": {}, "resource_id": {}, "since": {}, "until": {}, +} + +// firstUnknownQueryParam returns the first query key not in allowed, in +// the request's own order, or "" when every key is declared. +func firstUnknownQueryParam(r *http.Request, allowed map[string]struct{}) string { + for _, pair := range strings.Split(r.URL.RawQuery, "&") { + if pair == "" { + continue + } + key := pair + if i := strings.IndexByte(pair, '='); i >= 0 { + key = pair[:i] + } + if unescaped, err := url.QueryUnescape(key); err == nil { + key = unescaped + } + if _, ok := allowed[key]; !ok { + return key + } + } + return "" +} + // GetAuditEventsExport streams the filtered audit events as a downloadable // CSV (default) or JSON file. audit:export gated, independently of the // audit:read list (v1.4.0; audit:read through 1.3.1, which let every reader @@ -32,14 +63,27 @@ func (h *handlers) GetAuditEventsExport(w http.ResponseWriter, r *http.Request, return } + // A filter the export does not declare is rejected, never ignored. The + // generated router drops unknown query parameters silently, and for + // this route that turns a misspelled filter into an export of the + // whole trail that the caller files as if it were the narrow one. The + // list endpoint stays lenient; the strictness is this route's alone + // (v1.5.0, CP bugs/OW-064). + if unknown := firstUnknownQueryParam(r, auditExportParams); unknown != "" { + writeError(w, http.StatusBadRequest, "request.unknown_parameter", "client", + "the export does not accept the "+unknown+" parameter", false) + return + } + // Reuse the list query with the same filters at the export cap. lp := api.GetAuditEventsParams{ - Action: params.Action, - ActorType: params.ActorType, - ResourceType: params.ResourceType, - ResourceId: params.ResourceId, - Since: params.Since, - Until: params.Until, + Action: params.Action, + CorrelationId: params.CorrelationId, + ActorType: params.ActorType, + ResourceType: params.ResourceType, + ResourceId: params.ResourceId, + Since: params.Since, + Until: params.Until, } rows, err := h.queryEvents(r.Context(), lp, auditExportCap) if err != nil { diff --git a/specs/api/audit-events-query.spec.yaml b/specs/api/audit-events-query.spec.yaml index 9e1ab6158..ea4510f69 100644 --- a/specs/api/audit-events-query.spec.yaml +++ b/specs/api/audit-events-query.spec.yaml @@ -8,7 +8,7 @@ spec: # bugs/OW-056, reproduced 2026-09-19). Founder decision 2026-09-19: the # registry is the intent. viewer and ops_lead lose the export route and # keep the list. AC-16 is new; C-08 and AC-13 say audit:export. - version: "1.4.0" + version: "1.5.0" status: approved tier: 2 @@ -63,7 +63,7 @@ spec: type: technical enforcement: error - id: C-08 - description: 'v1.3.0 — GET /api/v1/audit/events/export MUST stream the filtered audit trail as a downloadable attachment (NIST 800-53 AU-7). It is audit:export gated (v1.4.0; audit:read through 1.3.1), independent of the list endpoint which stays audit:read, accepts the same filters (action, actor_type, resource_type, resource_id, since, until), and returns the whole filtered set newest-first capped at 10000 rows (not one page). format=csv (default) emits a header row + one row per event with columns occurred_at, action, message, severity, actor_type, actor_label, actor_id, resource_type, resource_id, correlation_id; format=json emits the AuditEvent array. Both set Content-Disposition: attachment with a timestamped filename. The message column reuses activity.FormatAudit (same as the list). v1.3.1 hardening: (a) every CSV cell MUST be neutralized against spreadsheet formula injection (CWE-1236) — a value whose first character is =, +, -, @, tab, or CR is prefixed with a single quote so it renders as literal text, not an executed formula; (b) a truncated export (row count == the 10000 cap) MUST set an X-OpenWatch-Export-Truncated response header and log a warning, so a capped export is never silently mistaken for the complete trail' + description: 'v1.3.0 — GET /api/v1/audit/events/export MUST stream the filtered audit trail as a downloadable attachment (NIST 800-53 AU-7). It is audit:export gated (v1.4.0; audit:read through 1.3.1), independent of the list endpoint which stays audit:read, accepts the same filters as the list (action, correlation_id, actor_type, resource_type, resource_id, since, until; correlation_id added in v1.5.0, CP bugs/OW-064), rejects a query parameter it does not declare with 400 request.unknown_parameter naming the parameter (v1.5.0; a misspelled filter must fail rather than silently widen an artifact someone will file, and this strictness is specific to the export route by founder decision, not a server-wide policy), and returns the whole filtered set newest-first capped at 10000 rows (not one page). format=csv (default) emits a header row + one row per event with columns occurred_at, action, message, severity, actor_type, actor_label, actor_id, resource_type, resource_id, correlation_id; format=json emits the AuditEvent array. Both set Content-Disposition: attachment with a timestamped filename. The message column reuses activity.FormatAudit (same as the list). v1.3.1 hardening: (a) every CSV cell MUST be neutralized against spreadsheet formula injection (CWE-1236) — a value whose first character is =, +, -, @, tab, or CR is prefixed with a single quote so it renders as literal text, not an executed formula; (b) a truncated export (row count == the 10000 cap) MUST set an X-OpenWatch-Export-Truncated response header and log a warning, so a capped export is never silently mistaken for the complete trail' type: technical enforcement: error @@ -134,3 +134,16 @@ spec: constant. priority: critical references_constraints: [C-08] + - id: AC-17 + description: > + v1.5.0 — Export filter parity and no silent widening. With events + seeded under two correlation ids, GET + /api/v1/audit/events/export?correlation_id= returns only the + rows for in both csv and json; GET + /api/v1/audit/events/export?correlation_id=&actr_type=user (a + misspelled filter) returns 400 request.unknown_parameter naming + actr_type and exports nothing; the same misspelling on GET + /api/v1/audit/events keeps the list endpoint's lenient behavior + (200), so the strictness is measurably confined to the export. + priority: high + references_constraints: [C-08] From d4f5eec91991ac2bf3f38df6bf36f8ea23e20458 Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Mon, 21 Sep 2026 20:16:38 -0400 Subject: [PATCH 3/4] docs(api): keep the export row inside the audit endpoints table A blank line between the two rows split the table, so the export row would have rendered as loose text. --- docs/guides/API_GUIDE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/guides/API_GUIDE.md b/docs/guides/API_GUIDE.md index 52e7a140e..f7d1e77e8 100644 --- a/docs/guides/API_GUIDE.md +++ b/docs/guides/API_GUIDE.md @@ -308,7 +308,6 @@ cursor-paginated, newest first. | Method | Path | Purpose | |--------|------|---------| | `GET` | `/api/v1/audit/events` | List audit events. | - | `GET` | `/api/v1/audit/events/export` | Download the filtered trail as CSV (default) or JSON (`format=json`). Requires `audit:export`. | List query parameters: `action`, `correlation_id`, `actor_type`, From 00227b96883269a84d0aaf8d8020913059c1675b Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Mon, 21 Sep 2026 21:51:28 -0400 Subject: [PATCH 4/4] chore(ci): rescan the secrets baseline for the refreshed tree Merging main kept main's baseline; this branch's regenerated server.gen.go shifts two findings by one line (4595 to 4596, 4733 to 4734). Rescanned with the pinned detect-secrets 1.5.0: 83 fingerprints, none added or removed, two locations corrected. --- .secrets.baseline | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index d56b0f108..1eb65fd26 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -549,14 +549,14 @@ "filename": "internal/server/api/server.gen.go", "hashed_secret": "9fd0aaae1a3d0bc789d081307161ea9a821f9dee", "is_verified": false, - "line_number": 4595 + "line_number": 4596 }, { "type": "Secret Keyword", "filename": "internal/server/api/server.gen.go", "hashed_secret": "eca525ee60b3564d9633eb140726685271d52341", "is_verified": false, - "line_number": 4733 + "line_number": 4734 } ], "internal/server/api_scans_test.go": [ @@ -809,5 +809,5 @@ } ] }, - "generated_at": "2026-09-22T00:14:21Z" + "generated_at": "2026-09-22T01:51:28Z" }