diff --git a/README.md b/README.md index 7d63f4b2ab..3f1141d875 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ upstream history, authorship, licenses, and notices. ## Current release -Server `v1.6.520` packages Web Console `1.6.181` -and Orchestration Engine `v0.183.336`. API Keys support full, custom and closed +Server `v1.6.521` packages Web Console `1.6.181` +and Orchestration Engine `v0.183.337`. API Keys support full, custom and closed policies, optional expiry, and key-scoped durable audit records. Their permissions remain bounded by the owner's current role and environment access. Delegated terminal/log access requires the verified Host API audit capability and the @@ -35,7 +35,10 @@ Audit export closes its own menu before locking the request, allowing consecutiv JSON, CSV and XLSX downloads without leaving a disabled menu open. Key scope choices use the viewer's authorized environment list. Environment, stack and resource selectors close before dependent loading or verification. -See the [release note](docs/releases/server-1.6.520.md) for exact +Failed Key edits use a consistent public Key ID in audit records. Per-Key audit +also reads historical numeric-ID records within the same ownership and environment +authorization checks; existing records are not rewritten. +See the [release note](docs/releases/server-1.6.521.md) for exact component identities and verification boundaries. A version or packaged feature does not establish that every resource/role/hardware combination has been tested. @@ -70,7 +73,7 @@ docker run -d --name pasturestack-server --restart unless-stopped -p 8080:8080 \ -v pasturestack-cattle:/var/lib/cattle \ -v pasturestack-mysql:/var/lib/mysql \ -v pasturestack-mysqllog:/var/log/mysql \ - ghcr.io/pasturestack/server:v1.6.520 + ghcr.io/pasturestack/server:v1.6.521 ``` For HTTPS termination at a reverse proxy, set the exact public origin: @@ -78,7 +81,7 @@ For HTTPS termination at a reverse proxy, set the exact public origin: ```yaml services: pasturestack-server: - image: ghcr.io/pasturestack/server:v1.6.520 + image: ghcr.io/pasturestack/server:v1.6.521 restart: unless-stopped ports: - "8080:8080" diff --git a/docs/releases/server-1.6.521.md b/docs/releases/server-1.6.521.md new file mode 100644 index 0000000000..692b8f16e6 --- /dev/null +++ b/docs/releases/server-1.6.521.md @@ -0,0 +1,32 @@ +# Server v1.6.521 + +Packages Engine `v0.183.337` and unchanged Web Console `1.6.181`. + +## API Key audit identity + +Rejected Key edits after request validation could be recorded under an internal +numeric Key ID, while the Key audit screen queried the public ID. The events +were stored but omitted from that query. Engine now consistently formats the +authoritative Key identity. The audit reader also recognizes historical numeric +IDs, but only as exact aliases of the Engine-authorized Key. Other resource IDs, +unknown ownership and inaccessible environments are not treated as aliases. +Stored events are not rewritten. Safe list/detail projections return public +Key IDs consistently, including pagination and outcome filters. + +## Verification boundary + +Focused tests cover new events, historical list/detail queries, both Key types, +pagination, filters, cross-Key and cross-environment exclusions, secret-field +projection, and unchanged MFA confirmation. Full DTO and database-row checks on +rejected policy edits are part of runtime acceptance. These checks are not a +claim that every unrelated resource/role combination has been exercised. + +## Upgrade and rollback + +Use `ghcr.io/pasturestack/server:v1.6.521`. Preserve existing environment variables, +volumes, networking, restart policy and security options. No database migration +or proxy/authentication configuration change is required. + +`v1.6.520` is the compatible rollback version; it restores the numeric-ID audit +query defect. Follow the [API Key rollback guidance](../upgrades/api-key-policy-rollback.md) +before considering an older, policy-unaware release. diff --git a/server/Dockerfile.web-compose-release b/server/Dockerfile.web-compose-release index 1b280bae0e..04fa467dd0 100644 --- a/server/Dockerfile.web-compose-release +++ b/server/Dockerfile.web-compose-release @@ -79,9 +79,9 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.4 ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=49c4579829a04e758045fae02a5a9fca12bb0ba3af0d5e979cf9eb97f23a88a9 ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=98c7faea665b7eb95206b8c73a6f47d644c5d2d0eae53f274f6a15faf5205744 ARG ORCHESTRATION_ENGINE_RELEASE_BASE_URL=https://github.com/PastureStack/orchestration-engine/releases/download -ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.336 +ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.337 ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar -ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=bd0dabf0bd87a859b97941657ae18e8dd551e390697494348c101782a508f136 +ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80 ARG COMPOSE_EXECUTOR_RELEASE_BASE_URL=https://github.com/PastureStack/compose-cli/releases/download ARG COMPOSE_EXECUTOR_VERSION=0.14.37 ARG COMPOSE_EXECUTOR_ARCHIVE_SHA256=5ff465601930218f531885a384f231d969033e4cef794ce33d16bfe24e5c1c81 @@ -565,9 +565,9 @@ FROM ${BASE_IMAGE} ARG SOURCE_DATE_EPOCH ARG PASTURESTACK_SERVER_REVISION -ARG SERVER_RELEASE_TAG=v1.6.520 -ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.336 -ARG ORCHESTRATION_ENGINE_VERSION=0.183.336 +ARG SERVER_RELEASE_TAG=v1.6.521 +ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.337 +ARG ORCHESTRATION_ENGINE_VERSION=0.183.337 ARG ENGINE_READONLY_SCHEMA_SHA256=7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233 ARG ENGINE_RESTRICTED_SCHEMA_SHA256=f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51 ARG HOST_API_VERSION=0.38.5 @@ -606,8 +606,8 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.4 ARG WEBHOOK_AUTOMATION_SERVICE_COMMIT=400118b893843d2a7d7c65cc70c3449d76c4a8d8 ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=49c4579829a04e758045fae02a5a9fca12bb0ba3af0d5e979cf9eb97f23a88a9 ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=98c7faea665b7eb95206b8c73a6f47d644c5d2d0eae53f274f6a15faf5205744 -ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=bd0dabf0bd87a859b97941657ae18e8dd551e390697494348c101782a508f136 -ARG ORCHESTRATION_ENGINE_COMMIT=1c7d4537becfd578d568582564b72e7c4d0b1e93 +ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80 +ARG ORCHESTRATION_ENGINE_COMMIT=ba041fe312dfe97fc1e5144af576ddcf1b573d8e ARG HOST_PROVISIONER_VERSION=0.39.8 ARG HOST_PROVISIONER_COMMIT=385e5b536c108f17fdfcdec84c01750a5be5ab1c ARG HOST_PROVISIONER_ARCHIVE_SHA256=d775f36a613b1a486a5e60d6ad61fdbd1ebb4bd22422cbb6dcb70fdd7c4abf0c diff --git a/server/build-api-explorer-patch-image.sh b/server/build-api-explorer-patch-image.sh index a03edf4190..3e045369e8 100755 --- a/server/build-api-explorer-patch-image.sh +++ b/server/build-api-explorer-patch-image.sh @@ -14,14 +14,14 @@ if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then fi revision=${PASTURESTACK_SERVER_REVISION:-$(git rev-parse HEAD)} -server_release_tag=${SERVER_RELEASE_TAG:-v1.6.520} +server_release_tag=${SERVER_RELEASE_TAG:-v1.6.521} source_date_epoch=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)} base_image=${BASE_IMAGE:-ghcr.io/pasturestack/server:v1.6.460@sha256:c855af8aea232dacc5bb6df68e2271d482c68b53c43ab0c108ec19118f5ab403} orchestration_engine_release_base_url=${ORCHESTRATION_ENGINE_RELEASE_BASE_URL:-https://github.com/PastureStack/orchestration-engine/releases/download} -orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.336} +orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.337} orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar} -orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-bd0dabf0bd87a859b97941657ae18e8dd551e390697494348c101782a508f136} -orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-1c7d4537becfd578d568582564b72e7c4d0b1e93} +orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80} +orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-ba041fe312dfe97fc1e5144af576ddcf1b573d8e} orchestration_engine_version=${orchestration_engine_release_tag#v} engine_readonly_schema_sha256=${ENGINE_READONLY_SCHEMA_SHA256:-7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233} engine_restricted_schema_sha256=${ENGINE_RESTRICTED_SCHEMA_SHA256:-f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51} @@ -101,7 +101,7 @@ usage_telemetry_agent_archive_sha256=${USAGE_TELEMETRY_AGENT_ARCHIVE_SHA256:-5ce usage_telemetry_agent_binary_sha256=${USAGE_TELEMETRY_AGENT_BINARY_SHA256:-e62a21270142181315293d7e11482288ffe8fc4d91cc8fe07dacc90b461f3709} supported_docker_range='~v1.12.3 || ~v1.13.0 || ~v17.03.0 || ~v17.06.0 || ~v17.09.0 || ~v17.12.0 || ~v18.03.0 || ~v18.06.0 || ~v18.09.0 || ~v19.03.2 || v24.0.9 || >=v29.4.1 <=v29.7.2 || v29.8.0' newest_docker_version=v29.8.0 -image=${IMAGE:-pasturestack-validation/server:v1.6.520} +image=${IMAGE:-pasturestack-validation/server:v1.6.521} build_options=() [[ "$revision" =~ ^[0-9a-f]{40}$ ]] diff --git a/server/build-component-candidate.sh b/server/build-component-candidate.sh index 2b0f7877a7..0698ba770d 100755 --- a/server/build-component-candidate.sh +++ b/server/build-component-candidate.sh @@ -14,8 +14,8 @@ for name in "${required[@]}"; do case "$name" in *_COMMIT) [[ ${!name} =~ ^[0-9a-f]{40}$ ]] ;; *) [[ ${!name} =~ ^[0-9a-f]{64}$ ]] ;; esac export "$name" done -export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.520} -export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.336} +export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.521} +export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.337} export ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar export WEB_CONSOLE_RELEASE_TAG=${WEB_CONSOLE_RELEASE_TAG:-1.6.181} export WEB_CONSOLE_ARTIFACT="web-console-${WEB_CONSOLE_RELEASE_TAG}.tar.gz" diff --git a/server/console-broker/audit.go b/server/console-broker/audit.go index cb57d73eb1..cf90cf0f0f 100644 --- a/server/console-broker/audit.go +++ b/server/console-broker/audit.go @@ -375,7 +375,7 @@ func (b *broker) runAuditQuery(ctx context.Context, incoming *http.Request, quer scoped := make([]map[string]any, 0, len(records)) for _, record := range records { - if query.KeyID != "" && keyAuditMetadataString(record, "keyId") != query.KeyID { + if query.KeyID != "" && !keyAuditIDMatches(keyAuditMetadataString(record, "keyId"), query.KeyID) { continue } projectID := auditString(record, "accountId") @@ -395,7 +395,7 @@ func (b *broker) runAuditQuery(ctx context.Context, incoming *http.Request, quer copyRecord := cloneAuditRecord(record) if query.KeyID != "" { - copyRecord = safeKeyAuditRecord(record) + copyRecord = safeKeyAuditRecord(record, query.KeyID) } copyRecord["environmentDisplayName"] = allowedProjects[projectID] actorID := auditString(record, "authenticatedAsAccountId") diff --git a/server/console-broker/keyaudit.go b/server/console-broker/keyaudit.go index 89c8949bfe..27e78e3c90 100644 --- a/server/console-broker/keyaudit.go +++ b/server/console-broker/keyaudit.go @@ -16,6 +16,7 @@ import ( const keyAuditQueryPath = "/v2-beta/pasturestack/key-audit-logs" var keyAuditIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,159}$`) +var canonicalKeyAuditIDPattern = regexp.MustCompile(`^1c([1-9][0-9]*)$`) func (b *broker) fetchKeyAuditLogs(ctx context.Context, incoming *http.Request, query auditQuery, allowedProjects map[string]string, baseValues url.Values) ([]map[string]any, error) { @@ -249,7 +250,21 @@ func keyAuditMetadataString(record map[string]any, field string) string { return "" } -func safeKeyAuditRecord(record map[string]any) map[string]any { +// The Engine used internal numeric credential IDs for some governance failures. +// Match that historical form only against the current Engine-verified Key ID; +// unrelated typed IDs and records without keyId must not enter the Key view. +func keyAuditIDMatches(recordedID, keyID string) bool { + if keyID == "" { + return false + } + if recordedID == keyID { + return true + } + canonical := canonicalKeyAuditIDPattern.FindStringSubmatch(keyID) + return canonical != nil && recordedID == canonical[1] +} + +func safeKeyAuditRecord(record map[string]any, keyID string) map[string]any { result := make(map[string]any) for _, field := range []string{ "type", "id", "created", "accountId", "authenticatedAsAccountId", "authenticatedAsIdentityId", @@ -267,5 +282,14 @@ func safeKeyAuditRecord(record map[string]any) map[string]any { result[field] = value } } + if keyAuditIDMatches(keyAuditMetadataString(record, "keyId"), keyID) { + result["keyId"] = keyID + } + switch strings.ToLower(keyAuditMetadataString(record, "targetType")) { + case "apikey", "apikeyrestricted": + if keyAuditIDMatches(keyAuditMetadataString(record, "targetId"), keyID) { + result["targetId"] = keyID + } + } return result } diff --git a/server/console-broker/keyaudit_test.go b/server/console-broker/keyaudit_test.go index 2082a00e3a..723564818d 100644 --- a/server/console-broker/keyaudit_test.go +++ b/server/console-broker/keyaudit_test.go @@ -14,6 +14,7 @@ import ( type keyAuditFixture struct { mu sync.Mutex + keyID string owner string keyStatus int restricted bool @@ -44,7 +45,7 @@ type keyAuditFixture struct { func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) { t.Helper() fixture := &keyAuditFixture{ - owner: "1a1", viewer: "1a1", keyStatus: http.StatusOK, + keyID: "1a99", owner: "1a1", viewer: "1a1", keyStatus: http.StatusOK, projects: []map[string]any{{"type": "project", "id": "1p1", "name": "Production"}}, } upstream := &auditUpstreamFixture{} @@ -57,7 +58,7 @@ func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) { defer fixture.mu.Unlock() writer.Header().Set("X-API-USER-ID", fixture.viewer) switch request.URL.Path { - case "/v2-beta/apikey/1a99", "/v2-beta/apikeyrestricted/1a99": + case "/v2-beta/apikey/" + fixture.keyID, "/v2-beta/apikeyrestricted/" + fixture.keyID: fixture.keyQueries = append(fixture.keyQueries, request.URL.Query()) fixture.keyProjectHeaders = append(fixture.keyProjectHeaders, request.Header.Get("X-API-Project-ID")) kind := "apiKey" @@ -87,7 +88,7 @@ func newKeyAuditFixture(t *testing.T) (*keyAuditFixture, *httptest.Server) { return } } - key := map[string]any{"type": kind, "id": "1a99", "accountId": fixture.owner, "secretValue": "never-return-key-secret"} + key := map[string]any{"type": kind, "id": fixture.keyID, "accountId": fixture.owner, "secretValue": "never-return-key-secret"} for field, value := range fixture.scopedKeyOverride { switch field { case "viewer": @@ -185,6 +186,130 @@ func keyAuditRecord(id, keyID, project, created string) map[string]any { return record } +func TestKeyAuditIDMatchesOnlyExactOrCanonicalCredentialAlias(t *testing.T) { + for _, tc := range []struct { + name, recorded, key string + match bool + }{ + {"typed", "1c99", "1c99", true}, + {"legacy", "99", "1c99", true}, + {"existing-exact-contract", "1a99", "1a99", true}, + {"different-key", "98", "1c99", false}, + {"different-type", "1a99", "1c99", false}, + {"non-credential-query", "99", "1a99", false}, + {"suffix-only", "prefix99", "1c99", false}, + {"leading-zero", "099", "1c99", false}, + {"noncanonical-query", "99", "1c099", false}, + {"missing-key", "", "1c99", false}, + {"empty-query", "", "", false}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := keyAuditIDMatches(tc.recorded, tc.key); got != tc.match { + t.Fatalf("key match = %v, want %v", got, tc.match) + } + }) + } +} + +func TestSafeKeyAuditCanonicalizesOnlyMatchingKeyTargets(t *testing.T) { + for _, tc := range []struct{ kind, target, expected string }{ + {"apiKey", "99", "1c99"}, + {"apiKeyRestricted", "99", "1c99"}, + {"apiKey", "1c99", "1c99"}, + {"apiKey", "98", "98"}, + {"apiKey", "1a99", "1a99"}, + {"stack", "99", "99"}, + {"container", "99", "99"}, + {"", "99", "99"}, + } { + t.Run(tc.kind+"/"+tc.target, func(t *testing.T) { + record := map[string]any{"keyId": "99", "targetType": tc.kind, "targetId": tc.target, + "requestObject": map[string]any{"secretValue": "must-not-leak"}} + result := safeKeyAuditRecord(record, "1c99") + if result["keyId"] != "1c99" || result["targetId"] != tc.expected { + t.Fatalf("incorrect canonical metadata: %#v", result) + } + if record["keyId"] != "99" || record["targetId"] != tc.target { + t.Fatal("canonical projection mutated the historical source record") + } + if _, present := result["requestObject"]; present { + t.Fatal("historical compatibility bypassed the safe projection") + } + }) + } +} + +func TestKeyAuditHistoricalNumericIdentifiersBeforeCountDetailAndFilters(t *testing.T) { + for _, restricted := range []bool{false, true} { + for _, nested := range []bool{false, true} { + t.Run("restricted="+strconv.FormatBool(restricted)+"/nested="+strconv.FormatBool(nested), func(t *testing.T) { + fixture, server := newKeyAuditFixture(t) + fixture.keyID, fixture.restricted, fixture.accountScoped = "1c99", restricted, true + kind := "apiKey" + if restricted { + kind = "apiKeyRestricted" + } + legacy := keyAuditRecord("legacy", "99", "1a1", "2021-01-01T00:00:00Z") + legacy["targetType"], legacy["targetId"], legacy["eventType"] = kind, "99", "api.apiKey.update" + legacy["outcome"], legacy["httpStatus"] = "FAILED", 400 + if nested { + fields := make(map[string]any) + for _, field := range []string{"keyId", "targetType", "targetId", "outcome", "httpStatus"} { + fields[field] = legacy[field] + delete(legacy, field) + } + legacy["data"] = map[string]any{"fields": fields, "secretValue": "nested-must-not-leak"} + } + denied := keyAuditRecord("denied", "99", "1p1", "2022-01-01T00:00:00Z") + denied["targetType"], denied["targetId"], denied["outcome"], denied["httpStatus"] = kind, "99", "DENIED", 403 + missing := keyAuditRecord("missing-key", "", "1a1", "2023-01-01T00:00:00Z") + missing["resourceId"] = "1c99" // Resource IDs cannot substitute for missing Key attribution. + fixture.records = []map[string]any{ + keyAuditRecord("modern", "1c99", "1a1", "2020-01-01T00:00:00Z"), legacy, denied, + keyAuditRecord("foreign-key", "98", "1a1", "2023-01-01T00:00:00Z"), + keyAuditRecord("foreign-type", "1a99", "1a1", "2023-01-01T00:00:00Z"), + keyAuditRecord("hidden-project", "99", "1p9", "2023-01-01T00:00:00Z"), missing, + } + response := performAuditRequest(t, server, keyAuditQueryPath+"?keyId=1c99&timeScope=all&limit=1&offset=1&order=asc") + var payload auditCollection + err := json.NewDecoder(response.Body).Decode(&payload) + response.Body.Close() + if response.StatusCode != http.StatusOK || err != nil || payload.Pagination["total"] != float64(3) || len(payload.Data) != 1 || + auditString(payload.Data[0], "id") != "legacy" || auditString(payload.Data[0], "keyId") != "1c99" || + auditString(payload.Data[0], "targetId") != "1c99" { + t.Fatalf("historical count/pagination/canonical metadata lost: status=%d err=%v payload=%#v", response.StatusCode, err, payload) + } + detail := performAuditRequest(t, server, keyAuditQueryPath+"/legacy?keyId=1c99") + var record map[string]any + err = json.NewDecoder(detail.Body).Decode(&record) + detail.Body.Close() + if detail.StatusCode != http.StatusOK || err != nil || auditString(record, "keyId") != "1c99" || auditString(record, "targetId") != "1c99" { + t.Fatalf("historical detail unavailable: status=%d err=%v record=%#v", detail.StatusCode, err, record) + } + serialized, _ := json.Marshal(record) + for _, secret := range []string{"must-not-leak", "Bearer-secret", "session-secret", "jwt-secret", "code-secret", "never-return-key-secret"} { + if strings.Contains(string(serialized), secret) { + t.Fatal("historical detail exposed a secret") + } + } + filtered := performAuditRequest(t, server, keyAuditQueryPath+"?keyId=1c99&timeScope=all&outcome=DENIED") + err = json.NewDecoder(filtered.Body).Decode(&payload) + filtered.Body.Close() + if filtered.StatusCode != http.StatusOK || err != nil || payload.Pagination["total"] != float64(1) || len(payload.Data) != 1 || auditString(payload.Data[0], "id") != "denied" { + t.Fatalf("historical denial filter lost: status=%d err=%v payload=%#v", filtered.StatusCode, err, payload) + } + for _, id := range []string{"foreign-key", "foreign-type", "hidden-project", "missing-key"} { + hidden := performAuditRequest(t, server, keyAuditQueryPath+"/"+id+"?keyId=1c99") + hidden.Body.Close() + if hidden.StatusCode != http.StatusNotFound { + t.Fatalf("historical alias admitted unrelated event %s: %d", id, hidden.StatusCode) + } + } + }) + } + } +} + func TestKeyAuditFiltersBeforeCountAndPaginationAndAllTimeIncludesOldReads(t *testing.T) { fixture, server := newKeyAuditFixture(t) fixture.records = []map[string]any{ diff --git a/server/security/openvex.json b/server/security/openvex.json index e2a75fbba8..28391fe212 100644 --- a/server/security/openvex.json +++ b/server/security/openvex.json @@ -1,6 +1,6 @@ { "@context": "https://openvex.dev/ns/v0.2.0", - "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.520", + "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.521", "author": "PastureStack Security", "timestamp": "2026-10-02T22:05:00Z", "version": 51, diff --git a/server/security/vendor-pending.json b/server/security/vendor-pending.json index baaef78ef1..3008895889 100644 --- a/server/security/vendor-pending.json +++ b/server/security/vendor-pending.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "release": "v1.6.520", + "release": "v1.6.521", "generatedAt": "2026-10-10T00:00:00Z", "policy": { "allowedSeverities": [