From fc3900429cec5e342e57b098a8941a96acd88c79 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Mon, 17 Aug 2026 22:09:35 +0200 Subject: [PATCH] Security: IME log pattern PUT must be GlobalAdminOnly PUT rules/ime-log-patterns/{patternId} sat on the TenantAdminOrGA tier while the handler only checks ?global=true and writes the GLOBAL pattern with no GA re-gate - any customer tenant admin could mutate platform-wide patterns via direct API call. The UI gate (isGlobalAdmin && globalAdminMode) was cosmetic only. Moved the entry to GlobalAdminOnly next to the reseed route and added a regression test pinning both IME pattern write routes to that tier. Co-Authored-By: Claude Fable 5 --- .../EndpointPolicyCatalogCompletenessTests.cs | 17 +++++++++++++++++ .../Security/EndpointAccessPolicyCatalog.cs | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/EndpointPolicyCatalogCompletenessTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/EndpointPolicyCatalogCompletenessTests.cs index 0f18913e..67f24aac 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/EndpointPolicyCatalogCompletenessTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/EndpointPolicyCatalogCompletenessTests.cs @@ -174,6 +174,23 @@ public class EndpointPolicyCatalogCompletenessTests Assert.Equal(EndpointPolicy.TenantAdminOrGA, postRules.Policy); } + /// + /// IME log pattern edits write GLOBAL patterns (all tenants) and the handler has no GA + /// re-gate, so the PUT must stay GlobalAdminOnly — a downgrade to TenantAdminOrGA would let + /// any customer tenant admin mutate platform-wide patterns via ?global=true. + /// + [Fact] + public void ImeLogPatternWrites_AreGlobalAdminOnly() + { + var putPattern = EndpointAccessPolicyCatalog.FindPolicy("PUT", "/api/rules/ime-log-patterns/IME-001"); + var reseed = EndpointAccessPolicyCatalog.FindPolicy("POST", "/api/rules/ime-log-patterns/reseed"); + + Assert.NotNull(putPattern); + Assert.NotNull(reseed); + Assert.Equal(EndpointPolicy.GlobalAdminOnly, putPattern.Policy); + Assert.Equal(EndpointPolicy.GlobalAdminOnly, reseed.Policy); + } + /// /// Unregistered routes must return null (fail-closed). /// diff --git a/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs index 2630e84f..fb507068 100644 --- a/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs +++ b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs @@ -311,7 +311,6 @@ public static class EndpointAccessPolicyCatalog new("POST", "rules/analyze/{ruleId}/create-from-template", EndpointPolicy.TenantAdminOrGA), new("PUT", "rules/analyze/{ruleId}", EndpointPolicy.TenantAdminOrGA), new("DELETE", "rules/analyze/{ruleId}", EndpointPolicy.TenantAdminOrGA), - new("PUT", "rules/ime-log-patterns/{patternId}", EndpointPolicy.TenantAdminOrGA), new("POST", "sessions/{sessionId}/mark-failed", EndpointPolicy.TenantAdminOrGA), new("POST", "sessions/{sessionId}/mark-succeeded", EndpointPolicy.TenantAdminOrGA), // Operator tier so troubleshooting staff can queue request_diagnostics ("Collect Logs" in @@ -541,6 +540,9 @@ public static class EndpointAccessPolicyCatalog new("GET", "vulnerability/ignored-software", EndpointPolicy.GlobalReadOrAdmin), new("POST", "vulnerability/ignored-software", EndpointPolicy.GlobalAdminOnly), new("DELETE", "vulnerability/ignored-software", EndpointPolicy.GlobalAdminOnly), + // Both routes mutate GLOBAL patterns (all tenants); there is no tenant-scoped variant yet, + // so PUT must not sit on the TenantAdminOrGA tier — the handler has no GA re-gate. + new("PUT", "rules/ime-log-patterns/{patternId}", EndpointPolicy.GlobalAdminOnly), new("POST", "rules/ime-log-patterns/reseed", EndpointPolicy.GlobalAdminOnly), new("GET", "global/mcp-users", EndpointPolicy.GlobalReadOrAdmin), new("POST", "global/mcp-users", EndpointPolicy.GlobalAdminOnly),