From 3c3891a413d8629ab1c14dc5b34c4a2eee656603 Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Thu, 7 May 2026 22:36:54 +0100 Subject: [PATCH 01/22] CWCOW: Remove hostedSystemConfig Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/handlers.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index a51c3fd8f5..47535bc3f5 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -57,10 +57,9 @@ func (b *Bridge) createContainer(req *request) (err error) { return errors.Wrap(err, "failed to unmarshal createContainer") } - // containerConfig can be of type uvnConfig or hcsschema.HostedSystem or guestresource.CWCOWHostedSystem + // containerConfig can be of type uvmConfig or guestresource.CWCOWHostedSystem var ( uvmConfig prot.UvmConfig - hostedSystemConfig hcsschema.HostedSystem cwcowHostedSystemConfig guestresource.CWCOWHostedSystem ) if err = commonutils.UnmarshalJSONWithHresult(containerConfig, &uvmConfig); err == nil && @@ -68,11 +67,6 @@ func (b *Bridge) createContainer(req *request) (err error) { systemType := uvmConfig.SystemType timeZoneInformation := uvmConfig.TimeZoneInformation log.G(ctx).Tracef("createContainer: uvmConfig: {systemType: %v, timeZoneInformation: %v}}", systemType, timeZoneInformation) - } else if err = commonutils.UnmarshalJSONWithHresult(containerConfig, &hostedSystemConfig); err == nil && - hostedSystemConfig.SchemaVersion != nil && hostedSystemConfig.Container != nil { - schemaVersion := hostedSystemConfig.SchemaVersion - container := hostedSystemConfig.Container - log.G(ctx).Tracef("rpcCreate: HostedSystemConfig: {schemaVersion: %v, container: %v}}", schemaVersion, container) } else if err = commonutils.UnmarshalJSONWithHresult(containerConfig, &cwcowHostedSystemConfig); err == nil && cwcowHostedSystemConfig.Spec.Version != "" && cwcowHostedSystemConfig.CWCOWHostedSystem.Container != nil { cwcowHostedSystem := cwcowHostedSystemConfig.CWCOWHostedSystem From 5a6f3f8c425e70676e826d4e0279c8cbf81ece5e Mon Sep 17 00:00:00 2001 From: Mahati Chamarthy Date: Thu, 7 May 2026 23:01:03 +0100 Subject: [PATCH 02/22] CWCOW: Add logging enforcement policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds guest-side enforcement: each requested ETW provider is validated against allowed_log_providers declared in the security policy (case-insensitive match). Providers not in the allowlist are silently dropped. The filtered config is re-encoded and forwarded to GCS with GUIDs resolved. Also removes the hostedSystemConfig dead code path from createContainer — the sidecar only runs for confidential containers, which always use CWCOWHostedSystem. Signed-off-by: Mahati Chamarthy --- internal/gcs-sidecar/handlers.go | 47 ++++++-- internal/vm/vmutils/etw/provider_map.go | 12 +- pkg/securitypolicy/api.rego | 1 + pkg/securitypolicy/framework.rego | 13 +++ pkg/securitypolicy/open_door.rego | 1 + pkg/securitypolicy/policy.rego | 1 + pkg/securitypolicy/regopolicy_windows_test.go | 110 ++++++++++++++++++ pkg/securitypolicy/securitypolicyenforcer.go | 9 ++ .../securitypolicyenforcer_rego.go | 8 ++ 9 files changed, 184 insertions(+), 18 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 47535bc3f5..ea8615624d 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -545,21 +545,44 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { switch settings.RPCType { case guestrequest.RPCModifyServiceSettings, guestrequest.RPCStartLogForwarding, guestrequest.RPCStopLogForwarding: log.G(req.ctx).Tracef("%v request received for LogForwardService, proceeding with policy enforcement for log sources", settings.RPCType) - // Enforce the policy for log sources in the request and update the settings with allowed log sources. - // For cwcow, the sidecar-GCS will verify the allowed log sources against policy and append the necessary GUIDs to the ones allowed. Rest are dropped. - // The Enforcer will have to unmarshal the log sources, enforce the policy and then marshal it back to a Base64 encoded JSON string which is what inbox GCS expects. - // It can query etw.GetDefaultLogSources to get the default log sources if the policy allows, and allow providers matching the default list during policy enforcement. - // This is because the log sources can be a combination of default and user specified log sources for which GUIDs need to be appended based on the policy enforcement. if settings.Settings != "" { - // - // allowedLogSources, err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogForwardServiceSettingsPolicy(req.ctx, settings.LogSources) + // Decode the base64-encoded log sources config + logSources, err := etw.DecodeAndUnmarshalLogSources(settings.Settings) + if err != nil { + return fmt.Errorf("failed to decode log sources: %w", err) + } + + // Filter providers against policy — keep only those allowed + var filteredSources []etw.Source + for _, source := range logSources.LogConfig.Sources { + var allowedProviders []etw.EtwProvider + for _, provider := range source.Providers { + if err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogProviderPolicy( + req.ctx, provider.ProviderName); err != nil { + log.G(req.ctx).Tracef("Log provider %q denied by policy", provider.ProviderName) + continue + } + allowedProviders = append(allowedProviders, provider) + } + if len(allowedProviders) > 0 { + filteredSources = append(filteredSources, etw.Source{ + Type: source.Type, + Providers: allowedProviders, + }) + } + } + + filteredLogSources := etw.LogSourcesInfo{ + LogConfig: etw.LogConfig{Sources: filteredSources}, + } - // For now, we are skipping the policy enforcement and allowing all log sources as the policy enforcer implementation is in progress. We will add the enforcement back once it's implemented. - allowedLogSources := settings.Settings // This is Base64 encoded JSON string of log sources - log.G(req.ctx).Tracef("Allowed log sources after policy enforcement: %v", allowedLogSources) + // Re-encode and apply GUID resolution + encodedFiltered, err := etw.MarshalAndEncodeLogSources(filteredLogSources) + if err != nil { + return fmt.Errorf("failed to encode filtered log sources: %w", err) + } - // Update the allowed log sources in the settings. This will be forwarded to inbox GCS which expects the log sources in a JSON string format with GUIDs for providers included. - allowedLogSources, err := etw.UpdateLogSources(allowedLogSources, false, true) + allowedLogSources, err := etw.UpdateLogSources(encodedFiltered, false, true) if err != nil { return fmt.Errorf("failed to update log sources: %w", err) } diff --git a/internal/vm/vmutils/etw/provider_map.go b/internal/vm/vmutils/etw/provider_map.go index 5b35206602..83ac060d8a 100644 --- a/internal/vm/vmutils/etw/provider_map.go +++ b/internal/vm/vmutils/etw/provider_map.go @@ -92,8 +92,8 @@ func mergeLogSources(resultSources []Source, userSources []Source) []Source { return resultSources } -// decodeAndUnmarshalLogSources decodes a base64-encoded JSON string and unmarshals it into a LogSourcesInfo. -func decodeAndUnmarshalLogSources(base64EncodedJSONLogConfig string) (LogSourcesInfo, error) { +// DecodeAndUnmarshalLogSources decodes a base64-encoded JSON string and unmarshals it into a LogSourcesInfo. +func DecodeAndUnmarshalLogSources(base64EncodedJSONLogConfig string) (LogSourcesInfo, error) { jsonBytes, err := base64.StdEncoding.DecodeString(base64EncodedJSONLogConfig) if err != nil { return LogSourcesInfo{}, fmt.Errorf("error decoding base64 log config: %w", err) @@ -174,9 +174,9 @@ func applyGUIDPolicy(sources []Source, includeGUIDs bool) ([]Source, error) { return stripRedundantGUIDs(sources) } -// marshalAndEncodeLogSources marshals the given LogSourcesInfo to JSON and encodes it as a base64 string. +// MarshalAndEncodeLogSources marshals the given LogSourcesInfo to JSON and encodes it as a base64 string. // On error, it logs and returns the original fallback string. -func marshalAndEncodeLogSources(logCfg LogSourcesInfo) (string, error) { +func MarshalAndEncodeLogSources(logCfg LogSourcesInfo) (string, error) { jsonBytes, err := json.Marshal(logCfg) if err != nil { return "", fmt.Errorf("error marshalling log config: %w", err) @@ -194,7 +194,7 @@ func UpdateLogSources(base64EncodedJSONLogConfig string, useDefaultLogSources bo } if base64EncodedJSONLogConfig != "" { - userLogSources, err := decodeAndUnmarshalLogSources(base64EncodedJSONLogConfig) + userLogSources, err := DecodeAndUnmarshalLogSources(base64EncodedJSONLogConfig) if err != nil { return "", fmt.Errorf("failed to decode and unmarshal user log sources: %w", err) } @@ -208,7 +208,7 @@ func UpdateLogSources(base64EncodedJSONLogConfig string, useDefaultLogSources bo return "", fmt.Errorf("failed to apply GUID policy: %w", err) } - result, err := marshalAndEncodeLogSources(resultLogCfg) + result, err := MarshalAndEncodeLogSources(resultLogCfg) if err != nil { return "", fmt.Errorf("failed to marshal and encode log sources: %w", err) } diff --git a/pkg/securitypolicy/api.rego b/pkg/securitypolicy/api.rego index 88c3d64d14..539f680f12 100644 --- a/pkg/securitypolicy/api.rego +++ b/pkg/securitypolicy/api.rego @@ -24,4 +24,5 @@ enforcement_points := { "load_fragment": {"introducedVersion": "0.9.0", "default_results": {"allowed": false, "add_module": false}, "use_framework": false}, "scratch_mount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false}, "scratch_unmount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false}, + "log_provider": {"introducedVersion": "0.11.0", "default_results": {"allowed": true}, "use_framework": false}, } diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index daa0fe864e..9450698d9b 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -1299,6 +1299,14 @@ scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} { } } +# Log provider validation for Windows containers +default log_provider := {"allowed": false} + +log_provider := {"allowed": true} { + some allowed_provider in data.policy.allowed_log_providers + lower(input.providerName) == lower(allowed_provider) +} + # Registry changes validation default registry_changes := {"allowed": false} @@ -1827,6 +1835,11 @@ errors["no scratch at path to unmount"] { not scratch_mounted(input.unmountTarget) } +errors["log provider not allowed by policy"] { + input.rule == "log_provider" + not log_provider.allowed +} + errors[framework_version_error] { policy_framework_version == null framework_version_error := concat(" ", ["framework_version is missing. Current version:", version]) diff --git a/pkg/securitypolicy/open_door.rego b/pkg/securitypolicy/open_door.rego index 02da3fa9b6..fa277d9e7e 100644 --- a/pkg/securitypolicy/open_door.rego +++ b/pkg/securitypolicy/open_door.rego @@ -23,3 +23,4 @@ runtime_logging := {"allowed": true} load_fragment := {"allowed": true} scratch_mount := {"allowed": true} scratch_unmount := {"allowed": true} +log_provider := {"allowed": true} diff --git a/pkg/securitypolicy/policy.rego b/pkg/securitypolicy/policy.rego index 195d462931..2702075305 100644 --- a/pkg/securitypolicy/policy.rego +++ b/pkg/securitypolicy/policy.rego @@ -26,4 +26,5 @@ runtime_logging := data.framework.runtime_logging load_fragment := data.framework.load_fragment scratch_mount := data.framework.scratch_mount scratch_unmount := data.framework.scratch_unmount +log_provider := data.framework.log_provider reason := data.framework.reason diff --git a/pkg/securitypolicy/regopolicy_windows_test.go b/pkg/securitypolicy/regopolicy_windows_test.go index 33b49a64f8..629f1b0773 100644 --- a/pkg/securitypolicy/regopolicy_windows_test.go +++ b/pkg/securitypolicy/regopolicy_windows_test.go @@ -1514,3 +1514,113 @@ func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { _ = sandboxID return m } + +// Tests for log provider enforcement + +func Test_Rego_EnforceLogProviderPolicy_Allowed_Windows(t *testing.T) { + rego := fmt.Sprintf(`package policy + api_version := "%s" + framework_version := "%s" + + allowed_log_providers := [ + "microsoft.windows.hyperv.compute", + "microsoft-windows-guest-network-service", + ] + + log_provider := data.framework.log_provider + `, apiVersion, frameworkVersion) + + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { + t.Fatalf("failed to create policy: %v", err) + } + + ctx := context.Background() + err = policy.EnforceLogProviderPolicy(ctx, "microsoft.windows.hyperv.compute") + if err != nil { + t.Errorf("expected allowed provider to pass: %v", err) + } +} + +func Test_Rego_EnforceLogProviderPolicy_Denied_Windows(t *testing.T) { + rego := fmt.Sprintf(`package policy + api_version := "%s" + framework_version := "%s" + + allowed_log_providers := [ + "microsoft.windows.hyperv.compute", + ] + + log_provider := data.framework.log_provider + `, apiVersion, frameworkVersion) + + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { + t.Fatalf("failed to create policy: %v", err) + } + + ctx := context.Background() + err = policy.EnforceLogProviderPolicy(ctx, "some-malicious-provider") + if err == nil { + t.Errorf("expected unknown provider to be denied") + } +} + +func Test_Rego_EnforceLogProviderPolicy_CaseInsensitive_Windows(t *testing.T) { + rego := fmt.Sprintf(`package policy + api_version := "%s" + framework_version := "%s" + + allowed_log_providers := [ + "microsoft.windows.hyperv.compute", + ] + + log_provider := data.framework.log_provider + `, apiVersion, frameworkVersion) + + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { + t.Fatalf("failed to create policy: %v", err) + } + + ctx := context.Background() + err = policy.EnforceLogProviderPolicy(ctx, "Microsoft.Windows.Hyperv.Compute") + if err != nil { + t.Errorf("expected case-insensitive match to pass: %v", err) + } +} + +func Test_Rego_EnforceLogProviderPolicy_OpenDoor_AllowsAll_Windows(t *testing.T) { + policy, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { + t.Fatalf("failed to create policy: %v", err) + } + + ctx := context.Background() + err = policy.EnforceLogProviderPolicy(ctx, "any-provider-at-all") + if err != nil { + t.Errorf("open door should allow any provider: %v", err) + } +} + +func Test_Rego_EnforceLogProviderPolicy_EmptyAllowList_DeniesAll_Windows(t *testing.T) { + rego := fmt.Sprintf(`package policy + api_version := "%s" + framework_version := "%s" + + allowed_log_providers := [] + + log_provider := data.framework.log_provider + `, apiVersion, frameworkVersion) + + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { + t.Fatalf("failed to create policy: %v", err) + } + + ctx := context.Background() + err = policy.EnforceLogProviderPolicy(ctx, "microsoft.windows.hyperv.compute") + if err == nil { + t.Errorf("expected empty allow list to deny all providers") + } +} diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 2a4edefce1..63a3b9e92f 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -128,6 +128,7 @@ type SecurityPolicyEnforcer interface { GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string, mountedCim []string) (err error) EnforceRegistryChangesPolicy(ctx context.Context, containerID string, registryValues interface{}) error + EnforceLogProviderPolicy(ctx context.Context, providerName string) error } //nolint:unused @@ -324,6 +325,10 @@ func (OpenDoorSecurityPolicyEnforcer) EnforceRegistryChangesPolicy(ctx context.C return nil } +func (OpenDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(context.Context, string) error { + return nil +} + type ClosedDoorSecurityPolicyEnforcer struct{} var _ SecurityPolicyEnforcer = (*ClosedDoorSecurityPolicyEnforcer)(nil) @@ -452,3 +457,7 @@ func (ClosedDoorSecurityPolicyEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Co func (ClosedDoorSecurityPolicyEnforcer) EnforceRegistryChangesPolicy(ctx context.Context, containerID string, registryValues interface{}) error { return errors.New("registry changes are denied by policy") } + +func (ClosedDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(context.Context, string) error { + return errors.New("log provider is denied by policy") +} diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index 96c5613dd6..718e0044d9 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -1188,6 +1188,14 @@ func (policy *regoEnforcer) EnforceRegistryChangesPolicy(ctx context.Context, co return err } +func (policy *regoEnforcer) EnforceLogProviderPolicy(ctx context.Context, providerName string) error { + input := inputData{ + "providerName": providerName, + } + _, err := policy.enforce(ctx, "log_provider", input) + return err +} + func (policy *regoEnforcer) GetUserInfo(process *oci.Process, rootPath string) (IDName, []IDName, string, error) { return GetAllUserInfo(process, rootPath) } From 9a0d5a1a596ffd384c5767ea23c98ade75a62f8b Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 1 Jun 2026 10:22:41 +0100 Subject: [PATCH 03/22] CWCOW: Enforce log_provider policy with allow_log_provider_dropping switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tightens log_provider enforcement so that, by default, the sidecar fails-close on any disallowed provider and locks down further policy calls (LockDown now installs the closed enforcer so SetConfidentialOptions cannot reinstall a permissive policy). The previous silent-drop behaviour is preserved behind a new allow_log_provider_dropping policy flag — when true, providers missing from allowed_log_providers are dropped and only the kept subset is re-encoded and forwarded to GCS. Threads the flag through the rego framework, marshaller, and Go enforcer (EnforceLogProviderPolicy now returns the providers to keep), and adds rego + sidecar tests covering both fail-close and dropping modes. Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 74 +++++-- internal/gcs-sidecar/handlers_test.go | 195 +++++++++++++++++ internal/tools/securitypolicy/main.go | 1 + internal/uvm/start.go | 17 ++ internal/vm/vmutils/etw/provider_map.go | 6 +- pkg/securitypolicy/api.rego | 2 +- pkg/securitypolicy/framework.rego | 44 +++- pkg/securitypolicy/opts.go | 7 + pkg/securitypolicy/rego_utils_test.go | 6 + pkg/securitypolicy/regopolicy_linux_test.go | 1 + pkg/securitypolicy/regopolicy_windows_test.go | 199 +++++++++++++----- pkg/securitypolicy/securitypolicy.go | 6 + pkg/securitypolicy/securitypolicy_internal.go | 4 + pkg/securitypolicy/securitypolicy_marshal.go | 17 +- pkg/securitypolicy/securitypolicy_options.go | 22 ++ .../securitypolicy_options_test.go | 86 ++++++++ pkg/securitypolicy/securitypolicyenforcer.go | 17 +- .../securitypolicyenforcer_rego.go | 43 +++- test/pkg/securitypolicy/policy.go | 1 + 19 files changed, 655 insertions(+), 93 deletions(-) create mode 100644 pkg/securitypolicy/securitypolicy_options_test.go diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index ea8615624d..0165bc71e0 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -4,6 +4,7 @@ package bridge import ( + "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -546,43 +547,70 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { case guestrequest.RPCModifyServiceSettings, guestrequest.RPCStartLogForwarding, guestrequest.RPCStopLogForwarding: log.G(req.ctx).Tracef("%v request received for LogForwardService, proceeding with policy enforcement for log sources", settings.RPCType) if settings.Settings != "" { - // Decode the base64-encoded log sources config + // Decode the base64-encoded log sources config so we can + // enforce policy on the requested provider list. logSources, err := etw.DecodeAndUnmarshalLogSources(settings.Settings) if err != nil { return fmt.Errorf("failed to decode log sources: %w", err) } - // Filter providers against policy — keep only those allowed - var filteredSources []etw.Source + // Collect every requested provider name and ask the + // enforcer to validate them as a batch. The enforcer's + // behaviour depends on allow_log_provider_dropping in the + // active policy: + // - false (default, fail-close): any disallowed provider + // causes the call to be denied. + // - true: disallowed providers are silently dropped and + // the kept subset is returned for forwarding. + var requestedNames []string for _, source := range logSources.LogConfig.Sources { - var allowedProviders []etw.EtwProvider for _, provider := range source.Providers { - if err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogProviderPolicy( - req.ctx, provider.ProviderName); err != nil { - log.G(req.ctx).Tracef("Log provider %q denied by policy", provider.ProviderName) - continue - } - allowedProviders = append(allowedProviders, provider) - } - if len(allowedProviders) > 0 { - filteredSources = append(filteredSources, etw.Source{ - Type: source.Type, - Providers: allowedProviders, - }) + requestedNames = append(requestedNames, provider.ProviderName) } } - filteredLogSources := etw.LogSourcesInfo{ - LogConfig: etw.LogConfig{Sources: filteredSources}, + keptNames, err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogProviderPolicy( + req.ctx, requestedNames) + if err != nil { + b.hostState.securityOptions.LockDown(req.ctx) + return fmt.Errorf("log providers denied by policy: %w", err) } - // Re-encode and apply GUID resolution - encodedFiltered, err := etw.MarshalAndEncodeLogSources(filteredLogSources) - if err != nil { - return fmt.Errorf("failed to encode filtered log sources: %w", err) + // Build a quick lookup for the kept set so we can trim the + // LogSourcesInfo to only those providers the policy allowed. + keepSet := make(map[string]struct{}, len(keptNames)) + for _, name := range keptNames { + keepSet[name] = struct{}{} + } + + payload := settings.Settings + if len(keptNames) != len(requestedNames) { + // Subset kept. Trim each source's provider list to + // only the allowed names, then re-encode for the + // inbox GCS. Empty sources are preserved to keep the + // shape stable; inbox GCS handles them as no-ops. + trimmed := logSources + for i := range trimmed.LogConfig.Sources { + src := &trimmed.LogConfig.Sources[i] + filtered := make([]etw.EtwProvider, 0, len(src.Providers)) + for _, p := range src.Providers { + if _, ok := keepSet[p.ProviderName]; ok { + filtered = append(filtered, p) + } + } + src.Providers = filtered + } + trimmedJSON, err := json.Marshal(trimmed) + if err != nil { + return fmt.Errorf("failed to marshal trimmed log sources: %w", err) + } + payload = base64.StdEncoding.EncodeToString(trimmedJSON) } - allowedLogSources, err := etw.UpdateLogSources(encodedFiltered, false, true) + // Apply GUID resolution (and any other inbox-GCS prep) + // against the policy-trimmed payload and hand off to + // inbox GCS. + allowedLogSources, err := etw.UpdateLogSources(payload, false, true) if err != nil { return fmt.Errorf("failed to update log sources: %w", err) } diff --git a/internal/gcs-sidecar/handlers_test.go b/internal/gcs-sidecar/handlers_test.go index 6de3a0a605..843d062f0c 100644 --- a/internal/gcs-sidecar/handlers_test.go +++ b/internal/gcs-sidecar/handlers_test.go @@ -5,6 +5,7 @@ package bridge import ( "context" + "encoding/base64" "encoding/json" "io" "testing" @@ -14,6 +15,7 @@ import ( "github.com/Microsoft/hcsshim/internal/gcs/prot" "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" + "github.com/Microsoft/hcsshim/internal/vm/vmutils/etw" "github.com/Microsoft/hcsshim/pkg/securitypolicy" ) @@ -327,3 +329,196 @@ func TestModifySettings_PolicyFragment_TypeAssertionFailure(t *testing.T) { t.Fatal("expected error for empty fragment, got nil") } } + +// buildLogForwardServiceRequest builds a serialized ServiceModificationRequest +// for the LogForwardService with the given provider names baked into a +// base64-encoded LogSourcesInfo payload. +func buildLogForwardServiceRequest(t *testing.T, providerNames ...string) []byte { + t.Helper() + + providers := make([]etw.EtwProvider, 0, len(providerNames)) + for _, name := range providerNames { + providers = append(providers, etw.EtwProvider{ProviderName: name}) + } + info := etw.LogSourcesInfo{ + LogConfig: etw.LogConfig{ + Sources: []etw.Source{{ + Type: "etw", + Providers: providers, + }}, + }, + } + infoBytes, err := json.Marshal(info) + if err != nil { + t.Fatalf("failed to marshal log sources: %v", err) + } + encoded := base64.StdEncoding.EncodeToString(infoBytes) + + inner := &guestrequest.LogForwardServiceRPCRequest{ + RPCType: guestrequest.RPCModifyServiceSettings, + Settings: encoded, + } + req := prot.ServiceModificationRequest{ + RequestBase: prot.RequestBase{ + ContainerID: UVMContainerID, + ActivityID: guid.GUID{}, + }, + PropertyType: string(prot.LogForwardService), + Settings: inner, + } + b, err := json.Marshal(req) + if err != nil { + t.Fatalf("failed to marshal request: %v", err) + } + return b +} + +// newModifyServiceSettingsRequest wraps the given LogForwardService payload +// in a bridge `request` ready for modifyServiceSettings. +func newModifyServiceSettingsRequest(payload []byte) *request { + return &request{ + ctx: context.Background(), + header: messageHeader{ + Type: prot.MsgTypeRequest | prot.MsgType(prot.RPCModifyServiceSettings), + Size: uint32(len(payload)) + prot.HdrSize, + ID: 1, + }, + activityID: guid.GUID{}, + message: payload, + } +} + +// TestModifyServiceSettings_LogForward_PolicyAllow_ForwardsToGCS verifies that +// when every requested provider is allowed by policy, the call succeeds and +// the (possibly GUID-resolved) request is forwarded to inbox GCS. +func TestModifyServiceSettings_LogForward_PolicyAllow_ForwardsToGCS(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + // Use a provider that is in the known etw_map so UpdateLogSources's GUID + // resolution succeeds. + payload := buildLogForwardServiceRequest(t, "microsoft.windows.hyperv.compute") + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err != nil { + t.Fatalf("modifyServiceSettings with allowed provider returned error: %v", err) + } + + select { + case <-b.sendToGCSCh: + // Forwarded to GCS as expected. + case <-time.After(time.Second): + t.Fatal("timed out waiting for request to be forwarded to GCS") + } +} + +// TestModifyServiceSettings_LogForward_PolicyDeny_ReturnsErrorAndLocksDown +// verifies that when any requested provider is denied by policy, the call +// fails (no forward to GCS) and the sidecar enforcer is locked down to +// closed-door so subsequent requests also deny. +func TestModifyServiceSettings_LogForward_PolicyDeny_ReturnsErrorAndLocksDown(t *testing.T) { + b := newTestBridge(&securitypolicy.ClosedDoorSecurityPolicyEnforcer{}) + + payload := buildLogForwardServiceRequest(t, "microsoft.windows.hyperv.compute") + req := newModifyServiceSettingsRequest(payload) + + err := b.modifyServiceSettings(req) + if err == nil { + t.Fatal("expected modifyServiceSettings to fail under ClosedDoor enforcer") + } + + // The request must NOT have been forwarded to GCS. + select { + case fwd := <-b.sendToGCSCh: + t.Fatalf("denied request must not be forwarded to GCS: %+v", fwd) + default: + // Good. + } + + // Enforcer should now be locked down. (ClosedDoor was already installed; + // LockDown's idempotency check keeps the same instance, so we verify the + // type rather than identity.) + if _, ok := b.hostState.securityOptions.PolicyEnforcer.(*securitypolicy.ClosedDoorSecurityPolicyEnforcer); !ok { + t.Errorf("after deny: expected ClosedDoor enforcer, got %T", b.hostState.securityOptions.PolicyEnforcer) + } +} + +// droppingLogProviderEnforcer is a test stub that approves only the configured +// allow-list of provider names; any others are silently dropped from the +// returned subset. It mirrors the regoEnforcer's behaviour under +// allow_log_provider_dropping := true and never returns an error. +type droppingLogProviderEnforcer struct { + securitypolicy.OpenDoorSecurityPolicyEnforcer + allowed map[string]struct{} +} + +func (e *droppingLogProviderEnforcer) EnforceLogProviderPolicy(_ context.Context, providerNames []string) ([]string, error) { + kept := make([]string, 0, len(providerNames)) + for _, name := range providerNames { + if _, ok := e.allowed[name]; ok { + kept = append(kept, name) + } + } + return kept, nil +} + +// TestModifyServiceSettings_LogForward_PolicyDropping_TrimsForwardedPayload +// verifies the silent-drop path in the sidecar: when the enforcer returns a +// strict subset of the requested providers, the call succeeds and the payload +// forwarded to inbox GCS contains only the kept providers (not the original +// disallowed ones). +func TestModifyServiceSettings_LogForward_PolicyDropping_TrimsForwardedPayload(t *testing.T) { + kept := "microsoft.windows.hyperv.compute" + dropped := "some-bogus-provider" + enforcer := &droppingLogProviderEnforcer{ + allowed: map[string]struct{}{kept: {}}, + } + b := newTestBridge(enforcer) + + payload := buildLogForwardServiceRequest(t, kept, dropped) + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err != nil { + t.Fatalf("modifyServiceSettings under dropping enforcer returned error: %v", err) + } + + var forwarded request + select { + case forwarded = <-b.sendToGCSCh: + case <-time.After(time.Second): + t.Fatal("timed out waiting for request to be forwarded to GCS") + } + + // Decode the forwarded request back into LogSourcesInfo and confirm the + // disallowed provider has been stripped while the allowed one survives. + var fwdReq prot.ServiceModificationRequest + fwdReq.Settings = &guestrequest.LogForwardServiceRPCRequest{} + if err := json.Unmarshal(forwarded.message, &fwdReq); err != nil { + t.Fatalf("failed to unmarshal forwarded request: %v", err) + } + innerSettings, ok := fwdReq.Settings.(*guestrequest.LogForwardServiceRPCRequest) + if !ok { + t.Fatalf("forwarded settings has unexpected type: %T", fwdReq.Settings) + } + logSources, err := etw.DecodeAndUnmarshalLogSources(innerSettings.Settings) + if err != nil { + t.Fatalf("failed to decode forwarded log sources: %v", err) + } + + var sawKept, sawDropped bool + for _, src := range logSources.LogConfig.Sources { + for _, p := range src.Providers { + if p.ProviderName == kept { + sawKept = true + } + if p.ProviderName == dropped { + sawDropped = true + } + } + } + if !sawKept { + t.Errorf("expected forwarded payload to contain kept provider %q", kept) + } + if sawDropped { + t.Errorf("expected dropped provider %q to be absent from forwarded payload", dropped) + } +} diff --git a/internal/tools/securitypolicy/main.go b/internal/tools/securitypolicy/main.go index d5ad89e28b..be1860959d 100644 --- a/internal/tools/securitypolicy/main.go +++ b/internal/tools/securitypolicy/main.go @@ -68,6 +68,7 @@ func main() { config.AllowEnvironmentVariableDropping, config.AllowUnencryptedScratch, config.AllowCapabilityDropping, + config.AllowLogProviderDropping, ) } if err != nil { diff --git a/internal/uvm/start.go b/internal/uvm/start.go index e429fcc4a0..5d59883cdb 100644 --- a/internal/uvm/start.go +++ b/internal/uvm/start.go @@ -289,11 +289,28 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { if uvm.OS() == "windows" && uvm.logForwardingEnabled { // If the UVM is Windows and log forwarding is enabled, set the log sources // and start the log forwarding service. + // + // For confidential (CWCOW) UVMs, a failure here may be a policy + // violation (e.g. log_provider denied by the rego). In that case we + // must fail-close: return the error so the deferred + // uvm.hcsSystem.Terminate above tears the UVM down rather than + // leaving it half-initialised with a known-violating log + // configuration. For non-confidential WCOW, log-forwarding is + // best-effort (transient ETW issues, missing providers, etc. must + // not abort UVM start), so preserve the original log-and-continue + // behaviour. + failClose := uvm.HasConfidentialPolicy() if err := uvm.SetLogSources(ctx); err != nil { e.WithError(err).Error("failed to set log sources") + if failClose { + return fmt.Errorf("failed to set log sources: %w", err) + } } if err := uvm.StartLogForwarding(ctx); err != nil { e.WithError(err).Error("failed to start log forwarding") + if failClose { + return fmt.Errorf("failed to start log forwarding: %w", err) + } } } diff --git a/internal/vm/vmutils/etw/provider_map.go b/internal/vm/vmutils/etw/provider_map.go index 83ac060d8a..962d0c586a 100644 --- a/internal/vm/vmutils/etw/provider_map.go +++ b/internal/vm/vmutils/etw/provider_map.go @@ -174,9 +174,9 @@ func applyGUIDPolicy(sources []Source, includeGUIDs bool) ([]Source, error) { return stripRedundantGUIDs(sources) } -// MarshalAndEncodeLogSources marshals the given LogSourcesInfo to JSON and encodes it as a base64 string. +// marshalAndEncodeLogSources marshals the given LogSourcesInfo to JSON and encodes it as a base64 string. // On error, it logs and returns the original fallback string. -func MarshalAndEncodeLogSources(logCfg LogSourcesInfo) (string, error) { +func marshalAndEncodeLogSources(logCfg LogSourcesInfo) (string, error) { jsonBytes, err := json.Marshal(logCfg) if err != nil { return "", fmt.Errorf("error marshalling log config: %w", err) @@ -208,7 +208,7 @@ func UpdateLogSources(base64EncodedJSONLogConfig string, useDefaultLogSources bo return "", fmt.Errorf("failed to apply GUID policy: %w", err) } - result, err := MarshalAndEncodeLogSources(resultLogCfg) + result, err := marshalAndEncodeLogSources(resultLogCfg) if err != nil { return "", fmt.Errorf("failed to marshal and encode log sources: %w", err) } diff --git a/pkg/securitypolicy/api.rego b/pkg/securitypolicy/api.rego index 539f680f12..9474c75bce 100644 --- a/pkg/securitypolicy/api.rego +++ b/pkg/securitypolicy/api.rego @@ -24,5 +24,5 @@ enforcement_points := { "load_fragment": {"introducedVersion": "0.9.0", "default_results": {"allowed": false, "add_module": false}, "use_framework": false}, "scratch_mount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false}, "scratch_unmount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false}, - "log_provider": {"introducedVersion": "0.11.0", "default_results": {"allowed": true}, "use_framework": false}, + "log_provider": {"introducedVersion": "0.11.0", "default_results": {"allowed": true, "providers_to_keep": null}, "use_framework": false}, } diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index 9450698d9b..3c204e3594 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -1299,12 +1299,45 @@ scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} { } } -# Log provider validation for Windows containers -default log_provider := {"allowed": false} +# Log provider validation for Windows containers. +# +# Two modes (mirrors allow_environment_variable_dropping): +# - allow_log_provider_dropping := false (default, fail-close): every +# requested provider name must appear in allowed_log_providers, otherwise +# the rule denies the entire request. +# - allow_log_provider_dropping := true: providers not in the allow-list are +# silently dropped from providers_to_keep; the call still allows and the +# caller is expected to only forward the remaining providers. +# +# Input: {"providers": [name, ...]} +# Output: {"allowed": bool, "providers_to_keep": [name, ...]} +default log_provider := {"allowed": false, "providers_to_keep": []} + +valid_log_providers := providers { + allow_log_provider_dropping + + providers := [name | + name := input.providers[_] + some allowed_provider in data.policy.allowed_log_providers + lower(name) == lower(allowed_provider) + ] +} + +valid_log_providers := providers { + not allow_log_provider_dropping + providers := input.providers +} + +log_providers_ok(providers) { + every name in providers { + some allowed_provider in data.policy.allowed_log_providers + lower(name) == lower(allowed_provider) + } +} -log_provider := {"allowed": true} { - some allowed_provider in data.policy.allowed_log_providers - lower(input.providerName) == lower(allowed_provider) +log_provider := {"allowed": true, "providers_to_keep": providers} { + providers := valid_log_providers + log_providers_ok(providers) } # Registry changes validation @@ -2320,6 +2353,7 @@ allow_dump_stacks := data.policy.allow_dump_stacks allow_runtime_logging := data.policy.allow_runtime_logging allow_environment_variable_dropping := data.policy.allow_environment_variable_dropping allow_unencrypted_scratch := data.policy.allow_unencrypted_scratch +allow_log_provider_dropping := data.policy.allow_log_provider_dropping # all flags not in the base set need to have default logic applied diff --git a/pkg/securitypolicy/opts.go b/pkg/securitypolicy/opts.go index a11685abc4..9446f9dd30 100644 --- a/pkg/securitypolicy/opts.go +++ b/pkg/securitypolicy/opts.go @@ -115,6 +115,13 @@ func WithAllowEnvVarDropping(allow bool) PolicyConfigOpt { } } +func WithAllowLogProviderDropping(allow bool) PolicyConfigOpt { + return func(config *PolicyConfig) error { + config.AllowLogProviderDropping = allow + return nil + } +} + func WithAllowCapabilityDropping(allow bool) PolicyConfigOpt { return func(config *PolicyConfig) error { config.AllowCapabilityDropping = allow diff --git a/pkg/securitypolicy/rego_utils_test.go b/pkg/securitypolicy/rego_utils_test.go index 2967b5a6b7..1e78278f9b 100644 --- a/pkg/securitypolicy/rego_utils_test.go +++ b/pkg/securitypolicy/rego_utils_test.go @@ -2009,6 +2009,7 @@ func (constraints *generatedConstraints) toPolicy() *securityPolicyInternal { AllowEnvironmentVariableDropping: constraints.allowEnvironmentVariableDropping, AllowUnencryptedScratch: constraints.allowUnencryptedScratch, AllowCapabilityDropping: constraints.allowCapabilityDropping, + AllowLogProviderDropping: constraints.allowLogProviderDropping, } } @@ -2270,6 +2271,7 @@ func generateConstraints(r *rand.Rand, maxContainers int32) *generatedConstraint namespace: generateFragmentNamespace(testRand), svn: generateSVN(testRand), allowCapabilityDropping: false, + allowLogProviderDropping: false, ctx: context.Background(), } } @@ -2909,6 +2911,7 @@ type generatedConstraints struct { namespace string svn string allowCapabilityDropping bool + allowLogProviderDropping bool ctx context.Context } @@ -2924,6 +2927,7 @@ type generatedWindowsConstraints struct { namespace string svn string allowCapabilityDropping bool + allowLogProviderDropping bool ctx context.Context } @@ -2938,6 +2942,7 @@ func (constraints *generatedWindowsConstraints) toPolicy() *securityPolicyWindow AllowEnvironmentVariableDropping: constraints.allowEnvironmentVariableDropping, AllowUnencryptedScratch: constraints.allowUnencryptedScratch, AllowCapabilityDropping: constraints.allowCapabilityDropping, + AllowLogProviderDropping: constraints.allowLogProviderDropping, } } @@ -2982,6 +2987,7 @@ func generateWindowsConstraints(r *rand.Rand, maxContainers int32) *generatedWin allowEnvironmentVariableDropping: false, allowUnencryptedScratch: false, allowCapabilityDropping: false, + allowLogProviderDropping: false, namespace: generateFragmentNamespace(r), svn: generateSVN(r), ctx: context.Background(), diff --git a/pkg/securitypolicy/regopolicy_linux_test.go b/pkg/securitypolicy/regopolicy_linux_test.go index 8dd409fccf..f40e6ffd3d 100644 --- a/pkg/securitypolicy/regopolicy_linux_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -74,6 +74,7 @@ func Test_MarshalRego_Policy(t *testing.T) { p.allowEnvironmentVariableDropping, p.allowUnencryptedScratch, p.allowCapabilityDropping, + p.allowLogProviderDropping, ) if err != nil { t.Error(err) diff --git a/pkg/securitypolicy/regopolicy_windows_test.go b/pkg/securitypolicy/regopolicy_windows_test.go index 629f1b0773..9f9a9bb0b6 100644 --- a/pkg/securitypolicy/regopolicy_windows_test.go +++ b/pkg/securitypolicy/regopolicy_windows_test.go @@ -1517,77 +1517,85 @@ func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { // Tests for log provider enforcement -func Test_Rego_EnforceLogProviderPolicy_Allowed_Windows(t *testing.T) { +// newLogProviderTestPolicy builds a Rego policy whose allowed_log_providers +// list contains the given providers and returns the compiled enforcer. +// Pass no providers to get an empty allow-list. +// +// allow_log_provider_dropping is left unset so the test exercises the +// default fail-close mode. Use newLogProviderTestPolicyWithDropping to flip +// the mode. +func newLogProviderTestPolicy(t *testing.T, allowedProviders ...string) *regoEnforcer { + t.Helper() + return newLogProviderTestPolicyWithDropping(t, false, allowedProviders...) +} + +// newLogProviderTestPolicyWithDropping is the more general helper used by the +// mode-specific tests. It compiles a Rego policy that defines +// allowed_log_providers, sets allow_log_provider_dropping to dropping, and +// routes log_provider through the framework rule. +func newLogProviderTestPolicyWithDropping(t *testing.T, dropping bool, allowedProviders ...string) *regoEnforcer { + t.Helper() + var listLines string + for _, p := range allowedProviders { + listLines += fmt.Sprintf("\t\t%q,\n", p) + } rego := fmt.Sprintf(`package policy api_version := "%s" framework_version := "%s" + allow_log_provider_dropping := %t + allowed_log_providers := [ - "microsoft.windows.hyperv.compute", - "microsoft-windows-guest-network-service", - ] +%s ] log_provider := data.framework.log_provider - `, apiVersion, frameworkVersion) + `, apiVersion, frameworkVersion, dropping, listLines) policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) if err != nil { t.Fatalf("failed to create policy: %v", err) } + return policy +} - ctx := context.Background() - err = policy.EnforceLogProviderPolicy(ctx, "microsoft.windows.hyperv.compute") +func Test_Rego_EnforceLogProviderPolicy_Allowed_Windows(t *testing.T) { + policy := newLogProviderTestPolicy(t, + "microsoft.windows.hyperv.compute", + "microsoft-windows-guest-network-service", + ) + + kept, err := policy.EnforceLogProviderPolicy(context.Background(), + []string{"microsoft.windows.hyperv.compute"}) if err != nil { t.Errorf("expected allowed provider to pass: %v", err) } + if len(kept) != 1 || kept[0] != "microsoft.windows.hyperv.compute" { + t.Errorf("expected kept=[microsoft.windows.hyperv.compute]; got %v", kept) + } } func Test_Rego_EnforceLogProviderPolicy_Denied_Windows(t *testing.T) { - rego := fmt.Sprintf(`package policy - api_version := "%s" - framework_version := "%s" + policy := newLogProviderTestPolicy(t, "microsoft.windows.hyperv.compute") - allowed_log_providers := [ - "microsoft.windows.hyperv.compute", - ] - - log_provider := data.framework.log_provider - `, apiVersion, frameworkVersion) - - policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) - if err != nil { - t.Fatalf("failed to create policy: %v", err) - } - - ctx := context.Background() - err = policy.EnforceLogProviderPolicy(ctx, "some-malicious-provider") + _, err := policy.EnforceLogProviderPolicy(context.Background(), + []string{"some-malicious-provider"}) if err == nil { t.Errorf("expected unknown provider to be denied") } } func Test_Rego_EnforceLogProviderPolicy_CaseInsensitive_Windows(t *testing.T) { - rego := fmt.Sprintf(`package policy - api_version := "%s" - framework_version := "%s" - - allowed_log_providers := [ - "microsoft.windows.hyperv.compute", - ] - - log_provider := data.framework.log_provider - `, apiVersion, frameworkVersion) - - policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) - if err != nil { - t.Fatalf("failed to create policy: %v", err) - } + policy := newLogProviderTestPolicy(t, "microsoft.windows.hyperv.compute") - ctx := context.Background() - err = policy.EnforceLogProviderPolicy(ctx, "Microsoft.Windows.Hyperv.Compute") + kept, err := policy.EnforceLogProviderPolicy(context.Background(), + []string{"Microsoft.Windows.Hyperv.Compute"}) if err != nil { t.Errorf("expected case-insensitive match to pass: %v", err) } + // Rego preserves the input casing; we just confirm the name survived. + if len(kept) != 1 || kept[0] != "Microsoft.Windows.Hyperv.Compute" { + t.Errorf("expected kept=[Microsoft.Windows.Hyperv.Compute]; got %v", kept) + } } func Test_Rego_EnforceLogProviderPolicy_OpenDoor_AllowsAll_Windows(t *testing.T) { @@ -1597,21 +1605,42 @@ func Test_Rego_EnforceLogProviderPolicy_OpenDoor_AllowsAll_Windows(t *testing.T) } ctx := context.Background() - err = policy.EnforceLogProviderPolicy(ctx, "any-provider-at-all") + kept, err := policy.EnforceLogProviderPolicy(ctx, []string{"any-provider-at-all"}) if err != nil { t.Errorf("open door should allow any provider: %v", err) } + if len(kept) != 1 || kept[0] != "any-provider-at-all" { + t.Errorf("open door should keep the requested provider; got %v", kept) + } } func Test_Rego_EnforceLogProviderPolicy_EmptyAllowList_DeniesAll_Windows(t *testing.T) { - rego := fmt.Sprintf(`package policy - api_version := "%s" - framework_version := "%s" + policy := newLogProviderTestPolicy(t) - allowed_log_providers := [] + _, err := policy.EnforceLogProviderPolicy(context.Background(), + []string{"microsoft.windows.hyperv.compute"}) + if err == nil { + t.Errorf("expected empty allow list to deny all providers") + } +} - log_provider := data.framework.log_provider - `, apiVersion, frameworkVersion) +// Test_Rego_EnforceLogProviderPolicy_PreFeatureAPIVersion_Allows_Windows pins +// the non-regression behaviour for policies authored before log_provider was +// introduced (api.rego entry: introducedVersion=0.11.0, default_results.allowed=true). +// Such policies omit allowed_log_providers entirely; EnforceLogProviderPolicy +// must return the input list unchanged with no error so existing CWCOW/WCOW +// policies do not break when the framework gains the new enforcement point. +func Test_Rego_EnforceLogProviderPolicy_PreFeatureAPIVersion_Allows_Windows(t *testing.T) { + // A pre-feature policy does not define log_provider at all (it was + // authored before the rule existed). The version-gated default_results + // path only fires when the policy has no rule for the enforcement + // point — including `log_provider := data.framework.log_provider` + // here would shadow the default and route through the framework rule, + // which defaults to deny. + rego := fmt.Sprintf(`package policy + api_version := "0.10.0" + framework_version := "%s" + `, frameworkVersion) policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) if err != nil { @@ -1619,8 +1648,78 @@ func Test_Rego_EnforceLogProviderPolicy_EmptyAllowList_DeniesAll_Windows(t *test } ctx := context.Background() - err = policy.EnforceLogProviderPolicy(ctx, "microsoft.windows.hyperv.compute") + kept, err := policy.EnforceLogProviderPolicy(ctx, + []string{"any-provider-not-in-any-list"}) + if err != nil { + t.Errorf("expected pre-0.11.0 policy to allow any provider via default_results: %v", err) + } + // default_results.providers_to_keep is null, so getProvidersToKeep + // returns the input list unchanged. + if len(kept) != 1 || kept[0] != "any-provider-not-in-any-list" { + t.Errorf("expected kept=[any-provider-not-in-any-list]; got %v", kept) + } +} + +// Test_Rego_EnforceLogProviderPolicy_EmptyProviderName_Denied_Windows pins +// the behaviour for the host-scrubbed-unknown-provider edge case: when the +// providerName is the empty string, no allow-list entry can match (allow-lists +// never contain ""), so enforcement must deny. +func Test_Rego_EnforceLogProviderPolicy_EmptyProviderName_Denied_Windows(t *testing.T) { + policy := newLogProviderTestPolicy(t, "microsoft.windows.hyperv.compute") + + _, err := policy.EnforceLogProviderPolicy(context.Background(), []string{""}) if err == nil { - t.Errorf("expected empty allow list to deny all providers") + t.Errorf("expected empty providerName to be denied") + } +} + +// Test_Rego_EnforceLogProviderPolicy_Dropping_KeepsSubset_Windows exercises +// the silent-drop mode: when allow_log_provider_dropping := true the call +// allows even if some requested providers are not on the allow-list, and only +// the matching subset is returned. +func Test_Rego_EnforceLogProviderPolicy_Dropping_KeepsSubset_Windows(t *testing.T) { + policy := newLogProviderTestPolicyWithDropping(t, true, + "microsoft.windows.hyperv.compute", + "microsoft-windows-guest-network-service", + ) + + kept, err := policy.EnforceLogProviderPolicy(context.Background(), []string{ + "microsoft.windows.hyperv.compute", + "some-bogus-provider", + "microsoft-windows-guest-network-service", + }) + if err != nil { + t.Errorf("dropping mode should allow regardless of unknown providers: %v", err) + } + + keptSet := make(map[string]struct{}, len(kept)) + for _, n := range kept { + keptSet[n] = struct{}{} + } + if _, ok := keptSet["microsoft.windows.hyperv.compute"]; !ok { + t.Errorf("expected 'microsoft.windows.hyperv.compute' kept; got %v", kept) + } + if _, ok := keptSet["microsoft-windows-guest-network-service"]; !ok { + t.Errorf("expected 'microsoft-windows-guest-network-service' kept; got %v", kept) + } + if _, ok := keptSet["some-bogus-provider"]; ok { + t.Errorf("expected 'some-bogus-provider' to be dropped; got %v", kept) + } +} + +// Test_Rego_EnforceLogProviderPolicy_FailClose_AnyMissDenies_Windows confirms +// the inverse of the dropping test: with allow_log_provider_dropping := false +// (default) even one unknown provider in a batch fails the entire call. +func Test_Rego_EnforceLogProviderPolicy_FailClose_AnyMissDenies_Windows(t *testing.T) { + policy := newLogProviderTestPolicyWithDropping(t, false, + "microsoft.windows.hyperv.compute", + ) + + _, err := policy.EnforceLogProviderPolicy(context.Background(), []string{ + "microsoft.windows.hyperv.compute", + "some-bogus-provider", + }) + if err == nil { + t.Errorf("fail-close mode should deny when any provider is unknown") } } diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index 5b05160492..7f5d2ac39b 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -67,6 +67,12 @@ type PolicyConfig struct { // all containers within a pod to be run without scratch encryption. AllowUnencryptedScratch bool `json:"allow_unencrypted_scratch" toml:"allow_unencrypted_scratch"` AllowCapabilityDropping bool `json:"allow_capability_dropping" toml:"allow_capability_dropping"` + // AllowLogProviderDropping controls how EnforceLogProviderPolicy handles + // requested ETW providers that are not on the allow-list. When false + // (default, fail-close) any disallowed provider causes the entire + // modifyServiceSettings call to be denied. When true, disallowed providers + // are silently dropped and the call continues with the kept subset. + AllowLogProviderDropping bool `json:"allow_log_provider_dropping" toml:"allow_log_provider_dropping"` } func NewPolicyConfig(opts ...PolicyConfigOpt) (*PolicyConfig, error) { diff --git a/pkg/securitypolicy/securitypolicy_internal.go b/pkg/securitypolicy/securitypolicy_internal.go index c736fb58ed..42c10dbce0 100644 --- a/pkg/securitypolicy/securitypolicy_internal.go +++ b/pkg/securitypolicy/securitypolicy_internal.go @@ -19,6 +19,7 @@ type securityPolicyInternal struct { AllowEnvironmentVariableDropping bool AllowUnencryptedScratch bool AllowCapabilityDropping bool + AllowLogProviderDropping bool } // Internal version of Windows SecurityPolicy @@ -32,6 +33,7 @@ type securityPolicyWindowsInternal struct { AllowEnvironmentVariableDropping bool AllowUnencryptedScratch bool AllowCapabilityDropping bool + AllowLogProviderDropping bool } type securityPolicyFragment struct { @@ -97,6 +99,7 @@ func newSecurityPolicyInternal( allowDropEnvironmentVariables bool, allowUnencryptedScratch bool, allowDropCapabilities bool, + allowLogProviderDropping bool, ) (*securityPolicyInternal, error) { containersInternal, err := containersToInternal(containers) if err != nil { @@ -113,6 +116,7 @@ func newSecurityPolicyInternal( AllowEnvironmentVariableDropping: allowDropEnvironmentVariables, AllowUnencryptedScratch: allowUnencryptedScratch, AllowCapabilityDropping: allowDropCapabilities, + AllowLogProviderDropping: allowLogProviderDropping, }, nil } diff --git a/pkg/securitypolicy/securitypolicy_marshal.go b/pkg/securitypolicy/securitypolicy_marshal.go index 665dc9e4f0..9db7e3f19d 100644 --- a/pkg/securitypolicy/securitypolicy_marshal.go +++ b/pkg/securitypolicy/securitypolicy_marshal.go @@ -67,6 +67,7 @@ type OSAwareMarshalFunc func( allowEnvironmentVariableDropping bool, allowUnencryptedScratch bool, allowCapabilityDropping bool, + allowLogProviderDropping bool, ) (string, error) // osAwareMarshalRego handles both Linux and Windows containers @@ -83,6 +84,7 @@ func osAwareMarshalRego( allowEnvironmentVariableDropping bool, allowUnencryptedScratch bool, allowCapabilityDropping bool, + allowLogProviderDropping bool, ) (string, error) { if allowAll { if len(linuxContainers) > 0 || len(windowsContainers) > 0 { @@ -98,7 +100,8 @@ func osAwareMarshalRego( } return marshalRego(allowAll, linuxContainers, externalProcesses, fragments, allowPropertiesAccess, allowDumpStacks, allowRuntimeLogging, - allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapabilityDropping) + allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapabilityDropping, + allowLogProviderDropping) case "windows": if len(linuxContainers) > 0 { @@ -106,7 +109,8 @@ func osAwareMarshalRego( } return marshalWindowsRego(allowAll, windowsContainers, externalProcesses, fragments, allowPropertiesAccess, allowDumpStacks, allowRuntimeLogging, - allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapabilityDropping) + allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapabilityDropping, + allowLogProviderDropping) default: return "", fmt.Errorf("unsupported OS type: %s", osType) @@ -125,6 +129,7 @@ func marshalWindowsRego( allowEnvironmentVariableDropping bool, allowUnencryptedScratch bool, allowCapabilityDropping bool, + allowLogProviderDropping bool, ) (string, error) { if allowAll { if len(containers) > 0 { @@ -149,6 +154,7 @@ func marshalWindowsRego( AllowEnvironmentVariableDropping: allowEnvironmentVariableDropping, AllowUnencryptedScratch: allowUnencryptedScratch, AllowCapabilityDropping: allowCapabilityDropping, + AllowLogProviderDropping: allowLogProviderDropping, } return policy.marshalWindowsRego(), nil @@ -167,6 +173,7 @@ func marshalJSON( _ bool, _ bool, _ bool, + _ bool, ) (string, error) { var policy *SecurityPolicy if allowAll { @@ -198,6 +205,7 @@ func marshalRego( allowEnvironmentVariableDropping bool, allowUnencryptedScratch bool, allowCapabilityDropping bool, + allowLogProviderDropping bool, ) (string, error) { if allowAll { if len(containers) > 0 { @@ -217,6 +225,7 @@ func marshalRego( allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapabilityDropping, + allowLogProviderDropping, ) if err != nil { return "", err @@ -251,6 +260,7 @@ func MarshalPolicy( allowEnvironmentVariableDropping bool, allowUnencryptedScratch bool, allowCapbilitiesDropping bool, + allowLogProviderDropping bool, ) (string, error) { if marshaller == "" { marshaller = defaultMarshaller @@ -272,6 +282,7 @@ func MarshalPolicy( allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapbilitiesDropping, + allowLogProviderDropping, ) } } @@ -596,6 +607,7 @@ func (p securityPolicyInternal) marshalRego() string { writeLine(builder, "allow_environment_variable_dropping := %t", p.AllowEnvironmentVariableDropping) writeLine(builder, "allow_unencrypted_scratch := %t", p.AllowUnencryptedScratch) writeLine(builder, "allow_capability_dropping := %t", p.AllowCapabilityDropping) + writeLine(builder, "allow_log_provider_dropping := %t", p.AllowLogProviderDropping) result := strings.Replace(policyRegoTemplate, "@@OBJECTS@@", builder.String(), 1) result = strings.Replace(result, "@@API_VERSION@@", apiVersion, 1) result = strings.Replace(result, "@@FRAMEWORK_VERSION@@", frameworkVersion, 1) @@ -621,6 +633,7 @@ func (p securityPolicyWindowsInternal) marshalWindowsRego() string { writeLine(builder, "allow_environment_variable_dropping := %t", p.AllowEnvironmentVariableDropping) writeLine(builder, "allow_unencrypted_scratch := %t", p.AllowUnencryptedScratch) writeLine(builder, "allow_capability_dropping := %t", p.AllowCapabilityDropping) + writeLine(builder, "allow_log_provider_dropping := %t", p.AllowLogProviderDropping) result := strings.Replace(policyRegoTemplate, "@@OBJECTS@@", builder.String(), 1) result = strings.Replace(result, "@@API_VERSION@@", apiVersion, 1) result = strings.Replace(result, "@@FRAMEWORK_VERSION@@", frameworkVersion, 1) diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index 35dd755367..9a21fd4e9f 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -40,6 +40,28 @@ func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmRe } } +// LockDown irreversibly replaces the active policy enforcer with a closed-door +// (deny-everything) enforcer in response to a policy violation. All subsequent +// Enforce* calls will deny. +// +// LockDown is idempotent: subsequent calls after the first are no-ops. +// +// LockDown also sets PolicyEnforcerSet so that a subsequent +// SetConfidentialOptions call (which checks PolicyEnforcerSet and refuses to +// install a policy if it is already set) cannot replace the closed-door +// enforcer with a permissive one. This makes lockdown sticky regardless of +// whether LockDown was called before or after the initial policy load. +func (s *SecurityOptions) LockDown(ctx context.Context) { + s.policyMutex.Lock() + defer s.policyMutex.Unlock() + if _, alreadyLocked := s.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); alreadyLocked { + return + } + log.G(ctx).Error("policy violation: locking down sidecar enforcer to closed-door; all subsequent policy decisions will deny") + s.PolicyEnforcer = &ClosedDoorSecurityPolicyEnforcer{} + s.PolicyEnforcerSet = true +} + // SetConfidentialOptions takes guestresource.ConfidentialOptions // to set up our internal data structures we use to store and enforce // security policy. The options can contain security policy enforcer type, diff --git a/pkg/securitypolicy/securitypolicy_options_test.go b/pkg/securitypolicy/securitypolicy_options_test.go new file mode 100644 index 0000000000..865243c08f --- /dev/null +++ b/pkg/securitypolicy/securitypolicy_options_test.go @@ -0,0 +1,86 @@ +package securitypolicy + +import ( + "context" + "io" + "testing" +) + +// TestLockDown_SwapsEnforcerToClosedDoor verifies that LockDown replaces +// the active enforcer with a ClosedDoorSecurityPolicyEnforcer. +func TestLockDown_SwapsEnforcerToClosedDoor(t *testing.T) { + opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, true, "", io.Discard) + + if _, ok := opts.PolicyEnforcer.(*OpenDoorSecurityPolicyEnforcer); !ok { + t.Fatalf("setup: expected initial enforcer to be OpenDoor, got %T", opts.PolicyEnforcer) + } + + opts.LockDown(context.Background()) + + if _, ok := opts.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); !ok { + t.Errorf("after LockDown: expected ClosedDoor enforcer, got %T", opts.PolicyEnforcer) + } +} + +// TestLockDown_Idempotent verifies that LockDown is a no-op once already locked. +// The same enforcer instance is preserved on the second call. +func TestLockDown_Idempotent(t *testing.T) { + opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, true, "", io.Discard) + ctx := context.Background() + + opts.LockDown(ctx) + firstLocked := opts.PolicyEnforcer + + opts.LockDown(ctx) + secondLocked := opts.PolicyEnforcer + + if firstLocked != secondLocked { + t.Errorf("LockDown should be idempotent: enforcer pointer changed between calls (%p -> %p)", firstLocked, secondLocked) + } +} + +// TestLockDown_SetsPolicyEnforcerSet verifies that LockDown marks the policy +// as set, so that a subsequent SetConfidentialOptions call cannot replace +// the closed-door enforcer with a permissive one. +// +// SetConfidentialOptions short-circuits with an error when PolicyEnforcerSet +// is already true; this is the mechanism that makes LockDown sticky against +// future policy-install attempts regardless of call order. +func TestLockDown_SetsPolicyEnforcerSet(t *testing.T) { + // Construct with PolicyEnforcerSet=false to simulate LockDown being + // called before the initial policy was loaded. + opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, false, "", io.Discard) + + if opts.PolicyEnforcerSet { + t.Fatal("setup: expected PolicyEnforcerSet=false before LockDown") + } + + opts.LockDown(context.Background()) + + if !opts.PolicyEnforcerSet { + t.Error("expected PolicyEnforcerSet=true after LockDown so subsequent SetConfidentialOptions refuses to install a policy") + } +} + +// TestLockDown_StickyAgainstSetConfidentialOptions verifies the end-to-end +// stickiness property: after LockDown, SetConfidentialOptions refuses to +// install a new (potentially permissive) policy and the enforcer remains +// closed-door. +func TestLockDown_StickyAgainstSetConfidentialOptions(t *testing.T) { + opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, false, "", io.Discard) + ctx := context.Background() + + opts.LockDown(ctx) + + // Try to install a fresh policy after lockdown. The actual policy + // content does not matter — SetConfidentialOptions should refuse on + // the PolicyEnforcerSet check before touching anything else. + err := opts.SetConfidentialOptions(ctx, "", "", "") + if err == nil { + t.Fatal("expected SetConfidentialOptions to refuse after LockDown") + } + + if _, ok := opts.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); !ok { + t.Errorf("after LockDown + SetConfidentialOptions: enforcer was replaced; got %T, want ClosedDoor", opts.PolicyEnforcer) + } +} diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 63a3b9e92f..37f77a81f4 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -128,7 +128,14 @@ type SecurityPolicyEnforcer interface { GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string, mountedCim []string) (err error) EnforceRegistryChangesPolicy(ctx context.Context, containerID string, registryValues interface{}) error - EnforceLogProviderPolicy(ctx context.Context, providerName string) error + // EnforceLogProviderPolicy validates a batch of requested ETW provider + // names against the policy's allowed_log_providers list. It returns the + // subset of provider names that the caller should forward to the inbox + // GCS, plus any policy error. When the policy has + // allow_log_provider_dropping := true, providers not on the allow-list are + // silently dropped from the returned slice; otherwise the whole call is + // denied (returning a non-nil error) if any provider is not allowed. + EnforceLogProviderPolicy(ctx context.Context, providerNames []string) ([]string, error) } //nolint:unused @@ -325,8 +332,8 @@ func (OpenDoorSecurityPolicyEnforcer) EnforceRegistryChangesPolicy(ctx context.C return nil } -func (OpenDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(context.Context, string) error { - return nil +func (OpenDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(_ context.Context, providerNames []string) ([]string, error) { + return providerNames, nil } type ClosedDoorSecurityPolicyEnforcer struct{} @@ -458,6 +465,6 @@ func (ClosedDoorSecurityPolicyEnforcer) EnforceRegistryChangesPolicy(ctx context return errors.New("registry changes are denied by policy") } -func (ClosedDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(context.Context, string) error { - return errors.New("log provider is denied by policy") +func (ClosedDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(context.Context, []string) ([]string, error) { + return nil, errors.New("log provider is denied by policy") } diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index 718e0044d9..6f255a33ec 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -580,6 +580,38 @@ func getEnvsToKeep(envList []string, results rpi.RegoQueryResult) ([]string, err return keepSet.toArray(), nil } +// getProvidersToKeep extracts the "providers_to_keep" field from the rego +// query results for log_provider enforcement and intersects it (defensively) +// with the originally requested providerNames. Mirrors getEnvsToKeep. +// +// When the policy is older / does not return providers_to_keep, the entire +// requested list is kept (analogous to env_list). +func getProvidersToKeep(providerNames []string, results rpi.RegoQueryResult) ([]string, error) { + value, err := results.Value("providers_to_keep") + if err != nil || value == nil { + // policy did not return 'providers_to_keep'. Interpret as + // "proceed with provided providers". + return providerNames, nil + } + + providersAsInterfaces, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf("policy returned incorrect type for 'providers_to_keep', expected []interface{}, received %T", value) + } + + keepSet := make(stringSet) + for _, providerAsInterface := range providersAsInterfaces { + if provider, ok := providerAsInterface.(string); ok { + keepSet.add(provider) + } else { + return nil, fmt.Errorf("members of providers_to_keep from policy must be strings, received %T", providerAsInterface) + } + } + + keepSet = keepSet.intersect(toStringSet(providerNames)) + return keepSet.toArray(), nil +} + func getCapsToKeep(capsList *oci.LinuxCapabilities, results rpi.RegoQueryResult) (*oci.LinuxCapabilities, error) { value, err := results.Value("caps_list") if err != nil || value == nil { @@ -1188,12 +1220,15 @@ func (policy *regoEnforcer) EnforceRegistryChangesPolicy(ctx context.Context, co return err } -func (policy *regoEnforcer) EnforceLogProviderPolicy(ctx context.Context, providerName string) error { +func (policy *regoEnforcer) EnforceLogProviderPolicy(ctx context.Context, providerNames []string) ([]string, error) { input := inputData{ - "providerName": providerName, + "providers": providerNames, } - _, err := policy.enforce(ctx, "log_provider", input) - return err + results, err := policy.enforce(ctx, "log_provider", input) + if err != nil { + return nil, err + } + return getProvidersToKeep(providerNames, results) } func (policy *regoEnforcer) GetUserInfo(process *oci.Process, rootPath string) (IDName, []IDName, string, error) { diff --git a/test/pkg/securitypolicy/policy.go b/test/pkg/securitypolicy/policy.go index eeb93f67c8..bdf748bac6 100644 --- a/test/pkg/securitypolicy/policy.go +++ b/test/pkg/securitypolicy/policy.go @@ -64,6 +64,7 @@ func PolicyWithOpts(tb testing.TB, policyType string, pOpts ...securitypolicy.Po config.AllowEnvironmentVariableDropping, config.AllowUnencryptedScratch, config.AllowCapabilityDropping, + config.AllowLogProviderDropping, ) if err != nil { tb.Fatal(err) From 146271584c01101dcab2fb9f25da9ed65a257c10 Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 1 Jun 2026 10:48:31 +0100 Subject: [PATCH 04/22] CWCOW: warn when log providers are trimmed by policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When allow_log_provider_dropping is enabled and the policy returns a strict subset of the requested providers, the sidecar silently rebuilt the LogSourcesInfo payload, leaving operators with no signal that any provider had been dropped — and under typical confidential setups forwardlogs itself may be off, so the trim was effectively invisible. Emit a single Warn at the moment of trimming with the requested, kept, and dropped provider names. The log still lands inside the UVM and is reachable via shimdiag even when forwardlogs is disabled. Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 0165bc71e0..1b8b1921dc 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -585,10 +585,26 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { payload := settings.Settings if len(keptNames) != len(requestedNames) { - // Subset kept. Trim each source's provider list to - // only the allowed names, then re-encode for the - // inbox GCS. Empty sources are preserved to keep the - // shape stable; inbox GCS handles them as no-ops. + // Subset kept. Surface the drop so operators have a + // breadcrumb — under allow_log_provider_dropping the + // pod boots silently, and forwardlogs may itself be + // off, so without this warning the trim is invisible. + dropped := make([]string, 0, len(requestedNames)-len(keptNames)) + for _, name := range requestedNames { + if _, ok := keepSet[name]; !ok { + dropped = append(dropped, name) + } + } + log.G(req.ctx).WithFields(map[string]interface{}{ + "requested": requestedNames, + "kept": keptNames, + "dropped": dropped, + }).Warn("log providers trimmed by policy (allow_log_provider_dropping)") + + // Trim each source's provider list to only the + // allowed names, then re-encode for the inbox GCS. + // Empty sources are preserved to keep the shape + // stable; inbox GCS handles them as no-ops. trimmed := logSources for i := range trimmed.LogConfig.Sources { src := &trimmed.LogConfig.Sources[i] From 7371616ee4c186e3be62cd592184019140d8bf4e Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 1 Jun 2026 12:48:21 +0100 Subject: [PATCH 05/22] ci: empty commit to retrigger checks Signed-off-by: Takuro Sato From 346bfb003f3f0b8934db5e292bc70faa19df602c Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 1 Jun 2026 14:05:51 +0100 Subject: [PATCH 06/22] securitypolicy: always set PolicyEnforcerSet in LockDown Without this, LockDown on a ClosedDoor enforcer with PolicyEnforcerSet=false (the sidecar's boot-time state) leaves the flag unset, and a later SetConfidentialOptions can still install a permissive policy. Signed-off-by: Takuro Sato --- pkg/securitypolicy/securitypolicy_options.go | 16 +++++++++--- .../securitypolicy_options_test.go | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index 9a21fd4e9f..a04b08f16c 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -44,22 +44,30 @@ func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmRe // (deny-everything) enforcer in response to a policy violation. All subsequent // Enforce* calls will deny. // -// LockDown is idempotent: subsequent calls after the first are no-ops. +// LockDown is idempotent with respect to the enforcer swap: if the enforcer is +// already a ClosedDoorSecurityPolicyEnforcer the swap and the error log are +// skipped. // -// LockDown also sets PolicyEnforcerSet so that a subsequent +// LockDown also sets PolicyEnforcerSet unconditionally so that a subsequent // SetConfidentialOptions call (which checks PolicyEnforcerSet and refuses to // install a policy if it is already set) cannot replace the closed-door // enforcer with a permissive one. This makes lockdown sticky regardless of -// whether LockDown was called before or after the initial policy load. +// whether LockDown was called before or after the initial policy load, and in +// particular covers the boot-time case where the sidecar starts with a +// ClosedDoorSecurityPolicyEnforcer but PolicyEnforcerSet is still false. func (s *SecurityOptions) LockDown(ctx context.Context) { s.policyMutex.Lock() defer s.policyMutex.Unlock() + // Mark the policy as set first, unconditionally. This is the property that + // makes lockdown sticky against a follow-up SetConfidentialOptions, and it + // must hold even when the enforcer is already a ClosedDoor instance + // (e.g. the sidecar's boot-time default before any user policy arrives). + s.PolicyEnforcerSet = true if _, alreadyLocked := s.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); alreadyLocked { return } log.G(ctx).Error("policy violation: locking down sidecar enforcer to closed-door; all subsequent policy decisions will deny") s.PolicyEnforcer = &ClosedDoorSecurityPolicyEnforcer{} - s.PolicyEnforcerSet = true } // SetConfidentialOptions takes guestresource.ConfidentialOptions diff --git a/pkg/securitypolicy/securitypolicy_options_test.go b/pkg/securitypolicy/securitypolicy_options_test.go index 865243c08f..71e71c3bf9 100644 --- a/pkg/securitypolicy/securitypolicy_options_test.go +++ b/pkg/securitypolicy/securitypolicy_options_test.go @@ -84,3 +84,29 @@ func TestLockDown_StickyAgainstSetConfidentialOptions(t *testing.T) { t.Errorf("after LockDown + SetConfidentialOptions: enforcer was replaced; got %T, want ClosedDoor", opts.PolicyEnforcer) } } + +// TestLockDown_StickyFromBootClosedDoor covers the boot-time case the +// sidecar actually hits: the enforcer is already a ClosedDoor instance (the +// PSP fail-close default in cmd/gcs-sidecar/main.go) and PolicyEnforcerSet +// is still false because no user policy has been loaded yet. LockDown must +// still flip PolicyEnforcerSet so that a follow-up SetConfidentialOptions +// is refused, otherwise a permissive policy could replace the closed door. +func TestLockDown_StickyFromBootClosedDoor(t *testing.T) { + opts := NewSecurityOptions(&ClosedDoorSecurityPolicyEnforcer{}, false, "", io.Discard) + ctx := context.Background() + + opts.LockDown(ctx) + + if !opts.PolicyEnforcerSet { + t.Fatal("expected PolicyEnforcerSet=true after LockDown on a boot-time ClosedDoor enforcer; otherwise SetConfidentialOptions would still accept a fresh policy") + } + + err := opts.SetConfidentialOptions(ctx, "", "", "") + if err == nil { + t.Fatal("expected SetConfidentialOptions to refuse after LockDown on a boot-time ClosedDoor enforcer") + } + + if _, ok := opts.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); !ok { + t.Errorf("after LockDown + SetConfidentialOptions: enforcer was replaced; got %T, want ClosedDoor", opts.PolicyEnforcer) + } +} From 7e5929a94454b0351fe816d36b11b2205f2ef1f2 Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 1 Jun 2026 14:24:30 +0100 Subject: [PATCH 07/22] gcs-sidecar: detect log-provider trim via missing names, not length The rego enforcer returns providers_to_keep as a set, so a request like [A, A] against an allowlist of [A] came back as [A] and tripped a spurious warning + re-marshal. Scan requestedNames against keepSet. Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 36 ++++++++++----- internal/gcs-sidecar/handlers_test.go | 63 +++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 1b8b1921dc..1e2c26f1db 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -583,18 +583,32 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { keepSet[name] = struct{}{} } - payload := settings.Settings - if len(keptNames) != len(requestedNames) { - // Subset kept. Surface the drop so operators have a - // breadcrumb — under allow_log_provider_dropping the - // pod boots silently, and forwardlogs may itself be - // off, so without this warning the trim is invisible. - dropped := make([]string, 0, len(requestedNames)-len(keptNames)) - for _, name := range requestedNames { - if _, ok := keepSet[name]; !ok { - dropped = append(dropped, name) - } + // Detect trimming by scanning requested names against + // keepSet. We cannot use len(kept) != len(requested): + // the rego enforcer returns providers_to_keep via a set + // (see getProvidersToKeep → keepSet.toArray()), so a + // duplicate-name request like [A, A, B] returns [A, B] + // even when nothing was dropped, which would otherwise + // trip a false-positive warning and a needless re-marshal. + dropped := make([]string, 0) + seenDropped := make(map[string]struct{}) + for _, name := range requestedNames { + if _, ok := keepSet[name]; ok { + continue + } + if _, dup := seenDropped[name]; dup { + continue } + seenDropped[name] = struct{}{} + dropped = append(dropped, name) + } + + payload := settings.Settings + if len(dropped) > 0 { + // Surface the drop so operators have a breadcrumb — + // under allow_log_provider_dropping the pod boots + // silently, and forwardlogs may itself be off, so + // without this warning the trim is invisible. log.G(req.ctx).WithFields(map[string]interface{}{ "requested": requestedNames, "kept": keptNames, diff --git a/internal/gcs-sidecar/handlers_test.go b/internal/gcs-sidecar/handlers_test.go index 843d062f0c..fbbda0e51f 100644 --- a/internal/gcs-sidecar/handlers_test.go +++ b/internal/gcs-sidecar/handlers_test.go @@ -17,6 +17,7 @@ import ( "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/internal/vm/vmutils/etw" "github.com/Microsoft/hcsshim/pkg/securitypolicy" + "github.com/sirupsen/logrus" ) // buildModifySettingsRequest creates a serialized ModifySettings request message @@ -522,3 +523,65 @@ func TestModifyServiceSettings_LogForward_PolicyDropping_TrimsForwardedPayload(t t.Errorf("expected dropped provider %q to be absent from forwarded payload", dropped) } } + +// captureHook is a tiny logrus hook that records every entry it sees. +// Used by TestModifyServiceSettings_LogForward_PolicyDropping_NoFalsePositive +// to assert the "log providers trimmed by policy" Warn is *not* emitted when +// the only reason kept and requested differ is set-deduplication. +type captureHook struct { + entries []*logrus.Entry +} + +func (h *captureHook) Levels() []logrus.Level { return logrus.AllLevels } +func (h *captureHook) Fire(e *logrus.Entry) error { + h.entries = append(h.entries, e) + return nil +} + +// TestModifyServiceSettings_LogForward_PolicyDropping_NoFalsePositive guards +// against a false-positive trim warning + needless re-marshal when the +// enforcer returns a deduplicated set. The rego implementation builds +// providers_to_keep via a stringSet (see getProvidersToKeep), so a request +// with duplicate provider names like [A, A] comes back as [A] even when +// nothing was actually dropped. Detection must be based on "some requested +// name is missing from keepSet", not len(kept) != len(requested). +func TestModifyServiceSettings_LogForward_PolicyDropping_NoFalsePositive(t *testing.T) { + name := "microsoft.windows.hyperv.compute" + enforcer := &droppingLogProviderEnforcer{ + allowed: map[string]struct{}{name: {}}, + } + b := newTestBridge(enforcer) + + // Two copies of the same allowed provider. dedup in the enforcer means + // kept=[name] while requested=[name, name]; the lengths differ but the + // set of requested names is fully covered, so this is NOT a trim. + payload := buildLogForwardServiceRequest(t, name, name) + req := newModifyServiceSettingsRequest(payload) + + hook := &captureHook{} + logrus.AddHook(hook) + defer func() { + // logrus has no public RemoveHook; reset all hooks to clear ours. + logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{}) + }() + + if err := b.modifyServiceSettings(req); err != nil { + t.Fatalf("modifyServiceSettings under dropping enforcer (dedup) returned error: %v", err) + } + + // Must forward to GCS. + select { + case <-b.sendToGCSCh: + case <-time.After(time.Second): + t.Fatal("timed out waiting for request to be forwarded to GCS") + } + + // Must NOT have emitted the trim warning: nothing was actually dropped. + for _, e := range hook.entries { + if e.Level == logrus.WarnLevel && + e.Message == "log providers trimmed by policy (allow_log_provider_dropping)" { + t.Errorf("false-positive trim warning emitted on a dedup-only mismatch (kept=%v requested=%v dropped=%v)", + e.Data["kept"], e.Data["requested"], e.Data["dropped"]) + } + } +} From 6f69276a0bc2e365e25cd6dd24d3196332683794 Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 1 Jun 2026 14:33:25 +0100 Subject: [PATCH 08/22] etw: add UpdateLogSourcesFromInfo to skip redundant decode The sidecar already decodes the base64+JSON payload to enforce log_provider policy. Hand the parsed LogSourcesInfo to a new UpdateLogSourcesFromInfo helper instead of re-encoding so the inbox prep can decode it again. UpdateLogSources is reimplemented on top. Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 20 ++++++++------------ internal/vm/vmutils/etw/provider_map.go | 24 +++++++++++++++++------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 1e2c26f1db..65edccd6cd 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -4,7 +4,6 @@ package bridge import ( - "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -603,7 +602,11 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { dropped = append(dropped, name) } - payload := settings.Settings + // Trim happens in-place on the parsed structure so we can + // hand it to UpdateLogSourcesFromInfo without a redundant + // base64-decode + JSON-unmarshal round-trip (we already + // decoded above for enforcement). + trimmed := logSources if len(dropped) > 0 { // Surface the drop so operators have a breadcrumb — // under allow_log_provider_dropping the pod boots @@ -616,10 +619,8 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { }).Warn("log providers trimmed by policy (allow_log_provider_dropping)") // Trim each source's provider list to only the - // allowed names, then re-encode for the inbox GCS. - // Empty sources are preserved to keep the shape - // stable; inbox GCS handles them as no-ops. - trimmed := logSources + // allowed names. Empty sources are preserved to keep + // the shape stable; inbox GCS handles them as no-ops. for i := range trimmed.LogConfig.Sources { src := &trimmed.LogConfig.Sources[i] filtered := make([]etw.EtwProvider, 0, len(src.Providers)) @@ -630,17 +631,12 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { } src.Providers = filtered } - trimmedJSON, err := json.Marshal(trimmed) - if err != nil { - return fmt.Errorf("failed to marshal trimmed log sources: %w", err) - } - payload = base64.StdEncoding.EncodeToString(trimmedJSON) } // Apply GUID resolution (and any other inbox-GCS prep) // against the policy-trimmed payload and hand off to // inbox GCS. - allowedLogSources, err := etw.UpdateLogSources(payload, false, true) + allowedLogSources, err := etw.UpdateLogSourcesFromInfo(trimmed, false, true) if err != nil { return fmt.Errorf("failed to update log sources: %w", err) } diff --git a/internal/vm/vmutils/etw/provider_map.go b/internal/vm/vmutils/etw/provider_map.go index 962d0c586a..f07d254af5 100644 --- a/internal/vm/vmutils/etw/provider_map.go +++ b/internal/vm/vmutils/etw/provider_map.go @@ -188,19 +188,29 @@ func marshalAndEncodeLogSources(logCfg LogSourcesInfo) (string, error) { // configuration and returns the updated log sources as a base64 encoded JSON string. // If there is an error in the process, it returns the original user provided log sources string. func UpdateLogSources(base64EncodedJSONLogConfig string, useDefaultLogSources bool, includeGUIDs bool) (string, error) { - var resultLogCfg LogSourcesInfo - if useDefaultLogSources { - resultLogCfg = defaultLogSourcesInfo - } - + var userLogSources LogSourcesInfo if base64EncodedJSONLogConfig != "" { - userLogSources, err := DecodeAndUnmarshalLogSources(base64EncodedJSONLogConfig) + var err error + userLogSources, err = DecodeAndUnmarshalLogSources(base64EncodedJSONLogConfig) if err != nil { return "", fmt.Errorf("failed to decode and unmarshal user log sources: %w", err) } - resultLogCfg.LogConfig.Sources = mergeLogSources(resultLogCfg.LogConfig.Sources, userLogSources.LogConfig.Sources) + } + return UpdateLogSourcesFromInfo(userLogSources, useDefaultLogSources, includeGUIDs) +} +// UpdateLogSourcesFromInfo is the parsed-input variant of UpdateLogSources for +// callers that already have a LogSourcesInfo in hand (e.g. the gcs-sidecar +// after it decoded the payload for policy enforcement) and want to avoid a +// second base64-decode + JSON-unmarshal round-trip. +// +// An empty userLogSources is equivalent to passing "" to UpdateLogSources. +func UpdateLogSourcesFromInfo(userLogSources LogSourcesInfo, useDefaultLogSources bool, includeGUIDs bool) (string, error) { + var resultLogCfg LogSourcesInfo + if useDefaultLogSources { + resultLogCfg = defaultLogSourcesInfo } + resultLogCfg.LogConfig.Sources = mergeLogSources(resultLogCfg.LogConfig.Sources, userLogSources.LogConfig.Sources) var err error resultLogCfg.LogConfig.Sources, err = applyGUIDPolicy(resultLogCfg.LogConfig.Sources, includeGUIDs) From 155f9b3052685dccdae734d87d272268241920b2 Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 22 Jun 2026 14:47:10 +0100 Subject: [PATCH 09/22] Remove LockDown() Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 1 - internal/gcs-sidecar/handlers_test.go | 14 +-- pkg/securitypolicy/securitypolicy_options.go | 30 ----- .../securitypolicy_options_test.go | 112 ------------------ 4 files changed, 3 insertions(+), 154 deletions(-) delete mode 100644 pkg/securitypolicy/securitypolicy_options_test.go diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 65edccd6cd..83e662ef4f 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -571,7 +571,6 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { keptNames, err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogProviderPolicy( req.ctx, requestedNames) if err != nil { - b.hostState.securityOptions.LockDown(req.ctx) return fmt.Errorf("log providers denied by policy: %w", err) } diff --git a/internal/gcs-sidecar/handlers_test.go b/internal/gcs-sidecar/handlers_test.go index fbbda0e51f..aaf6e0f7f4 100644 --- a/internal/gcs-sidecar/handlers_test.go +++ b/internal/gcs-sidecar/handlers_test.go @@ -412,11 +412,10 @@ func TestModifyServiceSettings_LogForward_PolicyAllow_ForwardsToGCS(t *testing.T } } -// TestModifyServiceSettings_LogForward_PolicyDeny_ReturnsErrorAndLocksDown +// TestModifyServiceSettings_LogForward_PolicyDeny_ReturnsErrorAndDoesNotForward // verifies that when any requested provider is denied by policy, the call -// fails (no forward to GCS) and the sidecar enforcer is locked down to -// closed-door so subsequent requests also deny. -func TestModifyServiceSettings_LogForward_PolicyDeny_ReturnsErrorAndLocksDown(t *testing.T) { +// fails and the request is not forwarded to inbox GCS. +func TestModifyServiceSettings_LogForward_PolicyDeny_ReturnsErrorAndDoesNotForward(t *testing.T) { b := newTestBridge(&securitypolicy.ClosedDoorSecurityPolicyEnforcer{}) payload := buildLogForwardServiceRequest(t, "microsoft.windows.hyperv.compute") @@ -434,13 +433,6 @@ func TestModifyServiceSettings_LogForward_PolicyDeny_ReturnsErrorAndLocksDown(t default: // Good. } - - // Enforcer should now be locked down. (ClosedDoor was already installed; - // LockDown's idempotency check keeps the same instance, so we verify the - // type rather than identity.) - if _, ok := b.hostState.securityOptions.PolicyEnforcer.(*securitypolicy.ClosedDoorSecurityPolicyEnforcer); !ok { - t.Errorf("after deny: expected ClosedDoor enforcer, got %T", b.hostState.securityOptions.PolicyEnforcer) - } } // droppingLogProviderEnforcer is a test stub that approves only the configured diff --git a/pkg/securitypolicy/securitypolicy_options.go b/pkg/securitypolicy/securitypolicy_options.go index a04b08f16c..35dd755367 100644 --- a/pkg/securitypolicy/securitypolicy_options.go +++ b/pkg/securitypolicy/securitypolicy_options.go @@ -40,36 +40,6 @@ func NewSecurityOptions(enforcer SecurityPolicyEnforcer, enforcerSet bool, uvmRe } } -// LockDown irreversibly replaces the active policy enforcer with a closed-door -// (deny-everything) enforcer in response to a policy violation. All subsequent -// Enforce* calls will deny. -// -// LockDown is idempotent with respect to the enforcer swap: if the enforcer is -// already a ClosedDoorSecurityPolicyEnforcer the swap and the error log are -// skipped. -// -// LockDown also sets PolicyEnforcerSet unconditionally so that a subsequent -// SetConfidentialOptions call (which checks PolicyEnforcerSet and refuses to -// install a policy if it is already set) cannot replace the closed-door -// enforcer with a permissive one. This makes lockdown sticky regardless of -// whether LockDown was called before or after the initial policy load, and in -// particular covers the boot-time case where the sidecar starts with a -// ClosedDoorSecurityPolicyEnforcer but PolicyEnforcerSet is still false. -func (s *SecurityOptions) LockDown(ctx context.Context) { - s.policyMutex.Lock() - defer s.policyMutex.Unlock() - // Mark the policy as set first, unconditionally. This is the property that - // makes lockdown sticky against a follow-up SetConfidentialOptions, and it - // must hold even when the enforcer is already a ClosedDoor instance - // (e.g. the sidecar's boot-time default before any user policy arrives). - s.PolicyEnforcerSet = true - if _, alreadyLocked := s.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); alreadyLocked { - return - } - log.G(ctx).Error("policy violation: locking down sidecar enforcer to closed-door; all subsequent policy decisions will deny") - s.PolicyEnforcer = &ClosedDoorSecurityPolicyEnforcer{} -} - // SetConfidentialOptions takes guestresource.ConfidentialOptions // to set up our internal data structures we use to store and enforce // security policy. The options can contain security policy enforcer type, diff --git a/pkg/securitypolicy/securitypolicy_options_test.go b/pkg/securitypolicy/securitypolicy_options_test.go deleted file mode 100644 index 71e71c3bf9..0000000000 --- a/pkg/securitypolicy/securitypolicy_options_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package securitypolicy - -import ( - "context" - "io" - "testing" -) - -// TestLockDown_SwapsEnforcerToClosedDoor verifies that LockDown replaces -// the active enforcer with a ClosedDoorSecurityPolicyEnforcer. -func TestLockDown_SwapsEnforcerToClosedDoor(t *testing.T) { - opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, true, "", io.Discard) - - if _, ok := opts.PolicyEnforcer.(*OpenDoorSecurityPolicyEnforcer); !ok { - t.Fatalf("setup: expected initial enforcer to be OpenDoor, got %T", opts.PolicyEnforcer) - } - - opts.LockDown(context.Background()) - - if _, ok := opts.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); !ok { - t.Errorf("after LockDown: expected ClosedDoor enforcer, got %T", opts.PolicyEnforcer) - } -} - -// TestLockDown_Idempotent verifies that LockDown is a no-op once already locked. -// The same enforcer instance is preserved on the second call. -func TestLockDown_Idempotent(t *testing.T) { - opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, true, "", io.Discard) - ctx := context.Background() - - opts.LockDown(ctx) - firstLocked := opts.PolicyEnforcer - - opts.LockDown(ctx) - secondLocked := opts.PolicyEnforcer - - if firstLocked != secondLocked { - t.Errorf("LockDown should be idempotent: enforcer pointer changed between calls (%p -> %p)", firstLocked, secondLocked) - } -} - -// TestLockDown_SetsPolicyEnforcerSet verifies that LockDown marks the policy -// as set, so that a subsequent SetConfidentialOptions call cannot replace -// the closed-door enforcer with a permissive one. -// -// SetConfidentialOptions short-circuits with an error when PolicyEnforcerSet -// is already true; this is the mechanism that makes LockDown sticky against -// future policy-install attempts regardless of call order. -func TestLockDown_SetsPolicyEnforcerSet(t *testing.T) { - // Construct with PolicyEnforcerSet=false to simulate LockDown being - // called before the initial policy was loaded. - opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, false, "", io.Discard) - - if opts.PolicyEnforcerSet { - t.Fatal("setup: expected PolicyEnforcerSet=false before LockDown") - } - - opts.LockDown(context.Background()) - - if !opts.PolicyEnforcerSet { - t.Error("expected PolicyEnforcerSet=true after LockDown so subsequent SetConfidentialOptions refuses to install a policy") - } -} - -// TestLockDown_StickyAgainstSetConfidentialOptions verifies the end-to-end -// stickiness property: after LockDown, SetConfidentialOptions refuses to -// install a new (potentially permissive) policy and the enforcer remains -// closed-door. -func TestLockDown_StickyAgainstSetConfidentialOptions(t *testing.T) { - opts := NewSecurityOptions(&OpenDoorSecurityPolicyEnforcer{}, false, "", io.Discard) - ctx := context.Background() - - opts.LockDown(ctx) - - // Try to install a fresh policy after lockdown. The actual policy - // content does not matter — SetConfidentialOptions should refuse on - // the PolicyEnforcerSet check before touching anything else. - err := opts.SetConfidentialOptions(ctx, "", "", "") - if err == nil { - t.Fatal("expected SetConfidentialOptions to refuse after LockDown") - } - - if _, ok := opts.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); !ok { - t.Errorf("after LockDown + SetConfidentialOptions: enforcer was replaced; got %T, want ClosedDoor", opts.PolicyEnforcer) - } -} - -// TestLockDown_StickyFromBootClosedDoor covers the boot-time case the -// sidecar actually hits: the enforcer is already a ClosedDoor instance (the -// PSP fail-close default in cmd/gcs-sidecar/main.go) and PolicyEnforcerSet -// is still false because no user policy has been loaded yet. LockDown must -// still flip PolicyEnforcerSet so that a follow-up SetConfidentialOptions -// is refused, otherwise a permissive policy could replace the closed door. -func TestLockDown_StickyFromBootClosedDoor(t *testing.T) { - opts := NewSecurityOptions(&ClosedDoorSecurityPolicyEnforcer{}, false, "", io.Discard) - ctx := context.Background() - - opts.LockDown(ctx) - - if !opts.PolicyEnforcerSet { - t.Fatal("expected PolicyEnforcerSet=true after LockDown on a boot-time ClosedDoor enforcer; otherwise SetConfidentialOptions would still accept a fresh policy") - } - - err := opts.SetConfidentialOptions(ctx, "", "", "") - if err == nil { - t.Fatal("expected SetConfidentialOptions to refuse after LockDown on a boot-time ClosedDoor enforcer") - } - - if _, ok := opts.PolicyEnforcer.(*ClosedDoorSecurityPolicyEnforcer); !ok { - t.Errorf("after LockDown + SetConfidentialOptions: enforcer was replaced; got %T, want ClosedDoor", opts.PolicyEnforcer) - } -} From 7e59bcf88c6835ace1824f9084c56ff992b00c74 Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 22 Jun 2026 15:43:57 +0100 Subject: [PATCH 10/22] Block unsupported property type and RPC type Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 5 +- internal/gcs-sidecar/handlers_test.go | 72 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 83e662ef4f..80d6200a62 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -530,7 +530,6 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { defer span.End() defer func() { oc.SetSpanStatus(span, err) }() - // Todo: Add policy enforcement for modifying service settings modifyRequest, err := unmarshalModifyServiceSettings(req) if err != nil { return fmt.Errorf("failed to unmarshal modifyServiceSettings request: %w", err) @@ -642,7 +641,7 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { settings.Settings = allowedLogSources } default: - log.G(req.ctx).Warningf("modifyServiceSettings for LogForwardService with RPCType: %v, skipping policy enforcement", settings.RPCType) + return fmt.Errorf("modifyServiceSettings for LogForwardService: unsupported RPCType %q", settings.RPCType) } modifyRequest.Settings = settings buf, err := json.Marshal(modifyRequest) @@ -659,7 +658,7 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { log.G(req.ctx).Warningf("modifyServiceSettings for LogForwardService with empty settings, skipping policy enforcement") } default: - log.G(req.ctx).Warningf("modifyServiceSettings with PropertyType: %v, skipping policy enforcement", modifyRequest.PropertyType) + return fmt.Errorf("modifyServiceSettings: unsupported PropertyType %q", modifyRequest.PropertyType) } b.forwardRequestToGcs(req) return nil diff --git a/internal/gcs-sidecar/handlers_test.go b/internal/gcs-sidecar/handlers_test.go index aaf6e0f7f4..fa7c4ca41f 100644 --- a/internal/gcs-sidecar/handlers_test.go +++ b/internal/gcs-sidecar/handlers_test.go @@ -577,3 +577,75 @@ func TestModifyServiceSettings_LogForward_PolicyDropping_NoFalsePositive(t *test } } } + +// TestModifyServiceSettings_UnsupportedPropertyType_Denied verifies that a +// ModifyServiceSettings request whose PropertyType is not one the sidecar +// structurally understands is rejected and not forwarded to inbox GCS. +// +// An empty PropertyType is used because unmarshalModifyServiceSettings only +// validates non-empty PropertyType values, so this is the path that actually +// reaches the handler's outer switch default. +func TestModifyServiceSettings_UnsupportedPropertyType_Denied(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + r := prot.ServiceModificationRequest{ + RequestBase: prot.RequestBase{ + ContainerID: UVMContainerID, + ActivityID: guid.GUID{}, + }, + // PropertyType deliberately empty to exercise the handler's + // outer-switch default branch. + PropertyType: "", + } + payload, err := json.Marshal(r) + if err != nil { + t.Fatalf("failed to marshal request: %v", err) + } + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err == nil { + t.Fatal("expected modifyServiceSettings to fail for unsupported PropertyType") + } + + select { + case fwd := <-b.sendToGCSCh: + t.Fatalf("request with unsupported PropertyType must not be forwarded to GCS: %+v", fwd) + default: + // Good. + } +} + +// TestModifyServiceSettings_LogForward_UnsupportedRPCType_Denied verifies +// that a LogForwardService request carrying an RPCType the sidecar does not +// recognise is rejected and not forwarded to inbox GCS. +func TestModifyServiceSettings_LogForward_UnsupportedRPCType_Denied(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + inner := &guestrequest.LogForwardServiceRPCRequest{ + RPCType: guestrequest.RPCType("UnsupportedRPCType"), + } + r := prot.ServiceModificationRequest{ + RequestBase: prot.RequestBase{ + ContainerID: UVMContainerID, + ActivityID: guid.GUID{}, + }, + PropertyType: string(prot.LogForwardService), + Settings: inner, + } + payload, err := json.Marshal(r) + if err != nil { + t.Fatalf("failed to marshal request: %v", err) + } + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err == nil { + t.Fatal("expected modifyServiceSettings to fail for unsupported RPCType") + } + + select { + case fwd := <-b.sendToGCSCh: + t.Fatalf("request with unsupported RPCType must not be forwarded to GCS: %+v", fwd) + default: + // Good. + } +} From b665de2b0099a2efc860f921dc1d3083d86f1c90 Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Mon, 22 Jun 2026 17:42:11 +0100 Subject: [PATCH 11/22] Validate Name and GUID pair before policy enforcement Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 53 ++++++ internal/gcs-sidecar/handlers_test.go | 176 +++++++++++++++++++ internal/vm/vmutils/etw/provider_map.go | 6 +- internal/vm/vmutils/etw/provider_map_test.go | 12 +- 4 files changed, 238 insertions(+), 9 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 80d6200a62..2802636cd6 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -552,6 +552,12 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { return fmt.Errorf("failed to decode log sources: %w", err) } + // Validate host-supplied (Name, GUID) pairs before + // name-based policy enforcement. + if err := validateLogProviders(logSources.LogConfig.Sources); err != nil { + return fmt.Errorf("log providers rejected: %w", err) + } + // Collect every requested provider name and ask the // enforcer to validate them as a batch. The enforcer's // behaviour depends on allow_log_provider_dropping in the @@ -664,6 +670,53 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { return nil } +// validateLogProviders validates host-supplied log providers before they +// reach the name-based policy enforcer. +// +// CWCOW policy approves provider names, but inbox GCS subscribes by GUID. If +// the host could send {Name: "allowed", GUID: ""} the name-based +// enforcer would approve and the disallowed GUID would still be forwarded +// (resolveGUIDsWithLookup keeps any GUID the host set). To close that bypass +// the sidecar rejects, before enforcement, any entry whose (Name, GUID) pair +// is not verifiable against the well-known ETW map: +// +// - Name == "": rejected. Policy is name-based; a GUID-only entry has +// nothing for the enforcer to evaluate. +// - Name + GUID where Name is not in the well-known map: rejected. We have +// no ground truth to compare the GUID against, so we cannot verify the +// host's claim. Name-only is still accepted for downstream resolution to +// stay best-effort. +// - Name + GUID where the GUID disagrees with the well-known lookup for +// Name: rejected. +// +// Name-only entries are passed through unchanged; the sidecar fills in the +// canonical GUID after enforcement via etw.UpdateLogSourcesFromInfo. +func validateLogProviders(sources []etw.Source) error { + for _, src := range sources { + for _, p := range src.Providers { + if p.ProviderName == "" { + return fmt.Errorf("provider with no name is not allowed (GUID %q)", p.ProviderGUID) + } + if p.ProviderGUID == "" { + continue + } + well := etw.GetProviderGUIDFromName(p.ProviderName) + if well == "" { + return fmt.Errorf("provider %q: name not in well-known ETW map; cannot verify supplied GUID %q", p.ProviderName, p.ProviderGUID) + } + suppliedTrimmed := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(p.ProviderGUID), "{"), "}") + supplied, err := guid.FromString(suppliedTrimmed) + if err != nil { + return fmt.Errorf("provider %q: invalid GUID %q: %w", p.ProviderName, p.ProviderGUID, err) + } + if !strings.EqualFold(supplied.String(), well) { + return fmt.Errorf("provider %q: supplied GUID %q does not match well-known GUID %q", p.ProviderName, p.ProviderGUID, well) + } + } + } + return nil +} + func volumeGUIDFromLayerPath(path string) (string, bool) { if p, ok := strings.CutPrefix(path, `\\?\Volume{`); ok { if q, ok := strings.CutSuffix(p, `}\Files`); ok { diff --git a/internal/gcs-sidecar/handlers_test.go b/internal/gcs-sidecar/handlers_test.go index fa7c4ca41f..923a72845a 100644 --- a/internal/gcs-sidecar/handlers_test.go +++ b/internal/gcs-sidecar/handlers_test.go @@ -649,3 +649,179 @@ func TestModifyServiceSettings_LogForward_UnsupportedRPCType_Denied(t *testing.T // Good. } } + +// buildLogForwardServiceRequestWithProviders is the variant of +// buildLogForwardServiceRequest that lets each test set ProviderName and +// ProviderGUID independently, so the validateLogProviders tests can +// exercise mismatched and GUID-only payloads. +func buildLogForwardServiceRequestWithProviders(t *testing.T, providers []etw.EtwProvider) []byte { + t.Helper() + + info := etw.LogSourcesInfo{ + LogConfig: etw.LogConfig{ + Sources: []etw.Source{{ + Type: "etw", + Providers: providers, + }}, + }, + } + infoBytes, err := json.Marshal(info) + if err != nil { + t.Fatalf("failed to marshal log sources: %v", err) + } + encoded := base64.StdEncoding.EncodeToString(infoBytes) + + inner := &guestrequest.LogForwardServiceRPCRequest{ + RPCType: guestrequest.RPCModifyServiceSettings, + Settings: encoded, + } + req := prot.ServiceModificationRequest{ + RequestBase: prot.RequestBase{ + ContainerID: UVMContainerID, + ActivityID: guid.GUID{}, + }, + PropertyType: string(prot.LogForwardService), + Settings: inner, + } + b, err := json.Marshal(req) + if err != nil { + t.Fatalf("failed to marshal request: %v", err) + } + return b +} + +// TestModifyServiceSettings_LogForward_GUIDOnly_Denied verifies that a +// provider entry with ProviderName=="" (GUID-only) is rejected before +// reaching policy enforcement. CWCOW policy is name-based, so a GUID-only +// entry has nothing for the enforcer to evaluate; accepting it would let the +// host smuggle a disallowed GUID past name-based policy. +func TestModifyServiceSettings_LogForward_GUIDOnly_Denied(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + payload := buildLogForwardServiceRequestWithProviders(t, []etw.EtwProvider{ + {ProviderName: "", ProviderGUID: "80ce50de-d264-4581-950d-abadeee0d340"}, + }) + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err == nil { + t.Fatal("expected modifyServiceSettings to reject GUID-only provider entry") + } + + select { + case fwd := <-b.sendToGCSCh: + t.Fatalf("rejected request must not be forwarded to GCS: %+v", fwd) + default: + // Good. + } +} + +// TestModifyServiceSettings_LogForward_NameGUIDMismatch_Denied verifies that +// a provider entry whose ProviderGUID disagrees with the well-known map +// lookup for ProviderName is rejected. Without this check a hostile host +// could pair an allowed Name with a disallowed GUID and bypass name-based +// policy because inbox GCS subscribes by GUID. +func TestModifyServiceSettings_LogForward_NameGUIDMismatch_Denied(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + // Name resolves to 80ce50de-d264-4581-950d-abadeee0d340 in the + // well-known map; deliberately supply an unrelated valid GUID. + payload := buildLogForwardServiceRequestWithProviders(t, []etw.EtwProvider{ + { + ProviderName: "microsoft.windows.hyperv.compute", + ProviderGUID: "11111111-2222-3333-4444-555555555555", + }, + }) + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err == nil { + t.Fatal("expected modifyServiceSettings to reject Name/GUID mismatch") + } + + select { + case fwd := <-b.sendToGCSCh: + t.Fatalf("rejected request must not be forwarded to GCS: %+v", fwd) + default: + // Good. + } +} + +// TestModifyServiceSettings_LogForward_UnknownNameWithGUID_Denied verifies +// that a provider entry whose ProviderName is not in the well-known ETW map +// is rejected when paired with a ProviderGUID: the sidecar has no ground +// truth to verify the host's claim against. +func TestModifyServiceSettings_LogForward_UnknownNameWithGUID_Denied(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + payload := buildLogForwardServiceRequestWithProviders(t, []etw.EtwProvider{ + { + ProviderName: "unknown-provider", + ProviderGUID: "11111111-2222-3333-4444-555555555555", + }, + }) + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err == nil { + t.Fatal("expected modifyServiceSettings to reject unknown Name + GUID") + } + + select { + case fwd := <-b.sendToGCSCh: + t.Fatalf("rejected request must not be forwarded to GCS: %+v", fwd) + default: + // Good. + } +} + +// TestModifyServiceSettings_LogForward_NameMatchingGUID_Allowed verifies the +// positive path of validateLogProviders: a provider entry where +// ProviderGUID matches the well-known lookup for ProviderName passes +// validation and is forwarded to inbox GCS. +func TestModifyServiceSettings_LogForward_NameMatchingGUID_Allowed(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + payload := buildLogForwardServiceRequestWithProviders(t, []etw.EtwProvider{ + { + ProviderName: "microsoft.windows.hyperv.compute", + ProviderGUID: "80ce50de-d264-4581-950d-abadeee0d340", + }, + }) + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err != nil { + t.Fatalf("modifyServiceSettings with matching Name/GUID returned error: %v", err) + } + + select { + case <-b.sendToGCSCh: + // Forwarded to GCS as expected. + case <-time.After(time.Second): + t.Fatal("timed out waiting for request to be forwarded to GCS") + } +} + +// TestModifyServiceSettings_LogForward_BracedGUID_Allowed verifies that the +// validator accepts GUID strings wrapped in `{...}` braces (the common +// canonical form on Windows). The well-known map stores the un-braced form, +// so the comparison must be brace-insensitive. +func TestModifyServiceSettings_LogForward_BracedGUID_Allowed(t *testing.T) { + b := newTestBridge(&securitypolicy.OpenDoorSecurityPolicyEnforcer{}) + + payload := buildLogForwardServiceRequestWithProviders(t, []etw.EtwProvider{ + { + ProviderName: "microsoft.windows.hyperv.compute", + ProviderGUID: "{80ce50de-d264-4581-950d-abadeee0d340}", + }, + }) + req := newModifyServiceSettingsRequest(payload) + + if err := b.modifyServiceSettings(req); err != nil { + t.Fatalf("modifyServiceSettings with braced matching GUID returned error: %v", err) + } + + select { + case <-b.sendToGCSCh: + // Forwarded to GCS as expected. + case <-time.After(time.Second): + t.Fatal("timed out waiting for request to be forwarded to GCS") + } +} diff --git a/internal/vm/vmutils/etw/provider_map.go b/internal/vm/vmutils/etw/provider_map.go index f07d254af5..9a37022c2a 100644 --- a/internal/vm/vmutils/etw/provider_map.go +++ b/internal/vm/vmutils/etw/provider_map.go @@ -36,7 +36,7 @@ func GetDefaultLogSources() LogSourcesInfo { } // GetProviderGUIDFromName returns the provider GUID for a given provider name. If the provider name is not found in the map, it returns an empty string. -func getProviderGUIDFromName(providerName string) string { +func GetProviderGUIDFromName(providerName string) string { if guid, ok := etwNameToGUIDMap[strings.ToLower(providerName)]; ok { return guid } @@ -127,7 +127,7 @@ func resolveGUIDsWithLookup(sources []Source) ([]Source, error) { sources[i].Providers[j].ProviderGUID = strings.ToLower(guid.String()) } if provider.ProviderName != "" && provider.ProviderGUID == "" { - sources[i].Providers[j].ProviderGUID = getProviderGUIDFromName(provider.ProviderName) + sources[i].Providers[j].ProviderGUID = GetProviderGUIDFromName(provider.ProviderName) } } } @@ -147,7 +147,7 @@ func stripRedundantGUIDs(sources []Source) ([]Source, error) { if err != nil { return nil, fmt.Errorf("invalid GUID %q for provider %q: %w", provider.ProviderGUID, provider.ProviderName, err) } - if strings.EqualFold(guid.String(), getProviderGUIDFromName(provider.ProviderName)) { + if strings.EqualFold(guid.String(), GetProviderGUIDFromName(provider.ProviderName)) { sources[i].Providers[j].ProviderGUID = "" } else { // If the GUID doesn't match the well-known GUID for the provider name, diff --git a/internal/vm/vmutils/etw/provider_map_test.go b/internal/vm/vmutils/etw/provider_map_test.go index 4aa62d861c..55ce1b4ce3 100644 --- a/internal/vm/vmutils/etw/provider_map_test.go +++ b/internal/vm/vmutils/etw/provider_map_test.go @@ -23,9 +23,9 @@ func TestGetProviderGUIDFromName(t *testing.T) { } for _, tt := range tests { - got := getProviderGUIDFromName(tt.name) + got := GetProviderGUIDFromName(tt.name) if got != tt.expected { - t.Errorf("getProviderGUIDFromName(%q) = %q, want %q", tt.name, got, tt.expected) + t.Errorf("GetProviderGUIDFromName(%q) = %q, want %q", tt.name, got, tt.expected) } } } @@ -126,11 +126,11 @@ func buildTestUserLogSources(t *testing.T) LogSourcesInfo { nameOnlyProvider := "Microsoft.Windows.HyperV.Compute" nameAndGUIDProvider := "Microsoft.Windows.Containers.Setup" - guid := getProviderGUIDFromName(nameAndGUIDProvider) + guid := GetProviderGUIDFromName(nameAndGUIDProvider) if guid == "" { t.Fatalf("missing GUID mapping for provider %q", nameAndGUIDProvider) } - if getProviderGUIDFromName(nameOnlyProvider) == "" { + if GetProviderGUIDFromName(nameOnlyProvider) == "" { t.Fatalf("missing GUID mapping for provider %q", nameOnlyProvider) } @@ -185,7 +185,7 @@ func applyExpectedGUIDBehavior(cfg *LogSourcesInfo, includeGUIDs bool) { } } if provider.ProviderName != "" && provider.ProviderGUID == "" { - cfg.LogConfig.Sources[i].Providers[j].ProviderGUID = getProviderGUIDFromName(provider.ProviderName) + cfg.LogConfig.Sources[i].Providers[j].ProviderGUID = GetProviderGUIDFromName(provider.ProviderName) } continue } @@ -195,7 +195,7 @@ func applyExpectedGUIDBehavior(cfg *LogSourcesInfo, includeGUIDs bool) { if err != nil { continue } - if strings.EqualFold(guid.String(), getProviderGUIDFromName(provider.ProviderName)) { + if strings.EqualFold(guid.String(), GetProviderGUIDFromName(provider.ProviderName)) { cfg.LogConfig.Sources[i].Providers[j].ProviderGUID = "" } else { cfg.LogConfig.Sources[i].Providers[j].ProviderGUID = strings.ToLower(guid.String()) From f97bf028c99ef47883558828222c81dcb9cbb6ee Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Thu, 2 Jul 2026 11:34:07 +0100 Subject: [PATCH 12/22] Extract the conversion from string to LogSourcesInfo to a helper function Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers.go | 98 +++++++++++++------------------- 1 file changed, 40 insertions(+), 58 deletions(-) diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 37b2cff8e3..ff066d57d1 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -4,6 +4,7 @@ package bridge import ( + "context" "encoding/hex" "encoding/json" "fmt" @@ -579,68 +580,12 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) { return fmt.Errorf("log providers denied by policy: %w", err) } - // Build a quick lookup for the kept set so we can trim the - // LogSourcesInfo to only those providers the policy allowed. - keepSet := make(map[string]struct{}, len(keptNames)) - for _, name := range keptNames { - keepSet[name] = struct{}{} - } - - // Detect trimming by scanning requested names against - // keepSet. We cannot use len(kept) != len(requested): - // the rego enforcer returns providers_to_keep via a set - // (see getProvidersToKeep → keepSet.toArray()), so a - // duplicate-name request like [A, A, B] returns [A, B] - // even when nothing was dropped, which would otherwise - // trip a false-positive warning and a needless re-marshal. - dropped := make([]string, 0) - seenDropped := make(map[string]struct{}) - for _, name := range requestedNames { - if _, ok := keepSet[name]; ok { - continue - } - if _, dup := seenDropped[name]; dup { - continue - } - seenDropped[name] = struct{}{} - dropped = append(dropped, name) - } - - // Trim happens in-place on the parsed structure so we can - // hand it to UpdateLogSourcesFromInfo without a redundant - // base64-decode + JSON-unmarshal round-trip (we already - // decoded above for enforcement). - trimmed := logSources - if len(dropped) > 0 { - // Surface the drop so operators have a breadcrumb — - // under allow_log_provider_dropping the pod boots - // silently, and forwardlogs may itself be off, so - // without this warning the trim is invisible. - log.G(req.ctx).WithFields(map[string]interface{}{ - "requested": requestedNames, - "kept": keptNames, - "dropped": dropped, - }).Warn("log providers trimmed by policy (allow_log_provider_dropping)") - - // Trim each source's provider list to only the - // allowed names. Empty sources are preserved to keep - // the shape stable; inbox GCS handles them as no-ops. - for i := range trimmed.LogConfig.Sources { - src := &trimmed.LogConfig.Sources[i] - filtered := make([]etw.EtwProvider, 0, len(src.Providers)) - for _, p := range src.Providers { - if _, ok := keepSet[p.ProviderName]; ok { - filtered = append(filtered, p) - } - } - src.Providers = filtered - } - } + filtered := filterLogSourcesToAllowed(req.ctx, logSources, keptNames) // Apply GUID resolution (and any other inbox-GCS prep) // against the policy-trimmed payload and hand off to // inbox GCS. - allowedLogSources, err := etw.UpdateLogSourcesFromInfo(trimmed, false, true) + allowedLogSources, err := etw.UpdateLogSourcesFromInfo(filtered, false, true) if err != nil { return fmt.Errorf("failed to update log sources: %w", err) } @@ -717,6 +662,43 @@ func validateLogProviders(sources []etw.Source) error { return nil } +func filterLogSourcesToAllowed(ctx context.Context, sources etw.LogSourcesInfo, keptNames []string) etw.LogSourcesInfo { + keepSet := make(map[string]struct{}, len(keptNames)) + for _, name := range keptNames { + keepSet[name] = struct{}{} + } + + var requestedNames []string + dropped := make([]string, 0) + seenDropped := make(map[string]struct{}) + for i := range sources.LogConfig.Sources { + src := &sources.LogConfig.Sources[i] + filtered := make([]etw.EtwProvider, 0, len(src.Providers)) + for _, p := range src.Providers { + requestedNames = append(requestedNames, p.ProviderName) + if _, ok := keepSet[p.ProviderName]; ok { + filtered = append(filtered, p) + continue + } + if _, dup := seenDropped[p.ProviderName]; !dup { + seenDropped[p.ProviderName] = struct{}{} + dropped = append(dropped, p.ProviderName) + } + } + src.Providers = filtered + } + + if len(dropped) > 0 { + log.G(ctx).WithFields(map[string]interface{}{ + "requested": requestedNames, + "kept": keptNames, + "dropped": dropped, + }).Warn("log providers trimmed by policy (allow_log_provider_dropping)") + } + + return sources +} + func volumeGUIDFromLayerPath(path string) (string, bool) { if p, ok := strings.CutPrefix(path, `\\?\Volume{`); ok { if q, ok := strings.CutSuffix(p, `}\Files`); ok { From 42e84eb98ef1f298a26d44da2dd59ee53005b00d Mon Sep 17 00:00:00 2001 From: Takuro Sato Date: Thu, 2 Jul 2026 11:43:41 +0100 Subject: [PATCH 13/22] Avoid change the global logger for testing Signed-off-by: Takuro Sato --- internal/gcs-sidecar/handlers_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/gcs-sidecar/handlers_test.go b/internal/gcs-sidecar/handlers_test.go index 923a72845a..1460063533 100644 --- a/internal/gcs-sidecar/handlers_test.go +++ b/internal/gcs-sidecar/handlers_test.go @@ -13,6 +13,7 @@ import ( "github.com/Microsoft/go-winio/pkg/guid" "github.com/Microsoft/hcsshim/internal/gcs/prot" + "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/internal/vm/vmutils/etw" @@ -550,12 +551,13 @@ func TestModifyServiceSettings_LogForward_PolicyDropping_NoFalsePositive(t *test payload := buildLogForwardServiceRequest(t, name, name) req := newModifyServiceSettingsRequest(payload) + // Scope the capture hook to a request-local logger (rather than mutating + // the global logrus) by injecting it into the request context. + logger := logrus.New() + logger.SetOutput(io.Discard) hook := &captureHook{} - logrus.AddHook(hook) - defer func() { - // logrus has no public RemoveHook; reset all hooks to clear ours. - logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{}) - }() + logger.AddHook(hook) + req.ctx, _ = log.WithContext(req.ctx, logrus.NewEntry(logger)) if err := b.modifyServiceSettings(req); err != nil { t.Fatalf("modifyServiceSettings under dropping enforcer (dedup) returned error: %v", err) From 75300bba012a0f4d3f152548e1ce2ba3ca6cf7ed Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 15:46:37 +0000 Subject: [PATCH 14/22] rego: Fix unable to start container with empty env list when non-required rules present This refactors rule_ok (and renames it) to fix the `some env in envList` being applied at the wrong level. Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index c92f83f73d..6552e8bad8 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -297,21 +297,26 @@ env_rule_ok(rule, env) { env_pattern_ok(rule_value, value_strategy, env_value) } -rule_ok(rule, env) { - not rule.required -} - -rule_ok(rule, env) { +# For a required env rule, check that envList contains a matching env var for +# it. +env_required_rule_ok(rule, envList) { rule.required + some env in envList env_rule_ok(rule, env) } +# If it's not required, skip the check +env_required_rule_ok(rule, envList) { + not rule.required +} + envList_ok(env_rules, envList) { + # Check that all required rules are satisfied every rule in env_rules { - some env in envList - rule_ok(rule, env) + env_required_rule_ok(rule, envList) } + # Check that any env provided is allowed every env in envList { some rule in env_rules env_rule_ok(rule, env) From 8839e484b0373856a8d47c5b56284f04040ade12 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 20:38:07 +0000 Subject: [PATCH 15/22] framework.rego: Refactor policy_containers to remove code duplication Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index 6552e8bad8..a59be39b25 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -146,25 +146,22 @@ overlay_mounted(target) { data.metadata.overlayTargets[target] } -default candidate_containers := [] +# Note that a valid policy might not even define (data.policy.)containers, if +# all its containers are coming from fragments. This default rule prevents +# breaking other rules in this case. +default policy_containers := [] -candidate_containers := containers { +policy_containers := pc { semver.compare(policy_framework_version, version) == 0 - - policy_containers := [c | c := data.policy.containers[_]] - fragment_containers := [c | - feed := data.metadata.issuers[_].feeds[_] - fragment := feed[_] - c := fragment.containers[_] - ] - - containers := array.concat(policy_containers, fragment_containers) + pc := data.policy.containers } -candidate_containers := containers { +policy_containers := pc { semver.compare(policy_framework_version, version) < 0 + pc := apply_defaults("container", data.policy.containers, policy_framework_version) +} - policy_containers := apply_defaults("container", data.policy.containers, policy_framework_version) +candidate_containers := containers { fragment_containers := [c | feed := data.metadata.issuers[_].feeds[_] fragment := feed[_] From a249f98713842a083773b100f973655cf5edc333 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Tue, 18 Nov 2025 23:38:56 +0000 Subject: [PATCH 16/22] rego: Implement platform_rules support This currently support containers[_].env_rules and containers[_].mounts. If multiple platform_rules are defined, a container matching either one can be started (but in a consistent manner - e.g. if two platforms have different environment variables or mounts, a container can't "mix and match" between them). In order to achieve the above consistency, we "patch" the container objects instead of adding logic to e.g. env_rule_ok or envList_ok. This also means that the error_objects of a denial message will reflect the platform rules inserted. Signed-off-by: Tingmao Wang --- pkg/securitypolicy/framework.rego | 102 ++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index a59be39b25..ef47564d83 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -168,7 +168,18 @@ candidate_containers := containers { c := fragment.containers[_] ] - containers := array.concat(policy_containers, fragment_containers) + containers_raw := array.concat(policy_containers, fragment_containers) + + # Each container definition applied with platform_rules might turn into + # multiple containers if multiple platforms are allowed. We flatten the + # result here. + after_platform_rules := [c2 | + c1 := containers_raw[_] + applied := apply_platform_rules("container", c1) + c2 := applied[_] + ] + + containers := after_platform_rules } default mount_cims := {"allowed": false} @@ -1196,21 +1207,28 @@ runtime_logging := {"allowed": true} { allow_runtime_logging } -default fragment_containers := [] +# Helpers to get data from the fragment that is currently being loaded. Since +# input.namespace is the package name the fragment loaded as, +# data[input.namespace] can be used to access the fragment. This is only valid +# during a load_fragment call - the content exported by the fragment needs to be +# persisted into the metadata for later enforcement use. (c.f. +# extract_fragment_includes) +default fragment_containers := [] fragment_containers := data[input.namespace].containers default fragment_fragments := [] - fragment_fragments := data[input.namespace].fragments default fragment_external_processes := [] - fragment_external_processes := data[input.namespace].external_processes default fragment_transparency_trust_lists := [] fragment_transparency_trust_lists := data[input.namespace].transparency_trust_lists +default fragment_platform_rules := [] +fragment_platform_rules := data[input.namespace].platform_rules + apply_defaults(name, raw_values, framework_version) := values { semver.compare(framework_version, version) == 0 values := raw_values @@ -1255,6 +1273,22 @@ apply_defaults("transparency_trust_lists", raw_values, framework_version) := val values := [] } +# platform_rules is introduced in framework version 0.5.0. If an old policy has it, +# silently ignore as it might be using the name for something else. + +apply_defaults("platform_rules", raw_values, framework_version) := values { + semver.compare(framework_version, version) < 0 + semver.compare(framework_version, "0.5.0") >= 0 + # This is currently unreachable, otherwise we would call something like + # check_platform_rule here, like above (not defined yet). + values := raw_values +} + +apply_defaults("platform_rules", raw_values, framework_version) := values { + semver.compare(framework_version, "0.5.0") < 0 + values := [] +} + default fragment_framework_version := null fragment_framework_version := data[input.namespace].framework_version @@ -1265,6 +1299,7 @@ extract_fragment_includes(includes) := fragment { "fragments": apply_defaults("fragment", fragment_fragments, framework_version), "external_processes": apply_defaults("external_process", fragment_external_processes, framework_version), "transparency_trust_lists": apply_defaults("transparency_trust_lists", fragment_transparency_trust_lists, framework_version), + "platform_rules": apply_defaults("platform_rules", fragment_platform_rules, framework_version), } fragment := { @@ -1770,6 +1805,65 @@ extract_parameter(name, fragment_parameters_obj, parameters_metadata) := fragmen "default" in object.keys(parameters_metadata[name]) } +default policy_platform_rules := [] + +policy_platform_rules := platform_rules { + semver.compare(policy_framework_version, version) == 0 + platform_rules := data.policy.platform_rules +} + +# For policy with framework_version < 0.5.0, apply_defaults will ignore +# platform_rules and return []. + +policy_platform_rules := platform_rules { + semver.compare(policy_framework_version, version) < 0 + platform_rules := apply_defaults("platform_rules", data.policy.platform_rules, policy_framework_version) +} + +default candidate_platform_rules := [] + +candidate_platform_rules := platform_rules { + fragment_platform_rules := [r | + feed := data.metadata.issuers[_].feeds[_] + fragment := feed[_] + r := fragment.platform_rules[_] + ] + + platform_rules := array.concat(policy_platform_rules, fragment_platform_rules) +} + +# apply_platform_rules("container", c) applies "platform_rules" to the container +# object c, and returns a list of containers which are the input container with +# platform rules applied. The return value may contain more than one container +# if multiple platform rules are defined. + +# No platform rules - return as-is. +apply_platform_rules("container", container) := updated_containers { + count(candidate_platform_rules) == 0 + updated_containers := [container] +} + +apply_platform_rules("container", container) := updated_containers { + count(candidate_platform_rules) > 0 + updated_containers := [updated_container | + platform_rule := candidate_platform_rules[_] + updated_container := apply_single_platform_rule("container", container, platform_rule) + ] +} + +apply_single_platform_rule("container", container, platform_rule) := updated_container { + container_env_rules := object.get(container, "env_rules", []) + updated_env_rules := array.concat(container_env_rules, object.get(platform_rule, "env_rules", [])) + + container_mounts := object.get(container, "mounts", []) + updated_mounts := array.concat(container_mounts, object.get(platform_rule, "mounts", [])) + + updated_container := object.union(container, { + "env_rules": updated_env_rules, + "mounts": updated_mounts, + }) +} + reason := { "errors": errors, "error_objects": error_objects From 3577113a8002002f087f6d89f1854fecd6e921e8 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Wed, 19 Nov 2025 15:02:29 +0000 Subject: [PATCH 17/22] rego: Fix create_container enforcement deny message missing input.rule field Currently we only add input.rule to the original input, not the redacted input. This results in the case of create_container not having the "rule" field in the final deny message, but other enforcement points do have it since in those cases the redactSensitiveData return the original input map. Signed-off-by: Tingmao Wang --- pkg/securitypolicy/securitypolicyenforcer_rego.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index f041c246aa..af9d59227c 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -408,9 +408,9 @@ func (policy *regoEnforcer) denyWithError(ctx context.Context, policyError error } func (policy *regoEnforcer) denyWithReason(ctx context.Context, enforcementPoint string, input inputData) error { + input["rule"] = enforcementPoint cleaned_input := policy.redactSensitiveData(input) cleaned_input = replaceCapabilitiesWithPlaceholders(cleaned_input) - input["rule"] = enforcementPoint policyDecision := map[string]interface{}{ "input": cleaned_input, "decision": "deny", From 4a42b64788920dc59dae19e6ee2f3b91e3761ddd Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Fri, 21 Nov 2025 01:11:35 +0000 Subject: [PATCH 18/22] regopolicy_linux_test: Test for platform_rules Signed-off-by: Tingmao Wang --- .../platform_rules.rego | 29 ++ .../policy_with_platform_rules.rego | 108 +++++ pkg/securitypolicy/rego_utils_test.go | 46 ++- pkg/securitypolicy/regopolicy_linux_test.go | 383 ++++++++++++++++++ 4 files changed, 545 insertions(+), 21 deletions(-) create mode 100644 pkg/securitypolicy/fragment_test_policies/platform_rules.rego create mode 100644 pkg/securitypolicy/policy_with_platform_rules.rego diff --git a/pkg/securitypolicy/fragment_test_policies/platform_rules.rego b/pkg/securitypolicy/fragment_test_policies/platform_rules.rego new file mode 100644 index 0000000000..b8d8e096a4 --- /dev/null +++ b/pkg/securitypolicy/fragment_test_policies/platform_rules.rego @@ -0,0 +1,29 @@ +package fragment + +svn := "1" +framework_version := "0.5.0" + +platform_rules := [ + { + "env_rules": [ + { + "name": "(?i)(FABRIC)_.+", + "name_strategy": "re2", + "value": ".+", + "value_strategy": "re2" + } + ], + "mounts": [ + { + "destination": "/var/run/secrets/kubernetes.io/serviceaccount", + "options": [ + "rbind", + "rshared", + "ro" + ], + "source": "sandbox:///tmp/atlas/emptydir/.+", + "type": "bind" + } + ] + } +] diff --git a/pkg/securitypolicy/policy_with_platform_rules.rego b/pkg/securitypolicy/policy_with_platform_rules.rego new file mode 100644 index 0000000000..a250992398 --- /dev/null +++ b/pkg/securitypolicy/policy_with_platform_rules.rego @@ -0,0 +1,108 @@ +package policy + +api_version := "@@API_VERSION@@" +framework_version := "@@FRAMEWORK_VERSION@@" + +fragments := [ + { + "feed": "@@FRAGMENT_FEED@@", + "includes": [ + "containers", + "fragments" + ], + "issuer": "@@FRAGMENT_ISSUER@@", + "minimum_svn": "0" + } +] + +platform_rules := [ + { + "env_rules": [ + { + "name": "(?i)(FABRIC)_.+", + "name_strategy": "re2", + "value": ".+", + "value_strategy": "re2" + } + ], + "mounts": [ + { + "destination": "/var/run/secrets/kubernetes.io/serviceaccount", + "options": [ + "rbind", + "rshared", + "ro" + ], + "source": "sandbox:///tmp/atlas/emptydir/.+", + "type": "bind" + } + ] + } +] + +containers := [ + { + "allow_elevated": false, + "allow_stdio_access": true, + "capabilities": { + "ambient": [], + "bounding": [], + "effective": [], + "inheritable": [], + "permitted": [] + }, + "command": [ "bash" ], + "env_rules": [], + "exec_processes": [], + "layers": [ + "0000000000000000000000000000000000000000000000000000000000000000", + ], + "mounts": [], + "no_new_privileges": false, + "seccomp_profile_sha256": "", + "signals": [], + "user": { + "group_idnames": [ + { + "pattern": "", + "strategy": "any" + } + ], + "umask": "0022", + "user_idname": { + "pattern": "", + "strategy": "any" + } + }, + "working_dir": "/" + } +] + +allow_properties_access := true +allow_dump_stacks := false +allow_runtime_logging := false +allow_environment_variable_dropping := true +allow_unencrypted_scratch := false +allow_capability_dropping := true + +mount_device := data.framework.mount_device +rw_mount_device := data.framework.rw_mount_device +unmount_device := data.framework.unmount_device +rw_unmount_device := data.framework.rw_unmount_device +mount_overlay := data.framework.mount_overlay +unmount_overlay := data.framework.unmount_overlay +mount_cims := data.framework.mount_cims +create_container := data.framework.create_container +exec_in_container := data.framework.exec_in_container +exec_external := data.framework.exec_external +shutdown_container := data.framework.shutdown_container +signal_container_process := data.framework.signal_container_process +plan9_mount := data.framework.plan9_mount +plan9_unmount := data.framework.plan9_unmount +get_properties := data.framework.get_properties +dump_stacks := data.framework.dump_stacks +runtime_logging := data.framework.runtime_logging +load_fragment := data.framework.load_fragment +scratch_mount := data.framework.scratch_mount +scratch_unmount := data.framework.scratch_unmount +reason := data.framework.reason diff --git a/pkg/securitypolicy/rego_utils_test.go b/pkg/securitypolicy/rego_utils_test.go index 4ece06a815..64f0ff2574 100644 --- a/pkg/securitypolicy/rego_utils_test.go +++ b/pkg/securitypolicy/rego_utils_test.go @@ -879,6 +879,30 @@ func setupRegoFragmentSVNMismatchTestConfig(gc *generatedConstraints) (*regoFrag return setupRegoFragmentTestConfig(gc, 2, []string{"containers"}, []string{}, false, false, false, true) } +func makeContainerFromGeneratedFragment(fragment *regoFragment) *regoFragmentContainer { + container := fragment.selectContainer() + + envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) + sandboxID := testDataGenerator.uniqueSandboxID() + user := buildIDNameFromConfig(container.User.UserIDName, testRand) + groups := buildGroupIDNamesFromUser(container.User, testRand) + capabilities := copyLinuxCapabilities(container.Capabilities.toExternal()) + seccomp := container.SeccompProfileSHA256 + + mounts := container.Mounts + mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) + return ®oFragmentContainer{ + container: container, + envList: envList, + sandboxID: sandboxID, + mounts: mountSpec.Mounts, + user: user, + groups: groups, + capabilities: &capabilities, + seccomp: seccomp, + } +} + func setupRegoFragmentTestConfig(gc *generatedConstraints, numFragments int, includes []string, excludes []string, svnError bool, sameIssuer bool, sameFeed bool, svnMismatch bool) (tc *regoFragmentTestConfig, err error) { gc.fragments = generateFragments(testRand, int32(numFragments)) @@ -902,27 +926,7 @@ func setupRegoFragmentTestConfig(gc *generatedConstraints, numFragments int, inc externalProcesses := make([]*externalProcess, numFragments) plan9Mounts := make([]string, numFragments) for i, fragment := range fragments { - container := fragment.selectContainer() - - envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) - sandboxID := testDataGenerator.uniqueSandboxID() - user := buildIDNameFromConfig(container.User.UserIDName, testRand) - groups := buildGroupIDNamesFromUser(container.User, testRand) - capabilities := copyLinuxCapabilities(container.Capabilities.toExternal()) - seccomp := container.SeccompProfileSHA256 - - mounts := container.Mounts - mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) - containers[i] = ®oFragmentContainer{ - container: container, - envList: envList, - sandboxID: sandboxID, - mounts: mountSpec.Mounts, - user: user, - groups: groups, - capabilities: &capabilities, - seccomp: seccomp, - } + containers[i] = makeContainerFromGeneratedFragment(fragment) for _, include := range fragment.info.includes { switch include { diff --git a/pkg/securitypolicy/regopolicy_linux_test.go b/pkg/securitypolicy/regopolicy_linux_test.go index 5a36461070..967b2076ef 100644 --- a/pkg/securitypolicy/regopolicy_linux_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -9345,6 +9345,374 @@ func Test_Rego_GetUserInfo_EtcPasswdOnly(t *testing.T) { } } +// Test platform rules in fragment, container in main policy +func Test_Rego_PlatformRules_InFragment1(t *testing.T) { + f := func(gc *generatedConstraints, skipLoadFragment bool, fragmentDontIncludePlatformRules bool) bool { + platformFragment := fragment{ + issuer: testDataGenerator.uniqueFragmentIssuer(), + feed: "infra", + minimumSVN: "1", + includes: []string{ + "platform_rules", + }, + } + + if fragmentDontIncludePlatformRules { + platformFragment.includes = []string{ + "containers", + } + } + + gc.fragments = []*fragment{&platformFragment} + + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Fatalf("failed to create rego policy: %v", err) + } + + if !skipLoadFragment { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: platformFragment.issuer, Feed: platformFragment.feed, Rego: platformRulesFragmentPolicyCode}) + if err != nil { + t.Fatalf("failed to load infra fragment: %v", err) + } + } + + container := selectContainerFromContainerList(gc.containers, testRand) + containerID, err := mountImageForContainer(policy, container) + if err != nil { + t.Errorf("failed to mount image for container: %v", err) + return false + } + + tc, err := createTestContainerSpec(gc, containerID, container, false, policy, defaultMounts, privilegedMounts) + if err != nil { + t.Fatalf("failed to create test container spec: %v", err) + } + tc.envList = append(tc.envList, "Fabric_NodeIPOrFqdn=10.0.0.1") + tc.mounts = append(tc.mounts, oci.Mount{ + Source: fmt.Sprintf("/run/gcs/c/%s/sandboxMounts/tmp/atlas/emptydir/serviceaccount", tc.sandboxID), + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }) + + envsToKeep, _, _, err := tc.policy.EnforceCreateContainerPolicy(gc.ctx, tc.sandboxID, tc.containerID, tc.argList, tc.envList, tc.workingDir, tc.mounts, false, tc.noNewPrivileges, tc.user, tc.groups, tc.umask, tc.capabilities, tc.seccomp) + + if skipLoadFragment || fragmentDontIncludePlatformRules { + if err == nil { + t.Error("expected error due to missing platform rules, got nil") + return false + } + assertDecisionJSONContains(t, err, "invalid env list: Fabric_NodeIPOrFqdn") + assertDecisionJSONContains(t, err, "invalid mount list: /var/run/secrets/kubernetes.io/serviceaccount") + return true + } + + // getting an error means something is broken + if err != nil { + t.Error(err) + return false + } + + slices.Sort(tc.envList) + slices.Sort(envsToKeep) + if !slices.Equal(tc.envList, envsToKeep) { + t.Errorf("expected envs to keep = %v, got %v", tc.envList, envsToKeep) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_PlatformRules_InFragment1 failed: %v", err) + } +} + +// Test platform rule and container both in separate fragment +func Test_Rego_PlatformRules_InFragment2(t *testing.T) { + f := func(gc *generatedConstraints, loadPlatformRulesFragmentFirst bool, skipLoadPlatformRulesFragment bool) bool { + // Generate some random fragments in the policy + gc.fragments = generateFragments(testRand, maxFragmentsInGeneratedConstraints) + // Pick one or two to use for container create test + containerFragments := selectFragmentsFromConstraints(gc, testRand.Intn(2)+1, []string{"containers"}, []string{}, false, frameworkVersion, false) + // Now add platform rules fragment in a random position + platformFragment := fragment{ + issuer: testDataGenerator.uniqueFragmentIssuer(), + feed: "infra", + minimumSVN: "1", + includes: []string{ + "platform_rules", + }, + } + gc.fragments = append(gc.fragments, &platformFragment) + testRand.Shuffle(len(gc.fragments), func(i, j int) { + gc.fragments[i], gc.fragments[j] = gc.fragments[j], gc.fragments[i] + }) + + containers := make([]*regoFragmentContainer, len(containerFragments)) + + // c.f. setupRegoFragmentTestConfig + for i, fragment := range containerFragments { + containers[i] = makeContainerFromGeneratedFragment(fragment) + + containers[i].envList = append(containers[i].envList, "Fabric_NodeIPOrFqdn=10.0.0.1") + containers[i].mounts = append(containers[i].mounts, oci.Mount{ + Source: fmt.Sprintf("/run/gcs/c/%s/sandboxMounts/tmp/atlas/emptydir/serviceaccount", containers[i].sandboxID), + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }) + + code := fragment.constraints.toFragment().marshalRego() + fragment.code = setFrameworkVersion(code, frameworkVersion) + } + + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Fatalf("failed to create rego policy: %v", err) + } + + if loadPlatformRulesFragmentFirst { + if !skipLoadPlatformRulesFragment { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: platformFragment.issuer, Feed: platformFragment.feed, Rego: platformRulesFragmentPolicyCode}) + if err != nil { + t.Fatalf("failed to load platform rules fragment: %v", err) + } + } + for _, containerFragment := range containerFragments { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: containerFragment.info.issuer, Feed: containerFragment.info.feed, Rego: containerFragment.code}) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + } else { + for _, containerFragment := range containerFragments { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: containerFragment.info.issuer, Feed: containerFragment.info.feed, Rego: containerFragment.code}) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + if !skipLoadPlatformRulesFragment { + err = policy.LoadFragment(gc.ctx, LoadFragmentOptions{Issuer: platformFragment.issuer, Feed: platformFragment.feed, Rego: platformRulesFragmentPolicyCode}) + if err != nil { + t.Fatalf("failed to load platform rules fragment: %v", err) + } + } + } + + for _, container := range containers { + containerID, err := mountImageForContainer(policy, container.container) + if err != nil { + t.Errorf("failed to mount image for container: %v", err) + return false + } + + _, _, _, err = policy.EnforceCreateContainerPolicy(gc.ctx, + container.sandboxID, + containerID, + copyStrings(container.container.Command), + copyStrings(container.envList), + container.container.WorkingDir, + copyMounts(container.mounts), + false, + container.container.NoNewPrivileges, + container.user, + container.groups, + container.container.User.Umask, + container.capabilities, + container.seccomp, + ) + + if !skipLoadPlatformRulesFragment { + if err != nil { + t.Errorf("unable to create container from fragment: %v", err) + return false + } + } else { + if err == nil { + t.Error("expected error due to missing platform rules, got nil") + return false + } + assertDecisionJSONContains(t, err, "invalid env list: Fabric_NodeIPOrFqdn") + assertDecisionJSONContains(t, err, "invalid mount list: /var/run/secrets/kubernetes.io/serviceaccount") + } + } + + return true + } + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_PlatformRules_InFragment2 failed: %v", err) + } +} + +// Test platform rules and container both in main policy +func Test_Rego_PlatformRules_InPolicy1(t *testing.T) { + // We don't actually use fragment in this test, but need some value as + // placeholder. + fragmentFeed := testDataGenerator.uniqueFragmentFeed() + fragmentIssuer := testDataGenerator.uniqueFragmentIssuer() + + rego := getPolicyCode_PolicyWithPlatformRules(fragmentFeed, fragmentIssuer) + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + p, err := newRegoPolicy(rego, + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Errorf("unable to create policy with platform rules: %v", err) + } + + container := &securityPolicyContainer{ + Command: []string{"bash"}, + EnvRules: []EnvRuleConfig{ + { + Required: true, + UseNameValue: true, + Name: "Fabric_NodeIPOrFqdn", + NameStrategy: EnvVarRuleString, + Value: "10.0.0.1", + ValueStrategy: EnvVarRuleString, + }, + }, + Layers: []string{ + paramTestImageBaseLayer, + }, + WorkingDir: "/", + Mounts: []mountInternal{ + { + Source: "sandbox:///tmp/atlas/emptydir/.+", + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }, + }, + User: UserConfig{ + UserIDName: generateIDNameConfig(testRand), + GroupIDNames: []IDNameConfig{ + generateIDNameConfig(testRand), + }, + Umask: "0022", + }, + Capabilities: &capabilitiesInternal{ + Ambient: []string{}, + Bounding: []string{}, + Effective: []string{}, + Inheritable: []string{}, + Permitted: []string{}, + }, + } + containerID, err := mountImageForContainer(p, container) + if err != nil { + t.Fatalf("failed to mount image for container: %v", err) + } + + tc, err := createTestContainerSpec(&generatedConstraints{ + ctx: context.Background(), + }, containerID, container, false, p, defaultMounts, privilegedMounts) + if err != nil { + t.Fatalf("failed to create test container spec: %v", err) + } + + envsToKeep, _, _, err := tc.policy.EnforceCreateContainerPolicy(tc.ctx, tc.sandboxID, tc.containerID, tc.argList, tc.envList, tc.workingDir, tc.mounts, false, tc.noNewPrivileges, tc.user, tc.groups, tc.umask, tc.capabilities, tc.seccomp) + + // getting an error means something is broken + if err != nil { + t.Fatal(err) + } + + if !slices.Equal(tc.envList, envsToKeep) { + t.Fatalf("expected envs to keep = %v, got %v", tc.envList, envsToKeep) + } +} + +// Test platform rule in main policy, container in fragment +func Test_Rego_PlatformRules_InPolicy2(t *testing.T) { + // Generate a random fragment. We only have one "slot" in + // policy_with_platform_rules.rego for an external fragment so we only do + // one. generateFragments' argument is only a minimum, so truncate to one to + // keep selectFragmentsFromConstraints' random pick aligned with + // fragments[0] below. + fragments := generateFragments(testRand, 1)[:1] + containerFragments := selectFragmentsFromConstraints(&generatedConstraints{ + fragments: fragments, + }, 1, []string{"containers"}, []string{}, false, frameworkVersion, false) + containers := make([]*regoFragmentContainer, len(containerFragments)) + + for i, fragment := range containerFragments { + containers[i] = makeContainerFromGeneratedFragment(fragment) + + containers[i].envList = append(containers[i].envList, "Fabric_NodeIPOrFqdn=10.0.0.1") + containers[i].mounts = append(containers[i].mounts, oci.Mount{ + Source: fmt.Sprintf("/run/gcs/c/%s/sandboxMounts/tmp/atlas/emptydir/serviceaccount", containers[i].sandboxID), + Destination: "/var/run/secrets/kubernetes.io/serviceaccount", + Type: "bind", + Options: []string{"rbind", "rshared", "ro"}, + }) + + code := fragment.constraints.toFragment().marshalRego() + fragment.code = setFrameworkVersion(code, frameworkVersion) + } + + rego := getPolicyCode_PolicyWithPlatformRules(fragments[0].feed, fragments[0].issuer) + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + p, err := newRegoPolicy(rego, + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Fatalf("unable to create policy with platform rules: %v", err) + } + + for _, containerFragment := range containerFragments { + err = p.LoadFragment(context.Background(), LoadFragmentOptions{Issuer: containerFragment.info.issuer, Feed: containerFragment.info.feed, Rego: containerFragment.code}) + if err != nil { + t.Fatalf("failed to load container fragment: %v", err) + } + } + + for _, container := range containers { + containerID, err := mountImageForContainer(p, container.container) + if err != nil { + t.Fatalf("failed to mount image for container: %v", err) + } + + _, _, _, err = p.EnforceCreateContainerPolicy(context.Background(), + container.sandboxID, + containerID, + copyStrings(container.container.Command), + copyStrings(container.envList), + container.container.WorkingDir, + copyMounts(container.mounts), + false, + container.container.NoNewPrivileges, + container.user, + container.groups, + container.container.User.Umask, + container.capabilities, + container.seccomp, + ) + + if err != nil { + t.Fatalf("unable to create container from fragment: %v", err) + } + } +} + type getUserInfoTestCase struct { userStrs []string additionalGIDs []uint32 @@ -9871,3 +10239,18 @@ func fragmentParameterTestCheckOneEnvWithLayer( assertDecisionJSONContains(t, err, "invalid env list") } } + +//go:embed fragment_test_policies/platform_rules.rego +var platformRulesFragmentPolicyCode string + +//go:embed policy_with_platform_rules.rego +var policyWithPlatformRules string + +func getPolicyCode_PolicyWithPlatformRules(fragmentFeed, fragmentIssuer string) string { + s := policyWithPlatformRules + s = strings.ReplaceAll(s, "@@API_VERSION@@", apiVersion) + s = strings.ReplaceAll(s, "@@FRAMEWORK_VERSION@@", frameworkVersion) + s = strings.ReplaceAll(s, "@@FRAGMENT_FEED@@", fragmentFeed) + s = strings.ReplaceAll(s, "@@FRAGMENT_ISSUER@@", fragmentIssuer) + return s +} From 7dd392de8180e1b0bf0affb4786fbc9d104225f4 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Mon, 8 Jun 2026 00:52:21 +0000 Subject: [PATCH 19/22] --------------------------------------------- From d0656fe3fba74b2c6acc86974d25968865a2e9f9 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Thu, 11 Dec 2025 12:45:29 +0000 Subject: [PATCH 20/22] regopolicyinterpreter: Actually use opa v1 Revert some "rego.SetRegoVersion(ast.RegoV0)" --- internal/regopolicyinterpreter/regopolicyinterpreter.go | 4 ---- pkg/securitypolicy/rego_utils_test.go | 3 --- 2 files changed, 7 deletions(-) diff --git a/internal/regopolicyinterpreter/regopolicyinterpreter.go b/internal/regopolicyinterpreter/regopolicyinterpreter.go index c83b4ae806..f255aea30e 100644 --- a/internal/regopolicyinterpreter/regopolicyinterpreter.go +++ b/internal/regopolicyinterpreter/regopolicyinterpreter.go @@ -554,9 +554,6 @@ func (r *RegoPolicyInterpreter) compile() error { options := ast.CompileOpts{ EnablePrintStatements: r.logLevel != LogNone, - ParserOptions: ast.ParserOptions{ - RegoVersion: ast.RegoV0, - }, } if compiled, err := ast.CompileModulesWithOpt(modules, options); err == nil { @@ -732,7 +729,6 @@ func (r *RegoPolicyInterpreter) query(rule string, input map[string]interface{}) rego.Query(rule), rego.Input(input), rego.Store(store), - rego.SetRegoVersion(ast.RegoV0), rego.EnablePrintStatements(r.logLevel != LogNone), rego.PrintHook(topdown.NewPrintHook(&buf)), rego.Compiler(r.compiledModules)) diff --git a/pkg/securitypolicy/rego_utils_test.go b/pkg/securitypolicy/rego_utils_test.go index f6c5df48c6..433d130981 100644 --- a/pkg/securitypolicy/rego_utils_test.go +++ b/pkg/securitypolicy/rego_utils_test.go @@ -25,7 +25,6 @@ import ( "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" rpi "github.com/Microsoft/hcsshim/internal/regopolicyinterpreter" "github.com/blang/semver/v4" - "github.com/open-policy-agent/opa/v1/ast" "github.com/open-policy-agent/opa/v1/rego" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -97,7 +96,6 @@ func init() { func Test_RegoTemplates(t *testing.T) { query := rego.New( rego.Query("data.api"), - rego.SetRegoVersion(ast.RegoV0), rego.Module("api.rego", APICode)) ctx := context.Background() @@ -127,7 +125,6 @@ func Test_RegoTemplates(t *testing.T) { func verifyPolicyRules(apiVersion string, enforcementPoints map[string]interface{}, policyCode string) error { query := rego.New( rego.Query("data.policy"), - rego.SetRegoVersion(ast.RegoV0), rego.Module("policy.rego", policyCode), rego.Module("framework.rego", FrameworkCode), ) From 18a10435553709c0ce680052b062d656054dee9d Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Thu, 11 Dec 2025 12:54:03 +0000 Subject: [PATCH 21/22] framework.rego: Fix v0->v1 rego syntax via Regex replace Replacing (^\w+(\(.+\)|\[(\w+|".+")\])?( := (\w+?|\{(.|\n)+?\}|\w+\[\w+\]))?) \{ with $1 if { --- pkg/securitypolicy/framework.rego | 538 +++++++++++++++--------------- 1 file changed, 269 insertions(+), 269 deletions(-) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index d2a8164eb3..eee7212b48 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -10,7 +10,7 @@ version := "@@FRAMEWORK_VERSION@@" # Policies should include .* explicitly at the beginning or end if partial # matches are to be allowed. -anchor_pattern(p) := p { +anchor_pattern(p) := p if { startswith(p, "^") endswith(p, "$") } else := concat("", ["^", p]) { @@ -19,24 +19,24 @@ anchor_pattern(p) := p { startswith(p, "^") } else := concat("", ["^", p, "$"]) -device_mounted(target) { +device_mounted(target) if { data.metadata.devices[target] } -device_mounted(target) { +device_mounted(target) if { data.metadata.rw_devices[target] } default deviceHash_ok := false # test if a device hash exists as a layer in a policy container -deviceHash_ok { +deviceHash_ok if { layer := data.policy.containers[_].layers[_] input.deviceHash == layer } # test if a device hash exists as a layer in a fragment container -deviceHash_ok { +deviceHash_ok if { feed := data.metadata.issuers[_].feeds[_] some fragment in feed layer := fragment.containers[_].layers[_] @@ -45,11 +45,11 @@ deviceHash_ok { default mount_device := {"allowed": false} -mount_target_ok { +mount_target_ok if { regex.match(anchor_pattern(input.mountPathRegex), input.target) } -mount_device := {"metadata": [addDevice], "allowed": true} { +mount_device := {"metadata": [addDevice], "allowed": true} if { not device_mounted(input.target) deviceHash_ok mount_target_ok @@ -64,17 +64,17 @@ mount_device := {"metadata": [addDevice], "allowed": true} { allowed_scratch_fs("ext4") allowed_scratch_fs("xfs") -rwmount_device_encrypt_ok { +rwmount_device_encrypt_ok if { input.encrypted } -rwmount_device_encrypt_ok { +rwmount_device_encrypt_ok if { allow_unencrypted_scratch } default rw_mount_device := {"allowed": false} -rw_mount_device := {"metadata": [addDevice], "allowed": true} { +rw_mount_device := {"metadata": [addDevice], "allowed": true} if { not device_mounted(input.target) rwmount_device_encrypt_ok input.ensureFilesystem @@ -90,7 +90,7 @@ rw_mount_device := {"metadata": [addDevice], "allowed": true} { default unmount_device := {"allowed": false} -unmount_device := {"metadata": [removeDevice], "allowed": true} { +unmount_device := {"metadata": [removeDevice], "allowed": true} if { data.metadata.devices[input.unmountTarget] removeDevice := { @@ -102,7 +102,7 @@ unmount_device := {"metadata": [removeDevice], "allowed": true} { default rw_unmount_device := {"allowed": false} -rw_unmount_device := {"metadata": [removeRWDevice], "allowed": true} { +rw_unmount_device := {"metadata": [removeRWDevice], "allowed": true} if { data.metadata.rw_devices[input.unmountTarget] removeRWDevice := { @@ -120,7 +120,7 @@ default mount_blockdev := {"allowed": false} default unmount_blockdev := {"allowed": false} -layerPaths_ok(layers) { +layerPaths_ok(layers) if { length := count(layers) count(input.layerPaths) == length every i, path in input.layerPaths { @@ -128,7 +128,7 @@ layerPaths_ok(layers) { } } -layerHashes_ok(layers) { +layerHashes_ok(layers) if { length := count(layers) count(input.layerHashes) == length every i, hash in input.layerHashes { @@ -138,11 +138,11 @@ layerHashes_ok(layers) { default overlay_exists := false -overlay_exists { +overlay_exists if { data.metadata.matches[input.containerID] } -overlay_mounted(target) { +overlay_mounted(target) if { data.metadata.overlayTargets[target] } @@ -151,17 +151,17 @@ overlay_mounted(target) { # breaking other rules in this case. default policy_containers := [] -policy_containers := pc { +policy_containers := pc if { semver.compare(policy_framework_version, version) == 0 pc := data.policy.containers } -policy_containers := pc { +policy_containers := pc if { semver.compare(policy_framework_version, version) < 0 pc := apply_defaults("container", data.policy.containers, policy_framework_version) } -candidate_containers := containers { +candidate_containers := containers if { fragment_containers := [c | feed := data.metadata.issuers[_].feeds[_] fragment := feed[_] @@ -184,7 +184,7 @@ candidate_containers := containers { default mount_cims := {"allowed": false} -mount_cims := {"metadata": [addMatches], "allowed": true} { +mount_cims := {"metadata": [addMatches], "allowed": true} if { not overlay_exists containers := [container | @@ -204,7 +204,7 @@ mount_cims := {"metadata": [addMatches], "allowed": true} { default mount_overlay := {"allowed": false} -mount_overlay := {"metadata": [addMatches, addOverlayTarget], "allowed": true} { +mount_overlay := {"metadata": [addMatches, addOverlayTarget], "allowed": true} if { not overlay_exists # sanity check, but due to checks in the Go code, this should always pass if @@ -234,7 +234,7 @@ mount_overlay := {"metadata": [addMatches, addOverlayTarget], "allowed": true} { default unmount_overlay := {"allowed": false} -unmount_overlay := {"metadata": [removeOverlayTarget], "allowed": true} { +unmount_overlay := {"metadata": [removeOverlayTarget], "allowed": true} if { overlay_mounted(input.unmountTarget) removeOverlayTarget := { "name": "overlayTargets", @@ -243,7 +243,7 @@ unmount_overlay := {"metadata": [removeOverlayTarget], "allowed": true} { } } -command_ok(command) { +command_ok(command) if { count(input.argList) == count(command) every i, arg in input.argList { command[i] == arg @@ -266,18 +266,18 @@ command_ok(command) { # env_pattern_ok(pattern, strategy, value) tests whether the given string # pattern matches the input value. -env_pattern_ok(pattern, "string", value) { +env_pattern_ok(pattern, "string", value) if { pattern == value } -env_pattern_ok(pattern, "re2", value) { +env_pattern_ok(pattern, "re2", value) if { regex.match(anchor_pattern(pattern), value) } # env_rule_ok accepts both forms of env rules described above, and matches it # against the given env string (of form name=value). -env_rule_ok(rule, env) { +env_rule_ok(rule, env) if { pattern := object.get(rule, "pattern", null) strategy := object.get(rule, "strategy", null) pattern != null @@ -285,7 +285,7 @@ env_rule_ok(rule, env) { env_pattern_ok(pattern, strategy, env) } -env_rule_ok(rule, env) { +env_rule_ok(rule, env) if { rule_name := object.get(rule, "name", null) name_strategy := object.get(rule, "name_strategy", null) rule_value := object.get(rule, "value", null) @@ -307,18 +307,18 @@ env_rule_ok(rule, env) { # For a required env rule, check that envList contains a matching env var for # it. -env_required_rule_ok(rule, envList) { +env_required_rule_ok(rule, envList) if { rule.required some env in envList env_rule_ok(rule, env) } # If it's not required, skip the check -env_required_rule_ok(rule, envList) { +env_required_rule_ok(rule, envList) if { not rule.required } -envList_ok(env_rules, envList) { +envList_ok(env_rules, envList) if { # Check that all required rules are satisfied every rule in env_rules { env_required_rule_ok(rule, envList) @@ -331,7 +331,7 @@ envList_ok(env_rules, envList) { } } -valid_envs_subset(env_rules) := envs { +valid_envs_subset(env_rules) := envs if { envs := {env | some env in input.envList some rule in env_rules @@ -339,7 +339,7 @@ valid_envs_subset(env_rules) := envs { } } -valid_envs_for_all(items) := envs { +valid_envs_for_all(items) := envs if { allow_environment_variable_dropping # for each item, find a subset of the environment rules @@ -374,65 +374,65 @@ valid_envs_for_all(items) := envs { envs := envs_i } -valid_envs_for_all(items) := envs { +valid_envs_for_all(items) := envs if { not allow_environment_variable_dropping # no dropping allowed, so we just return the input envs := input.envList } -workingDirectory_ok(working_dir) { +workingDirectory_ok(working_dir) if { input.workingDir == working_dir } -privileged_ok(elevation_allowed) { +privileged_ok(elevation_allowed) if { is_linux not input.privileged } -privileged_ok(elevation_allowed) { +privileged_ok(elevation_allowed) if { is_linux input.privileged input.privileged == elevation_allowed } -privileged_ok(no_new_privileges) { +privileged_ok(no_new_privileges) if { # no-op for windows is_windows } -noNewPrivileges_ok(no_new_privileges) { +noNewPrivileges_ok(no_new_privileges) if { is_linux no_new_privileges input.noNewPrivileges } -noNewPrivileges_ok(no_new_privileges) { +noNewPrivileges_ok(no_new_privileges) if { is_linux not no_new_privileges } -noNewPrivileges_ok(obj) { +noNewPrivileges_ok(obj) if { is_windows } -idName_ok(pattern, "any", value) { +idName_ok(pattern, "any", value) if { true } -idName_ok(pattern, "id", value) { +idName_ok(pattern, "id", value) if { pattern == value.id } -idName_ok(pattern, "name", value) { +idName_ok(pattern, "name", value) if { pattern == value.name } -idName_ok(pattern, "re2", value) { +idName_ok(pattern, "re2", value) if { regex.match(anchor_pattern(pattern), value.name) } -user_ok(user) { +user_ok(user) if { is_linux user.umask == input.umask idName_ok(user.user_idname.pattern, user.user_idname.strategy, input.user) @@ -442,21 +442,21 @@ user_ok(user) { } } -user_ok(user) { +user_ok(user) if { is_windows input.user == user } -seccomp_ok(seccomp_profile_sha256) { +seccomp_ok(seccomp_profile_sha256) if { is_linux input.seccompProfileSHA256 == seccomp_profile_sha256 } -seccomp_ok(seccomp_profile_sha256) { +seccomp_ok(seccomp_profile_sha256) if { is_windows } -devices_ok(expected_devices, actual_devices) { +devices_ok(expected_devices, actual_devices) if { # Allow out of order but not duplicates set_expected := {dev | dev := expected_devices[_]} set_actual := {dev | dev := actual_devices[_]} @@ -466,18 +466,18 @@ devices_ok(expected_devices, actual_devices) { default container_started := false -container_started { +container_started if { data.metadata.started[input.containerID] } default container_privileged := false -container_privileged { +container_privileged if { is_linux data.metadata.started[input.containerID].privileged } -capsList_ok(allowed_caps_list, requested_caps_list) { +capsList_ok(allowed_caps_list, requested_caps_list) if { count(allowed_caps_list) == count(requested_caps_list) every cap in requested_caps_list { @@ -491,7 +491,7 @@ capsList_ok(allowed_caps_list, requested_caps_list) { } } -filter_capsList_by_allowed(allowed_caps_list, requested_caps_list) := caps { +filter_capsList_by_allowed(allowed_caps_list, requested_caps_list) := caps if { # find a subset of the capabilities that are valid caps := {cap | some cap in requested_caps_list @@ -500,7 +500,7 @@ filter_capsList_by_allowed(allowed_caps_list, requested_caps_list) := caps { } } -filter_capsList_for_single_container(allowed_caps) := caps { +filter_capsList_for_single_container(allowed_caps) := caps if { bounding := filter_capsList_by_allowed(allowed_caps.bounding, input.capabilities.bounding) effective := filter_capsList_by_allowed(allowed_caps.effective, input.capabilities.effective) inheritable := filter_capsList_by_allowed(allowed_caps.inheritable, input.capabilities.inheritable) @@ -516,7 +516,7 @@ filter_capsList_for_single_container(allowed_caps) := caps { } } -largest_caps_sets_for_all(containers, privileged) := largest_caps_sets { +largest_caps_sets_for_all(containers, privileged) := largest_caps_sets if { filtered := [caps | container := containers[_] capabilities := get_capabilities(container, privileged) @@ -541,7 +541,7 @@ largest_caps_sets_for_all(containers, privileged) := largest_caps_sets { ] } -all_caps_sets_are_equal(sets) := caps { +all_caps_sets_are_equal(sets) := caps if { # if there is more than one set with the same size, we # can only proceed if they are all the same, so we verify # that the intersection is equal to the union. For a single @@ -573,7 +573,7 @@ all_caps_sets_are_equal(sets) := caps { } } -valid_caps_for_all(containers, privileged) := caps { +valid_caps_for_all(containers, privileged) := caps if { is_linux allow_capability_dropping @@ -585,7 +585,7 @@ valid_caps_for_all(containers, privileged) := caps { caps := all_caps_sets_are_equal(largest_caps_sets) } -valid_caps_for_all(containers, privileged) := caps { +valid_caps_for_all(containers, privileged) := caps if { is_linux not allow_capability_dropping @@ -593,7 +593,7 @@ valid_caps_for_all(containers, privileged) := caps { caps := input.capabilities } -caps_ok(allowed_caps, requested_caps) { +caps_ok(allowed_caps, requested_caps) if { is_linux capsList_ok(allowed_caps.bounding, requested_caps.bounding) capsList_ok(allowed_caps.effective, requested_caps.effective) @@ -602,16 +602,16 @@ caps_ok(allowed_caps, requested_caps) { capsList_ok(allowed_caps.ambient, requested_caps.ambient) } -caps_ok(allowed_caps, requested_caps) { +caps_ok(allowed_caps, requested_caps) if { is_windows } -get_capabilities(container, privileged) := capabilities { +get_capabilities(container, privileged) := capabilities if { container.capabilities != null capabilities := container.capabilities } -default_privileged_capabilities := capabilities { +default_privileged_capabilities := capabilities if { caps := {cap | cap := data.defaultPrivilegedCapabilities[_]} capabilities := { "bounding": caps, @@ -622,13 +622,13 @@ default_privileged_capabilities := capabilities { } } -get_capabilities(container, true) := capabilities { +get_capabilities(container, true) := capabilities if { container.capabilities == null container.allow_elevated capabilities := default_privileged_capabilities } -default_unprivileged_capabilities := capabilities { +default_unprivileged_capabilities := capabilities if { caps := {cap | cap := data.defaultUnprivilegedCapabilities[_]} capabilities := { "bounding": caps, @@ -639,13 +639,13 @@ default_unprivileged_capabilities := capabilities { } } -get_capabilities(container, false) := capabilities { +get_capabilities(container, false) := capabilities if { container.capabilities == null container.allow_elevated capabilities := default_unprivileged_capabilities } -get_capabilities(container, privileged) := capabilities { +get_capabilities(container, privileged) := capabilities if { container.capabilities == null not container.allow_elevated capabilities := default_unprivileged_capabilities @@ -657,7 +657,7 @@ create_container := {"metadata": [updateMatches, addStarted], "env_list": env_list, "caps_list": caps_list, "allow_stdio_access": allow_stdio_access, - "allowed": true} { + "allowed": true} if { is_linux not container_started @@ -733,7 +733,7 @@ create_container := {"metadata": [updateMatches, addStarted], create_container := {"metadata": [updateMatches, addStarted], "env_list": env_list, "allow_stdio_access": allow_stdio_access, - "allowed": true} { + "allowed": true} if { is_windows not container_started @@ -790,7 +790,7 @@ create_container := {"metadata": [updateMatches, addStarted], } } -security_ok(current_container) { +security_ok(current_container) if { is_linux noNewPrivileges_ok(current_container.no_new_privileges) privileged_ok(current_container.allow_elevated) @@ -798,34 +798,34 @@ security_ok(current_container) { mountList_ok(current_container.mounts, current_container.allow_elevated) } -security_ok(current_container) { +security_ok(current_container) if { is_windows } -mountSource_ok(constraint, source) { +mountSource_ok(constraint, source) if { startswith(constraint, data.sandboxPrefix) newConstraint := replace(constraint, data.sandboxPrefix, input.sandboxDir) regex.match(anchor_pattern(newConstraint), source) } -mountSource_ok(constraint, source) { +mountSource_ok(constraint, source) if { startswith(constraint, data.hugePagesPrefix) newConstraint := replace(constraint, data.hugePagesPrefix, input.hugePagesDir) regex.match(anchor_pattern(newConstraint), source) } -mountSource_ok(constraint, source) { +mountSource_ok(constraint, source) if { startswith(constraint, data.plan9Prefix) some target, containerID in data.metadata.p9mounts source == target input.containerID == containerID } -mountSource_ok(constraint, source) { +mountSource_ok(constraint, source) if { constraint == source } -mountConstraint_ok(constraint, mount) { +mountConstraint_ok(constraint, mount) if { mount.type == constraint.type mountSource_ok(constraint.source, mount.source) mount.destination != "" @@ -846,17 +846,17 @@ mountConstraint_ok(constraint, mount) { } } -mount_ok(mounts, allow_elevated, mount) { +mount_ok(mounts, allow_elevated, mount) if { some constraint in mounts mountConstraint_ok(constraint, mount) } -mount_ok(mounts, allow_elevated, mount) { +mount_ok(mounts, allow_elevated, mount) if { some constraint in data.defaultMounts mountConstraint_ok(constraint, mount) } -mount_ok(mounts, allow_elevated, mount) { +mount_ok(mounts, allow_elevated, mount) if { allow_elevated some constraint in data.privilegedMounts mountConstraint_ok(constraint, mount) @@ -879,7 +879,7 @@ mount_ok(mounts, allow_elevated, mount) { # We have to allow this special case whether or not this policy currently allows # any privileged containers at all, since a fragment that is loaded in the # future may allow privileged containers. -mount_ok(mounts, allow_elevated, mount) { +mount_ok(mounts, allow_elevated, mount) if { input.isSandboxContainer # we allow allow_elevated to be false since this is what existing policies @@ -896,22 +896,22 @@ mount_ok(mounts, allow_elevated, mount) { "rw" in mount.options } -mountList_ok(mounts, allow_elevated) { +mountList_ok(mounts, allow_elevated) if { is_linux every mount in input.mounts { mount_ok(mounts, allow_elevated, mount) } } -mountList_ok(mounts, allow_elevated) { +mountList_ok(mounts, allow_elevated) if { # no-op for windows is_windows } -is_linux { +is_linux if { data.metadata.operatingsystem[ostype] == "linux" } -is_windows { +is_windows if { data.metadata.operatingsystem[ostype] == "windows" } @@ -920,7 +920,7 @@ default exec_in_container := {"allowed": false} exec_in_container := {"metadata": [updateMatches], "env_list": env_list, "caps_list": caps_list, - "allowed": true} { + "allowed": true} if { is_linux container_started @@ -972,7 +972,7 @@ exec_in_container := {"metadata": [updateMatches], exec_in_container := {"metadata": [updateMatches], "env_list": env_list, - "allowed": true} { + "allowed": true} if { is_windows container_started @@ -1014,7 +1014,7 @@ exec_in_container := {"metadata": [updateMatches], default shutdown_container := {"allowed": false} -shutdown_container := {"metadata": [remove], "allowed": true} { +shutdown_container := {"metadata": [remove], "allowed": true} if { container_started remove := { "name": "matches", @@ -1025,7 +1025,7 @@ shutdown_container := {"metadata": [remove], "allowed": true} { default signal_container_process := {"allowed": false} -signal_container_process := {"metadata": [updateMatches], "allowed": true} { +signal_container_process := {"metadata": [updateMatches], "allowed": true} if { container_started input.isInitProcess containers := [container | @@ -1042,7 +1042,7 @@ signal_container_process := {"metadata": [updateMatches], "allowed": true} { } } -signal_container_process := {"metadata": [updateMatches], "allowed": true} { +signal_container_process := {"metadata": [updateMatches], "allowed": true} if { container_started not input.isInitProcess containers := [container | @@ -1061,18 +1061,18 @@ signal_container_process := {"metadata": [updateMatches], "allowed": true} { } } -signal_ok(signals) { +signal_ok(signals) if { some signal in signals input.signal == signal } -plan9_mounted(target) { +plan9_mounted(target) if { data.metadata.p9mounts[target] } default plan9_mount := {"allowed": false} -plan9_mount := {"metadata": [addPlan9Target], "allowed": true} { +plan9_mount := {"metadata": [addPlan9Target], "allowed": true} if { not plan9_mounted(input.target) some containerID, _ in data.metadata.matches pattern := concat("", ["^", input.rootPrefix, "/", containerID, input.mountPathPrefix, "$"]) @@ -1087,7 +1087,7 @@ plan9_mount := {"metadata": [addPlan9Target], "allowed": true} { default plan9_unmount := {"allowed": false} -plan9_unmount := {"metadata": [removePlan9Target], "allowed": true} { +plan9_unmount := {"metadata": [removePlan9Target], "allowed": true} if { plan9_mounted(input.unmountTarget) removePlan9Target := { "name": "p9mounts", @@ -1106,11 +1106,11 @@ default enforcement_point_info := { "use_framework": false } -enforcement_point_info := {"available": false, "default_results": {"allow": false}, "unknown": false, "invalid": false, "version_missing": true, "use_framework": false} { +enforcement_point_info := {"available": false, "default_results": {"allow": false}, "unknown": false, "invalid": false, "version_missing": true, "use_framework": false} if { policy_api_version == null } -enforcement_point_info := {"available": available, "default_results": default_results, "unknown": false, "invalid": false, "version_missing": false, "use_framework": use_framework} { +enforcement_point_info := {"available": available, "default_results": default_results, "unknown": false, "invalid": false, "version_missing": false, "use_framework": use_framework} if { enforcement_point := data.api.enforcement_points[input.name] semver.compare(data.api.version, enforcement_point.introducedVersion) >= 0 available := semver.compare(policy_api_version, enforcement_point.introducedVersion) >= 0 @@ -1118,14 +1118,14 @@ enforcement_point_info := {"available": available, "default_results": default_re use_framework := enforcement_point.use_framework } -enforcement_point_info := {"available": false, "default_results": {"allow": false}, "unknown": false, "invalid": true, "version_missing": false, "use_framework": false} { +enforcement_point_info := {"available": false, "default_results": {"allow": false}, "unknown": false, "invalid": true, "version_missing": false, "use_framework": false} if { enforcement_point := data.api.enforcement_points[input.name] semver.compare(data.api.version, enforcement_point.introducedVersion) < 0 } default candidate_external_processes := [] -candidate_external_processes := external_processes { +candidate_external_processes := external_processes if { semver.compare(policy_framework_version, version) == 0 policy_external_processes := [e | e := data.policy.external_processes[_]] @@ -1138,7 +1138,7 @@ candidate_external_processes := external_processes { external_processes := array.concat(policy_external_processes, fragment_external_processes) } -candidate_external_processes := external_processes { +candidate_external_processes := external_processes if { semver.compare(policy_framework_version, version) < 0 policy_external_processes := apply_defaults("external_process", data.policy.external_processes, policy_framework_version) @@ -1151,7 +1151,7 @@ candidate_external_processes := external_processes { external_processes := array.concat(policy_external_processes, fragment_external_processes) } -external_process_ok(process) { +external_process_ok(process) if { command_ok(process.command) envList_ok(process.env_rules, input.envList) workingDirectory_ok(process.working_dir) @@ -1161,7 +1161,7 @@ default exec_external := {"allowed": false} exec_external := {"allowed": true, "allow_stdio_access": allow_stdio_access, - "env_list": env_list} { + "env_list": env_list} if { possible_processes := [process | process := candidate_external_processes[_] # NB any change to these narrowing conditions should be reflected in @@ -1191,19 +1191,19 @@ exec_external := {"allowed": true, default get_properties := {"allowed": false} -get_properties := {"allowed": true} { +get_properties := {"allowed": true} if { allow_properties_access } default dump_stacks := {"allowed": false} -dump_stacks := {"allowed": true} { +dump_stacks := {"allowed": true} if { allow_dump_stacks } default runtime_logging := {"allowed": false} -runtime_logging := {"allowed": true} { +runtime_logging := {"allowed": true} if { allow_runtime_logging } @@ -1229,12 +1229,12 @@ fragment_transparency_trust_lists := data[input.namespace].transparency_trust_li default fragment_platform_rules := [] fragment_platform_rules := data[input.namespace].platform_rules -apply_defaults(name, raw_values, framework_version) := values { +apply_defaults(name, raw_values, framework_version) := values if { semver.compare(framework_version, version) == 0 values := raw_values } -apply_defaults("container", raw_values, framework_version) := values { +apply_defaults("container", raw_values, framework_version) := values if { semver.compare(framework_version, version) < 0 values := [checked | raw := raw_values[_] @@ -1242,7 +1242,7 @@ apply_defaults("container", raw_values, framework_version) := values { ] } -apply_defaults("external_process", raw_values, framework_version) := values { +apply_defaults("external_process", raw_values, framework_version) := values if { semver.compare(framework_version, version) < 0 values := [checked | raw := raw_values[_] @@ -1250,7 +1250,7 @@ apply_defaults("external_process", raw_values, framework_version) := values { ] } -apply_defaults("fragment", raw_values, framework_version) := values { +apply_defaults("fragment", raw_values, framework_version) := values if { semver.compare(framework_version, version) < 0 values := [checked | raw := raw_values[_] @@ -1262,13 +1262,13 @@ apply_defaults("fragment", raw_values, framework_version) := values { # policy has it, silently ignore as it might be using the name for something # else. -apply_defaults("transparency_trust_lists", raw_values, framework_version) := values { +apply_defaults("transparency_trust_lists", raw_values, framework_version) := values if { semver.compare(framework_version, version) < 0 semver.compare(framework_version, "0.5.0") >= 0 values := raw_values } -apply_defaults("transparency_trust_lists", raw_values, framework_version) := values { +apply_defaults("transparency_trust_lists", raw_values, framework_version) := values if { semver.compare(framework_version, "0.5.0") < 0 values := [] } @@ -1276,7 +1276,7 @@ apply_defaults("transparency_trust_lists", raw_values, framework_version) := val # platform_rules is introduced in framework version 0.5.0. If an old policy has it, # silently ignore as it might be using the name for something else. -apply_defaults("platform_rules", raw_values, framework_version) := values { +apply_defaults("platform_rules", raw_values, framework_version) := values if { semver.compare(framework_version, version) < 0 semver.compare(framework_version, "0.5.0") >= 0 # This is currently unreachable, otherwise we would call something like @@ -1284,7 +1284,7 @@ apply_defaults("platform_rules", raw_values, framework_version) := values { values := raw_values } -apply_defaults("platform_rules", raw_values, framework_version) := values { +apply_defaults("platform_rules", raw_values, framework_version) := values if { semver.compare(framework_version, "0.5.0") < 0 values := [] } @@ -1292,7 +1292,7 @@ apply_defaults("platform_rules", raw_values, framework_version) := values { default fragment_framework_version := null fragment_framework_version := data[input.namespace].framework_version -extract_fragment_includes(includes) := fragment { +extract_fragment_includes(includes) := fragment if { framework_version := fragment_framework_version objects := { "containers": apply_defaults("container", fragment_containers, framework_version), @@ -1329,15 +1329,15 @@ extract_fragment_includes(includes) := fragment { # This map does not contain any containers / fragments allowed by the top-level # policy itself. The candidate_* rules need to combine both sources. -issuer_exists(iss) { +issuer_exists(iss) if { data.metadata.issuers[iss] } -feed_exists(iss, feed) { +feed_exists(iss, feed) if { data.metadata.issuers[iss].feeds[feed] } -update_issuer(includes) := issuer { +update_issuer(includes) := issuer if { feed_exists(input.issuer, input.feed) old_issuer := data.metadata.issuers[input.issuer] old_fragments := old_issuer.feeds[input.feed] @@ -1346,7 +1346,7 @@ update_issuer(includes) := issuer { issuer := object.union(old_issuer, new_issuer) } -update_issuer(includes) := issuer { +update_issuer(includes) := issuer if { not feed_exists(input.issuer, input.feed) old_issuer := data.metadata.issuers[input.issuer] new_issuer := {"feeds": {input.feed: [extract_fragment_includes(includes)]}} @@ -1354,7 +1354,7 @@ update_issuer(includes) := issuer { issuer := object.union(old_issuer, new_issuer) } -update_issuer(includes) := issuer { +update_issuer(includes) := issuer if { not issuer_exists(input.issuer) issuer := {"feeds": {input.feed: [extract_fragment_includes(includes)]}} } @@ -1363,12 +1363,12 @@ update_issuer(includes) := issuer { # to [] to prevent breaking other rules. default policy_fragments := [] -policy_fragments := pf { +policy_fragments := pf if { semver.compare(policy_framework_version, version) == 0 pf := data.policy.fragments } -policy_fragments := pf { +policy_fragments := pf if { semver.compare(policy_framework_version, version) < 0 pf := apply_defaults("fragment", data.policy.fragments, policy_framework_version) } @@ -1415,7 +1415,7 @@ policy_fragments := pf { default fragment_parameters_for(_, _) := [] -fragment_parameters_for(iss, feed) := params { +fragment_parameters_for(iss, feed) := params if { params_nested := [ p.parameters | p := data.metadata.fragment_parameters[_] @@ -1432,7 +1432,7 @@ fragment_parameters_for(iss, feed) := params { params := array.concat(params_nested, params_policy) } -candidate_fragments := fragments { +candidate_fragments := fragments if { fragment_fragments := [f | feed := data.metadata.issuers[_].feeds[_] fragment := feed[_] @@ -1442,18 +1442,18 @@ candidate_fragments := fragments { fragments := array.concat(policy_fragments, fragment_fragments) } -svn_ok(svn, minimum_svn) { +svn_ok(svn, minimum_svn) if { # deprecated semver.is_valid(svn) semver.is_valid(minimum_svn) semver.compare(svn, minimum_svn) >= 0 } -svn_ok(svn, minimum_svn) { +svn_ok(svn, minimum_svn) if { to_number(svn) >= to_number(minimum_svn) } -fragment_issuer_feed_ok(fragment) { +fragment_issuer_feed_ok(fragment) if { input.issuer == fragment.issuer input.feed == fragment.feed } @@ -1465,22 +1465,22 @@ fragment_issuer_feed_ok(fragment) { # neither the header nor the fragment Rego declares an SVN is tested in # Test_Rego_LoadFragment_MissingSVN. -header_svn_ok(fragment) { +header_svn_ok(fragment) if { not input.has_header_svn } -header_svn_ok(fragment) { +header_svn_ok(fragment) if { input.has_header_svn svn_ok(input.header_svn, fragment.minimum_svn) } -svn_ok_if_defined(minimum_svn) { +svn_ok_if_defined(minimum_svn) if { data[input.namespace].svn # This also works if the svn is 0 not input.has_header_svn svn_ok(data[input.namespace].svn, minimum_svn) } -svn_ok_if_defined(minimum_svn) { +svn_ok_if_defined(minimum_svn) if { data[input.namespace].svn input.has_header_svn # Use to_number as fragment may define svn as a string @@ -1489,7 +1489,7 @@ svn_ok_if_defined(minimum_svn) { } # If not defined in fragment, require SVN to present in the header -svn_ok_if_defined(minimum_svn) { +svn_ok_if_defined(minimum_svn) if { not data[input.namespace].svn input.has_header_svn svn_ok(input.header_svn, minimum_svn) @@ -1507,7 +1507,7 @@ svn_ok_if_defined(minimum_svn) { # feeds of the TTLs containing the key for the receipt. If not set, no receipts # are required. The list is an AND: every required entry must be satisfied. # One receipt can satisfy multiple such requirement entries. -fragment_receipts_ok(fragment) { +fragment_receipts_ok(fragment) if { required := object.get(fragment, "required_receipts", []) every required_issuer in required { receipt_requirement_satisfied(required_issuer) @@ -1522,19 +1522,19 @@ fragment_receipts_ok(fragment) { # - "TTL:": satisfied by a validated receipt that was signed by a key # contributed by a TTL with the given subject. # - a literal ledger name: satisfied by a validated receipt with that issuer. -receipt_requirement_satisfied(required_issuer) { +receipt_requirement_satisfied(required_issuer) if { required_issuer == "*" count(input.receipts) > 0 } -receipt_requirement_satisfied(required_issuer) { +receipt_requirement_satisfied(required_issuer) if { startswith(required_issuer, "TTL:") subject := substring(required_issuer, count("TTL:"), -1) some receipt in input.receipts subject in receipt.ttl_subjects } -receipt_requirement_satisfied(required_issuer) { +receipt_requirement_satisfied(required_issuer) if { required_issuer != "*" not startswith(required_issuer, "TTL:") some receipt in input.receipts @@ -1552,7 +1552,7 @@ default load_fragment := {"allowed": false} # in the header, and thus we could not have checked earlier), and if successful, # add the fragment to the metadata. -load_fragment := {"allowed": true, "parameters": possibleParams} { +load_fragment := {"allowed": true, "parameters": possibleParams} if { not input.fragment_loaded some fragment in candidate_fragments fragment_issuer_feed_ok(fragment) @@ -1562,7 +1562,7 @@ load_fragment := {"allowed": true, "parameters": possibleParams} { possibleParams := fragment_parameters_for(fragment.issuer, fragment.feed) } -load_fragment := {"metadata": array.concat([updateIssuer], updateParameters), "add_module": add_module, "allowed": true} { +load_fragment := {"metadata": array.concat([updateIssuer], updateParameters), "add_module": add_module, "allowed": true} if { input.fragment_loaded some fragment in candidate_fragments fragment_issuer_feed_ok(fragment) @@ -1605,7 +1605,7 @@ load_fragment := {"metadata": array.concat([updateIssuer], updateParameters), "a default policy_transparency_trust_lists := [] policy_transparency_trust_lists := data.policy.transparency_trust_lists -candidate_transparency_trust_lists := ttls { +candidate_transparency_trust_lists := ttls if { fragment_ttls := [r | feed := data.metadata.issuers[_].feeds[_] fragment := feed[_] @@ -1617,7 +1617,7 @@ candidate_transparency_trust_lists := ttls { # The set of ledger names a matching TTL authorizes for the given (issuer, # subject, svn). "*" is a wildcard meaning "any ledger". -ttl_allowed_ledgers_for_issuer_subject_svn(issuer, subject, svn) := allowed_ledgers { +ttl_allowed_ledgers_for_issuer_subject_svn(issuer, subject, svn) := allowed_ledgers if { allowed_ledgers := {l | ttl := candidate_transparency_trust_lists[_] ttl.issuer == issuer @@ -1627,19 +1627,19 @@ ttl_allowed_ledgers_for_issuer_subject_svn(issuer, subject, svn) := allowed_ledg } } -ttl_intersect_or_allow_all_if_wildcard(allowed_ledgers, input_ledgers) := result { +ttl_intersect_or_allow_all_if_wildcard(allowed_ledgers, input_ledgers) := result if { not "*" in allowed_ledgers result := {l | l := input_ledgers[_]; l in allowed_ledgers} } -ttl_intersect_or_allow_all_if_wildcard(allowed_ledgers, input_ledgers) := result { +ttl_intersect_or_allow_all_if_wildcard(allowed_ledgers, input_ledgers) := result if { "*" in allowed_ledgers result := {l | l := input_ledgers[_]} } default load_transparency_trust_list := {"allowed": false} -load_transparency_trust_list := {"allowed": true, "allowed_ledgers": allowed_ledgers} { +load_transparency_trust_list := {"allowed": true, "allowed_ledgers": allowed_ledgers} if { ttl_ledgers := ttl_allowed_ledgers_for_issuer_subject_svn(input.issuer, input.subject, input.svn) allowed_ledgers := ttl_intersect_or_allow_all_if_wildcard(ttl_ledgers, input.ledgers) count(allowed_ledgers) > 0 @@ -1647,11 +1647,11 @@ load_transparency_trust_list := {"allowed": true, "allowed_ledgers": allowed_led default scratch_mount := {"allowed": false} -scratch_mounted(target) { +scratch_mounted(target) if { data.metadata.scratch_mounts[target] } -scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} { +scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} if { not scratch_mounted(input.target) allow_unencrypted_scratch add_scratch_mount := { @@ -1662,7 +1662,7 @@ scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} { } } -scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} { +scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} if { not scratch_mounted(input.target) not allow_unencrypted_scratch input.encrypted @@ -1676,7 +1676,7 @@ scratch_mount := {"metadata": [add_scratch_mount], "allowed": true} { default scratch_unmount := {"allowed": false} -scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} { +scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} if { scratch_mounted(input.unmountTarget) remove_scratch_mount := { "name": "scratch_mounts", @@ -1699,7 +1699,7 @@ scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} { # Output: {"allowed": bool, "providers_to_keep": [name, ...]} default log_provider := {"allowed": false, "providers_to_keep": []} -valid_log_providers := providers { +valid_log_providers := providers if { allow_log_provider_dropping providers := [name | @@ -1709,19 +1709,19 @@ valid_log_providers := providers { ] } -valid_log_providers := providers { +valid_log_providers := providers if { not allow_log_provider_dropping providers := input.providers } -log_providers_ok(providers) { +log_providers_ok(providers) if { every name in providers { some allowed_provider in data.policy.allowed_log_providers lower(name) == lower(allowed_provider) } } -log_provider := {"allowed": true, "providers_to_keep": providers} { +log_provider := {"allowed": true, "providers_to_keep": providers} if { providers := valid_log_providers log_providers_ok(providers) } @@ -1730,7 +1730,7 @@ log_provider := {"allowed": true, "providers_to_keep": providers} { default registry_changes := {"allowed": false} # Helper function to compare registry keys -registry_keys_match(policy_key, input_key) { +registry_keys_match(policy_key, input_key) if { policy_key.hive == input_key.Hive policy_key.name == input_key.Name # Volatile field comparison (default to false if not specified) @@ -1741,7 +1741,7 @@ registry_keys_match(policy_key, input_key) { # Helper function to compare registry values # STRING type -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1750,7 +1750,7 @@ registry_value_matches(policy_value, input_value) { } # EXPANDED_STRING type (uses StringValue field) -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1759,7 +1759,7 @@ registry_value_matches(policy_value, input_value) { } # MULTI_STRING type (uses StringValue field) -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1768,7 +1768,7 @@ registry_value_matches(policy_value, input_value) { } # D_WORD type -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1777,7 +1777,7 @@ registry_value_matches(policy_value, input_value) { } # Q_WORD type -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1786,7 +1786,7 @@ registry_value_matches(policy_value, input_value) { } # BINARY type -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1795,7 +1795,7 @@ registry_value_matches(policy_value, input_value) { } # CUSTOM_TYPE - both CustomType field and BinaryValue must match -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1805,7 +1805,7 @@ registry_value_matches(policy_value, input_value) { } # NONE type - no value to compare, just key and name -registry_value_matches(policy_value, input_value) { +registry_value_matches(policy_value, input_value) if { registry_keys_match(policy_value.key, input_value.Key) policy_value.name == input_value.Name policy_value.type == input_value.Type @@ -1819,7 +1819,7 @@ filtered_registry_values(input_values, policy_values) := [input_val | registry_value_matches(policy_val, input_val) ] -registry_changes := {"allowed": true} { +registry_changes := {"allowed": true} if { containers := data.metadata.matches[input.containerID] container := containers[_] @@ -1840,7 +1840,7 @@ registry_changes := {"allowed": true} { # injected into fragments, and is not otherwise intended to be called by user # directly. -extract_parameter(name, fragment_parameters_obj, parameters_metadata) := fragment_parameters_obj[name] { +extract_parameter(name, fragment_parameters_obj, parameters_metadata) := fragment_parameters_obj[name] if { name in object.keys(fragment_parameters_obj) } else := parameters_metadata[name]["default"] { "default" in object.keys(parameters_metadata[name]) @@ -1848,7 +1848,7 @@ extract_parameter(name, fragment_parameters_obj, parameters_metadata) := fragmen default policy_platform_rules := [] -policy_platform_rules := platform_rules { +policy_platform_rules := platform_rules if { semver.compare(policy_framework_version, version) == 0 platform_rules := data.policy.platform_rules } @@ -1856,14 +1856,14 @@ policy_platform_rules := platform_rules { # For policy with framework_version < 0.5.0, apply_defaults will ignore # platform_rules and return []. -policy_platform_rules := platform_rules { +policy_platform_rules := platform_rules if { semver.compare(policy_framework_version, version) < 0 platform_rules := apply_defaults("platform_rules", data.policy.platform_rules, policy_framework_version) } default candidate_platform_rules := [] -candidate_platform_rules := platform_rules { +candidate_platform_rules := platform_rules if { fragment_platform_rules := [r | feed := data.metadata.issuers[_].feeds[_] fragment := feed[_] @@ -1879,12 +1879,12 @@ candidate_platform_rules := platform_rules { # if multiple platform rules are defined. # No platform rules - return as-is. -apply_platform_rules("container", container) := updated_containers { +apply_platform_rules("container", container) := updated_containers if { count(candidate_platform_rules) == 0 updated_containers := [container] } -apply_platform_rules("container", container) := updated_containers { +apply_platform_rules("container", container) := updated_containers if { count(candidate_platform_rules) > 0 updated_containers := [updated_container | platform_rule := candidate_platform_rules[_] @@ -1892,7 +1892,7 @@ apply_platform_rules("container", container) := updated_containers { ] } -apply_single_platform_rule("container", container, platform_rule) := updated_container { +apply_single_platform_rule("container", container, platform_rule) := updated_container if { container_env_rules := object.get(container, "env_rules", []) updated_env_rules := array.concat(container_env_rules, object.get(platform_rule, "env_rules", [])) @@ -1914,107 +1914,107 @@ reason := { # Error messages ################################################################ -errors["blockdev mounts are not supported"] { +errors["blockdev mounts are not supported"] if { input.rule in ["mount_blockdev", "unmount_blockdev"] } -errors["deviceHash not found"] { +errors["deviceHash not found"] if { input.rule == "mount_device" not deviceHash_ok } -errors["device already mounted at path"] { +errors["device already mounted at path"] if { input.rule in ["mount_device", "rw_mount_device"] device_mounted(input.target) } -errors["mountpoint invalid"] { +errors["mountpoint invalid"] if { input.rule in ["mount_device", "rw_mount_device"] not mount_target_ok } -errors["no device at path to unmount"] { +errors["no device at path to unmount"] if { input.rule == "unmount_device" not data.metadata.devices[input.unmountTarget] not data.metadata.rw_devices[input.unmountTarget] } -errors["received read-only unmount request, but device provided is read-write"] { +errors["received read-only unmount request, but device provided is read-write"] if { input.rule == "unmount_device" not data.metadata.devices[input.unmountTarget] data.metadata.rw_devices[input.unmountTarget] } -errors["no device at path to unmount"] { +errors["no device at path to unmount"] if { input.rule == "rw_unmount_device" not data.metadata.devices[input.unmountTarget] not data.metadata.rw_devices[input.unmountTarget] } -errors["received read-write unmount request, but device provided is read-only"] { +errors["received read-write unmount request, but device provided is read-only"] if { input.rule == "rw_unmount_device" not data.metadata.rw_devices[input.unmountTarget] data.metadata.devices[input.unmountTarget] } # Error string tested in azcri-containerd Test_RunPodSandboxNotAllowed_WithPolicy_EncryptedScratchPolicy -errors["unencrypted scratch not allowed, non-readonly mount request for SCSI disk must request encryption"] { +errors["unencrypted scratch not allowed, non-readonly mount request for SCSI disk must request encryption"] if { input.rule == "rw_mount_device" not allow_unencrypted_scratch not input.encrypted } -errors["ensureFilesystem must be set on rw device mounts"] { +errors["ensureFilesystem must be set on rw device mounts"] if { input.rule == "rw_mount_device" not input.ensureFilesystem } -errors["rw device mounts uses a filesystem that is not allowed"] { +errors["rw device mounts uses a filesystem that is not allowed"] if { input.rule == "rw_mount_device" not allowed_scratch_fs(input.filesystem) } -errors["container already started"] { +errors["container already started"] if { input.rule == "create_container" container_started } -errors["container not started"] { +errors["container not started"] if { input.rule in ["exec_in_container", "shutdown_container", "signal_container_process"] not container_started } -errors["overlay has already been mounted"] { +errors["overlay has already been mounted"] if { input.rule == "mount_overlay" overlay_exists } default overlay_matches := false -overlay_matches { +overlay_matches if { some container in candidate_containers layerPaths_ok(container.layers) } -errors["no overlay at path to unmount"] { +errors["no overlay at path to unmount"] if { input.rule == "unmount_overlay" not overlay_mounted(input.unmountTarget) } -errors["no matching containers for overlay"] { +errors["no matching containers for overlay"] if { input.rule == "mount_overlay" not overlay_matches } default privileged_matches := false -privileged_matches { +privileged_matches if { input.rule == "create_container" some container in data.metadata.matches[input.containerID] privileged_ok(container.allow_elevated) } -errors["privileged escalation not allowed"] { +errors["privileged escalation not allowed"] if { is_linux input.rule in ["create_container"] not privileged_matches @@ -2022,45 +2022,45 @@ errors["privileged escalation not allowed"] { default command_matches := false -command_matches { +command_matches if { input.rule == "create_container" some container in data.metadata.matches[input.containerID] command_ok(container.command) } -command_matches { +command_matches if { input.rule == "exec_in_container" some container in data.metadata.matches[input.containerID] some process in container.exec_processes command_ok(process.command) } -command_matches { +command_matches if { input.rule == "exec_external" some process in candidate_external_processes command_ok(process.command) } -errors["invalid command"] { +errors["invalid command"] if { input.rule in ["create_container", "exec_in_container", "exec_external"] not command_matches } -env_matches(env) { +env_matches(env) if { input.rule in ["create_container", "exec_in_container"] some container in data.metadata.matches[input.containerID] some rule in container.env_rules env_rule_ok(rule, env) } -env_matches(env) { +env_matches(env) if { input.rule in ["exec_external"] some process in candidate_external_processes some rule in process.env_rules env_rule_ok(rule, env) } -errors[envError] { +errors[envError] if { input.rule in ["create_container", "exec_in_container", "exec_external"] bad_envs := [invalid | env := input.envList[_] @@ -2073,12 +2073,12 @@ errors[envError] { envError := concat(" ", ["invalid env list:", concat(",", bad_envs)]) } -env_rule_matches(rule) { +env_rule_matches(rule) if { some env in input.envList env_rule_ok(rule, env) } -errors["missing required environment variable"] { +errors["missing required environment variable"] if { is_linux input.rule == "create_container" @@ -2111,7 +2111,7 @@ errors["missing required environment variable"] { count(containers) > 0 } -errors["missing required environment variable"] { +errors["missing required environment variable"] if { input.rule == "exec_in_container" container_started @@ -2142,7 +2142,7 @@ errors["missing required environment variable"] { count(containers) > 0 } -errors["missing required environment variable"] { +errors["missing required environment variable"] if { input.rule == "exec_external" possible_processes := [process | @@ -2172,7 +2172,7 @@ errors["missing required environment variable"] { # All environment variables matches some rule in some container, but there are # no containers with exactly the given combination of rules (i.e. for every # container, there is at least one mismatching rule). -errors["invalid env list"] { +errors["invalid env list"] if { input.rule in ["create_container"] every container in data.metadata.matches[input.containerID] { @@ -2192,29 +2192,29 @@ errors["invalid env list"] { default workingDirectory_matches := false -workingDirectory_matches { +workingDirectory_matches if { input.rule in ["create_container", "exec_in_container"] some container in data.metadata.matches[input.containerID] workingDirectory_ok(container.working_dir) } -workingDirectory_matches { +workingDirectory_matches if { input.rule == "exec_external" some process in candidate_external_processes workingDirectory_ok(process.working_dir) } -errors["invalid working directory"] { +errors["invalid working directory"] if { input.rule in ["create_container", "exec_in_container", "exec_external"] not workingDirectory_matches } -mount_matches(mount) { +mount_matches(mount) if { some container in data.metadata.matches[input.containerID] mount_ok(container.mounts, container.allow_elevated, mount) } -errors[mountError] { +errors[mountError] if { is_linux input.rule == "create_container" bad_mounts := [mount.destination | @@ -2228,13 +2228,13 @@ errors[mountError] { default signal_allowed := false -signal_allowed { +signal_allowed if { input.isInitProcess some container in data.metadata.matches[input.containerID] signal_ok(container.signals) } -signal_allowed { +signal_allowed if { not input.isInitProcess some container in data.metadata.matches[input.containerID] some process in container.exec_processes @@ -2242,46 +2242,46 @@ signal_allowed { signal_ok(process.signals) } -errors["target isn't allowed to receive the signal"] { +errors["target isn't allowed to receive the signal"] if { input.rule == "signal_container_process" not signal_allowed } -errors["device already mounted at path"] { +errors["device already mounted at path"] if { input.rule == "plan9_mount" plan9_mounted(input.target) } -errors["no device at path to unmount"] { +errors["no device at path to unmount"] if { input.rule == "plan9_unmount" not plan9_mounted(input.unmountTarget) } default fragment_issuer_matches := false -fragment_issuer_matches { +fragment_issuer_matches if { some fragment in candidate_fragments fragment.issuer == input.issuer } -errors["invalid fragment issuer"] { +errors["invalid fragment issuer"] if { input.rule == "load_fragment" not fragment_issuer_matches } default fragment_feed_matches := false -fragment_feed_matches { +fragment_feed_matches if { some fragment in candidate_fragments fragment.issuer == input.issuer fragment.feed == input.feed } -fragment_feed_matches { +fragment_feed_matches if { input.feed in data.metadata.issuers[input.issuer] } -errors["invalid fragment feed"] { +errors["invalid fragment feed"] if { input.rule == "load_fragment" fragment_issuer_matches not fragment_feed_matches @@ -2289,7 +2289,7 @@ errors["invalid fragment feed"] { default fragment_version_is_valid := false -fragment_version_is_valid { +fragment_version_is_valid if { some fragment in candidate_fragments input.fragment_loaded fragment.issuer == input.issuer @@ -2297,7 +2297,7 @@ fragment_version_is_valid { svn_ok(data[input.namespace].svn, fragment.minimum_svn) } -fragment_version_is_valid { +fragment_version_is_valid if { some fragment in candidate_fragments fragment.issuer == input.issuer fragment.feed == input.feed @@ -2307,7 +2307,7 @@ fragment_version_is_valid { default svn_mismatch := false -svn_mismatch { +svn_mismatch if { some fragment in candidate_fragments fragment.issuer == input.issuer fragment.feed == input.feed @@ -2316,7 +2316,7 @@ svn_mismatch { semver.is_valid(fragment.minimum_svn) } -svn_mismatch { +svn_mismatch if { some fragment in candidate_fragments fragment.issuer == input.issuer fragment.feed == input.feed @@ -2326,7 +2326,7 @@ svn_mismatch { } # Header SVN is always a number, not semver -svn_mismatch { +svn_mismatch if { some fragment in candidate_fragments fragment.issuer == input.issuer fragment.feed == input.feed @@ -2337,7 +2337,7 @@ svn_mismatch { default header_svn_not_match_fragment := false -header_svn_not_match_fragment { +header_svn_not_match_fragment if { input.has_header_svn some fragment in candidate_fragments fragment.issuer == input.issuer @@ -2349,7 +2349,7 @@ header_svn_not_match_fragment { default missing_svn := false -missing_svn { +missing_svn if { not input.has_header_svn some fragment in candidate_fragments fragment.issuer == input.issuer @@ -2358,7 +2358,7 @@ missing_svn { not data[input.namespace].svn } -errors["fragment svn is below the specified minimum"] { +errors["fragment svn is below the specified minimum"] if { input.rule == "load_fragment" fragment_feed_matches input.fragment_loaded @@ -2366,14 +2366,14 @@ errors["fragment svn is below the specified minimum"] { not fragment_version_is_valid } -errors["fragment svn and the specified minimum are different types"] { +errors["fragment svn and the specified minimum are different types"] if { input.rule == "load_fragment" fragment_feed_matches input.fragment_loaded svn_mismatch } -errors[svnMismatchError] { +errors[svnMismatchError] if { input.rule == "load_fragment" fragment_feed_matches input.fragment_loaded @@ -2382,7 +2382,7 @@ errors[svnMismatchError] { svnMismatchError := sprintf("svn in header %v does not match svn in fragment rego %v", [input.header_svn, data[input.namespace].svn]) } -errors["missing fragment svn in either header or rego payload"] { +errors["missing fragment svn in either header or rego payload"] if { input.rule == "load_fragment" fragment_feed_matches input.fragment_loaded @@ -2390,7 +2390,7 @@ errors["missing fragment svn in either header or rego payload"] { } # This will result in one error per missing receipt requirement -errors[receipt_error] { +errors[receipt_error] if { input.rule == "load_fragment" not input.fragment_loaded some fragment in candidate_fragments @@ -2403,19 +2403,19 @@ errors[receipt_error] { default ttl_matches := false -ttl_matches { +ttl_matches if { some ttl in candidate_transparency_trust_lists ttl.issuer == input.issuer ttl.subject == input.subject svn_ok(input.svn, ttl.minimum_svn) } -errors["no TTL candidate matches the provided TTL's issuer, subject and svn"] { +errors["no TTL candidate matches the provided TTL's issuer, subject and svn"] if { input.rule == "load_transparency_trust_list" not ttl_matches } -errors["The provided TTL does not contain any ledgers it is allowed to load"] { +errors["The provided TTL does not contain any ledgers it is allowed to load"] if { input.rule == "load_transparency_trust_list" ttl_matches ttl_ledgers := ttl_allowed_ledgers_for_issuer_subject_svn(input.issuer, input.subject, input.svn) @@ -2423,38 +2423,38 @@ errors["The provided TTL does not contain any ledgers it is allowed to load"] { count(allowed_ledgers) == 0 } -errors["scratch already mounted at path"] { +errors["scratch already mounted at path"] if { input.rule == "scratch_mount" scratch_mounted(input.target) } -errors["unencrypted scratch not allowed"] { +errors["unencrypted scratch not allowed"] if { input.rule == "scratch_mount" not allow_unencrypted_scratch not input.encrypted } -errors["no scratch at path to unmount"] { +errors["no scratch at path to unmount"] if { input.rule == "scratch_unmount" not scratch_mounted(input.unmountTarget) } -errors["log provider not allowed by policy"] { +errors["log provider not allowed by policy"] if { input.rule == "log_provider" not log_provider.allowed } -errors[framework_version_error] { +errors[framework_version_error] if { policy_framework_version == null framework_version_error := concat(" ", ["framework_version is missing. Current version:", version]) } -errors[framework_version_error] { +errors[framework_version_error] if { semver.compare(policy_framework_version, version) > 0 framework_version_error := concat(" ", ["framework_version is ahead of the current version:", policy_framework_version, "is greater than", version]) } -errors[fragment_framework_version_error] { +errors[fragment_framework_version_error] if { input.rule == "load_fragment" input.fragment_loaded input.namespace @@ -2462,7 +2462,7 @@ errors[fragment_framework_version_error] { fragment_framework_version_error := concat(" ", ["fragment framework_version is missing. Current version:", version]) } -errors[fragment_framework_version_error] { +errors[fragment_framework_version_error] if { input.rule == "load_fragment" input.fragment_loaded input.namespace @@ -2470,7 +2470,7 @@ errors[fragment_framework_version_error] { fragment_framework_version_error := concat(" ", ["fragment framework_version is ahead of the current version:", fragment_framework_version, "is greater than", version]) } -errors["containers only distinguishable by allow_stdio_access"] { +errors["containers only distinguishable by allow_stdio_access"] if { is_linux input.rule == "create_container" @@ -2519,7 +2519,7 @@ errors["containers only distinguishable by allow_stdio_access"] { c.allow_stdio_access != allow_stdio_access } -errors["containers only distinguishable by allow_stdio_access"] { +errors["containers only distinguishable by allow_stdio_access"] if { is_windows input.rule == "create_container" @@ -2557,7 +2557,7 @@ errors["containers only distinguishable by allow_stdio_access"] { c.allow_stdio_access != allow_stdio_access } -errors["external processes only distinguishable by allow_stdio_access"] { +errors["external processes only distinguishable by allow_stdio_access"] if { input.rule == "exec_external" possible_processes := [process | @@ -2586,13 +2586,13 @@ errors["external processes only distinguishable by allow_stdio_access"] { default noNewPrivileges_matches := false -noNewPrivileges_matches { +noNewPrivileges_matches if { input.rule == "create_container" some container in data.metadata.matches[input.containerID] noNewPrivileges_ok(container.no_new_privileges) } -noNewPrivileges_matches { +noNewPrivileges_matches if { input.rule == "exec_in_container" some container in data.metadata.matches[input.containerID] some process in container.exec_processes @@ -2601,7 +2601,7 @@ noNewPrivileges_matches { noNewPrivileges_ok(process.no_new_privileges) } -errors["invalid noNewPrivileges"] { +errors["invalid noNewPrivileges"] if { is_linux input.rule in ["create_container", "exec_in_container"] not noNewPrivileges_matches @@ -2609,13 +2609,13 @@ errors["invalid noNewPrivileges"] { default user_matches := false -user_matches { +user_matches if { input.rule == "create_container" some container in data.metadata.matches[input.containerID] user_ok(container.user) } -user_matches { +user_matches if { input.rule == "exec_in_container" some container in data.metadata.matches[input.containerID] some process in container.exec_processes @@ -2624,12 +2624,12 @@ user_matches { user_ok(process.user) } -errors["invalid user"] { +errors["invalid user"] if { input.rule in ["create_container", "exec_in_container"] not user_matches } -errors["capabilities don't match"] { +errors["capabilities don't match"] if { is_linux input.rule == "create_container" @@ -2669,7 +2669,7 @@ errors["capabilities don't match"] { count(possible_after_caps_containers) == 0 } -errors["capabilities don't match"] { +errors["capabilities don't match"] if { is_linux input.rule == "exec_in_container" @@ -2707,7 +2707,7 @@ errors["capabilities don't match"] { count(possible_after_caps_containers) == 0 } -errors["devices not supported"] { +errors["devices not supported"] if { is_linux input.rule == "create_container" not devices_ok([], input.devices) @@ -2715,7 +2715,7 @@ errors["devices not supported"] { # covers exec_in_container as well. it shouldn't be possible to ever get # an exec_in_container as it "inherits" capabilities rules from create_container -errors["containers only distinguishable by capabilties"] { +errors["containers only distinguishable by capabilties"] if { is_linux input.rule == "create_container" @@ -2755,13 +2755,13 @@ errors["containers only distinguishable by capabilties"] { default seccomp_matches := false -seccomp_matches { +seccomp_matches if { input.rule == "create_container" some container in data.metadata.matches[input.containerID] seccomp_ok(container.seccomp_profile_sha256) } -errors["invalid seccomp"] { +errors["invalid seccomp"] if { is_linux input.rule == "create_container" not seccomp_matches @@ -2769,12 +2769,12 @@ errors["invalid seccomp"] { default error_objects := null -error_objects := containers { +error_objects := containers if { input.rule == "create_container" containers := data.metadata.matches[input.containerID] } -error_objects := processes { +error_objects := processes if { input.rule == "exec_in_container" processes := [process | container := data.metadata.matches[input.containerID][_] @@ -2782,12 +2782,12 @@ error_objects := processes { ] } -error_objects := processes { +error_objects := processes if { input.rule == "exec_external" processes := candidate_external_processes } -error_objects := fragments { +error_objects := fragments if { input.rule == "load_fragment" fragments := candidate_fragments } @@ -2798,12 +2798,12 @@ error_objects := fragments { ################################################################################ -check_container(raw_container, framework_version) := container { +check_container(raw_container, framework_version) := container if { semver.compare(framework_version, version) == 0 container := raw_container } -check_container(raw_container, framework_version) := container { +check_container(raw_container, framework_version) := container if { semver.compare(framework_version, version) < 0 container := { # Base fields @@ -2824,22 +2824,22 @@ check_container(raw_container, framework_version) := container { } } -check_no_new_privileges(raw_container, framework_version) := no_new_privileges { +check_no_new_privileges(raw_container, framework_version) := no_new_privileges if { semver.compare(framework_version, "0.2.0") >= 0 no_new_privileges := raw_container.no_new_privileges } -check_no_new_privileges(raw_container, framework_version) := no_new_privileges { +check_no_new_privileges(raw_container, framework_version) := no_new_privileges if { semver.compare(framework_version, "0.2.0") < 0 no_new_privileges := false } -check_user(raw_container, framework_version) := user { +check_user(raw_container, framework_version) := user if { semver.compare(framework_version, "0.2.1") >= 0 user := raw_container.user } -check_user(raw_container, framework_version) := user { +check_user(raw_container, framework_version) := user if { semver.compare(framework_version, "0.2.1") < 0 user := { "umask": "0022", @@ -2856,12 +2856,12 @@ check_user(raw_container, framework_version) := user { } } -check_capabilities(raw_container, framework_version) := capabilities { +check_capabilities(raw_container, framework_version) := capabilities if { semver.compare(framework_version, "0.2.2") >= 0 capabilities := raw_container.capabilities } -check_capabilities(raw_container, framework_version) := capabilities { +check_capabilities(raw_container, framework_version) := capabilities if { semver.compare(framework_version, "0.2.2") < 0 # we cannot determine a reasonable default at the time this is called, # which is either during `mount_overlay` or `load_fragment`, and so @@ -2870,32 +2870,32 @@ check_capabilities(raw_container, framework_version) := capabilities { capabilities := null } -check_seccomp_profile_sha256(raw_container, framework_version) := seccomp_profile_sha256 { +check_seccomp_profile_sha256(raw_container, framework_version) := seccomp_profile_sha256 if { semver.compare(framework_version, "0.2.3") >= 0 seccomp_profile_sha256 := raw_container.seccomp_profile_sha256 } -check_seccomp_profile_sha256(raw_container, framework_version) := seccomp_profile_sha256 { +check_seccomp_profile_sha256(raw_container, framework_version) := seccomp_profile_sha256 if { semver.compare(framework_version, "0.2.3") < 0 seccomp_profile_sha256 := "" } -check_signals(raw_container, framework_version) := signals { +check_signals(raw_container, framework_version) := signals if { semver.compare(framework_version, "0.4.1") >= 0 signals := raw_container.signals } -check_signals(raw_container, framework_version) := signals { +check_signals(raw_container, framework_version) := signals if { semver.compare(framework_version, "0.4.1") < 0 signals := array.concat(raw_container.signals, [9, 15]) } -check_external_process(raw_process, framework_version) := process { +check_external_process(raw_process, framework_version) := process if { semver.compare(framework_version, version) == 0 process := raw_process } -check_external_process(raw_process, framework_version) := process { +check_external_process(raw_process, framework_version) := process if { semver.compare(framework_version, version) < 0 process := { # Base fields @@ -2907,12 +2907,12 @@ check_external_process(raw_process, framework_version) := process { } } -check_fragment(raw_fragment, framework_version) := fragment { +check_fragment(raw_fragment, framework_version) := fragment if { semver.compare(framework_version, version) == 0 fragment := raw_fragment } -check_fragment(raw_fragment, framework_version) := fragment { +check_fragment(raw_fragment, framework_version) := fragment if { semver.compare(framework_version, version) < 0 fragment := { # Base fields @@ -2951,7 +2951,7 @@ allow_log_provider_dropping := data.policy.allow_log_provider_dropping default allow_capability_dropping := false -allow_capability_dropping := flag { +allow_capability_dropping := flag if { semver.compare(policy_framework_version, "0.2.2") >= 0 flag := data.policy.allow_capability_dropping } From 632d2d60634edcd09100462c0a485c65a8ed2794 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Thu, 11 Dec 2025 12:56:08 +0000 Subject: [PATCH 22/22] Fix remaining missing if --- pkg/securitypolicy/fragment_definition.rego | 2 +- pkg/securitypolicy/framework.rego | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/securitypolicy/fragment_definition.rego b/pkg/securitypolicy/fragment_definition.rego index 027f7f341d..58a77c51cd 100644 --- a/pkg/securitypolicy/fragment_definition.rego +++ b/pkg/securitypolicy/fragment_definition.rego @@ -1,5 +1,5 @@ default __fragment_parameters_metadata := {} -__fragment_parameters_metadata := data[input.namespace].parameters_api { +__fragment_parameters_metadata := data[input.namespace].parameters_api if { data[input.namespace].parameters_api } parameter(name) := data.framework.extract_parameter(name, __fragment_parameters, __fragment_parameters_metadata) diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index eee7212b48..7c3b614110 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -13,9 +13,9 @@ version := "@@FRAMEWORK_VERSION@@" anchor_pattern(p) := p if { startswith(p, "^") endswith(p, "$") -} else := concat("", ["^", p]) { +} else := concat("", ["^", p]) if { endswith(p, "$") -} else := concat("", [p, "$"]) { +} else := concat("", [p, "$"]) if { startswith(p, "^") } else := concat("", ["^", p, "$"]) @@ -1842,7 +1842,7 @@ registry_changes := {"allowed": true} if { extract_parameter(name, fragment_parameters_obj, parameters_metadata) := fragment_parameters_obj[name] if { name in object.keys(fragment_parameters_obj) -} else := parameters_metadata[name]["default"] { +} else := parameters_metadata[name]["default"] if { "default" in object.keys(parameters_metadata[name]) }