diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 510bfd6677..dfac537dd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,9 +249,6 @@ jobs: - name: Install gotestsum run: go install gotest.tools/gotestsum@${{ env.GOTESTSUM_VERSION }} - - name: Test standard security policy - run: ${{ env.GOTESTSUM_CMD }} -timeout=30m -gcflags=all=-d=checkptr ./pkg/securitypolicy/... - - name: Test rego security policy run: ${{ env.GOTESTSUM_CMD }} -tags=rego -timeout=30m -gcflags=all=-d=checkptr ./pkg/securitypolicy/... @@ -314,6 +311,9 @@ jobs: & '${{ github.workspace }}/bin/psexec' -accepteula -nobanner cmd /c "exit 0" # run tests + - name: Test rego security policy + run: ${{ env.GOTESTSUM_CMD }} -tags=rego -timeout=30m -gcflags=all=-d=checkptr ./pkg/securitypolicy/... + - name: Test repo run: ${{ env.GOTESTSUM_CMD }} -gcflags=all=-d=checkptr -tags admin -timeout=20m ./... diff --git a/cmd/gcs-sidecar/main.go b/cmd/gcs-sidecar/main.go index 71e27dbc05..60f23f6928 100644 --- a/cmd/gcs-sidecar/main.go +++ b/cmd/gcs-sidecar/main.go @@ -151,7 +151,6 @@ func main() { logrus.Fatal(err) } logrus.SetLevel(level) - logrus.SetOutput(logFileHandle) trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()}) trace.RegisterExporter(&oc.LogrusExporter{}) @@ -227,7 +226,7 @@ func main() { initialEnforcer := &securitypolicy.ClosedDoorSecurityPolicyEnforcer{} // 3. Create bridge and initializa - brdg := sidecar.NewBridge(shimCon, gcsCon, initialEnforcer) + brdg := sidecar.NewBridge(shimCon, gcsCon, initialEnforcer, logFileHandle) brdg.AssignHandlers() // 3. Listen and serve for hcsshim requests. diff --git a/internal/gcs-sidecar/bridge.go b/internal/gcs-sidecar/bridge.go index 545d2b7eb3..1c1cefdcd9 100644 --- a/internal/gcs-sidecar/bridge.go +++ b/internal/gcs-sidecar/bridge.go @@ -32,6 +32,9 @@ import ( type Bridge struct { mu sync.Mutex + pendingMu sync.Mutex + pending map[sequenceID]*prot.ContainerExecuteProcessResponse + hostState *Host // List of handlers for handling different rpc message requests. rpcHandlerList map[prot.RPCProc]HandlerFunc @@ -44,6 +47,9 @@ type Bridge struct { // and send responses back to hcsshim respectively. sendToGCSCh chan request sendToShimCh chan bridgeResponse + + // logging target + logWriter io.Writer } // SequenceID is used to correlate requests and responses. @@ -74,22 +80,17 @@ type request struct { message []byte } -func NewBridge(shimConn io.ReadWriteCloser, inboxGCSConn io.ReadWriteCloser, initialEnforcer securitypolicy.SecurityPolicyEnforcer) *Bridge { +func NewBridge(shimConn io.ReadWriteCloser, inboxGCSConn io.ReadWriteCloser, initialEnforcer securitypolicy.SecurityPolicyEnforcer, logWriter io.Writer) *Bridge { hostState := NewHost(initialEnforcer) return &Bridge{ + pending: make(map[sequenceID]*prot.ContainerExecuteProcessResponse), rpcHandlerList: make(map[prot.RPCProc]HandlerFunc), hostState: hostState, shimConn: shimConn, inboxGCSConn: inboxGCSConn, sendToGCSCh: make(chan request), sendToShimCh: make(chan bridgeResponse), - } -} - -func NewPolicyEnforcer(initialEnforcer securitypolicy.SecurityPolicyEnforcer) *SecurityPolicyEnforcer { - return &SecurityPolicyEnforcer{ - securityPolicyEnforcerSet: false, - securityPolicyEnforcer: initialEnforcer, + logWriter: logWriter, } } @@ -448,6 +449,23 @@ func (b *Bridge) ListenAndServeShimRequests() error { _ = sendWithContextCancel(ctx, sidecarErrChan, recverr) return } + // If this is a ContainerExecuteProcessResponse, notify + const MsgExecuteProcessResponse prot.MsgType = prot.MsgTypeResponse | prot.MsgType(prot.RPCExecuteProcess) + + if header.Type == MsgExecuteProcessResponse { + logrus.Tracef("Printing after inbox exec resp") + var procResp prot.ContainerExecuteProcessResponse + if err := json.Unmarshal(message, &procResp); err != nil { + logrus.Tracef("unmarshal failed") + } + + b.pendingMu.Lock() + if _, exists := b.pending[header.ID]; exists { + logrus.Tracef("Header ID in pending exists") + b.pending[header.ID] = &procResp + } + b.pendingMu.Unlock() + } // Forward to shim resp := bridgeResponse{ diff --git a/internal/gcs-sidecar/handlers.go b/internal/gcs-sidecar/handlers.go index 9dc0818d31..7660e6d069 100644 --- a/internal/gcs-sidecar/handlers.go +++ b/internal/gcs-sidecar/handlers.go @@ -4,10 +4,12 @@ package bridge import ( + "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" + "strings" "time" "github.com/Microsoft/hcsshim/hcn" @@ -17,16 +19,22 @@ import ( hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/oc" + "github.com/Microsoft/hcsshim/internal/oci" "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/internal/windevice" + "github.com/Microsoft/hcsshim/pkg/annotations" "github.com/Microsoft/hcsshim/pkg/cimfs" + "github.com/Microsoft/hcsshim/pkg/securitypolicy" + "github.com/pkg/errors" + "golang.org/x/sys/windows" ) const ( sandboxStateDirName = "WcSandboxState" hivesDirName = "Hives" devPathFormat = "\\\\.\\PHYSICALDRIVE%d" + UVMContainerID = "00000000-0000-0000-0000-000000000000" ) // - Handler functions handle the incoming message requests. It @@ -41,17 +49,18 @@ func (b *Bridge) createContainer(req *request) (err error) { defer span.End() defer func() { oc.SetSpanStatus(span, err) }() - var r prot.ContainerCreate + var createContainerRequest prot.ContainerCreate var containerConfig json.RawMessage - r.ContainerConfig.Value = &containerConfig - if err = commonutils.UnmarshalJSONWithHresult(req.message, &r); err != nil { - return fmt.Errorf("failed to unmarshal createContainer: %w", err) + createContainerRequest.ContainerConfig.Value = &containerConfig + if err = commonutils.UnmarshalJSONWithHresult(req.message, &createContainerRequest); err != nil { + return errors.Wrap(err, "failed to unmarshal createContainer") } - // containerConfig can be of type uvnConfig or hcsschema.HostedSystem + // containerConfig can be of type uvnConfig or hcsschema.HostedSystem or guestresource.CWCOWHostedSystem var ( - uvmConfig prot.UvmConfig - hostedSystemConfig hcsschema.HostedSystem + uvmConfig prot.UvmConfig + hostedSystemConfig hcsschema.HostedSystem + cwcowHostedSystemConfig guestresource.CWCOWHostedSystem ) if err = commonutils.UnmarshalJSONWithHresult(containerConfig, &uvmConfig); err == nil && uvmConfig.SystemType != "" { @@ -62,7 +71,117 @@ func (b *Bridge) createContainer(req *request) (err error) { hostedSystemConfig.SchemaVersion != nil && hostedSystemConfig.Container != nil { schemaVersion := hostedSystemConfig.SchemaVersion container := hostedSystemConfig.Container - log.G(ctx).Tracef("createContainer: HostedSystemConfig: {schemaVersion: %v, container: %v}}", schemaVersion, 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 + schemaVersion := cwcowHostedSystem.SchemaVersion + container := cwcowHostedSystem.Container + spec := cwcowHostedSystemConfig.Spec + containerID := createContainerRequest.ContainerID + log.G(ctx).Tracef("rpcCreate: CWCOWHostedSystemConfig {spec: %v, schemaVersion: %v, container: %v}}", string(req.message), schemaVersion, container) + + user := securitypolicy.IDName{ + Name: spec.Process.User.Username, + } + _, _, _, err := b.hostState.securityPolicyEnforcer.EnforceCreateContainerPolicyV2(req.ctx, containerID, spec.Process.Args, spec.Process.Env, spec.Process.Cwd, spec.Mounts, user, nil) + + if err != nil { + return fmt.Errorf("CreateContainer operation is denied by policy: %w", err) + } + c := &Container{ + id: containerID, + spec: spec, + processes: make(map[uint32]*containerProcess), + } + log.G(ctx).Tracef("Adding ContainerID: %v", containerID) + if err := b.hostState.AddContainer(req.ctx, containerID, c); err != nil { + log.G(ctx).Tracef("Container exists in the map.") + return err + } + defer func(err error) { + if err != nil { + b.hostState.RemoveContainer(ctx, containerID) + } + }(err) + // Write security policy, signed UVM reference and host AMD certificate to + // container's rootfs, so that application and sidecar containers can have + // access to it. The security policy is required by containers which need to + // extract init-time claims found in the security policy. The directory path + // containing the files is exposed via UVM_SECURITY_CONTEXT_DIR env var. + // It may be an error to have a security policy but not expose it to the + // container as in that case it can never be checked as correct by a verifier. + if oci.ParseAnnotationsBool(ctx, spec.Annotations, annotations.WCOWSecurityPolicyEnv, true) { + encodedPolicy := b.hostState.securityPolicyEnforcer.EncodedSecurityPolicy() + hostAMDCert := spec.Annotations[annotations.WCOWHostAMDCertificate] + if len(encodedPolicy) > 0 || len(hostAMDCert) > 0 || len(b.hostState.uvmReferenceInfo) > 0 { + // Use os.MkdirTemp to make sure that the directory is unique. + securityContextDir, err := os.MkdirTemp(spec.Root.Path, securitypolicy.SecurityContextDirTemplate) + if err != nil { + return fmt.Errorf("failed to create security context directory: %w", err) + } + // Make sure that files inside directory are readable + if err := os.Chmod(securityContextDir, 0755); err != nil { + return fmt.Errorf("failed to chmod security context directory: %w", err) + } + + if len(encodedPolicy) > 0 { + if err := writeFileInDir(securityContextDir, securitypolicy.PolicyFilename, []byte(encodedPolicy), 0777); err != nil { + return fmt.Errorf("failed to write security policy: %w", err) + } + } + if len(b.hostState.uvmReferenceInfo) > 0 { + if err := writeFileInDir(securityContextDir, securitypolicy.ReferenceInfoFilename, []byte(b.hostState.uvmReferenceInfo), 0777); err != nil { + return fmt.Errorf("failed to write UVM reference info: %w", err) + } + } + + if len(hostAMDCert) > 0 { + if err := writeFileInDir(securityContextDir, securitypolicy.HostAMDCertFilename, []byte(hostAMDCert), 0777); err != nil { + return fmt.Errorf("failed to write host AMD certificate: %w", err) + } + } + + containerCtxDir := fmt.Sprintf("/%s", filepath.Base(securityContextDir)) + secCtxEnv := fmt.Sprintf("UVM_SECURITY_CONTEXT_DIR=%s", containerCtxDir) + spec.Process.Env = append(spec.Process.Env, secCtxEnv) + } + } + + // Strip the spec field + hostedSystemBytes, err := json.Marshal(cwcowHostedSystem) + + if err != nil { + return fmt.Errorf("failed to marshal hostedSystem: %w", err) + } + + // marshal it again into a JSON-escaped string which inbox GCS expects + hostedSystemEscapedBytes, err := json.Marshal(string(hostedSystemBytes)) + if err != nil { + return fmt.Errorf("failed to marshal hostedSystem JSON: %w", err) + } + + // Prepare a fixed struct that takes in raw message + type containerCreateModified struct { + prot.RequestBase + ContainerConfig json.RawMessage + } + createContainerRequestModified := containerCreateModified{ + RequestBase: createContainerRequest.RequestBase, + ContainerConfig: hostedSystemEscapedBytes, + } + + buf, err := json.Marshal(createContainerRequestModified) + log.G(ctx).Tracef("marshaled request buffer: %s", string(buf)) + if err != nil { + return fmt.Errorf("failed to marshal rpcCreatecontainer: %w", err) + } + var newRequest request + newRequest.ctx = req.ctx + newRequest.header = req.header + newRequest.header.Size = uint32(len(buf)) + prot.HdrSize + newRequest.message = buf + req = &newRequest } else { return fmt.Errorf("invalid request to createContainer") } @@ -71,6 +190,34 @@ func (b *Bridge) createContainer(req *request) (err error) { return nil } +func writeFileInDir(dir string, filename string, data []byte, perm os.FileMode) error { + st, err := os.Stat(dir) + if err != nil { + return err + } + + if !st.IsDir() { + return fmt.Errorf("not a directory %q", dir) + } + + targetFilename := filepath.Join(dir, filename) + return os.WriteFile(targetFilename, data, perm) +} + +// processParamEnvToOCIEnv converts an Environment field from ProcessParameters +// (a map from environment variable to value) into an array of environment +// variable assignments (where each is in the form "=") which +// can be used by an oci.Process. +func processParamEnvToOCIEnv(environment map[string]string) []string { + environmentList := make([]string, 0, len(environment)) + for k, v := range environment { + // TODO: Do we need to escape things like quotation marks in + // environment variable values? + environmentList = append(environmentList, fmt.Sprintf("%s=%s", k, v)) + } + return environmentList +} + func (b *Bridge) startContainer(req *request) (err error) { _, span := oc.StartSpan(req.ctx, "sidecar::startContainer") defer span.End() @@ -95,9 +242,10 @@ func (b *Bridge) shutdownGraceful(req *request) (err error) { return fmt.Errorf("failed to unmarshal shutdownGraceful: %w", err) } - // TODO (kiashok/Mahati): Since gcs-sidecar can be used for all types of windows - // containers, it is important to check if we want to - // enforce policy or not. + err = b.hostState.securityPolicyEnforcer.EnforceShutdownContainerPolicy(req.ctx, r.ContainerID) + if err != nil { + return fmt.Errorf("rpcShudownGraceful operation not allowed: %w", err) + } b.forwardRequestToGcs(req) return nil @@ -117,6 +265,15 @@ func (b *Bridge) shutdownForced(req *request) (err error) { return nil } +// escapeArgs makes a Windows-style escaped command line from a set of arguments. +func escapeArgs(args []string) string { + escapedArgs := make([]string, len(args)) + for i, a := range args { + escapedArgs[i] = windows.EscapeArg(a) + } + return strings.Join(escapedArgs, " ") +} + func (b *Bridge) executeProcess(req *request) (err error) { _, span := oc.StartSpan(req.ctx, "sidecar::executeProcess") defer span.End() @@ -128,13 +285,100 @@ func (b *Bridge) executeProcess(req *request) (err error) { if err := commonutils.UnmarshalJSONWithHresult(req.message, &r); err != nil { return fmt.Errorf("failed to unmarshal executeProcess: %w", err) } - + containerID := r.ContainerID var processParams hcsschema.ProcessParameters if err := commonutils.UnmarshalJSONWithHresult(processParamSettings, &processParams); err != nil { return fmt.Errorf("executeProcess: invalid params type for request: %w", err) } - b.forwardRequestToGcs(req) + commandLine := []string{processParams.CommandLine} + + if containerID == UVMContainerID { + log.G(req.ctx).Tracef("Enforcing policy on external exec process") + _, _, err := b.hostState.securityPolicyEnforcer.EnforceExecExternalProcessPolicy( + req.ctx, + commandLine, + processParamEnvToOCIEnv(processParams.Environment), + processParams.WorkingDirectory, + ) + if err != nil { + return errors.Wrapf(err, "exec is denied due to policy") + } + b.forwardRequestToGcs(req) + } else { + // fetch the container command line + c, err := b.hostState.GetCreatedContainer(req.ctx, containerID) + if err != nil { + log.G(req.ctx).Tracef("Container not found during exec: %v", containerID) + return fmt.Errorf("failed to get created container: %w", err) + } + + // if this is an exec of Container command line, then it's already enforced + // during container creation, hence skip it here + containerCommandLine := escapeArgs(c.spec.Process.Args) + if processParams.CommandLine != containerCommandLine { + + user := securitypolicy.IDName{ + Name: processParams.User, + } + + log.G(req.ctx).Tracef("Enforcing policy on exec in container") + _, _, _, err = b.hostState.securityPolicyEnforcer. + EnforceExecInContainerPolicyV2( + req.ctx, + containerID, + commandLine, + processParamEnvToOCIEnv(processParams.Environment), + processParams.WorkingDirectory, + user, + nil, + ) + if err != nil { + return errors.Wrapf(err, "exec in container denied due to policy") + } + } + headerID := req.header.ID + + // initiate process ID + b.pendingMu.Lock() + b.pending[headerID] = nil // nil means not yet received + b.pendingMu.Unlock() + + defer func() { + b.pendingMu.Lock() + delete(b.pending, headerID) + b.pendingMu.Unlock() + }() + + // forward the request to gcs + b.forwardRequestToGcs(req) + + // fetch the process ID from response + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + log.G(req.ctx).Tracef("waiting for exec resp") + b.pendingMu.Lock() + resp := b.pending[headerID] + b.pendingMu.Unlock() + + // capture the Process details, so that we can later enforce + // on the allowed signals on the Process + if resp != nil { + log.G(req.ctx).Tracef("Got response: %+v", resp) + c.processesMutex.Lock() + defer c.processesMutex.Unlock() + c.processes[resp.ProcessID] = &containerProcess{ + processspec: processParams, + cid: c.id, + pid: resp.ProcessID, + } + return nil + } + time.Sleep(10 * time.Millisecond) // backoff + } + + return errors.Wrap(err, "timedout waiting for exec response") + } return nil } @@ -163,14 +407,37 @@ func (b *Bridge) signalProcess(req *request) (err error) { if err := commonutils.UnmarshalJSONWithHresult(req.message, &r); err != nil { return fmt.Errorf("failed to unmarshal signalProcess: %w", err) } - var wcowOptions guestresource.SignalProcessOptionsWCOW if rawOpts != nil { if err := commonutils.UnmarshalJSONWithHresult(rawOpts, &wcowOptions); err != nil { return fmt.Errorf("signalProcess: invalid Options type for request: %w", err) } - } + log.G(req.ctx).Tracef("RawOpts are not nil") + containerID := r.ContainerID + c, err := b.hostState.GetCreatedContainer(req.ctx, containerID) + if err != nil { + return fmt.Errorf("failed to get created container: %w", err) + } + + p, err := c.GetProcess(r.ProcessID) + if err != nil { + log.G(req.ctx).Tracef("Process not found %v", r.ProcessID) + return err + } + cmdLine := p.processspec.CommandLine + commandLine := []string{cmdLine} + opts := &securitypolicy.SignalContainerOptions{ + IsInitProcess: false, + WindowsSignal: wcowOptions.Signal, + WindowsCommand: commandLine, + } + err = b.hostState.securityPolicyEnforcer.EnforceSignalContainerProcessPolicyV2(req.ctx, containerID, opts) + if err != nil { + return err + } + + } b.forwardRequestToGcs(req) return nil } @@ -194,6 +461,10 @@ func (b *Bridge) getProperties(req *request) (err error) { defer span.End() defer func() { oc.SetSpanStatus(span, err) }() + if err := b.hostState.securityPolicyEnforcer.EnforceGetPropertiesPolicy(req.ctx); err != nil { + return errors.Wrapf(err, "get properties denied due to policy") + } + var getPropReqV2 prot.ContainerGetPropertiesV2 if err := commonutils.UnmarshalJSONWithHresult(req.message, &getPropReqV2); err != nil { return fmt.Errorf("failed to unmarshal getProperties: %v: %w", string(req.message), err) @@ -241,6 +512,13 @@ func (b *Bridge) deleteContainerState(req *request) (err error) { if err := commonutils.UnmarshalJSONWithHresult(req.message, &r); err != nil { return fmt.Errorf("failed to unmarshal deleteContainerState: %w", err) } + _, err = b.hostState.GetCreatedContainer(req.ctx, r.ContainerID) + if err != nil { + log.G(req.ctx).Tracef("Container not found during deleteContainerState: %v", r.ContainerID) + return fmt.Errorf("container not found: %w", err) + } + // remove container state regardless of delete's success + defer b.hostState.RemoveContainer(req.ctx, r.ContainerID) b.forwardRequestToGcs(req) return nil @@ -323,19 +601,27 @@ func (b *Bridge) modifySettings(req *request) (err error) { case guestresource.ResourceTypeSecurityPolicy: securityPolicyRequest := modifyGuestSettingsRequest.Settings.(*guestresource.WCOWConfidentialOptions) log.G(ctx).Tracef("WCOWConfidentialOptions: { %v}", securityPolicyRequest) - _ = b.hostState.SetWCOWConfidentialUVMOptions(ctx, securityPolicyRequest) - + err := b.hostState.SetWCOWConfidentialUVMOptions(req.ctx, securityPolicyRequest, b.logWriter) + if err != nil { + return errors.Wrap(err, "error creating enforcer") + } // Send response back to shim resp := &prot.ResponseBase{ Result: 0, // 0 means success ActivityID: req.activityID, } - err := b.sendResponseToShim(req.ctx, prot.RPCModifySettings, req.header.ID, resp) + err = b.sendResponseToShim(req.ctx, prot.RPCModifySettings, req.header.ID, resp) if err != nil { return fmt.Errorf("error sending response to hcsshim: %w", err) } return nil - + case guestresource.ResourceTypePolicyFragment: + //Note: Reusing the same type LCOWSecurityPolicyFragment for CWCOW. + r, ok := modifyGuestSettingsRequest.Settings.(*guestresource.LCOWSecurityPolicyFragment) + if !ok { + return errors.New("the request settings are not of type LCOWSecurityPolicyFragment") + } + return b.hostState.InjectFragment(ctx, r) case guestresource.ResourceTypeWCOWBlockCims: // This is request to mount the merged cim at given volumeGUID if modifyGuestSettingsRequest.RequestType == guestrequest.RequestTypeRemove { @@ -343,28 +629,53 @@ func (b *Bridge) modifySettings(req *request) (err error) { } wcowBlockCimMounts := modifyGuestSettingsRequest.Settings.(*guestresource.CWCOWBlockCIMMounts) + containerID := wcowBlockCimMounts.ContainerID log.G(ctx).Tracef("WCOWBlockCIMMounts { %v}", wcowBlockCimMounts) // The block device takes some time to show up. Wait for a few seconds. time.Sleep(2 * time.Second) + //TODO(Mahati) : test and verify CIM hashes var layerCIMs []*cimfs.BlockCIM - for _, blockCimDevice := range wcowBlockCimMounts.BlockCIMs { + layerHashes := make([]string, len(wcowBlockCimMounts.BlockCIMs)) + layerDigests := make([][]byte, len(wcowBlockCimMounts.BlockCIMs)) + for i, blockCimDevice := range wcowBlockCimMounts.BlockCIMs { // Get the scsi device path for the blockCim lun devNumber, err := windevice.GetDeviceNumberFromControllerLUN( - ctx, + req.ctx, 0, /* controller is always 0 for wcow */ uint8(blockCimDevice.Lun)) if err != nil { return fmt.Errorf("err getting scsiDevPath: %w", err) } + physicalDevPath := fmt.Sprintf(devPathFormat, devNumber) layerCim := cimfs.BlockCIM{ Type: cimfs.BlockCIMTypeDevice, - BlockPath: fmt.Sprintf(devPathFormat, devNumber), + BlockPath: physicalDevPath, CimName: blockCimDevice.CimName, } + cimRootDigestBytes, err := cimfs.GetVerificationInfo(physicalDevPath) + if err != nil { + return fmt.Errorf("failed to get CIM verification info: %w", err) + } + layerDigests[i] = cimRootDigestBytes + layerHashes[i] = base64.URLEncoding.EncodeToString(cimRootDigestBytes) layerCIMs = append(layerCIMs, &layerCim) + + log.G(ctx).Debugf("block CIM layer digest %s, path: %s\n", layerHashes[i], physicalDevPath) + } + + // skip the merged cim and verify individual layer hashes + hashesToVerify := layerHashes + if len(layerHashes) > 1 { + hashesToVerify = layerHashes[1:] } + + err := b.hostState.securityPolicyEnforcer.EnforceVerifiedCIMsPolicy(req.ctx, containerID, hashesToVerify) + if err != nil { + return errors.Wrap(err, "CIM mount is denied by policy") + } + if len(layerCIMs) > 1 { // Get the topmost merge CIM and invoke the MountMergedBlockCIMs _, err := cimfs.MountMergedBlockCIMs(layerCIMs[0], layerCIMs[1:], wcowBlockCimMounts.MountFlags, wcowBlockCimMounts.VolumeGUID) @@ -400,8 +711,10 @@ func (b *Bridge) modifySettings(req *request) (err error) { log.G(ctx).Tracef("CWCOWCombinedLayers:: ContainerID: %v, ContainerRootPath: %v, Layers: %v, ScratchPath: %v", containerID, settings.CombinedLayers.ContainerRootPath, settings.CombinedLayers.Layers, settings.CombinedLayers.ScratchPath) - // TODO: Update modifyCombinedLayers with verified CimFS API - + //Since unencrypted scratch is not an option, always pass true + if err := b.hostState.securityPolicyEnforcer.EnforceScratchMountPolicy(ctx, settings.CombinedLayers.ContainerRootPath, true); err != nil { + return fmt.Errorf("scratch mounting denied by policy: %w", err) + } // The following two folders are expected to be present in the scratch. // But since we have just formatted the scratch we would need to // create them manually. @@ -437,11 +750,8 @@ func (b *Bridge) modifySettings(req *request) (err error) { wcowMappedVirtualDisk := modifyGuestSettingsRequest.Settings.(*guestresource.WCOWMappedVirtualDisk) log.G(ctx).Tracef("ResourceTypeMappedVirtualDiskForContainerScratch: { %v }", wcowMappedVirtualDisk) - // 1. TODO (Mahati): Need to enforce policy before calling into fsFormatter - // 2. Call fsFormatter to format the scratch disk. // This will return the volume path of the mounted scratch. // Scratch disk should be >= 30 GB for refs formatter to work. - // fsFormatter understands only virtualDevObjectPathFormat. Therefore fetch the // disk number for the corresponding lun var devNumber uint32 diff --git a/internal/gcs-sidecar/host.go b/internal/gcs-sidecar/host.go index 9e0602aaa8..85930cd496 100644 --- a/internal/gcs-sidecar/host.go +++ b/internal/gcs-sidecar/host.go @@ -5,36 +5,135 @@ package bridge import ( "context" + "crypto/sha256" + "encoding/base64" "fmt" + "io" + "os" + "path/filepath" "sync" + "time" + "github.com/Microsoft/cosesign1go/pkg/cosesign1" + didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver" + "github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr" + hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" + "github.com/Microsoft/hcsshim/internal/log" + "github.com/Microsoft/hcsshim/internal/logfields" "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/internal/pspdriver" "github.com/Microsoft/hcsshim/pkg/securitypolicy" + oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) type Host struct { + containersMutex sync.Mutex + containers map[string]*Container + // state required for the security policy enforcement policyMutex sync.Mutex securityPolicyEnforcer securitypolicy.SecurityPolicyEnforcer securityPolicyEnforcerSet bool + uvmReferenceInfo string } -type SecurityPolicyEnforcer struct { - // State required for the security policy enforcement - securityPolicyEnforcer securitypolicy.SecurityPolicyEnforcer - securityPolicyEnforcerSet bool +type Container struct { + id string + spec oci.Spec + processesMutex sync.Mutex + processes map[uint32]*containerProcess +} + +// Process is a struct that defines the lifetime and operations associated with +// an oci.Process. +type containerProcess struct { + processspec hcsschema.ProcessParameters + // cid is the container id that owns this process. + cid string + pid uint32 } func NewHost(initialEnforcer securitypolicy.SecurityPolicyEnforcer) *Host { return &Host{ + containers: make(map[string]*Container), securityPolicyEnforcer: initialEnforcer, securityPolicyEnforcerSet: false, } } -func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.WCOWConfidentialOptions) error { +// InjectFragment extends current security policy with additional constraints +// from the incoming fragment. Note that it is base64 encoded over the bridge/ +// +// There are three checking steps: +// 1 - Unpack the cose document and check it was actually signed with the cert +// chain inside its header +// 2 - Check that the issuer field did:x509 identifier is for that cert chain +// (ie fingerprint of a non leaf cert and the subject matches the leaf cert) +// 3 - Check that this issuer/feed match the requirement of the user provided +// security policy (done in the regoby LoadFragment) +func (h *Host) InjectFragment(ctx context.Context, fragment *guestresource.LCOWSecurityPolicyFragment) (err error) { + log.G(ctx).WithField("fragment", fmt.Sprintf("%+v", fragment)).Debug("GCS Host.InjectFragment") + + raw, err := base64.StdEncoding.DecodeString(fragment.Fragment) + if err != nil { + return err + } + blob := []byte(fragment.Fragment) + // keep a copy of the fragment, so we can manually figure out what went wrong + // will be removed eventually. Give it a unique name to avoid any potential + // race conditions. + sha := sha256.New() + sha.Write(blob) + timestamp := time.Now() + fragmentPath := fmt.Sprintf("fragment-%x-%d.blob", sha.Sum(nil), timestamp.UnixMilli()) + _ = os.WriteFile(filepath.Join(os.TempDir(), fragmentPath), blob, 0644) + + unpacked, err := cosesign1.UnpackAndValidateCOSE1CertChain(raw) + if err != nil { + return fmt.Errorf("InjectFragment failed COSE validation: %w", err) + } + + payloadString := string(unpacked.Payload[:]) + issuer := unpacked.Issuer + feed := unpacked.Feed + chainPem := unpacked.ChainPem + + log.G(ctx).WithFields(logrus.Fields{ + "issuer": issuer, // eg the DID:x509:blah.... + "feed": feed, + "cty": unpacked.ContentType, + "chainPem": chainPem, + }).Debugf("unpacked COSE1 cert chain") + + log.G(ctx).WithFields(logrus.Fields{ + "payload": payloadString, + }).Tracef("unpacked COSE1 payload") + + if len(issuer) == 0 || len(feed) == 0 { // must both be present + return fmt.Errorf("either issuer and feed must both be provided in the COSE_Sign1 protected header") + } + + // Resolve returns a did doc that we don't need + // we only care if there was an error or not + _, err = didx509resolver.Resolve(unpacked.ChainPem, issuer, true) + if err != nil { + log.G(ctx).Printf("Badly formed fragment - did resolver failed to match fragment did:x509 from chain with purported issuer %s, feed %s - err %s", issuer, feed, err.Error()) + return err + } + + // now offer the payload fragment to the policy + err = h.securityPolicyEnforcer.LoadFragment(ctx, issuer, feed, payloadString) + if err != nil { + return fmt.Errorf("InjectFragment failed policy load: %w", err) + } + log.G(ctx).Printf("passed fragment into the enforcer.") + + return nil +} + +func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicyRequest *guestresource.WCOWConfidentialOptions, logWriter io.Writer) error { h.policyMutex.Lock() defer h.policyMutex.Unlock() @@ -65,7 +164,7 @@ func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicy // Initialize security policy enforcer for a given enforcer type and // encoded security policy. p, err := securitypolicy.CreateSecurityPolicyEnforcer( - "rego", + securityPolicyRequest.EnforcerType, securityPolicyRequest.EncodedSecurityPolicy, DefaultCRIMounts(), DefaultCRIPrivilegedMounts(), @@ -75,8 +174,71 @@ func (h *Host) SetWCOWConfidentialUVMOptions(ctx context.Context, securityPolicy return fmt.Errorf("error creating security policy enforcer: %w", err) } + if err = p.EnforceRuntimeLoggingPolicy(ctx); err == nil { + logrus.SetOutput(logWriter) + } else { + logrus.SetOutput(io.Discard) + } + h.securityPolicyEnforcer = p h.securityPolicyEnforcerSet = true return nil } + +func (h *Host) AddContainer(ctx context.Context, id string, c *Container) error { + h.containersMutex.Lock() + defer h.containersMutex.Unlock() + + if _, ok := h.containers[id]; ok { + log.G(ctx).Tracef("Container exists in the map: %v", ok) + return gcserr.NewHresultError(gcserr.HrVmcomputeSystemAlreadyExists) + } + log.G(ctx).Tracef("AddContainer: ID: %v", id) + h.containers[id] = c + return nil +} + +func (h *Host) RemoveContainer(ctx context.Context, id string) { + h.containersMutex.Lock() + defer h.containersMutex.Unlock() + + _, ok := h.containers[id] + if !ok { + log.G(ctx).Tracef("RemoveContainer: Container not found: ID: %v", id) + return + } + + delete(h.containers, id) +} + +func (h *Host) GetCreatedContainer(ctx context.Context, id string) (*Container, error) { + h.containersMutex.Lock() + defer h.containersMutex.Unlock() + + c, ok := h.containers[id] + if !ok { + log.G(ctx).Tracef("GetCreatedContainer: Container not found: ID: %v", id) + return nil, gcserr.NewHresultError(gcserr.HrVmcomputeSystemNotFound) + } + return c, nil +} + +// GetProcess returns the Process with the matching 'pid'. If the 'pid' does +// not exit returns error. +func (c *Container) GetProcess(pid uint32) (*containerProcess, error) { + //todo: thread a context to this function call + logrus.WithFields(logrus.Fields{ + logfields.ContainerID: c.id, + logfields.ProcessID: pid, + }).Info("opengcs::Container::GetProcess") + + c.processesMutex.Lock() + defer c.processesMutex.Unlock() + + p, ok := c.processes[pid] + if !ok { + return nil, gcserr.NewHresultError(gcserr.HrErrNotFound) + } + return p, nil +} diff --git a/internal/hcsoci/create.go b/internal/hcsoci/create.go index ab0fa6c272..317eef6629 100644 --- a/internal/hcsoci/create.go +++ b/internal/hcsoci/create.go @@ -23,6 +23,7 @@ import ( "github.com/Microsoft/hcsshim/internal/layers" "github.com/Microsoft/hcsshim/internal/log" "github.com/Microsoft/hcsshim/internal/oci" + "github.com/Microsoft/hcsshim/internal/protocol/guestresource" "github.com/Microsoft/hcsshim/internal/resources" "github.com/Microsoft/hcsshim/internal/schemaversion" "github.com/Microsoft/hcsshim/internal/uvm" @@ -264,10 +265,21 @@ func CreateContainer(ctx context.Context, createOptions *CreateOptions) (_ cow.C // v1 Argon or Xenon. Pass the document directly to HCS. hcsDocument = v1 } else if coi.HostingSystem != nil { - // v2 Xenon. Pass the container object to the UVM. - gcsDocument = &hcsschema.HostedSystem{ - SchemaVersion: schemaversion.SchemaV21(), - Container: v2, + if coi.HostingSystem.HasConfidentialPolicy() { + // confidential wcow uvm + gcsDocument = &guestresource.CWCOWHostedSystem{ + Spec: *createOptions.Spec, + CWCOWHostedSystem: hcsschema.HostedSystem{ + SchemaVersion: schemaversion.SchemaV21(), + Container: v2, + }, + } + } else { + // v2 Xenon. Pass the container object to the UVM. + gcsDocument = &hcsschema.HostedSystem{ + SchemaVersion: schemaversion.SchemaV21(), + Container: v2, + } } } else { // v2 Argon. Pass the container object to the HCS. diff --git a/internal/protocol/guestresource/resources.go b/internal/protocol/guestresource/resources.go index 3e69e6cba7..5fe47b24ca 100644 --- a/internal/protocol/guestresource/resources.go +++ b/internal/protocol/guestresource/resources.go @@ -82,6 +82,11 @@ type CWCOWCombinedLayers struct { CombinedLayers WCOWCombinedLayers `json:"CombinedLayers,omitempty"` } +type CWCOWHostedSystem struct { + Spec specs.Spec + CWCOWHostedSystem hcsschema.HostedSystem +} + // Defines the schema for hosted settings passed to GCS and/or OpenGCS // SCSIDevice represents a SCSI device that is attached to the system. diff --git a/internal/regopolicyinterpreter/regopolicyinterpreter.go b/internal/regopolicyinterpreter/regopolicyinterpreter.go index 47dbee28ea..047a4a27b7 100644 --- a/internal/regopolicyinterpreter/regopolicyinterpreter.go +++ b/internal/regopolicyinterpreter/regopolicyinterpreter.go @@ -269,6 +269,20 @@ func (m regoMetadata) getOrCreate(name string) map[string]interface{} { return metadata } +func (r *RegoPolicyInterpreter) UpdateOSType(os string) error { + r.dataAndModulesMutex.Lock() + defer r.dataAndModulesMutex.Unlock() + ops := []*regoMetadataOperation{ + { + Action: metadataAdd, + Name: "operatingsystem", + Key: "ostype", + Value: os, + }, + } + return r.updateMetadata(ops) +} + func (r *RegoPolicyInterpreter) updateMetadata(ops []*regoMetadataOperation) error { // dataAndModulesMutex must be held before calling this diff --git a/internal/tools/policyenginesimulator/main.go b/internal/tools/policyenginesimulator/main.go index 7eec934631..be4cbb466d 100644 --- a/internal/tools/policyenginesimulator/main.go +++ b/internal/tools/policyenginesimulator/main.go @@ -90,10 +90,17 @@ func createInterpreter() *rpi.RegoPolicyInterpreter { } r, err := rpi.NewRegoPolicyInterpreter(policyCode, data) + if err != nil { log.Fatal(err) } + err = r.UpdateOSType("linux") + + if err != nil { + log.Fatalf("error updating OS type: %v", err) + } + if len(*logPath) > 0 { if _, err := os.Stat(*logPath); err == nil { os.Remove(*logPath) diff --git a/internal/uvm/security_policy.go b/internal/uvm/security_policy.go index 2346864afe..24ce7bae0b 100644 --- a/internal/uvm/security_policy.go +++ b/internal/uvm/security_policy.go @@ -34,6 +34,51 @@ func WithSecurityPolicyEnforcer(enforcer string) ConfidentialUVMOpt { } } +// TODO (Mahati): Move this block out later +type WCOWConfidentialUVMOpt func(ctx context.Context, r *guestresource.WCOWConfidentialOptions) error + +// WithSecurityPolicy sets the desired security policy for the resource. +func WithWCOWSecurityPolicy(policy string) WCOWConfidentialUVMOpt { + return func(ctx context.Context, r *guestresource.WCOWConfidentialOptions) error { + r.EncodedSecurityPolicy = policy + return nil + } +} + +// WithSecurityPolicyEnforcer sets the desired enforcer type for the resource. +func WithWCOWSecurityPolicyEnforcer(enforcer string) WCOWConfidentialUVMOpt { + return func(ctx context.Context, r *guestresource.WCOWConfidentialOptions) error { + r.EnforcerType = enforcer + return nil + } +} + +func (uvm *UtilityVM) SetWCOWConfidentialUVMOptions(ctx context.Context, opts ...WCOWConfidentialUVMOpt) error { + if uvm.operatingSystem != "windows" { + return errNotSupported + } + uvm.m.Lock() + defer uvm.m.Unlock() + confOpts := &guestresource.WCOWConfidentialOptions{} + for _, o := range opts { + if err := o(ctx, confOpts); err != nil { + return err + } + } + modification := &hcsschema.ModifySettingRequest{ + RequestType: guestrequest.RequestTypeAdd, + GuestRequest: guestrequest.ModificationRequest{ + ResourceType: guestresource.ResourceTypeSecurityPolicy, + RequestType: guestrequest.RequestTypeAdd, + Settings: *confOpts, + }, + } + if err := uvm.modify(ctx, modification); err != nil { + return fmt.Errorf("uvm::Policy: failed to modify utility VM configuration: %w", err) + } + return nil +} + func base64EncodeFileContents(filePath string) (string, error) { if filePath == "" { return "", nil @@ -105,9 +150,7 @@ func (uvm *UtilityVM) SetConfidentialUVMOptions(ctx context.Context, opts ...Con // InjectPolicyFragment sends policy fragment to GCS. func (uvm *UtilityVM) InjectPolicyFragment(ctx context.Context, fragment *ctrdtaskapi.PolicyFragment) error { - if uvm.operatingSystem != "linux" { - return errNotSupported - } + mod := &hcsschema.ModifySettingRequest{ RequestType: guestrequest.RequestTypeUpdate, GuestRequest: guestrequest.ModificationRequest{ diff --git a/internal/uvm/start.go b/internal/uvm/start.go index b73143e0a9..de8046f94d 100644 --- a/internal/uvm/start.go +++ b/internal/uvm/start.go @@ -337,6 +337,16 @@ func (uvm *UtilityVM) Start(ctx context.Context) (err error) { } } + if uvm.HasConfidentialPolicy() && uvm.OS() == "windows" { + copts := []WCOWConfidentialUVMOpt{ + WithWCOWSecurityPolicy(uvm.createOpts.(OptionsWCOW).SecurityPolicy), + WithWCOWSecurityPolicyEnforcer(uvm.createOpts.(OptionsWCOW).SecurityPolicyEnforcer), + } + if err := uvm.SetWCOWConfidentialUVMOptions(ctx, copts...); err != nil { + return err + } + } + return nil } diff --git a/pkg/annotations/annotations.go b/pkg/annotations/annotations.go index 9e857340d2..6e76e59423 100644 --- a/pkg/annotations/annotations.go +++ b/pkg/annotations/annotations.go @@ -214,6 +214,18 @@ const ( // fallback mechanics. WCOWSecurityPolicyEnforcer = "io.microsoft.virtualmachine.wcow.enforcer" + // The certificate is expected to be located in the same directory as the shim executable. + WCOWHostAMDCertificate = "io.microsoft.virtualmachine.wcow.amd-certificate" + + // WCOWSecurityPolicyEnv specifies if confidential containers' related information + // should be written to containers' rootfs. The filenames and location are defined + // by securitypolicy.PolicyFilename, securitypolicy.HostAMDCertFilename and + // securitypolicy.ReferenceInfoFilename. + WCOWSecurityPolicyEnv = "io.microsoft.virtualmachine.wcow.securitypolicy.env" + + // WCOWReferenceInfoFile specifies the filename of a signed UVM reference file to be passed to UVM. + WCOWReferenceInfoFile = "io.microsoft.virtualmachine.wcow.uvm-reference-info-file" + // WCOWIsolationType allows overriding isolation type of a confidential pod. // Default is "SecureNestedPaging" and valid override values are // "VirtualizationBasedSecurity" and "GuestStateOnly" @@ -328,6 +340,7 @@ const ( // uVM NUMA annotations. const ( + // NumaMaximumProcessorsPerNode is the maximum number of processors per vNUMA node. // This should be used for implicit vNUMA topology. NumaMaximumProcessorsPerNode = "io.microsoft.virtualmachine.computetopology.processor.numa.max-processors-per-node" diff --git a/pkg/securitypolicy/api.rego b/pkg/securitypolicy/api.rego index 82e79c9040..36a197ebc2 100644 --- a/pkg/securitypolicy/api.rego +++ b/pkg/securitypolicy/api.rego @@ -5,6 +5,7 @@ version := "@@API_VERSION@@" enforcement_points := { "mount_device": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}}, "mount_overlay": {"introducedVersion": "0.1.0", "default_results": {"allowed": false}}, + "mount_cims": {"introducedVersion": "0.11.0", "default_results": {"allowed": false}}, "create_container": {"introducedVersion": "0.1.0", "default_results": {"allowed": false, "env_list": null, "allow_stdio_access": false}}, "unmount_device": {"introducedVersion": "0.2.0", "default_results": {"allowed": true}}, "unmount_overlay": {"introducedVersion": "0.6.0", "default_results": {"allowed": true}}, diff --git a/pkg/securitypolicy/framework.rego b/pkg/securitypolicy/framework.rego index 3002f50462..8a28f3e312 100644 --- a/pkg/securitypolicy/framework.rego +++ b/pkg/securitypolicy/framework.rego @@ -57,6 +57,14 @@ layerPaths_ok(layers) { } } +layerHashes_ok(layers) { + length := count(layers) + count(input.layerHashes) == length + every i, hash in input.layerHashes { + layers[(length - i) - 1] == hash + } +} + default overlay_exists := false overlay_exists { @@ -95,6 +103,25 @@ candidate_containers := containers { containers := array.concat(policy_containers, fragment_containers) } +default mount_cims := {"allowed": false} + +mount_cims := {"metadata": [addMatches], "allowed": true} { + not overlay_exists + + containers := [container | + container := candidate_containers[_] + layerHashes_ok(container.layers) + ] + + count(containers) > 0 + addMatches := { + "name": "matches", + "action": "add", + "key": input.containerID, + "value": containers, + } +} + default mount_overlay := {"allowed": false} mount_overlay := {"metadata": [addMatches, addOverlayTarget], "allowed": true} { @@ -246,21 +273,34 @@ workingDirectory_ok(working_dir) { } privileged_ok(elevation_allowed) { + is_linux not input.privileged } privileged_ok(elevation_allowed) { + is_linux input.privileged input.privileged == elevation_allowed } +privileged_ok(no_new_privileges) { + # no-op for windows + is_windows +} + noNewPrivileges_ok(no_new_privileges) { + is_linux no_new_privileges input.noNewPrivileges } noNewPrivileges_ok(no_new_privileges) { - no_new_privileges == false + is_linux + not no_new_privileges +} + +noNewPrivileges_ok(obj) { + is_windows } idName_ok(pattern, "any", value) { @@ -280,6 +320,7 @@ idName_ok(pattern, "re2", value) { } user_ok(user) { + is_linux user.umask == input.umask idName_ok(user.user_idname.pattern, user.user_idname.strategy, input.user) every group in input.groups { @@ -288,10 +329,20 @@ user_ok(user) { } } +user_ok(user) { + is_windows + input.user == user +} + seccomp_ok(seccomp_profile_sha256) { + is_linux input.seccompProfileSHA256 == seccomp_profile_sha256 } +seccomp_ok(seccomp_profile_sha256) { + is_windows +} + default container_started := false container_started { @@ -301,6 +352,7 @@ container_started { default container_privileged := false container_privileged { + is_linux data.metadata.started[input.containerID].privileged } @@ -401,6 +453,7 @@ all_caps_sets_are_equal(sets) := caps { } valid_caps_for_all(containers, privileged) := caps { + is_linux allow_capability_dropping # find largest matching capabilities sets aka "the most specific" @@ -412,6 +465,7 @@ valid_caps_for_all(containers, privileged) := caps { } valid_caps_for_all(containers, privileged) := caps { + is_linux not allow_capability_dropping # no dropping allowed, so we just return the input @@ -419,6 +473,7 @@ valid_caps_for_all(containers, privileged) := caps { } caps_ok(allowed_caps, requested_caps) { + is_linux capsList_ok(allowed_caps.bounding, requested_caps.bounding) capsList_ok(allowed_caps.effective, requested_caps.effective) capsList_ok(allowed_caps.inheritable, requested_caps.inheritable) @@ -426,6 +481,10 @@ caps_ok(allowed_caps, requested_caps) { capsList_ok(allowed_caps.ambient, requested_caps.ambient) } +caps_ok(allowed_caps, requested_caps) { + is_windows +} + get_capabilities(container, privileged) := capabilities { container.capabilities != null capabilities := container.capabilities @@ -478,6 +537,7 @@ create_container := {"metadata": [updateMatches, addStarted], "caps_list": caps_list, "allow_stdio_access": allow_stdio_access, "allowed": true} { + is_linux not container_started # narrow the matches based upon command, working directory, and @@ -547,6 +607,78 @@ create_container := {"metadata": [updateMatches, addStarted], } } +create_container := {"metadata": [updateMatches, addStarted], + "env_list": env_list, + "allow_stdio_access": allow_stdio_access, + "allowed": true} { + is_windows + not container_started + + # narrow the matches based upon command, working directory, and + # mount list + possible_after_initial_containers := [container | + container := data.metadata.matches[input.containerID][_] + # NB any change to these narrowing conditions should be reflected in + # the error handling, such that error messaging correctly reflects + # the narrowing process. + user_ok(container.user) + workingDirectory_ok(container.working_dir) + command_ok(container.command) + ] + + count(possible_after_initial_containers) > 0 + + # check to see if the environment variables match, dropping + # them if allowed (and necessary) + env_list := valid_envs_for_all(possible_after_initial_containers) + possible_after_env_containers := [container | + container := possible_after_initial_containers[_] + envList_ok(container.env_rules, env_list) + ] + + count(possible_after_env_containers) > 0 + + # set final container list + containers := possible_after_env_containers + + # we can't do narrowing based on allowing stdio access so at this point + # every container from the policy that might match this create request + # must have the same allow stdio value otherwise, we are in an undecidable + # state + allow_stdio_access := containers[0].allow_stdio_access + every c in containers { + c.allow_stdio_access == allow_stdio_access + } + + updateMatches := { + "name": "matches", + "action": "update", + "key": input.containerID, + "value": containers, + } + + addStarted := { + "name": "started", + "action": "add", + "key": input.containerID, + "value": { + "privileged": false, + }, + } +} + +security_ok(current_container) { + is_linux + noNewPrivileges_ok(current_container.no_new_privileges) + privileged_ok(current_container.allow_elevated) + seccomp_ok(current_container.seccomp_profile_sha256) + mountList_ok(current_container.mounts, current_container.allow_elevated) +} + +security_ok(current_container) { + is_windows +} + mountSource_ok(constraint, source) { startswith(constraint, data.sandboxPrefix) newConstraint := replace(constraint, data.sandboxPrefix, input.sandboxDir) @@ -608,10 +740,23 @@ mount_ok(mounts, allow_elevated, mount) { } mountList_ok(mounts, allow_elevated) { + is_linux every mount in input.mounts { mount_ok(mounts, allow_elevated, mount) } } +mountList_ok(mounts, allow_elevated) { + # no-op for windows + is_windows +} + +is_linux { + data.metadata.operatingsystem[ostype] == "linux" +} + +is_windows { + data.metadata.operatingsystem[ostype] == "windows" +} default exec_in_container := {"allowed": false} @@ -619,6 +764,7 @@ exec_in_container := {"metadata": [updateMatches], "env_list": env_list, "caps_list": caps_list, "allowed": true} { + is_linux container_started # narrow our matches based upon the process requested @@ -667,6 +813,48 @@ exec_in_container := {"metadata": [updateMatches], } } +exec_in_container := {"metadata": [updateMatches], + "env_list": env_list, + "allowed": true} { + + is_windows + container_started + + # narrow our matches based upon the process requested + possible_after_initial_containers := [container | + container := data.metadata.matches[input.containerID][_] + # NB any change to these narrowing conditions should be reflected in + # the error handling, such that error messaging correctly reflects + # the narrowing process. + workingDirectory_ok(container.working_dir) + user_ok(container.user) + some process in container.exec_processes + command_ok(process.command) + ] + + count(possible_after_initial_containers) > 0 + + # check to see if the environment variables match, dropping + # them if allowed (and necessary) + env_list := valid_envs_for_all(possible_after_initial_containers) + possible_after_env_containers := [container | + container := possible_after_initial_containers[_] + envList_ok(container.env_rules, env_list) + ] + + count(possible_after_env_containers) > 0 + + # set final container list + containers := possible_after_env_containers + + updateMatches := { + "name": "matches", + "action": "update", + "key": input.containerID, + "value": containers, + } +} + default shutdown_container := {"allowed": false} shutdown_container := {"started": remove, "metadata": [remove], "allowed": true} { @@ -1108,6 +1296,7 @@ privileged_matches { } errors["privileged escalation not allowed"] { + is_linux input.rule in ["create_container"] not privileged_matches } @@ -1285,6 +1474,7 @@ mount_matches(mount) { } errors[mountError] { + is_linux input.rule == "create_container" bad_mounts := [mount.destination | mount := input.mounts[_] @@ -1433,9 +1623,13 @@ errors[fragment_framework_version_error] { } errors["containers only distinguishable by allow_stdio_access"] { + is_linux input.rule == "create_container" - not container_started + not container_started + + # narrow the matches based upon command, working directory, and + # mount list possible_after_initial_containers := [container | container := data.metadata.matches[input.containerID][_] noNewPrivileges_ok(container.no_new_privileges) @@ -1477,6 +1671,44 @@ errors["containers only distinguishable by allow_stdio_access"] { c.allow_stdio_access != allow_stdio_access } +errors["containers only distinguishable by allow_stdio_access"] { + is_windows + input.rule == "create_container" + + not container_started + + # narrow the matches based upon command, working directory, and + # mount list + possible_after_initial_containers := [container | + container := data.metadata.matches[input.containerID][_] + # NB any change to these narrowing conditions should be reflected in + # the error handling, such that error messaging correctly reflects + # the narrowing process. + user_ok(container.user) + workingDirectory_ok(container.working_dir) + command_ok(container.command) + ] + + count(possible_after_initial_containers) > 0 + + # check to see if the environment variables match, dropping + # them if allowed (and necessary) + env_list := valid_envs_for_all(possible_after_initial_containers) + possible_after_env_containers := [container | + container := possible_after_initial_containers[_] + envList_ok(container.env_rules, env_list) + ] + + count(possible_after_env_containers) > 0 + + # set final container list + containers := possible_after_env_containers + + allow_stdio_access := containers[0].allow_stdio_access + some c in containers + c.allow_stdio_access != allow_stdio_access +} + errors["external processes only distinguishable by allow_stdio_access"] { input.rule == "exec_external" @@ -1522,6 +1754,7 @@ noNewPrivileges_matches { } errors["invalid noNewPrivileges"] { + is_linux input.rule in ["create_container", "exec_in_container"] not noNewPrivileges_matches } @@ -1549,6 +1782,7 @@ errors["invalid user"] { } errors["capabilities don't match"] { + is_linux input.rule == "create_container" not container_started @@ -1588,6 +1822,7 @@ errors["capabilities don't match"] { } errors["capabilities don't match"] { + is_linux input.rule == "exec_in_container" container_started @@ -1627,6 +1862,7 @@ errors["capabilities don't match"] { # 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"] { + is_linux input.rule == "create_container" allow_capability_dropping @@ -1672,6 +1908,7 @@ seccomp_matches { } errors["invalid seccomp"] { + is_linux input.rule == "create_container" not seccomp_matches } diff --git a/pkg/securitypolicy/open_door.rego b/pkg/securitypolicy/open_door.rego index 2bc36123d8..a8e283092d 100644 --- a/pkg/securitypolicy/open_door.rego +++ b/pkg/securitypolicy/open_door.rego @@ -5,6 +5,7 @@ api_version := "@@API_VERSION@@" mount_device := {"allowed": true} mount_overlay := {"allowed": true} create_container := {"allowed": true, "env_list": null, "allow_stdio_access": true} +mount_cims := {"allowed": true} unmount_device := {"allowed": true} unmount_overlay := {"allowed": true} exec_in_container := {"allowed": true, "env_list": null} diff --git a/pkg/securitypolicy/policy.rego b/pkg/securitypolicy/policy.rego index 139d340048..9414116c19 100644 --- a/pkg/securitypolicy/policy.rego +++ b/pkg/securitypolicy/policy.rego @@ -9,6 +9,7 @@ mount_device := data.framework.mount_device unmount_device := data.framework.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 diff --git a/pkg/securitypolicy/rego_utils_test.go b/pkg/securitypolicy/rego_utils_test.go new file mode 100644 index 0000000000..341741eef2 --- /dev/null +++ b/pkg/securitypolicy/rego_utils_test.go @@ -0,0 +1,2971 @@ +//go:build rego +// +build rego + +package securitypolicy + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "math/rand" + "os" + "path" + "reflect" + "sort" + "strconv" + "strings" + "syscall" + "testing" + "time" + + "github.com/Microsoft/hcsshim/internal/guestpath" + "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/rego" + oci "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +const ( + // variables that influence generated rego-only test fixtures + maxDiffLength = 64 + maxExternalProcessesInGeneratedConstraints = 16 + maxFragmentsInGeneratedConstraints = 4 + maxGeneratedExternalProcesses = 12 + maxGeneratedSandboxIDLength = 32 + maxGeneratedEnforcementPointLength = 64 + maxGeneratedPlan9Mounts = 8 + maxGeneratedFragmentFeedLength = 256 + maxGeneratedFragmentIssuerLength = 16 + maxPlan9MountTargetLength = 64 + maxPlan9MountIndex = 16 + + // variables that influence generated test fixtures + minStringLength = 10 + maxContainersInGeneratedConstraints = 32 + maxLayersInGeneratedContainer = 32 + maxGeneratedContainerID = 1000000 + maxGeneratedCommandLength = 128 + maxGeneratedCommandArgs = 12 + maxGeneratedEnvironmentVariables = 16 + maxGeneratedEnvironmentVariableRuleLength = 64 + maxGeneratedEnvironmentVariableRules = 8 + maxGeneratedFragmentNamespaceLength = 32 + maxGeneratedMountTargetLength = 256 + maxGeneratedVersion = 10 + rootHashLength = 64 + maxGeneratedMounts = 4 + maxGeneratedMountSourceLength = 32 + maxGeneratedMountDestinationLength = 32 + maxGeneratedMountOptions = 5 + maxGeneratedMountOptionLength = 32 + maxGeneratedExecProcesses = 4 + maxGeneratedWorkingDirLength = 128 + maxSignalNumber = 64 + maxGeneratedNameLength = 8 + maxGeneratedGroupNames = 4 + maxGeneratedCapabilities = 12 + maxGeneratedCapabilitesLength = 24 + maxWindowsSignalLength = 64 + // additional consts + // the standard enforcer tests don't do anything with the encoded policy + // string. this const exists to make that explicit + ignoredEncodedPolicyString = "" +) + +var testRand *rand.Rand +var testDataGenerator *dataGenerator + +func init() { + seed := time.Now().Unix() + if seedStr, ok := os.LookupEnv("SEED"); ok { + if parsedSeed, err := strconv.ParseInt(seedStr, 10, 64); err != nil { + fmt.Fprintf(os.Stderr, "failed to parse seed: %d\n", seed) + } else { + seed = parsedSeed + } + } + testRand = rand.New(rand.NewSource(seed)) + fmt.Fprintf(os.Stdout, "securitypolicy_test seed: %d\n", seed) + testDataGenerator = newDataGenerator(testRand) +} + +func Test_RegoTemplates(t *testing.T) { + query := rego.New( + rego.Query("data.api"), + rego.Module("api.rego", APICode)) + + ctx := context.Background() + resultSet, err := query.Eval(ctx) + if err != nil { + t.Fatalf("unable to query API enforcement points: %s", err) + } + + apiRules := resultSet[0].Expressions[0].Value.(map[string]interface{}) + enforcementPoints := apiRules["enforcement_points"].(map[string]interface{}) + + policyCode := strings.Replace(policyRegoTemplate, "@@OBJECTS@@", "", 1) + policyCode = strings.Replace(policyCode, "@@API_VERSION@@", apiVersion, 1) + policyCode = strings.Replace(policyCode, "@@FRAMEWORK_VERSION@@", frameworkVersion, 1) + + err = verifyPolicyRules(apiVersion, enforcementPoints, policyCode) + if err != nil { + t.Errorf("Policy Rego Template is invalid: %s", err) + } + + err = verifyPolicyRules(apiVersion, enforcementPoints, openDoorRego) + if err != nil { + t.Errorf("Open Door Rego Template is invalid: %s", err) + } +} + +func verifyPolicyRules(apiVersion string, enforcementPoints map[string]interface{}, policyCode string) error { + query := rego.New( + rego.Query("data.policy"), + rego.Module("policy.rego", policyCode), + rego.Module("framework.rego", FrameworkCode), + ) + + ctx := context.Background() + resultSet, err := query.Eval(ctx) + if err != nil { + return fmt.Errorf("unable to query policy template rules: %w", err) + } + + policyTemplateRules := resultSet[0].Expressions[0].Value.(map[string]interface{}) + policyTemplateAPIVersion := policyTemplateRules["api_version"].(string) + + if policyTemplateAPIVersion != apiVersion { + return fmt.Errorf("Policy template version != api version: %s != %s", apiVersion, policyTemplateAPIVersion) + } + + for rule := range enforcementPoints { + if _, ok := policyTemplateRules[rule]; !ok { + return fmt.Errorf("Rule %s in API is missing from policy template", rule) + } + } + + for rule := range policyTemplateRules { + if rule == "api_version" || rule == "framework_version" || rule == "reason" { + continue + } + + if _, ok := enforcementPoints[rule]; !ok { + return fmt.Errorf("Rule %s in policy template is missing from API", rule) + } + } + + return nil +} + +func copyMounts(mounts []oci.Mount) []oci.Mount { + bytes, err := json.Marshal(mounts) + if err != nil { + panic(err) + } + + mountsCopy := make([]oci.Mount, len(mounts)) + err = json.Unmarshal(bytes, &mountsCopy) + if err != nil { + panic(err) + } + + return mountsCopy +} + +func copyMountsInternal(mounts []mountInternal) []mountInternal { + var mountsCopy []mountInternal + + for _, in := range mounts { + out := mountInternal{ + Source: in.Source, + Destination: in.Destination, + Type: in.Type, + Options: copyStrings(in.Options), + } + + mountsCopy = append(mountsCopy, out) + } + + return mountsCopy +} + +func copyLinuxCapabilities(caps oci.LinuxCapabilities) oci.LinuxCapabilities { + bytes, err := json.Marshal(caps) + if err != nil { + panic(err) + } + + capsCopy := oci.LinuxCapabilities{} + err = json.Unmarshal(bytes, &capsCopy) + if err != nil { + panic(err) + } + + return capsCopy +} + +func copyLinuxSeccomp(seccomp oci.LinuxSeccomp) oci.LinuxSeccomp { + bytes, err := json.Marshal(seccomp) + if err != nil { + panic(err) + } + + seccompCopy := oci.LinuxSeccomp{} + err = json.Unmarshal(bytes, &seccompCopy) + if err != nil { + panic(err) + } + + return seccompCopy +} + +type regoOverlayTestConfig struct { + layers []string + containerID string + policy *regoEnforcer +} + +func setupRegoOverlayTest(gc *generatedConstraints, valid bool) (tc *regoOverlayTestConfig, err error) { + securityPolicy := gc.toPolicy() + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + + if err != nil { + return nil, err + } + + containerID := testDataGenerator.uniqueContainerID() + c := selectContainerFromContainerList(gc.containers, testRand) + + var layerPaths []string + if valid { + layerPaths, err = testDataGenerator.createValidOverlayForContainer(policy, c) + if err != nil { + return nil, fmt.Errorf("error creating valid overlay: %w", err) + } + } else { + layerPaths, err = testDataGenerator.createInvalidOverlayForContainer(policy, c) + if err != nil { + return nil, fmt.Errorf("error creating invalid overlay: %w", err) + } + } + + // see NOTE_TESTCOPY + return ®oOverlayTestConfig{ + layers: copyStrings(layerPaths), + containerID: containerID, + policy: policy, + }, nil +} + +func setupPlan9MountTest(gc *generatedConstraints) (tc *regoPlan9MountTestConfig, err error) { + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + testContainer := selectContainerFromContainerList(gc.containers, testRand) + mountIndex := atMost(testRand, int32(len(testContainer.Mounts)-1)) + testMount := &testContainer.Mounts[mountIndex] + testMount.Source = plan9Prefix + testMount.Type = "secret" + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + containerID, err := mountImageForContainer(policy, testContainer) + if err != nil { + return nil, err + } + + uvmPathForShare := generateUVMPathForShare(testRand, containerID) + + envList := buildEnvironmentVariablesFromEnvRules(testContainer.EnvRules, testRand) + sandboxID := testDataGenerator.uniqueSandboxID() + + mounts := testContainer.Mounts + mounts = append(mounts, defaultMounts...) + + if testContainer.AllowElevated { + mounts = append(mounts, privilegedMounts...) + } + mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) + mountSpec.Mounts = append(mountSpec.Mounts, oci.Mount{ + Source: uvmPathForShare, + Destination: testMount.Destination, + Options: testMount.Options, + Type: testMount.Type, + }) + + user := buildIDNameFromConfig(testContainer.User.UserIDName, testRand) + groups := buildGroupIDNamesFromUser(testContainer.User, testRand) + umask := testContainer.User.Umask + + capabilities := testContainer.Capabilities.toExternal() + seccomp := testContainer.SeccompProfileSHA256 + + // see NOTE_TESTCOPY + return ®oPlan9MountTestConfig{ + envList: copyStrings(envList), + argList: copyStrings(testContainer.Command), + workingDir: testContainer.WorkingDir, + containerID: containerID, + sandboxID: sandboxID, + mounts: copyMounts(mountSpec.Mounts), + noNewPrivileges: testContainer.NoNewPrivileges, + user: user, + groups: groups, + umask: umask, + uvmPathForShare: uvmPathForShare, + policy: policy, + capabilities: &capabilities, + seccomp: seccomp, + }, nil +} + +type regoPlan9MountTestConfig struct { + envList []string + argList []string + workingDir string + containerID string + sandboxID string + mounts []oci.Mount + uvmPathForShare string + noNewPrivileges bool + user IDName + groups []IDName + umask string + policy *regoEnforcer + capabilities *oci.LinuxCapabilities + seccomp string +} + +func mountImageForContainer(policy *regoEnforcer, container *securityPolicyContainer) (string, error) { + ctx := context.Background() + containerID := testDataGenerator.uniqueContainerID() + + layerPaths, err := testDataGenerator.createValidOverlayForContainer(policy, container) + if err != nil { + return "", fmt.Errorf("error creating valid overlay: %w", err) + } + + // see NOTE_TESTCOPY + err = policy.EnforceOverlayMountPolicy(ctx, containerID, copyStrings(layerPaths), testDataGenerator.uniqueMountTarget()) + if err != nil { + return "", fmt.Errorf("error mounting filesystem: %w", err) + } + + return containerID, nil +} + +func buildMountSpecFromMountArray(mounts []mountInternal, sandboxID string, r *rand.Rand) *oci.Spec { + mountSpec := new(oci.Spec) + + // Select some number of the valid, matching rules to be environment + // variable + numberOfMounts := int32(len(mounts)) + numberOfMatches := randMinMax(r, 1, numberOfMounts) + usedIndexes := map[int]struct{}{} + for numberOfMatches > 0 { + anIndex := -1 + if (numberOfMatches * 2) > numberOfMounts { + // if we have a lot of matches, randomly select + exists := true + + for exists { + anIndex = int(randMinMax(r, 0, numberOfMounts-1)) + _, exists = usedIndexes[anIndex] + } + } else { + // we have a "smaller set of rules. we'll just iterate and select from + // available + exists := true + + for exists { + anIndex++ + _, exists = usedIndexes[anIndex] + } + } + + mount := mounts[anIndex] + + source := substituteUVMPath(sandboxID, mount).Source + mountSpec.Mounts = append(mountSpec.Mounts, oci.Mount{ + Source: source, + Destination: mount.Destination, + Options: mount.Options, + Type: mount.Type, + }) + usedIndexes[anIndex] = struct{}{} + + numberOfMatches-- + } + + return mountSpec +} + +func selectExecProcess(processes []containerExecProcess, r *rand.Rand) containerExecProcess { + numProcesses := len(processes) + return processes[r.Intn(numProcesses)] +} + +func selectWindowsExecProcess(processes []windowsContainerExecProcess, r *rand.Rand) windowsContainerExecProcess { + numProcesses := len(processes) + return processes[r.Intn(numProcesses)] +} + +func selectSignalFromSignals(r *rand.Rand, signals []syscall.Signal) syscall.Signal { + numSignals := len(signals) + return signals[r.Intn(numSignals)] +} + +func selectSignalFromWindowsSignals(r *rand.Rand, signals []guestrequest.SignalValueWCOW) guestrequest.SignalValueWCOW { + numSignals := len(signals) + return signals[r.Intn(numSignals)] +} + +func generateUVMPathForShare(r *rand.Rand, containerID string) string { + return fmt.Sprintf("%s/%s%s", + guestpath.LCOWRootPrefixInUVM, + containerID, + fmt.Sprintf(guestpath.LCOWMountPathPrefixFmt, atMost(r, maxPlan9MountIndex))) +} + +func generateLinuxID(r *rand.Rand) uint32 { + return r.Uint32() +} + +type regoScratchMountPolicyTestConfig struct { + policy *regoEnforcer +} + +func setupRegoScratchMountTest( + gc *generatedConstraints, + unencryptedScratch bool, +) (tc *regoScratchMountPolicyTestConfig, err error) { + securityPolicy := gc.toPolicy() + securityPolicy.AllowUnencryptedScratch = unencryptedScratch + + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), toOCIMounts(privilegedMounts), testOSType) + + if err != nil { + return nil, err + } + return ®oScratchMountPolicyTestConfig{ + policy: policy, + }, nil +} + +func generateCapabilities(r *rand.Rand) *oci.LinuxCapabilities { + return &oci.LinuxCapabilities{ + Bounding: generateCapabilitiesSet(r, 0), + Effective: generateCapabilitiesSet(r, 0), + Inheritable: generateCapabilitiesSet(r, 0), + Permitted: generateCapabilitiesSet(r, 0), + Ambient: generateCapabilitiesSet(r, 0), + } +} + +func alterCapabilitySet(r *rand.Rand, set []string) []string { + newSet := copyStrings(set) + + if len(newSet) == 0 { + return generateCapabilitiesSet(r, 1) + } + + alterations := atLeastNAtMostM(r, 1, 4) + for i := alterations; i > 0; i-- { + if len(newSet) == 0 { + newSet = generateCapabilitiesSet(r, 1) + } else { + action := atMost(r, 2) + if action == 0 { + newSet = superCapabilitySet(r, newSet) + } else if action == 1 { + newSet = subsetCapabilitySet(r, newSet) + } else { + replace := atMost(r, int32((len(newSet) - 1))) + newSet[replace] = generateCapability(r) + } + } + } + + return newSet +} + +func subsetCapabilitySet(r *rand.Rand, set []string) []string { + newSet := make([]string, 0) + + setSize := int32(len(set)) + if setSize == 0 { + // no subset is possible + return newSet + } else if setSize == 1 { + // only one possibility + return newSet + } + + // We need to remove at least 1 item, potentially all + numberOfMatches := randMinMax(r, 0, setSize-1) + usedIndexes := map[int]struct{}{} + for i := numberOfMatches; i > 0; i-- { + anIndex := -1 + if ((setSize - int32(len(usedIndexes))) * 2) > i { + // the set is pretty large compared to our number to select, + // we will gran randomly + exists := true + + for exists { + anIndex = int(randMinMax(r, 0, setSize-1)) + _, exists = usedIndexes[anIndex] + } + } else { + // we have a "smaller set of capabilities. we'll just iterate and + // select from available + exists := true + + for exists { + anIndex++ + _, exists = usedIndexes[anIndex] + } + } + + newSet = append(newSet, set[anIndex]) + usedIndexes[anIndex] = struct{}{} + } + + return newSet +} + +func superCapabilitySet(r *rand.Rand, set []string) []string { + newSet := copyStrings(set) + + additions := atLeastNAtMostM(r, 1, 12) + for i := additions; i > 0; i-- { + newSet = append(newSet, generateCapability(r)) + } + + return newSet +} + +func (c capabilitiesInternal) toExternal() oci.LinuxCapabilities { + return oci.LinuxCapabilities{ + Bounding: c.Bounding, + Effective: c.Effective, + Inheritable: c.Inheritable, + Permitted: c.Permitted, + Ambient: c.Ambient, + } +} + +func buildIDNameFromConfig(config IDNameConfig, r *rand.Rand) IDName { + switch config.Strategy { + case IDNameStrategyName: + return IDName{ + ID: generateIDNameID(r), + Name: config.Rule, + } + + case IDNameStrategyID: + return IDName{ + ID: config.Rule, + Name: generateIDNameName(r), + } + + case IDNameStrategyAny: + return generateIDName(r) + + default: + panic(fmt.Sprintf("unsupported ID Name strategy: %v", config.Strategy)) + } +} + +func buildGroupIDNamesFromUser(user UserConfig, r *rand.Rand) []IDName { + groupIDNames := make([]IDName, 0) + + // Select some number of the valid, matching rules to be groups + numberOfGroups := int32(len(user.GroupIDNames)) + numberOfMatches := randMinMax(r, 1, numberOfGroups) + usedIndexes := map[int]struct{}{} + for numberOfMatches > 0 { + anIndex := -1 + if (numberOfMatches * 2) > numberOfGroups { + // if we have a lot of matches, randomly select + exists := true + + for exists { + anIndex = int(randMinMax(r, 0, numberOfGroups-1)) + _, exists = usedIndexes[anIndex] + } + } else { + // we have a "smaller set of rules. we'll just iterate and select from + // available + exists := true + + for exists { + anIndex++ + _, exists = usedIndexes[anIndex] + } + } + + if user.GroupIDNames[anIndex].Strategy == IDNameStrategyRegex { + // we don't match from regex groups or any groups + numberOfMatches-- + continue + } + + groupIDName := buildIDNameFromConfig(user.GroupIDNames[anIndex], r) + groupIDNames = append(groupIDNames, groupIDName) + usedIndexes[anIndex] = struct{}{} + + numberOfMatches-- + } + + return groupIDNames +} + +func generateIDNameName(r *rand.Rand) string { + return randVariableString(r, maxGeneratedNameLength) +} + +func generateIDNameID(r *rand.Rand) string { + id := r.Uint32() + return strconv.FormatUint(uint64(id), 10) +} + +func generateIDName(r *rand.Rand) IDName { + return IDName{ + ID: generateIDNameID(r), + Name: generateIDNameName(r), + } +} + +func toOCIMounts(mounts []mountInternal) []oci.Mount { + result := make([]oci.Mount, len(mounts)) + for i, mount := range mounts { + result[i] = oci.Mount{ + Source: mount.Source, + Destination: mount.Destination, + Options: mount.Options, + Type: mount.Type, + } + } + return result +} + +func setupExternalProcessTest(gc *generatedConstraints) (tc *regoExternalPolicyTestConfig, err error) { + gc.externalProcesses = generateExternalProcesses(testRand) + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + return ®oExternalPolicyTestConfig{ + policy: policy, + }, nil +} + +func setupWindowsExternalProcessTest(gc *generatedWindowsConstraints) (tc *regoExternalPolicyTestConfig, err error) { + gc.externalProcesses = generateExternalProcesses(testRand) + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + return ®oExternalPolicyTestConfig{ + policy: policy, + }, nil +} + +type regoExternalPolicyTestConfig struct { + policy *regoEnforcer +} + +func setupGetPropertiesTest(gc *generatedConstraints, allowPropertiesAccess bool) (tc *regoGetPropertiesTestConfig, err error) { + gc.allowGetProperties = allowPropertiesAccess + + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + return ®oGetPropertiesTestConfig{ + policy: policy, + }, nil +} + +func setupGetPropertiesTestWindows(gc *generatedWindowsConstraints, allowPropertiesAccess bool) (tc *regoGetPropertiesTestConfig, err error) { + gc.allowGetProperties = allowPropertiesAccess + + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + return ®oGetPropertiesTestConfig{ + policy: policy, + }, nil +} + +type regoGetPropertiesTestConfig struct { + policy *regoEnforcer +} + +func setupDumpStacksTest(constraints *generatedConstraints, allowDumpStacks bool) (tc *regoGetPropertiesTestConfig, err error) { + constraints.allowDumpStacks = allowDumpStacks + + securityPolicy := constraints.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + return ®oGetPropertiesTestConfig{ + policy: policy, + }, nil +} + +func setupDumpStacksTestWindows(constraints *generatedWindowsConstraints, allowDumpStacks bool) (tc *regoGetPropertiesTestConfig, err error) { + constraints.allowDumpStacks = allowDumpStacks + + securityPolicy := constraints.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + return ®oGetPropertiesTestConfig{ + policy: policy, + }, nil +} + +type regoDumpStacksTestConfig struct { + policy *regoEnforcer +} + +type regoPolicyOnlyTestConfig struct { + policy *regoEnforcer +} + +func setupRegoPolicyOnlyTest(gc *generatedConstraints) (tc *regoPolicyOnlyTestConfig, err error) { + securityPolicy := gc.toPolicy() + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + + if err != nil { + return nil, err + } + + // see NOTE_TESTCOPY + return ®oPolicyOnlyTestConfig{ + policy: policy, + }, nil +} + +type regoFragmentTestConfig struct { + fragments []*regoFragment + containers []*regoFragmentContainer + externalProcesses []*externalProcess + subFragments []*regoFragment + plan9Mounts []string + mountSpec []string + policy *regoEnforcer +} + +type regoFragmentContainer struct { + container *securityPolicyContainer + envList []string + sandboxID string + mounts []oci.Mount + user IDName + groups []IDName + capabilities *oci.LinuxCapabilities + seccomp string +} + +// Fragment tests set up for Linux +func setupSimpleRegoFragmentTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { + return setupRegoFragmentTestConfig(gc, 1, []string{"containers"}, []string{}, false, false, false, false) +} + +func setupRegoFragmentTestConfigWithIncludes(gc *generatedConstraints, includes []string) (*regoFragmentTestConfig, error) { + return setupRegoFragmentTestConfig(gc, 1, includes, []string{}, false, false, false, false) +} + +func setupRegoFragmentTestConfigWithExcludes(gc *generatedConstraints, excludes []string) (*regoFragmentTestConfig, error) { + return setupRegoFragmentTestConfig(gc, 1, []string{}, excludes, false, false, false, false) +} + +func setupRegoFragmentSVNErrorTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { + return setupRegoFragmentTestConfig(gc, 1, []string{"containers"}, []string{}, true, false, false, false) +} + +func setupRegoSubfragmentSVNErrorTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { + return setupRegoFragmentTestConfig(gc, 1, []string{"fragments"}, []string{}, true, false, false, false) +} + +func setupRegoFragmentTwoFeedTestConfig(gc *generatedConstraints, sameIssuer bool, sameFeed bool) (*regoFragmentTestConfig, error) { + return setupRegoFragmentTestConfig(gc, 2, []string{"containers"}, []string{}, false, sameIssuer, sameFeed, false) +} + +func setupRegoFragmentSVNMismatchTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { + return setupRegoFragmentTestConfig(gc, 2, []string{"containers"}, []string{}, false, false, false, true) +} + +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)) + + if sameIssuer { + for _, fragment := range gc.fragments { + fragment.issuer = gc.fragments[0].issuer + if sameFeed { + fragment.feed = gc.fragments[0].feed + } + } + } + + subSVNError := svnError + if len(includes) > 0 && includes[0] == "fragments" { + svnError = false + } + fragments := selectFragmentsFromConstraints(gc, numFragments, includes, excludes, svnError, frameworkVersion, svnMismatch) + + containers := make([]*regoFragmentContainer, numFragments) + subFragments := make([]*regoFragment, numFragments) + 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, + } + + for _, include := range fragment.info.includes { + switch include { + case "fragments": + subFragments[i] = selectFragmentsFromConstraints(fragment.constraints, 1, []string{"containers"}, []string{}, subSVNError, frameworkVersion, false)[0] + break + + case "external_processes": + externalProcesses[i] = selectExternalProcessFromConstraints(fragment.constraints, testRand) + break + } + } + + // now that we've explicitly added the excluded items to the fragment + // we remove the include string so that the generated policy + // does not include them. + fragment.info.includes = removeStringsFromArray(fragment.info.includes, excludes) + + code := fragment.constraints.toFragment().marshalRego() + fragment.code = setFrameworkVersion(code, frameworkVersion) + } + + if sameFeed { + includeSet := make(map[string]bool) + minSVN := strconv.Itoa(maxGeneratedVersion) + for _, fragment := range gc.fragments { + svn := fragment.minimumSVN + if compareSVNs(svn, minSVN) < 0 { + minSVN = svn + } + for _, include := range fragment.includes { + includeSet[include] = true + } + } + frag := gc.fragments[0] + frag.minimumSVN = minSVN + frag.includes = make([]string, 0, len(includeSet)) + for include := range includeSet { + frag.includes = append(frag.includes, include) + } + + gc.fragments = []*fragment{frag} + + } + + securityPolicy := gc.toPolicy() + defaultMounts := toOCIMounts(generateMounts(testRand)) + privilegedMounts := toOCIMounts(generateMounts(testRand)) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), defaultMounts, privilegedMounts, testOSType) + + if err != nil { + return nil, err + } + + return ®oFragmentTestConfig{ + fragments: fragments, + containers: containers, + subFragments: subFragments, + externalProcesses: externalProcesses, + plan9Mounts: plan9Mounts, + policy: policy, + }, nil +} + +func compareSVNs(lhs string, rhs string) int { + lhs_int, err := strconv.Atoi(lhs) + if err == nil { + rhs_int, err := strconv.Atoi(rhs) + if err == nil { + return lhs_int - rhs_int + } + } + + panic("unable to compare SVNs") +} + +type regoDropEnvsTestConfig struct { + envList []string + expected []string + argList []string + workingDir string + containerID string + sandboxID string + mounts []oci.Mount + policy *regoEnforcer + capabilities oci.LinuxCapabilities +} + +func setupEnvRuleSets(count int) [][]EnvRuleConfig { + numEnvRules := []int{int(randMinMax(testRand, 1, 4)), + int(randMinMax(testRand, 1, 4)), + int(randMinMax(testRand, 1, 4))} + envRuleLookup := make(stringSet) + envRules := make([][]EnvRuleConfig, count) + + for i := 0; i < count; i++ { + rules := envRuleLookup.randUniqueArray(testRand, func(r *rand.Rand) string { + return randVariableString(r, 10) + }, int32(numEnvRules[i])) + + envRules[i] = make([]EnvRuleConfig, numEnvRules[i]) + for j, rule := range rules { + envRules[i][j] = EnvRuleConfig{ + Strategy: "string", + Rule: rule, + } + } + } + + return envRules +} + +func setupRegoDropEnvsTest(disjoint bool) (*regoContainerTestConfig, error) { + gc := generateConstraints(testRand, 1) + gc.allowEnvironmentVariableDropping = true + + const numContainers int = 3 + envRules := setupEnvRuleSets(numContainers) + containers := make([]*securityPolicyContainer, numContainers) + envs := make([][]string, numContainers) + + for i := 0; i < numContainers; i++ { + c, err := gc.containers[0].clone() + if err != nil { + return nil, err + } + containers[i] = c + envs[i] = buildEnvironmentVariablesFromEnvRules(envRules[i], testRand) + if i == 0 { + c.EnvRules = envRules[i] + } else if disjoint { + c.EnvRules = append(envRules[0], envRules[i]...) + } else { + c.EnvRules = append(containers[i-1].EnvRules, envRules[i]...) + } + } + + gc.containers = containers + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + + if err != nil { + return nil, err + } + + containerIDs := make([]string, numContainers) + for i, c := range gc.containers { + containerID, err := mountImageForContainer(policy, c) + if err != nil { + return nil, err + } + + containerIDs[i] = containerID + } + + var envList []string + if disjoint { + var extraLen int + if len(envs[1]) < len(envs[2]) { + extraLen = len(envs[1]) + } else { + extraLen = len(envs[2]) + } + envList = append(envs[0], envs[1][:extraLen]...) + envList = append(envList, envs[2][:extraLen]...) + } else { + envList = append(envs[0], envs[1]...) + envList = append(envList, envs[2]...) + } + + user := buildIDNameFromConfig(containers[2].User.UserIDName, testRand) + groups := buildGroupIDNamesFromUser(containers[2].User, testRand) + umask := containers[2].User.Umask + + sandboxID := testDataGenerator.uniqueSandboxID() + + mounts := containers[2].Mounts + mounts = append(mounts, defaultMounts...) + if containers[2].AllowElevated { + mounts = append(mounts, privilegedMounts...) + } + + mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) + capabilities := copyLinuxCapabilities(containers[2].Capabilities.toExternal()) + seccomp := containers[2].SeccompProfileSHA256 + + // see NOTE_TESTCOPY + return ®oContainerTestConfig{ + envList: copyStrings(envList), + argList: copyStrings(containers[2].Command), + workingDir: containers[2].WorkingDir, + containerID: containerIDs[2], + sandboxID: sandboxID, + mounts: copyMounts(mountSpec.Mounts), + noNewPrivileges: containers[2].NoNewPrivileges, + user: user, + groups: groups, + umask: umask, + policy: policy, + capabilities: &capabilities, + seccomp: seccomp, + ctx: gc.ctx, + }, nil +} + +func setupRegoDropEnvsTestWindows(disjoint bool) (*regoContainerTestConfig, error) { + gc := generateWindowsConstraints(testRand, 1) + gc.allowEnvironmentVariableDropping = true + + const numContainers int = 3 + envRules := setupEnvRuleSets(numContainers) + containers := make([]*securityPolicyWindowsContainer, numContainers) + envs := make([][]string, numContainers) + + for i := 0; i < numContainers; i++ { + c, err := gc.containers[0].clone() + if err != nil { + return nil, err + } + containers[i] = c + envs[i] = buildEnvironmentVariablesFromEnvRules(envRules[i], testRand) + if i == 0 { + c.EnvRules = envRules[i] + } else if disjoint { + c.EnvRules = append(envRules[0], envRules[i]...) + } else { + c.EnvRules = append(containers[i-1].EnvRules, envRules[i]...) + } + } + + gc.containers = containers + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + + if err != nil { + return nil, err + } + + containerIDs := make([]string, numContainers) + for i, c := range gc.containers { + containerID, err := mountImageForWindowsContainer(policy, c) + if err != nil { + return nil, err + } + + containerIDs[i] = containerID + } + + var envList []string + if disjoint { + var extraLen int + if len(envs[1]) < len(envs[2]) { + extraLen = len(envs[1]) + } else { + extraLen = len(envs[2]) + } + envList = append(envs[0], envs[1][:extraLen]...) + envList = append(envList, envs[2][:extraLen]...) + } else { + envList = append(envs[0], envs[1]...) + envList = append(envList, envs[2]...) + } + + // Handle Windows user configuration + user := IDName{} + if containers[2].User != "" { + user = IDName{Name: containers[2].User} + } else { + user = IDName{Name: generateIDNameName(testRand)} + } + + sandboxID := testDataGenerator.uniqueSandboxID() + + // see NOTE_TESTCOPY + return ®oContainerTestConfig{ + envList: copyStrings(envList), + argList: copyStrings(containers[2].Command), + workingDir: containers[2].WorkingDir, + containerID: containerIDs[2], + sandboxID: sandboxID, + mounts: []oci.Mount{}, + noNewPrivileges: false, + user: user, + groups: []IDName{}, + umask: "", + capabilities: nil, + seccomp: "", + policy: policy, + ctx: gc.ctx, + }, nil +} + +type regoFrameworkVersionTestConfig struct { + policy *regoEnforcer + fragments []*regoFragment +} + +func setFrameworkVersion(code string, version string) string { + template := `framework_version := "%s"` + old := fmt.Sprintf(template, frameworkVersion) + if version == "" { + return strings.Replace(code, old, "", 1) + } + + new := fmt.Sprintf(template, version) + return strings.Replace(code, old, new, 1) +} + +func setupFrameworkVersionSimpleTest(gc *generatedConstraints, policyVersion string, version string) (*regoFrameworkVersionTestConfig, error) { + return setupFrameworkVersionTest(gc, policyVersion, version, 0, "", []string{}) +} + +func setupFrameworkVersionTest(gc *generatedConstraints, policyVersion string, version string, numFragments int, fragmentVersion string, includes []string) (*regoFrameworkVersionTestConfig, error) { + fragments := make([]*regoFragment, 0, numFragments) + if numFragments > 0 { + gc.fragments = generateFragments(testRand, int32(numFragments)) + fragments = selectFragmentsFromConstraints(gc, numFragments, includes, []string{}, false, fragmentVersion, false) + } + + securityPolicy := gc.toPolicy() + policy, err := newRegoPolicy(setFrameworkVersion(securityPolicy.marshalRego(), policyVersion), []oci.Mount{}, []oci.Mount{}, testOSType) + + if err != nil { + return nil, err + } + + code := strings.Replace(frameworkCodeTemplate, "@@FRAMEWORK_VERSION@@", version, 1) + policy.rego.RemoveModule("framework.rego") + policy.rego.AddModule("framework.rego", &rpi.RegoModule{Namespace: "framework", Code: code}) + err = policy.rego.Compile() + if err != nil { + return nil, err + } + + return ®oFrameworkVersionTestConfig{policy: policy, fragments: fragments}, nil +} + +type regoFragment struct { + info *fragment + constraints *generatedConstraints + code string +} + +func (f *regoFragment) selectContainer() *securityPolicyContainer { + return selectContainerFromContainerList(f.constraints.containers, testRand) +} + +func mustIncrementSVN(svn string) string { + svn_semver, err := semver.Parse(svn) + + if err == nil { + svn_semver.IncrementMajor() + return svn_semver.String() + } + + svn_int, err := strconv.Atoi(svn) + + if err == nil { + return strconv.Itoa(svn_int + 1) + } + + panic("Could not increment SVN") +} + +func selectFragmentsFromConstraints(gc *generatedConstraints, numFragments int, includes []string, excludes []string, svnError bool, frameworkVersion string, svnMismatch bool) []*regoFragment { + choices := randChoices(testRand, numFragments, len(gc.fragments)) + fragments := make([]*regoFragment, numFragments) + for i, choice := range choices { + config := gc.fragments[choice] + config.includes = addStringsToArray(config.includes, includes) + // since we want to test that the policy cannot include an excluded + // quantity, we must first ensure they are in the fragment + config.includes = addStringsToArray(config.includes, excludes) + + constraints := generateConstraints(testRand, maxContainersInGeneratedConstraints) + for _, include := range config.includes { + switch include { + case "fragments": + constraints.fragments = generateFragments(testRand, 1) + for _, fragment := range constraints.fragments { + fragment.includes = addStringsToArray(fragment.includes, []string{"containers"}) + } + break + + case "external_processes": + constraints.externalProcesses = generateExternalProcesses(testRand) + break + } + } + + svn := config.minimumSVN + if svnMismatch { + if randBool(testRand) { + svn = generateSemver(testRand) + } else { + config.minimumSVN = generateSemver(testRand) + } + } + + constraints.svn = svn + if svnError { + config.minimumSVN = mustIncrementSVN(config.minimumSVN) + } + + code := constraints.toFragment().marshalRego() + code = setFrameworkVersion(code, frameworkVersion) + + fragments[i] = ®oFragment{ + info: config, + constraints: constraints, + code: code, + } + } + + return fragments +} + +func generateSandboxID(r *rand.Rand) string { + return randVariableString(r, maxGeneratedSandboxIDLength) +} + +func generateEnforcementPoint(r *rand.Rand) string { + first := randChar(r) + return first + randString(r, atMost(r, maxGeneratedEnforcementPointLength)) +} + +func (gen *dataGenerator) uniqueSandboxID() string { + return gen.sandboxIDs.randUnique(gen.rng, generateSandboxID) +} + +func (gen *dataGenerator) uniqueEnforcementPoint() string { + return gen.enforcementPoints.randUnique(gen.rng, generateEnforcementPoint) +} + +type regoContainerTestConfig struct { + envList []string + argList []string + workingDir string + containerID string + sandboxID string + mounts []oci.Mount + noNewPrivileges bool + user IDName + groups []IDName + umask string + capabilities *oci.LinuxCapabilities + seccomp string + policy *regoEnforcer + ctx context.Context +} + +func setupSimpleRegoCreateContainerTest(gc *generatedConstraints) (tc *regoContainerTestConfig, err error) { + c := selectContainerFromContainerList(gc.containers, testRand) + return setupRegoCreateContainerTest(gc, c, false) +} + +func setupRegoPrivilegedMountTest(gc *generatedConstraints) (tc *regoContainerTestConfig, err error) { + c := selectContainerFromContainerList(gc.containers, testRand) + return setupRegoCreateContainerTest(gc, c, true) +} + +func setupRegoCreateContainerTest(gc *generatedConstraints, testContainer *securityPolicyContainer, privilegedError bool) (tc *regoContainerTestConfig, err error) { + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + containerID, err := mountImageForContainer(policy, testContainer) + if err != nil { + return nil, err + } + + envList := buildEnvironmentVariablesFromEnvRules(testContainer.EnvRules, testRand) + sandboxID := testDataGenerator.uniqueSandboxID() + + mounts := testContainer.Mounts + mounts = append(mounts, defaultMounts...) + if privilegedError { + testContainer.AllowElevated = false + } + + if testContainer.AllowElevated || privilegedError { + mounts = append(mounts, privilegedMounts...) + } + mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) + + // Handle user configuration based on OS type + user := IDName{} + if testContainer.User.UserIDName.Strategy != IDNameStrategyRegex { + user = buildIDNameFromConfig(testContainer.User.UserIDName, testRand) + } + groups := buildGroupIDNamesFromUser(testContainer.User, testRand) + umask := testContainer.User.Umask + + var capabilities *oci.LinuxCapabilities + if testContainer.Capabilities != nil { + capsExternal := copyLinuxCapabilities(testContainer.Capabilities.toExternal()) + capabilities = &capsExternal + } else { + capabilities = nil + } + + seccomp := testContainer.SeccompProfileSHA256 + + // Return full config for Linux + return ®oContainerTestConfig{ + envList: copyStrings(envList), + argList: copyStrings(testContainer.Command), + workingDir: testContainer.WorkingDir, + containerID: containerID, + sandboxID: sandboxID, + mounts: copyMounts(mountSpec.Mounts), + noNewPrivileges: testContainer.NoNewPrivileges, + user: user, + groups: groups, + umask: umask, + capabilities: capabilities, + seccomp: seccomp, + policy: policy, + ctx: gc.ctx, + }, nil + +} + +func setupRegoRunningContainerTest(gc *generatedConstraints, privileged bool) (tc *regoRunningContainerTestConfig, err error) { + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + var runningContainers []regoRunningContainer + numOfRunningContainers := int(atLeastOneAtMost(testRand, int32(len(gc.containers)))) + containersToRun := randChoicesWithReplacement(testRand, numOfRunningContainers, len(gc.containers)) + for _, i := range containersToRun { + containerToStart := gc.containers[i] + r, err := runContainer(policy, containerToStart, defaultMounts, privilegedMounts, privileged) + if err != nil { + return nil, err + } + runningContainers = append(runningContainers, *r) + } + + return ®oRunningContainerTestConfig{ + runningContainers: runningContainers, + policy: policy, + defaultMounts: copyMountsInternal(defaultMounts), + privilegedMounts: copyMountsInternal(privilegedMounts), + }, nil +} + +func runContainer(enforcer *regoEnforcer, container *securityPolicyContainer, defaultMounts []mountInternal, privilegedMounts []mountInternal, privileged bool) (*regoRunningContainer, error) { + ctx := context.Background() + containerID, err := mountImageForContainer(enforcer, container) + if err != nil { + return nil, err + } + + envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) + user := buildIDNameFromConfig(container.User.UserIDName, testRand) + groups := buildGroupIDNamesFromUser(container.User, testRand) + umask := container.User.Umask + sandboxID := generateSandboxID(testRand) + + mounts := container.Mounts + mounts = append(mounts, defaultMounts...) + if container.AllowElevated { + mounts = append(mounts, privilegedMounts...) + } + mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) + var capabilities oci.LinuxCapabilities + if container.Capabilities == nil { + if privileged { + capabilities = capabilitiesInternal{ + Bounding: DefaultPrivilegedCapabilities(), + Inheritable: DefaultPrivilegedCapabilities(), + Effective: DefaultPrivilegedCapabilities(), + Permitted: DefaultPrivilegedCapabilities(), + Ambient: []string{}, + }.toExternal() + } else { + capabilities = capabilitiesInternal{ + Bounding: DefaultUnprivilegedCapabilities(), + Inheritable: []string{}, + Effective: DefaultUnprivilegedCapabilities(), + Permitted: DefaultUnprivilegedCapabilities(), + Ambient: []string{}, + }.toExternal() + } + } else { + capabilities = container.Capabilities.toExternal() + } + seccomp := container.SeccompProfileSHA256 + + _, _, _, err = enforcer.EnforceCreateContainerPolicy(ctx, sandboxID, containerID, container.Command, envList, container.WorkingDir, mountSpec.Mounts, privileged, container.NoNewPrivileges, user, groups, umask, &capabilities, seccomp) + if err != nil { + return nil, err + } + + return ®oRunningContainer{ + container: container, + envList: envList, + containerID: containerID, + }, nil +} + +type regoRunningContainerTestConfig struct { + runningContainers []regoRunningContainer + policy *regoEnforcer + defaultMounts []mountInternal + privilegedMounts []mountInternal +} + +type regoRunningContainer struct { + container *securityPolicyContainer + windowsContainer *securityPolicyWindowsContainer + envList []string + containerID string +} + +func setupRegoRunningWindowsContainerTest(gc *generatedWindowsConstraints) (tc *regoRunningContainerTestConfig, err error) { + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + //fmt.Printf("Generated Rego policy:\n%s\n", securityPolicy.marshalWindowsRego()) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + var runningContainers []regoRunningContainer + numOfRunningContainers := int(atLeastOneAtMost(testRand, int32(len(gc.containers)))) + containersToRun := randChoicesWithReplacement(testRand, numOfRunningContainers, len(gc.containers)) + for _, i := range containersToRun { + containerToStart := gc.containers[i] + r, err := runWindowsContainer(policy, containerToStart) + if err != nil { + return nil, err + } + runningContainers = append(runningContainers, *r) + } + + return ®oRunningContainerTestConfig{ + runningContainers: runningContainers, + policy: policy, + defaultMounts: copyMountsInternal(defaultMounts), + privilegedMounts: copyMountsInternal(privilegedMounts), + }, nil +} + +func runWindowsContainer(enforcer *regoEnforcer, container *securityPolicyWindowsContainer) (*regoRunningContainer, error) { + ctx := context.Background() + containerID, err := mountImageForWindowsContainer(enforcer, container) + if err != nil { + return nil, err + } + + envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) + user := IDName{Name: container.User} + + _, _, _, err = enforcer.EnforceCreateContainerPolicyV2(ctx, containerID, container.Command, envList, container.WorkingDir, nil, user, nil) + + if err != nil { + return nil, err + } + + return ®oRunningContainer{ + windowsContainer: container, + envList: envList, + containerID: containerID, + }, nil +} + +func copyStrings(values []string) []string { + valuesCopy := make([]string, len(values)) + copy(valuesCopy, values) + return valuesCopy +} + +//go:embed api_test.rego +var apiTestCode string + +func (p *regoEnforcer) injectTestAPI() error { + p.rego.RemoveModule("api.rego") + p.rego.AddModule("api.rego", &rpi.RegoModule{Namespace: "api", Code: apiTestCode}) + + return p.rego.Compile() +} + +func selectContainerFromRunningContainers(containers []regoRunningContainer, r *rand.Rand) regoRunningContainer { + numContainers := len(containers) + return containers[r.Intn(numContainers)] +} + +func idForRunningContainer(container *securityPolicyContainer, running []regoRunningContainer) (string, error) { + for _, c := range running { + if c.container == container { + return c.containerID, nil + } + } + + return "", errors.New("Container isn't running") +} + +func idForRunningWindowsContainer(container *securityPolicyWindowsContainer, running []regoRunningContainer) (string, error) { + for _, c := range running { + if c.windowsContainer == container { + return c.containerID, nil + } + } + + return "", errors.New("Container isn't running") +} + +func generateFragments(r *rand.Rand, minFragments int32) []*fragment { + numFragments := randMinMax(r, minFragments, maxFragmentsInGeneratedConstraints) + + fragments := make([]*fragment, numFragments) + for i := 0; i < int(numFragments); i++ { + fragments[i] = generateFragment(r) + } + + return fragments +} + +func generateFragmentIssuer(r *rand.Rand) string { + return randString(r, maxGeneratedFragmentIssuerLength) +} + +func generateFragmentFeed(r *rand.Rand) string { + return randString(r, maxGeneratedFragmentFeedLength) +} + +func (gen *dataGenerator) uniqueFragmentNamespace() string { + return gen.fragmentNamespaces.randUnique(gen.rng, generateFragmentNamespace) +} + +func (gen *dataGenerator) uniqueFragmentIssuer() string { + return gen.fragmentIssuers.randUnique(gen.rng, generateFragmentIssuer) +} + +func (gen *dataGenerator) uniqueFragmentFeed() string { + return gen.fragmentFeeds.randUnique(gen.rng, generateFragmentFeed) +} + +func generateFragment(r *rand.Rand) *fragment { + possibleIncludes := []string{"containers", "fragments", "external_processes"} + numChoices := int(atLeastOneAtMost(r, int32(len(possibleIncludes)))) + includes := randChooseStrings(r, possibleIncludes, numChoices) + return &fragment{ + issuer: testDataGenerator.uniqueFragmentIssuer(), + feed: testDataGenerator.uniqueFragmentFeed(), + minimumSVN: generateSVN(r), + includes: includes, + } +} + +func addStringsToArray(values []string, valuesToAdd []string) []string { + toAdd := []string{} + for _, valueToAdd := range valuesToAdd { + add := true + for _, value := range values { + if value == valueToAdd { + add = false + break + } + } + if add { + toAdd = append(toAdd, valueToAdd) + } + } + + return append(values, toAdd...) +} + +func removeStringsFromArray(values []string, valuesToRemove []string) []string { + remain := make([]string, 0, len(values)) + for _, value := range values { + keep := true + for _, toRemove := range valuesToRemove { + if value == toRemove { + keep = false + break + } + } + if keep { + remain = append(remain, value) + } + } + + return remain +} + +func areStringArraysEqual(lhs []string, rhs []string) bool { + if len(lhs) != len(rhs) { + return false + } + + sort.Strings(lhs) + sort.Strings(rhs) + + for i, a := range lhs { + if a != rhs[i] { + return false + } + } + + return true +} + +func (c securityPolicyContainer) clone() (*securityPolicyContainer, error) { + contents, err := json.Marshal(c) + if err != nil { + return nil, err + } + + var clone securityPolicyContainer + err = json.Unmarshal(contents, &clone) + if err != nil { + return nil, err + } + + return &clone, nil +} + +func (c securityPolicyWindowsContainer) clone() (*securityPolicyWindowsContainer, error) { + contents, err := json.Marshal(c) + if err != nil { + return nil, err + } + + var clone securityPolicyWindowsContainer + err = json.Unmarshal(contents, &clone) + if err != nil { + return nil, err + } + + return &clone, nil +} + +func (p externalProcess) clone() *externalProcess { + envRules := make([]EnvRuleConfig, len(p.envRules)) + copy(envRules, p.envRules) + + return &externalProcess{ + command: copyStrings(p.command), + envRules: envRules, + workingDir: p.workingDir, + allowStdioAccess: p.allowStdioAccess, + } +} + +func (p containerExecProcess) clone() containerExecProcess { + return containerExecProcess{ + Command: copyStrings(p.Command), + Signals: p.Signals, + } +} + +func (c *securityPolicyContainer) toContainer() *Container { + execProcesses := make([]ExecProcessConfig, len(c.ExecProcesses)) + for i, ep := range c.ExecProcesses { + execProcesses[i] = ExecProcessConfig(ep) + } + + capabilities := CapabilitiesConfig{ + Bounding: c.Capabilities.Bounding, + Effective: c.Capabilities.Effective, + Inheritable: c.Capabilities.Inheritable, + Permitted: c.Capabilities.Permitted, + Ambient: c.Capabilities.Ambient, + } + + return &Container{ + Command: CommandArgs(stringArrayToStringMap(c.Command)), + EnvRules: envRuleArrayToEnvRules(c.EnvRules), + Layers: Layers(stringArrayToStringMap(c.Layers)), + WorkingDir: c.WorkingDir, + Mounts: mountArrayToMounts(c.Mounts), + AllowElevated: c.AllowElevated, + ExecProcesses: execProcesses, + Signals: c.Signals, + AllowStdioAccess: c.AllowStdioAccess, + NoNewPrivileges: c.NoNewPrivileges, + User: c.User, + Capabilities: &capabilities, + SeccompProfileSHA256: c.SeccompProfileSHA256, + } +} + +func (c *securityPolicyWindowsContainer) toWindowsContainer() *WindowsContainer { + execProcesses := make([]WindowsExecProcessConfig, len(c.ExecProcesses)) + for i, ep := range c.ExecProcesses { + execProcesses[i] = WindowsExecProcessConfig(ep) + } + + return &WindowsContainer{ + Command: CommandArgs(stringArrayToStringMap(c.Command)), + EnvRules: envRuleArrayToEnvRules(c.EnvRules), + Layers: Layers(stringArrayToStringMap(c.Layers)), + WorkingDir: c.WorkingDir, + ExecProcesses: execProcesses, + Signals: c.Signals, + User: c.User, + } +} + +func envRuleArrayToEnvRules(envRules []EnvRuleConfig) EnvRules { + elements := make(map[string]EnvRuleConfig) + for i, envRule := range envRules { + elements[strconv.Itoa(i)] = envRule + } + return EnvRules{ + Elements: elements, + Length: len(envRules), + } +} + +func mountArrayToMounts(mounts []mountInternal) Mounts { + elements := make(map[string]Mount) + for i, mount := range mounts { + elements[strconv.Itoa(i)] = Mount{ + Source: mount.Source, + Destination: mount.Destination, + Type: mount.Type, + Options: Options(stringArrayToStringMap(mount.Options)), + } + } + + return Mounts{ + Elements: elements, + Length: len(mounts), + } +} + +func (p externalProcess) toConfig() ExternalProcessConfig { + return ExternalProcessConfig{ + Command: p.command, + WorkingDir: p.workingDir, + AllowStdioAccess: p.allowStdioAccess, + } +} + +func (f fragment) toConfig() FragmentConfig { + return FragmentConfig{ + Issuer: f.issuer, + Feed: f.feed, + MinimumSVN: f.minimumSVN, + Includes: f.includes, + } +} + +func stringArrayToStringMap(values []string) StringArrayMap { + elements := make(map[string]string) + for i, value := range values { + elements[strconv.Itoa(i)] = value + } + + return StringArrayMap{ + Elements: elements, + Length: len(values), + } +} + +func (s *stringSet) randUniqueArray(r *rand.Rand, generator func(*rand.Rand) string, numItems int32) []string { + items := make([]string, numItems) + for i := 0; i < int(numItems); i++ { + items[i] = s.randUnique(r, generator) + } + return items +} + +func generateExternalProcesses(r *rand.Rand) []*externalProcess { + var processes []*externalProcess + + numProcesses := atLeastOneAtMost(r, maxExternalProcessesInGeneratedConstraints) + for i := 0; i < int(numProcesses); i++ { + processes = append(processes, generateExternalProcess(r)) + } + + return processes +} + +func generateExternalProcess(r *rand.Rand) *externalProcess { + return &externalProcess{ + command: generateCommand(r), + envRules: generateEnvironmentVariableRules(r), + workingDir: generateWorkingDir(r), + allowStdioAccess: randBool(r), + } +} + +func randChoices(r *rand.Rand, numChoices int, numItems int) []int { + shuffle := r.Perm(numItems) + if numChoices > numItems { + return shuffle + } + + return shuffle[:numChoices] +} + +func randChoicesWithReplacement(r *rand.Rand, numChoices int, numItems int) []int { + choices := make([]int, numChoices) + for i := 0; i < numChoices; i++ { + choices[i] = r.Intn(numItems) + } + + return choices +} + +func randChooseStrings(r *rand.Rand, items []string, numChoices int) []string { + numItems := len(items) + choiceIndices := randChoices(r, numChoices, numItems) + choices := make([]string, numChoices) + for i, index := range choiceIndices { + choices[i] = items[index] + } + return choices +} + +func randChooseStringsWithReplacement(r *rand.Rand, items []string, numChoices int) []string { + numItems := len(items) + choiceIndices := randChoicesWithReplacement(r, numChoices, numItems) + choices := make([]string, numChoices) + for i, index := range choiceIndices { + choices[i] = items[index] + } + return choices +} + +func selectExternalProcessFromConstraints(constraints *generatedConstraints, r *rand.Rand) *externalProcess { + numberOfProcessesInConstraints := len(constraints.externalProcesses) + return constraints.externalProcesses[r.Intn(numberOfProcessesInConstraints)] +} + +func selectWindowsExternalProcessFromConstraints(constraints *generatedWindowsConstraints, r *rand.Rand) *externalProcess { + numberOfProcessesInConstraints := len(constraints.externalProcesses) + return constraints.externalProcesses[r.Intn(numberOfProcessesInConstraints)] +} + +func (constraints *generatedConstraints) toPolicy() *securityPolicyInternal { + return &securityPolicyInternal{ + Containers: constraints.containers, + ExternalProcesses: constraints.externalProcesses, + Fragments: constraints.fragments, + AllowPropertiesAccess: constraints.allowGetProperties, + AllowDumpStacks: constraints.allowDumpStacks, + AllowRuntimeLogging: constraints.allowRuntimeLogging, + AllowEnvironmentVariableDropping: constraints.allowEnvironmentVariableDropping, + AllowUnencryptedScratch: constraints.allowUnencryptedScratch, + AllowCapabilityDropping: constraints.allowCapabilityDropping, + } +} + +func (constraints *generatedConstraints) toFragment() *securityPolicyFragment { + return &securityPolicyFragment{ + Namespace: constraints.namespace, + SVN: constraints.svn, + Containers: constraints.containers, + ExternalProcesses: constraints.externalProcesses, + Fragments: constraints.fragments, + } +} + +func generateSemver(r *rand.Rand) string { + major := randMinMax(r, 0, maxGeneratedVersion) + minor := randMinMax(r, 0, maxGeneratedVersion) + patch := randMinMax(r, 0, maxGeneratedVersion) + return fmt.Sprintf("%d.%d.%d", major, minor, patch) +} + +func assertKeyValue(object map[string]interface{}, key string, expectedValue interface{}) error { + if actualValue, ok := object[key]; ok { + if actualValue != expectedValue { + return fmt.Errorf("incorrect value for no_new_privileges: %t != %t (expected)", actualValue, expectedValue) + } + } else { + return fmt.Errorf("missing value for %s", key) + } + + return nil +} + +func assertDecisionJSONContains(t *testing.T, err error, expectedValues ...string) bool { + if err == nil { + t.Errorf("expected error to contain %v but got nil", expectedValues) + return false + } + + policyDecision, err := ExtractPolicyDecision(err.Error()) + if err != nil { + t.Errorf("unable to extract policy decision from error: %v", err) + return false + } + + for _, expected := range expectedValues { + if !strings.Contains(policyDecision, expected) { + t.Errorf("expected error to contain %q", expected) + return false + } + } + + return true +} + +func assertDecisionJSONDoesNotContain(t *testing.T, err error, expectedValues ...string) bool { + if err == nil { + t.Errorf("expected error to contain %v but got nil", expectedValues) + return false + } + + policyDecision, err := ExtractPolicyDecision(err.Error()) + if err != nil { + t.Errorf("unable to extract policy decision from error: %v", err) + return false + } + + for _, expected := range expectedValues { + if strings.Contains(policyDecision, expected) { + t.Errorf("expected error to not contain %q", expected) + return false + } + } + + return true +} + +// Windows-specific container selection function +func selectWindowsContainerFromContainerList(containers []*securityPolicyWindowsContainer, r *rand.Rand) *securityPolicyWindowsContainer { + return containers[r.Intn(len(containers))] +} + +// Windows-specific simple setup function +func setupSimpleRegoCreateContainerTestWindows(gc *generatedWindowsConstraints) (tc *regoContainerTestConfig, err error) { + c := selectWindowsContainerFromContainerList(gc.containers, testRand) + return setupRegoCreateContainerTestWindows(gc, c, false) +} + +// Windows-specific container test setup +func setupRegoCreateContainerTestWindows(gc *generatedWindowsConstraints, testContainer *securityPolicyWindowsContainer, privilegedError bool) (tc *regoContainerTestConfig, err error) { + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + return nil, err + } + + // Debug: print the OS type being used + //fmt.Printf("OS type being used: %s\n", testOSType) + + // Debug: print the generated Rego policy + //fmt.Printf("Generated Rego policy:\n%s\n", securityPolicy.marshalWindowsRego()) + + containerID, err := mountImageForWindowsContainer(policy, testContainer) + if err != nil { + return nil, err + } + + envList := buildEnvironmentVariablesFromEnvRules(testContainer.EnvRules, testRand) + sandboxID := testDataGenerator.uniqueSandboxID() + + // Handle Windows user configuration + user := IDName{} + if testContainer.User != "" { + user = IDName{Name: testContainer.User} + } else { + user = IDName{Name: generateIDNameName(testRand)} + } + + return ®oContainerTestConfig{ + envList: copyStrings(envList), + argList: copyStrings(testContainer.Command), + workingDir: testContainer.WorkingDir, + containerID: containerID, + sandboxID: sandboxID, + mounts: []oci.Mount{}, + noNewPrivileges: false, + user: user, + groups: []IDName{}, + umask: "", + capabilities: nil, + seccomp: "", + policy: policy, + ctx: gc.ctx, + }, nil +} + +//nolint:unused +func mountImageForWindowsContainer(policy *regoEnforcer, container *securityPolicyWindowsContainer) (string, error) { + ctx := context.Background() + containerID := testDataGenerator.uniqueContainerID() + + // For Windows containers, we need to mount using CIMFS (container image mount) + // The layerHashes_ok function expects hashes in reverse order compared to how they're stored + layerHashes := make([]string, len(container.Layers)) + for i, layer := range container.Layers { + // Reverse the order: last layer becomes first in the input + layerHashes[len(container.Layers)-1-i] = layer + } + + // Mount the CIMFS for the Windows container + err := policy.EnforceVerifiedCIMsPolicy(ctx, containerID, layerHashes) + if err != nil { + return "", fmt.Errorf("error mounting CIMFS: %w", err) + } + + //fmt.Printf("CIMFS mounted successfully for container %s with layers %v\n", containerID, layerHashes) + + return containerID, nil +} + +// +// Setup and "fixtures" follow... +// + +func (*SecurityPolicy) Generate(r *rand.Rand, _ int) reflect.Value { + // This fixture setup is used from 1 test. Given the limited scope it is + // used from, all functionality is in this single function. That saves having + // confusing fixture name functions where we have generate* for both internal + // and external versions + p := &SecurityPolicy{ + Containers: Containers{ + Elements: map[string]Container{}, + }, + } + p.AllowAll = false + numContainers := int(atLeastOneAtMost(r, maxContainersInGeneratedConstraints)) + for i := 0; i < numContainers; i++ { + c := Container{ + Command: CommandArgs{ + Elements: map[string]string{}, + }, + EnvRules: EnvRules{ + Elements: map[string]EnvRuleConfig{}, + }, + Layers: Layers{ + Elements: map[string]string{}, + }, + } + + // command + numArgs := int(atLeastOneAtMost(r, maxGeneratedCommandArgs)) + for j := 0; j < numArgs; j++ { + c.Command.Elements[strconv.Itoa(j)] = randVariableString(r, maxGeneratedCommandLength) + } + c.Command.Length = numArgs + + // layers + numLayers := int(atLeastOneAtMost(r, maxLayersInGeneratedContainer)) + for j := 0; j < numLayers; j++ { + c.Layers.Elements[strconv.Itoa(j)] = generateRootHash(r) + } + c.Layers.Length = numLayers + + // env variable rules + numEnvRules := int(atMost(r, maxGeneratedEnvironmentVariableRules)) + for j := 0; j < numEnvRules; j++ { + rule := EnvRuleConfig{ + Strategy: "string", + Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), + Required: false, + } + c.EnvRules.Elements[strconv.Itoa(j)] = rule + } + c.EnvRules.Length = numEnvRules + + p.Containers.Elements[strconv.Itoa(i)] = c + } + + p.Containers.Length = numContainers + + return reflect.ValueOf(p) +} + +func (*generatedConstraints) Generate(r *rand.Rand, _ int) reflect.Value { + //c := generateConstraints(r, maxContainersInGeneratedConstraints) + c := generateConstraints(r, maxContainersInGeneratedConstraints) + return reflect.ValueOf(c) +} + +type testConfig struct { + container *securityPolicyContainer + layers []string + containerID string + policy *StandardSecurityPolicyEnforcer +} + +func setupContainerWithOverlay(gc *generatedConstraints, valid bool) (tc *testConfig, err error) { + sp := NewStandardSecurityPolicyEnforcer(gc.containers, ignoredEncodedPolicyString) + + containerID := testDataGenerator.uniqueContainerID() + c := selectContainerFromContainerList(gc.containers, testRand) + + var layerPaths []string + if valid { + layerPaths, err = testDataGenerator.createValidOverlayForContainer(sp, c) + if err != nil { + return nil, fmt.Errorf("error creating valid overlay: %w", err) + } + } else { + layerPaths, err = testDataGenerator.createInvalidOverlayForContainer(sp, c) + if err != nil { + return nil, fmt.Errorf("error creating invalid overlay: %w", err) + } + } + + return &testConfig{ + container: c, + layers: layerPaths, + containerID: containerID, + policy: sp, + }, nil +} + +func generateConstraints(r *rand.Rand, maxContainers int32) *generatedConstraints { + var containers []*securityPolicyContainer + + numContainers := (int)(atLeastOneAtMost(r, maxContainers)) + if testOSType == "windows" { + // Windows containers + //for i := 0; i < numContainers; i++ { + // containers = append(containers, generateConstraintsWindowsContainer(r, 1, 5)) + // } + } else if testOSType == "linux" { + // Linux containers + for i := 0; i < numContainers; i++ { + containers = append(containers, generateConstraintsContainer(r, 1, maxLayersInGeneratedContainer)) + } + } + + return &generatedConstraints{ + containers: containers, + externalProcesses: make([]*externalProcess, 0), + fragments: make([]*fragment, 0), + allowGetProperties: randBool(r), + allowDumpStacks: randBool(r), + allowRuntimeLogging: false, + allowEnvironmentVariableDropping: false, + allowUnencryptedScratch: randBool(r), + namespace: generateFragmentNamespace(testRand), + svn: generateSVN(testRand), + allowCapabilityDropping: false, + ctx: context.Background(), + } +} + +func generateConstraintsContainer(r *rand.Rand, minNumberOfLayers, maxNumberOfLayers int32) *securityPolicyContainer { + c := securityPolicyContainer{} + p := generateContainerInitProcess(r) + c.Command = p.Command + c.EnvRules = p.EnvRules + c.WorkingDir = p.WorkingDir + c.Mounts = generateMounts(r) + numLayers := int(atLeastNAtMostM(r, minNumberOfLayers, maxNumberOfLayers)) + for i := 0; i < numLayers; i++ { + c.Layers = append(c.Layers, generateRootHash(r)) + } + c.ExecProcesses = generateExecProcesses(r) + c.Signals = generateListOfSignals(r, 0, maxSignalNumber) + c.AllowStdioAccess = randBool(r) + c.NoNewPrivileges = randBool(r) + c.User = generateUser(r) + c.Capabilities = generateInternalCapabilities(r) + c.SeccompProfileSHA256 = generateSeccomp(r) + + return &c +} + +func generateConstraintsWindowsContainer(r *rand.Rand, minNumberOfLayers, maxNumberOfLayers int32) *securityPolicyWindowsContainer { + c := securityPolicyWindowsContainer{} + p := generateContainerInitProcess(r) + c.Command = p.Command + c.EnvRules = p.EnvRules + c.WorkingDir = p.WorkingDir + numLayers := int(atLeastNAtMostM(r, minNumberOfLayers, maxNumberOfLayers)) + for i := 0; i < numLayers; i++ { + c.Layers = append(c.Layers, generateRootHash(r)) + } + c.ExecProcesses = generateWindowsExecProcesses(r) + c.Signals = generateWindowsSignals(r) + c.AllowStdioAccess = randBool(r) + c.User = generateWindowsUser(r) + + return &c +} + +func generateSeccomp(r *rand.Rand) string { + if randBool(r) { + // 50% chance of no seccomp profile + return "" + } + + return generateRootHash(r) +} + +func generateInternalCapabilities(r *rand.Rand) *capabilitiesInternal { + return &capabilitiesInternal{ + Bounding: generateCapabilitiesSet(r, 0), + Effective: generateCapabilitiesSet(r, 0), + Inheritable: generateCapabilitiesSet(r, 0), + Permitted: generateCapabilitiesSet(r, 0), + Ambient: generateCapabilitiesSet(r, 0), + } +} + +func generateCapabilitiesSet(r *rand.Rand, minSize int32) []string { + capabilities := make([]string, 0) + + numArgs := atLeastNAtMostM(r, minSize, maxGeneratedCapabilities) + for i := 0; i < int(numArgs); i++ { + capabilities = append(capabilities, generateCapability(r)) + } + + return capabilities +} + +func generateCapability(r *rand.Rand) string { + return randVariableString(r, maxGeneratedCapabilitesLength) +} + +func generateContainerInitProcess(r *rand.Rand) containerInitProcess { + return containerInitProcess{ + Command: generateCommand(r), + EnvRules: generateEnvironmentVariableRules(r), + WorkingDir: generateWorkingDir(r), + AllowStdioAccess: randBool(r), + } +} + +func generateContainerExecProcess(r *rand.Rand) containerExecProcess { + return containerExecProcess{ + Command: generateCommand(r), + Signals: generateListOfSignals(r, 0, maxSignalNumber), + } +} + +func generateWindowsContainerExecProcess(r *rand.Rand) windowsContainerExecProcess { + return windowsContainerExecProcess{ + Command: generateWindowsUser(r), + Signals: generateWindowsSignals(r), + } +} + +func generateRootHash(r *rand.Rand) string { + return randString(r, rootHashLength) +} + +func generateWorkingDir(r *rand.Rand) string { + return randVariableString(r, maxGeneratedWorkingDirLength) +} + +func generateWindowsUser(r *rand.Rand) string { + return randVariableString(r, maxGeneratedWorkingDirLength) +} + +func generateCommand(r *rand.Rand) []string { + var args []string + + numArgs := atLeastOneAtMost(r, maxGeneratedCommandArgs) + for i := 0; i < int(numArgs); i++ { + args = append(args, randVariableString(r, maxGeneratedCommandLength)) + } + + return args +} + +func generateWindowsSignals(r *rand.Rand) []guestrequest.SignalValueWCOW { + var args []guestrequest.SignalValueWCOW + + numArgs := atLeastOneAtMost(r, maxGeneratedCommandArgs) + for i := 0; i < int(numArgs); i++ { + var str string = randVariableString(r, maxGeneratedCommandLength) + var sig guestrequest.SignalValueWCOW = guestrequest.SignalValueWCOW(str) + args = append(args, sig) + } + + return args +} + +func generateEnvironmentVariableRules(r *rand.Rand) []EnvRuleConfig { + var rules []EnvRuleConfig + + numArgs := atLeastOneAtMost(r, maxGeneratedEnvironmentVariableRules) + for i := 0; i < int(numArgs); i++ { + rule := EnvRuleConfig{ + Strategy: "string", + Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), + } + rules = append(rules, rule) + } + + return rules +} + +func generateExecProcesses(r *rand.Rand) []containerExecProcess { + var processes []containerExecProcess + + numProcesses := atLeastOneAtMost(r, maxGeneratedExecProcesses) + + for i := 0; i < int(numProcesses); i++ { + processes = append(processes, generateContainerExecProcess(r)) + } + + return processes +} + +func generateWindowsExecProcesses(r *rand.Rand) []windowsContainerExecProcess { + var processes []windowsContainerExecProcess + + numProcesses := atLeastOneAtMost(r, maxGeneratedExecProcesses) + for i := 0; i < int(numProcesses); i++ { + processes = append(processes, generateWindowsContainerExecProcess(r)) + } + + return processes +} + +func generateUmask(r *rand.Rand) string { + // we are generating values from 000 to 777 as decimal values + // to ensure we cover the full range of umask values + // and so the resulting string will be a 4 digit octal representation + // even though we are using decimal values + return fmt.Sprintf("%04d", randMinMax(r, 0, 777)) +} + +func generateIDNameConfig(r *rand.Rand) IDNameConfig { + strategies := []IDNameStrategy{IDNameStrategyName, IDNameStrategyID} + strategy := strategies[randMinMax(r, 0, int32(len(strategies)-1))] + switch strategy { + case IDNameStrategyName: + return IDNameConfig{ + Strategy: IDNameStrategyName, + Rule: randVariableString(r, maxGeneratedNameLength), + } + + case IDNameStrategyID: + return IDNameConfig{ + Strategy: IDNameStrategyID, + Rule: fmt.Sprintf("%d", r.Uint32()), + } + } + panic("unreachable") +} + +func generateUser(r *rand.Rand) UserConfig { + numGroups := int(atLeastOneAtMost(r, maxGeneratedGroupNames)) + groupIDs := make([]IDNameConfig, numGroups) + for i := 0; i < numGroups; i++ { + groupIDs[i] = generateIDNameConfig(r) + } + + return UserConfig{ + UserIDName: generateIDNameConfig(r), + GroupIDNames: groupIDs, + Umask: generateUmask(r), + } +} + +func generateEnvironmentVariables(r *rand.Rand) []string { + var envVars []string + + numVars := atLeastOneAtMost(r, maxGeneratedEnvironmentVariables) + for i := 0; i < int(numVars); i++ { + variable := randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) + envVars = append(envVars, variable) + } + + return envVars +} + +func generateNeverMatchingEnvironmentVariable(r *rand.Rand) string { + return randString(r, maxGeneratedEnvironmentVariableRuleLength+1) +} + +func buildEnvironmentVariablesFromEnvRules(rules []EnvRuleConfig, r *rand.Rand) []string { + vars := make([]string, 0) + + // Select some number of the valid, matching rules to be environment + // variable + numberOfRules := int32(len(rules)) + if numberOfRules == 0 { + return vars + } + numberOfMatches := randMinMax(r, 1, numberOfRules) + + // Build in all required rules, this isn't a setup method of "missing item" + // tests + for _, rule := range rules { + + if rule.Required { + if rule.Strategy != EnvVarRuleRegex { + vars = append(vars, rule.Rule) + } + numberOfMatches-- + } + } + + usedIndexes := map[int]struct{}{} + for numberOfMatches > 0 { + anIndex := -1 + if (numberOfMatches * 2) > numberOfRules { + // if we have a lot of matches, randomly select + exists := true + + for exists { + anIndex = int(randMinMax(r, 0, numberOfRules-1)) + _, exists = usedIndexes[anIndex] + } + } else { + // we have a "smaller set of rules. we'll just iterate and select from + // available + exists := true + + for exists { + anIndex++ + _, exists = usedIndexes[anIndex] + } + } + + // include it if it's not regex + if rules[anIndex].Strategy != EnvVarRuleRegex { + vars = append(vars, rules[anIndex].Rule) + usedIndexes[anIndex] = struct{}{} + } + numberOfMatches-- + + } + + return vars +} + +func generateMountTarget(r *rand.Rand) string { + return randVariableString(r, maxGeneratedMountTargetLength) +} + +func generateInvalidRootHash(r *rand.Rand) string { + // Guaranteed to be an incorrect size as it maxes out in size at one less + // than the correct length. If this ever creates a hash that passes, we + // have a seriously weird bug + return randVariableString(r, rootHashLength-1) +} + +func generateFragmentNamespace(r *rand.Rand) string { + return randChar(r) + randVariableString(r, maxGeneratedFragmentNamespaceLength) +} + +func generateSVN(r *rand.Rand) string { + return strconv.FormatInt(int64(randMinMax(r, 0, maxGeneratedVersion)), 10) +} + +func selectRootHashFromConstraints(constraints *generatedConstraints, r *rand.Rand) string { + numberOfContainersInConstraints := len(constraints.containers) + container := constraints.containers[r.Intn(numberOfContainersInConstraints)] + numberOfLayersInContainer := len(container.Layers) + + return container.Layers[r.Intn(numberOfLayersInContainer)] +} + +func generateContainerID(r *rand.Rand) string { + id := atLeastOneAtMost(r, maxGeneratedContainerID) + return strconv.FormatInt(int64(id), 10) +} + +func generateMounts(r *rand.Rand) []mountInternal { + numberOfMounts := atLeastOneAtMost(r, maxGeneratedMounts) + mounts := make([]mountInternal, numberOfMounts) + + for i := 0; i < int(numberOfMounts); i++ { + numberOfOptions := atLeastOneAtMost(r, maxGeneratedMountOptions) + options := make([]string, numberOfOptions) + for j := 0; j < int(numberOfOptions); j++ { + options[j] = randVariableString(r, maxGeneratedMountOptionLength) + } + + sourcePrefix := "" + // select a "source type". our default is "no special prefix" ie a + // "standard source". + prefixType := randMinMax(r, 1, 3) + switch prefixType { + case 2: + // sandbox mount, gets special handling + sourcePrefix = guestpath.SandboxMountPrefix + case 3: + // huge page mount, gets special handling + sourcePrefix = guestpath.HugePagesMountPrefix + } + + source := path.Join(sourcePrefix, randVariableString(r, maxGeneratedMountSourceLength)) + destination := randVariableString(r, maxGeneratedMountDestinationLength) + + mounts[i] = mountInternal{ + Source: source, + Destination: destination, + Options: options, + Type: "bind", + } + } + + return mounts +} + +func generateListOfSignals(r *rand.Rand, atLeast int32, atMost int32) []syscall.Signal { + numSignals := int(atLeastNAtMostM(r, atLeast, atMost)) + signalSet := make(map[syscall.Signal]struct{}) + + for i := 0; i < numSignals; i++ { + signal := generateSignal(r) + signalSet[signal] = struct{}{} + } + + var signals []syscall.Signal + for k := range signalSet { + signals = append(signals, k) + } + + return signals +} + +func generateListOfWindowsSignals(r *rand.Rand, atLeast int32, atMost int32) []guestrequest.SignalValueWCOW { + numSignals := int(atLeastNAtMostM(r, atLeast, atMost)) + signalSet := make(map[string]struct{}) + + for i := 0; i < numSignals; i++ { + signal := randVariableString(r, maxWindowsSignalLength) + signalSet[signal] = struct{}{} + } + + var signals []guestrequest.SignalValueWCOW + for k := range signalSet { + signals = append(signals, guestrequest.SignalValueWCOW(k)) + } + + return signals +} + +func generateWindowsSignal(r *rand.Rand) guestrequest.SignalValueWCOW { + return guestrequest.SignalValueWCOW(randVariableString(r, maxWindowsSignalLength)) +} +func generateSignal(r *rand.Rand) syscall.Signal { + return syscall.Signal(atLeastOneAtMost(r, maxSignalNumber)) +} + +func selectContainerFromContainerList(containers []*securityPolicyContainer, r *rand.Rand) *securityPolicyContainer { + if len(containers) == 0 { + panic("selectContainerFromContainerList: no containers available to select from") + } + return containers[r.Intn(len(containers))] +} + +type dataGenerator struct { + rng *rand.Rand + mountTargets stringSet + containerIDs stringSet + sandboxIDs stringSet + enforcementPoints stringSet + fragmentIssuers stringSet + fragmentFeeds stringSet + fragmentNamespaces stringSet +} + +func newDataGenerator(rng *rand.Rand) *dataGenerator { + return &dataGenerator{ + rng: rng, + mountTargets: make(stringSet), + containerIDs: make(stringSet), + sandboxIDs: make(stringSet), + enforcementPoints: make(stringSet), + fragmentIssuers: make(stringSet), + fragmentFeeds: make(stringSet), + fragmentNamespaces: make(stringSet), + } +} + +func (s *stringSet) randUnique(r *rand.Rand, generator func(*rand.Rand) string) string { + for { + item := generator(r) + if !s.contains(item) { + s.add(item) + return item + } + } +} + +func (gen *dataGenerator) uniqueMountTarget() string { + return gen.mountTargets.randUnique(gen.rng, generateMountTarget) +} + +func (gen *dataGenerator) uniqueContainerID() string { + return gen.containerIDs.randUnique(gen.rng, generateContainerID) +} + +func (gen *dataGenerator) createValidOverlayForContainer(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { + ctx := context.Background() + // storage for our mount paths + overlay := make([]string, len(container.Layers)) + + for i := 0; i < len(container.Layers); i++ { + mount := gen.uniqueMountTarget() + err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) + if err != nil { + return overlay, err + } + + overlay[len(overlay)-i-1] = mount + } + + return overlay, nil +} + +func (gen *dataGenerator) createInvalidOverlayForContainer(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { + method := gen.rng.Intn(3) + switch method { + case 0: + return gen.invalidOverlaySameSizeWrongMounts(enforcer, container) + case 1: + return gen.invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer, container) + default: + return gen.invalidOverlayRandomJunk(enforcer, container) + } +} + +func (gen *dataGenerator) invalidOverlaySameSizeWrongMounts(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { + ctx := context.Background() + // storage for our mount paths + overlay := make([]string, len(container.Layers)) + + for i := 0; i < len(container.Layers); i++ { + mount := gen.uniqueMountTarget() + err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) + if err != nil { + return overlay, err + } + + // generate a random new mount point to cause an error + overlay[len(overlay)-i-1] = gen.uniqueMountTarget() + } + + return overlay, nil +} + +func (gen *dataGenerator) invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { + ctx := context.Background() + if len(container.Layers) == 1 { + // won't work with only 1, we need to bail out to another method + return gen.invalidOverlayRandomJunk(enforcer, container) + } + // storage for our mount paths + var overlay []string + + for i := 0; i < len(container.Layers); i++ { + mount := gen.uniqueMountTarget() + err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) + if err != nil { + return overlay, err + } + + if gen.rng.Intn(10) != 0 { + overlay = append(overlay, mount) + } + } + + return overlay, nil +} + +func (gen *dataGenerator) invalidOverlayRandomJunk(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { + ctx := context.Background() + // create "junk" for entry + layersToCreate := gen.rng.Int31n(maxLayersInGeneratedContainer) + overlay := make([]string, layersToCreate) + + for i := 0; i < int(layersToCreate); i++ { + overlay[i] = gen.uniqueMountTarget() + } + + // setup entirely different and "correct" expected mounting + for i := 0; i < len(container.Layers); i++ { + mount := gen.uniqueMountTarget() + err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) + if err != nil { + return overlay, err + } + } + + return overlay, nil +} + +func randVariableString(r *rand.Rand, maxLen int32) string { + return randString(r, atLeastOneAtMost(r, maxLen)) +} + +func randString(r *rand.Rand, length int32) string { + if length < minStringLength { + length = minStringLength + } + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + sb := strings.Builder{} + sb.Grow(int(length)) + for i := 0; i < (int)(length); i++ { + sb.WriteByte(charset[r.Intn(len(charset))]) + } + + return sb.String() +} + +func randChar(r *rand.Rand) string { + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + return string(charset[r.Intn(len(charset))]) +} + +func randBool(r *rand.Rand) bool { + return randMinMax(r, 0, 1) == 0 +} + +func randMinMax(r *rand.Rand, min int32, max int32) int32 { + return r.Int31n(max-min+1) + min +} + +func atLeastNAtMostM(r *rand.Rand, min, max int32) int32 { + return randMinMax(r, min, max) +} + +func atLeastOneAtMost(r *rand.Rand, most int32) int32 { + return atLeastNAtMostM(r, 1, most) +} + +func atMost(r *rand.Rand, most int32) int32 { + return randMinMax(r, 0, most) +} + +type generatedConstraints struct { + containers []*securityPolicyContainer + externalProcesses []*externalProcess + fragments []*fragment + allowGetProperties bool + allowDumpStacks bool + allowRuntimeLogging bool + allowEnvironmentVariableDropping bool + allowUnencryptedScratch bool + namespace string + svn string + allowCapabilityDropping bool + ctx context.Context +} + +type generatedWindowsConstraints struct { + containers []*securityPolicyWindowsContainer + externalProcesses []*externalProcess + fragments []*fragment + allowGetProperties bool + allowDumpStacks bool + allowRuntimeLogging bool + allowEnvironmentVariableDropping bool + allowUnencryptedScratch bool + namespace string + svn string + allowCapabilityDropping bool + ctx context.Context +} + +func (constraints *generatedWindowsConstraints) toPolicy() *securityPolicyWindowsInternal { + return &securityPolicyWindowsInternal{ + Containers: constraints.containers, + ExternalProcesses: constraints.externalProcesses, + Fragments: constraints.fragments, + AllowPropertiesAccess: constraints.allowGetProperties, + AllowDumpStacks: constraints.allowDumpStacks, + AllowRuntimeLogging: constraints.allowRuntimeLogging, + AllowEnvironmentVariableDropping: constraints.allowEnvironmentVariableDropping, + AllowUnencryptedScratch: constraints.allowUnencryptedScratch, + AllowCapabilityDropping: constraints.allowCapabilityDropping, + } +} + +func (constraints *generatedWindowsConstraints) toFragment() *securityPolicyFragment { + // Convert Windows containers to regular containers for fragment compatibility + linuxContainers := make([]*securityPolicyContainer, len(constraints.containers)) + for i, winContainer := range constraints.containers { + // This is a placeholder conversion - you may need to implement proper conversion + linuxContainers[i] = &securityPolicyContainer{ + Command: winContainer.Command, + EnvRules: winContainer.EnvRules, + WorkingDir: winContainer.WorkingDir, + Layers: winContainer.Layers, + // Note: Some Windows-specific fields may not have Linux equivalents + } + } + + return &securityPolicyFragment{ + Namespace: constraints.namespace, + SVN: constraints.svn, + Containers: linuxContainers, + ExternalProcesses: constraints.externalProcesses, + Fragments: constraints.fragments, + } +} + +func generateWindowsConstraints(r *rand.Rand, maxContainers int32) *generatedWindowsConstraints { + var containers []*securityPolicyWindowsContainer + + numContainers := (int)(atLeastOneAtMost(r, maxContainers)) + for i := 0; i < numContainers; i++ { + containers = append(containers, generateConstraintsWindowsContainer(r, 1, maxLayersInGeneratedContainer)) + } + + return &generatedWindowsConstraints{ + containers: containers, + externalProcesses: make([]*externalProcess, 0), + fragments: make([]*fragment, 0), + allowGetProperties: randBool(r), + allowDumpStacks: randBool(r), + allowRuntimeLogging: false, + allowEnvironmentVariableDropping: false, + allowUnencryptedScratch: false, + allowCapabilityDropping: false, + namespace: generateFragmentNamespace(r), + svn: generateSVN(r), + ctx: context.Background(), + } +} + +func (*generatedWindowsConstraints) Generate(r *rand.Rand, _ int) reflect.Value { + c := generateWindowsConstraints(r, maxContainersInGeneratedConstraints) + return reflect.ValueOf(c) +} + +type containerInitProcess struct { + Command []string + EnvRules []EnvRuleConfig + WorkingDir string + AllowStdioAccess bool +} diff --git a/pkg/securitypolicy/regopolicy_test.go b/pkg/securitypolicy/regopolicy_linux_test.go similarity index 80% rename from pkg/securitypolicy/regopolicy_test.go rename to pkg/securitypolicy/regopolicy_linux_test.go index 61953966d3..0e782724da 100644 --- a/pkg/securitypolicy/regopolicy_test.go +++ b/pkg/securitypolicy/regopolicy_linux_test.go @@ -5,71 +5,23 @@ package securitypolicy import ( "context" - _ "embed" "encoding/json" "fmt" "math/rand" "os" "path/filepath" "slices" - "sort" "strconv" "strings" "syscall" "testing" "testing/quick" - "github.com/Microsoft/hcsshim/internal/guestpath" rpi "github.com/Microsoft/hcsshim/internal/regopolicyinterpreter" - "github.com/blang/semver/v4" - "github.com/open-policy-agent/opa/rego" oci "github.com/opencontainers/runtime-spec/specs-go" - "github.com/pkg/errors" ) -const ( - // variables that influence generated rego-only test fixtures - maxDiffLength = 64 - maxExternalProcessesInGeneratedConstraints = 16 - maxFragmentsInGeneratedConstraints = 4 - maxGeneratedExternalProcesses = 12 - maxGeneratedSandboxIDLength = 32 - maxGeneratedEnforcementPointLength = 64 - maxGeneratedPlan9Mounts = 8 - maxGeneratedFragmentFeedLength = 256 - maxGeneratedFragmentIssuerLength = 16 - maxPlan9MountTargetLength = 64 - maxPlan9MountIndex = 16 -) - -func Test_RegoTemplates(t *testing.T) { - query := rego.New( - rego.Query("data.api"), - rego.Module("api.rego", APICode)) - - ctx := context.Background() - resultSet, err := query.Eval(ctx) - if err != nil { - t.Fatalf("unable to query API enforcement points: %s", err) - } - - apiRules := resultSet[0].Expressions[0].Value.(map[string]interface{}) - enforcementPoints := apiRules["enforcement_points"].(map[string]interface{}) - - policyCode := strings.Replace(policyRegoTemplate, "@@OBJECTS@@", "", 1) - policyCode = strings.Replace(policyCode, "@@API_VERSION@@", apiVersion, 1) - policyCode = strings.Replace(policyCode, "@@FRAMEWORK_VERSION@@", frameworkVersion, 1) - - err = verifyPolicyRules(apiVersion, enforcementPoints, policyCode) - if err != nil { - t.Errorf("Policy Rego Template is invalid: %s", err) - } - - err = verifyPolicyRules(apiVersion, enforcementPoints, openDoorRego) - if err != nil { - t.Errorf("Open Door Rego Template is invalid: %s", err) - } -} +const testOSType = "linux" func Test_MarshalRego_Policy(t *testing.T) { f := func(p *generatedConstraints) bool { @@ -149,7 +101,8 @@ func Test_MarshalRego_Policy(t *testing.T) { return false } - _, err = newRegoPolicy(expected, defaultMounts, privilegedMounts) + _, err = newRegoPolicy(expected, defaultMounts, privilegedMounts, testOSType) + if err != nil { t.Errorf("unable to convert policy to rego: %v", err) return false @@ -235,7 +188,8 @@ func Test_MarshalRego_Fragment(t *testing.T) { func Test_Rego_EnforceDeviceMountPolicy_No_Matches(t *testing.T) { f := func(p *generatedConstraints) bool { securityPolicy := p.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Errorf("unable to convert policy to rego: %v", err) return false @@ -260,7 +214,8 @@ func Test_Rego_EnforceDeviceMountPolicy_No_Matches(t *testing.T) { func Test_Rego_EnforceDeviceMountPolicy_Matches(t *testing.T) { f := func(p *generatedConstraints) bool { securityPolicy := p.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Errorf("unable to convert policy to rego: %v", err) return false @@ -283,7 +238,8 @@ func Test_Rego_EnforceDeviceMountPolicy_Matches(t *testing.T) { func Test_Rego_EnforceDeviceUmountPolicy_Removes_Device_Entries(t *testing.T) { f := func(p *generatedConstraints) bool { securityPolicy := p.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Error(err) return false @@ -321,7 +277,8 @@ func Test_Rego_EnforceDeviceUmountPolicy_Removes_Device_Entries(t *testing.T) { func Test_Rego_EnforceDeviceMountPolicy_Duplicate_Device_Target(t *testing.T) { f := func(p *generatedConstraints) bool { securityPolicy := p.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Errorf("unable to convert policy to rego: %v", err) return false @@ -416,7 +373,8 @@ func Test_Rego_EnforceOverlayMountPolicy_Layers_With_Same_Root_Hash(t *testing.T constraints.containers = []*securityPolicyContainer{container} constraints.externalProcesses = generateExternalProcesses(testRand) securityPolicy := constraints.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatal("Unable to create security policy") } @@ -452,7 +410,8 @@ func Test_Rego_EnforceOverlayMountPolicy_Layers_Shared_Layers(t *testing.T) { constraints.externalProcesses = generateExternalProcesses(testRand) securityPolicy := constraints.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatal("Unable to create security policy") } @@ -562,7 +521,7 @@ func Test_Rego_EnforceOverlayMountPolicy_Reusing_ID_Across_Overlays(t *testing.T policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), testOSType) if err != nil { t.Fatal(err) } @@ -614,7 +573,8 @@ func Test_Rego_EnforceOverlayMountPolicy_Multiple_Instances_Same_Container(t *te } securityPolicy := constraints.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("failed create enforcer") } @@ -921,7 +881,7 @@ func Test_Rego_EnforceCreateContainer(t *testing.T) { t.Error(err) return false } - + //t.Logf("Policy: %s", tc.policy.base64policy) _, _, _, err = tc.policy.EnforceCreateContainerPolicy(p.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 @@ -941,7 +901,7 @@ func Test_Rego_EnforceCreateContainer_Start_All_Containers(t *testing.T) { policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), testOSType) if err != nil { t.Error(err) return false @@ -1821,7 +1781,8 @@ func Test_Rego_MountPolicy_MountPrivilegedWhenNotAllowed(t *testing.T) { func Test_Rego_Version_Unregistered_Enforcement_Point(t *testing.T) { gc := generateConstraints(testRand, maxContainersInGeneratedConstraints) securityPolicy := gc.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create a new Rego policy: %v", err) } @@ -1842,7 +1803,8 @@ func Test_Rego_Version_Unregistered_Enforcement_Point(t *testing.T) { func Test_Rego_Version_Future_Enforcement_Point(t *testing.T) { gc := generateConstraints(testRand, maxContainersInGeneratedConstraints) securityPolicy := gc.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create a new Rego policy: %v", err) } @@ -1871,7 +1833,8 @@ func Test_Rego_Version_Future_Enforcement_Point(t *testing.T) { // by their respective version information. func Test_Rego_Version_Unavailable_Enforcement_Point(t *testing.T) { code := "package policy\n\napi_version := \"0.0.1\"" - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create a new Rego policy: %v", err) } @@ -1904,7 +1867,8 @@ func Test_Rego_Version_Unavailable_Enforcement_Point(t *testing.T) { func Test_Rego_Enforcement_Point_Allowed(t *testing.T) { code := "package policy\n\napi_version := \"0.0.1\"" - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create a new Rego policy: %v", err) } @@ -1955,7 +1919,8 @@ api_version := "0.0.1" __fixture_for_allowed_extra__ := {"allowed": true} ` - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create a new Rego policy: %v", err) } @@ -1992,7 +1957,8 @@ __fixture_for_allowed_extra__ := {"allowed": true} func Test_Rego_No_API_Version(t *testing.T) { code := "package policy" - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create a new Rego policy: %v", err) } @@ -2475,7 +2441,8 @@ exec_external := { strings.Join(generateEnvs(envSet), `","`), strings.Join(generateEnvs(envSet), `","`)) - policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Errorf("error creating policy: %v", err) return false @@ -2532,7 +2499,8 @@ func Test_Rego_InvalidEnvList(t *testing.T) { "env_list": true }`, apiVersion, frameworkVersion) - policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("error creating policy: %v", err) } @@ -2581,7 +2549,8 @@ func Test_Rego_InvalidEnvList_Member(t *testing.T) { "env_list": ["one", ["two"], "three"] }`, apiVersion, frameworkVersion) - policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("error creating policy: %v", err) } @@ -2838,7 +2807,8 @@ func Test_Rego_ExecExternalProcessPolicy_DropEnvs_Multiple(t *testing.T) { policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), + testOSType) if err != nil { t.Fatal(err) } @@ -2882,7 +2852,8 @@ func Test_Rego_ExecExternalProcessPolicy_DropEnvs_Multiple_NoMatch(t *testing.T) policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), + testOSType) if err != nil { t.Fatal(err) } @@ -3838,7 +3809,7 @@ func Test_Rego_LoadFragment_SemverVersion(t *testing.T) { defaultMounts := toOCIMounts(generateMounts(testRand)) privilegedMounts := toOCIMounts(generateMounts(testRand)) - policy, err := newRegoPolicy(securityPolicy.marshalRego(), defaultMounts, privilegedMounts) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), defaultMounts, privilegedMounts, testOSType) if err != nil { t.Fatalf("error compiling policy: %v", err) @@ -4201,7 +4172,8 @@ load_fragment := {"allowed": true, "add_module": true} { mount_device := data.fragment.mount_device `, apiVersion, frameworkVersion, issuer, feed) - policy, err := newRegoPolicy(policyCode, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(policyCode, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create Rego policy: %v", err) } @@ -4483,7 +4455,8 @@ func Test_Rego_ExecExternal_StdioAccess_NotAllowed(t *testing.T) { gc.externalProcesses = append(gc.externalProcesses, gc.externalProcesses[0].clone()) gc.externalProcesses[0].allowStdioAccess = !gc.externalProcesses[0].allowStdioAccess - policy, err := newRegoPolicy(gc.toPolicy().marshalRego(), []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(gc.toPolicy().marshalRego(), []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("error marshaling policy: %v", err) } @@ -4893,7 +4866,8 @@ func Test_Rego_MissingEnvList(t *testing.T) { exec_external := {"allowed": true} `, apiVersion) - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("error compiling the rego policy: %v", err) } @@ -5046,7 +5020,8 @@ func Test_Rego_ExecExternalProcessPolicy_ConflictingAllowStdioAccessHasErrorMess policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), + testOSType) if err != nil { t.Fatal(err) } @@ -5163,7 +5138,8 @@ func Test_Rego_ExecExternalProcessPolicy_RequiredEnvMissingHasErrorMessage(t *te policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), + testOSType) if err != nil { t.Fatal(err) } @@ -5639,7 +5615,8 @@ func Test_Rego_FrameworkSVN(t *testing.T) { policy, err := newRegoPolicy(code, toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), + testOSType) if err != nil { t.Fatalf("unable to create policy: %v", err) } @@ -5669,7 +5646,7 @@ func Test_Rego_Fragment_FrameworkSVN(t *testing.T) { defaultMounts := toOCIMounts(generateMounts(testRand)) privilegedMounts := toOCIMounts(generateMounts(testRand)) - policy, err := newRegoPolicy(securityPolicy.marshalRego(), defaultMounts, privilegedMounts) + policy, err := newRegoPolicy(securityPolicy.marshalRego(), defaultMounts, privilegedMounts, testOSType) if err != nil { t.Fatalf("error compiling policy: %v", err) @@ -5717,7 +5694,8 @@ func Test_Rego_APISVN(t *testing.T) { policy, err := newRegoPolicy(code, toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) + toOCIMounts(privilegedMounts), + testOSType) if err != nil { t.Fatalf("unable to create policy: %v", err) } @@ -5744,7 +5722,8 @@ func Test_Rego_NoReason(t *testing.T) { mount_device := {"allowed": false} ` - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create policy: %v", err) } @@ -5837,7 +5816,8 @@ func Test_Rego_ErrorTruncation_CustomPolicy(t *testing.T) { reason := {"custom_error": "%s"} `, randString(testRand, 2048)) - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create policy: %v", err) } @@ -5865,7 +5845,8 @@ func Test_Rego_Missing_Enforcement_Point(t *testing.T) { reason := {"errors": data.framework.errors} ` - policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}) + policy, err := newRegoPolicy(code, []oci.Mount{}, []oci.Mount{}, testOSType) + if err != nil { t.Fatalf("unable to create policy: %v", err) } @@ -5982,7 +5963,7 @@ func Test_Rego_GetUserInfo_WithEtcPasswdAndGroup(t *testing.T) { t.Fatalf("Failed to write /etc/group: %v", err) } - regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}) + regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}, testOSType) if err != nil { t.Errorf("cannot compile open door rego policy: %v", err) return @@ -6135,7 +6116,7 @@ func Test_Rego_GetUserInfo_WithEtcPasswdAndGroup(t *testing.T) { func Test_Rego_GetUserInfo_NoEtc(t *testing.T) { testDir := t.TempDir() - regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}) + regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}, testOSType) if err != nil { t.Errorf("cannot compile open door rego policy: %v", err) return @@ -6216,7 +6197,7 @@ func Test_Rego_GetUserInfo_EtcPasswdOnly(t *testing.T) { t.Fatalf("Failed to write /etc/passwd: %v", err) } - regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}) + regoEnforcer, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}, testOSType) if err != nil { t.Errorf("cannot compile open door rego policy: %v", err) return @@ -6268,1673 +6249,6 @@ func Test_Rego_GetUserInfo_EtcPasswdOnly(t *testing.T) { } } -// -// Setup and "fixtures" follow... -// - -func generateExternalProcesses(r *rand.Rand) []*externalProcess { - var processes []*externalProcess - - numProcesses := atLeastOneAtMost(r, maxExternalProcessesInGeneratedConstraints) - for i := 0; i < int(numProcesses); i++ { - processes = append(processes, generateExternalProcess(r)) - } - - return processes -} - -func generateExternalProcess(r *rand.Rand) *externalProcess { - return &externalProcess{ - command: generateCommand(r), - envRules: generateEnvironmentVariableRules(r), - workingDir: generateWorkingDir(r), - allowStdioAccess: randBool(r), - } -} - -func randChoices(r *rand.Rand, numChoices int, numItems int) []int { - shuffle := r.Perm(numItems) - if numChoices > numItems { - return shuffle - } - - return shuffle[:numChoices] -} - -func randChoicesWithReplacement(r *rand.Rand, numChoices int, numItems int) []int { - choices := make([]int, numChoices) - for i := 0; i < numChoices; i++ { - choices[i] = r.Intn(numItems) - } - - return choices -} - -func randChooseStrings(r *rand.Rand, items []string, numChoices int) []string { - numItems := len(items) - choiceIndices := randChoices(r, numChoices, numItems) - choices := make([]string, numChoices) - for i, index := range choiceIndices { - choices[i] = items[index] - } - return choices -} - -func randChooseStringsWithReplacement(r *rand.Rand, items []string, numChoices int) []string { - numItems := len(items) - choiceIndices := randChoicesWithReplacement(r, numChoices, numItems) - choices := make([]string, numChoices) - for i, index := range choiceIndices { - choices[i] = items[index] - } - return choices -} - -func selectExternalProcessFromConstraints(constraints *generatedConstraints, r *rand.Rand) *externalProcess { - numberOfProcessesInConstraints := len(constraints.externalProcesses) - return constraints.externalProcesses[r.Intn(numberOfProcessesInConstraints)] -} - -func (constraints *generatedConstraints) toPolicy() *securityPolicyInternal { - return &securityPolicyInternal{ - Containers: constraints.containers, - ExternalProcesses: constraints.externalProcesses, - Fragments: constraints.fragments, - AllowPropertiesAccess: constraints.allowGetProperties, - AllowDumpStacks: constraints.allowDumpStacks, - AllowRuntimeLogging: constraints.allowRuntimeLogging, - AllowEnvironmentVariableDropping: constraints.allowEnvironmentVariableDropping, - AllowUnencryptedScratch: constraints.allowUnencryptedScratch, - AllowCapabilityDropping: constraints.allowCapabilityDropping, - } -} - -func (constraints *generatedConstraints) toFragment() *securityPolicyFragment { - return &securityPolicyFragment{ - Namespace: constraints.namespace, - SVN: constraints.svn, - Containers: constraints.containers, - ExternalProcesses: constraints.externalProcesses, - Fragments: constraints.fragments, - } -} - -func toOCIMounts(mounts []mountInternal) []oci.Mount { - result := make([]oci.Mount, len(mounts)) - for i, mount := range mounts { - result[i] = oci.Mount{ - Source: mount.Source, - Destination: mount.Destination, - Options: mount.Options, - Type: mount.Type, - } - } - return result -} - -/** - * NOTE_TESTCOPY: the following "copy*" functions are provided to ensure that - * everything passed to the policy is a new object which will not be shared in - * any way with other policy objects in other tests. In any additional fixture - * setup routines these functions (or others like them) should be used. - */ - -func copyStrings(values []string) []string { - valuesCopy := make([]string, len(values)) - copy(valuesCopy, values) - return valuesCopy -} - -func copyMounts(mounts []oci.Mount) []oci.Mount { - bytes, err := json.Marshal(mounts) - if err != nil { - panic(err) - } - - mountsCopy := make([]oci.Mount, len(mounts)) - err = json.Unmarshal(bytes, &mountsCopy) - if err != nil { - panic(err) - } - - return mountsCopy -} - -func copyMountsInternal(mounts []mountInternal) []mountInternal { - var mountsCopy []mountInternal - - for _, in := range mounts { - out := mountInternal{ - Source: in.Source, - Destination: in.Destination, - Type: in.Type, - Options: copyStrings(in.Options), - } - - mountsCopy = append(mountsCopy, out) - } - - return mountsCopy -} - -func copyLinuxCapabilities(caps oci.LinuxCapabilities) oci.LinuxCapabilities { - bytes, err := json.Marshal(caps) - if err != nil { - panic(err) - } - - capsCopy := oci.LinuxCapabilities{} - err = json.Unmarshal(bytes, &capsCopy) - if err != nil { - panic(err) - } - - return capsCopy -} - -func copyLinuxSeccomp(seccomp oci.LinuxSeccomp) oci.LinuxSeccomp { - bytes, err := json.Marshal(seccomp) - if err != nil { - panic(err) - } - - seccompCopy := oci.LinuxSeccomp{} - err = json.Unmarshal(bytes, &seccompCopy) - if err != nil { - panic(err) - } - - return seccompCopy -} - -type regoOverlayTestConfig struct { - layers []string - containerID string - policy *regoEnforcer -} - -func setupRegoOverlayTest(gc *generatedConstraints, valid bool) (tc *regoOverlayTestConfig, err error) { - securityPolicy := gc.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) - if err != nil { - return nil, err - } - - containerID := testDataGenerator.uniqueContainerID() - c := selectContainerFromContainerList(gc.containers, testRand) - - var layerPaths []string - if valid { - layerPaths, err = testDataGenerator.createValidOverlayForContainer(policy, c) - if err != nil { - return nil, fmt.Errorf("error creating valid overlay: %w", err) - } - } else { - layerPaths, err = testDataGenerator.createInvalidOverlayForContainer(policy, c) - if err != nil { - return nil, fmt.Errorf("error creating invalid overlay: %w", err) - } - } - - // see NOTE_TESTCOPY - return ®oOverlayTestConfig{ - layers: copyStrings(layerPaths), - containerID: containerID, - policy: policy, - }, nil -} - -type regoContainerTestConfig struct { - envList []string - argList []string - workingDir string - containerID string - sandboxID string - mounts []oci.Mount - noNewPrivileges bool - user IDName - groups []IDName - umask string - capabilities *oci.LinuxCapabilities - seccomp string - policy *regoEnforcer - ctx context.Context -} - -func setupSimpleRegoCreateContainerTest(gc *generatedConstraints) (tc *regoContainerTestConfig, err error) { - c := selectContainerFromContainerList(gc.containers, testRand) - return setupRegoCreateContainerTest(gc, c, false) -} - -func setupRegoPrivilegedMountTest(gc *generatedConstraints) (tc *regoContainerTestConfig, err error) { - c := selectContainerFromContainerList(gc.containers, testRand) - return setupRegoCreateContainerTest(gc, c, true) -} - -func setupRegoCreateContainerTest(gc *generatedConstraints, testContainer *securityPolicyContainer, privilegedError bool) (tc *regoContainerTestConfig, err error) { - securityPolicy := gc.toPolicy() - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - - policy, err := newRegoPolicy(securityPolicy.marshalRego(), - toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) - if err != nil { - return nil, err - } - - containerID, err := mountImageForContainer(policy, testContainer) - if err != nil { - return nil, err - } - - envList := buildEnvironmentVariablesFromEnvRules(testContainer.EnvRules, testRand) - sandboxID := testDataGenerator.uniqueSandboxID() - - mounts := testContainer.Mounts - mounts = append(mounts, defaultMounts...) - if privilegedError { - testContainer.AllowElevated = false - } - - if testContainer.AllowElevated || privilegedError { - mounts = append(mounts, privilegedMounts...) - } - mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) - - user := IDName{} - if testContainer.User.UserIDName.Strategy != IDNameStrategyRegex { - user = buildIDNameFromConfig(testContainer.User.UserIDName, testRand) - } - groups := buildGroupIDNamesFromUser(testContainer.User, testRand) - umask := testContainer.User.Umask - - var capabilities *oci.LinuxCapabilities - if testContainer.Capabilities != nil { - capsExternal := copyLinuxCapabilities(testContainer.Capabilities.toExternal()) - capabilities = &capsExternal - } else { - capabilities = nil - } - seccomp := testContainer.SeccompProfileSHA256 - - // see NOTE_TESTCOPY - return ®oContainerTestConfig{ - envList: copyStrings(envList), - argList: copyStrings(testContainer.Command), - workingDir: testContainer.WorkingDir, - containerID: containerID, - sandboxID: sandboxID, - mounts: copyMounts(mountSpec.Mounts), - noNewPrivileges: testContainer.NoNewPrivileges, - user: user, - groups: groups, - umask: umask, - capabilities: capabilities, - seccomp: seccomp, - policy: policy, - ctx: gc.ctx, - }, nil -} - -func setupRegoRunningContainerTest(gc *generatedConstraints, privileged bool) (tc *regoRunningContainerTestConfig, err error) { - securityPolicy := gc.toPolicy() - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - - policy, err := newRegoPolicy(securityPolicy.marshalRego(), - toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) - if err != nil { - return nil, err - } - - var runningContainers []regoRunningContainer - numOfRunningContainers := int(atLeastOneAtMost(testRand, int32(len(gc.containers)))) - containersToRun := randChoicesWithReplacement(testRand, numOfRunningContainers, len(gc.containers)) - for _, i := range containersToRun { - containerToStart := gc.containers[i] - r, err := runContainer(policy, containerToStart, defaultMounts, privilegedMounts, privileged) - if err != nil { - return nil, err - } - runningContainers = append(runningContainers, *r) - } - - return ®oRunningContainerTestConfig{ - runningContainers: runningContainers, - policy: policy, - defaultMounts: copyMountsInternal(defaultMounts), - privilegedMounts: copyMountsInternal(privilegedMounts), - }, nil -} - -func runContainer(enforcer *regoEnforcer, container *securityPolicyContainer, defaultMounts []mountInternal, privilegedMounts []mountInternal, privileged bool) (*regoRunningContainer, error) { - ctx := context.Background() - containerID, err := mountImageForContainer(enforcer, container) - if err != nil { - return nil, err - } - - envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) - user := buildIDNameFromConfig(container.User.UserIDName, testRand) - groups := buildGroupIDNamesFromUser(container.User, testRand) - umask := container.User.Umask - sandboxID := generateSandboxID(testRand) - - mounts := container.Mounts - mounts = append(mounts, defaultMounts...) - if container.AllowElevated { - mounts = append(mounts, privilegedMounts...) - } - mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) - var capabilities oci.LinuxCapabilities - if container.Capabilities == nil { - if privileged { - capabilities = capabilitiesInternal{ - Bounding: DefaultPrivilegedCapabilities(), - Inheritable: DefaultPrivilegedCapabilities(), - Effective: DefaultPrivilegedCapabilities(), - Permitted: DefaultPrivilegedCapabilities(), - Ambient: []string{}, - }.toExternal() - } else { - capabilities = capabilitiesInternal{ - Bounding: DefaultUnprivilegedCapabilities(), - Inheritable: []string{}, - Effective: DefaultUnprivilegedCapabilities(), - Permitted: DefaultUnprivilegedCapabilities(), - Ambient: []string{}, - }.toExternal() - } - } else { - capabilities = container.Capabilities.toExternal() - } - seccomp := container.SeccompProfileSHA256 - - _, _, _, err = enforcer.EnforceCreateContainerPolicy(ctx, sandboxID, containerID, container.Command, envList, container.WorkingDir, mountSpec.Mounts, privileged, container.NoNewPrivileges, user, groups, umask, &capabilities, seccomp) - if err != nil { - return nil, err - } - - return ®oRunningContainer{ - container: container, - envList: envList, - containerID: containerID, - }, nil -} - -type regoRunningContainerTestConfig struct { - runningContainers []regoRunningContainer - policy *regoEnforcer - defaultMounts []mountInternal - privilegedMounts []mountInternal -} - -type regoRunningContainer struct { - container *securityPolicyContainer - envList []string - containerID string -} - -func setupExternalProcessTest(gc *generatedConstraints) (tc *regoExternalPolicyTestConfig, err error) { - gc.externalProcesses = generateExternalProcesses(testRand) - securityPolicy := gc.toPolicy() - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - - policy, err := newRegoPolicy(securityPolicy.marshalRego(), - toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) - if err != nil { - return nil, err - } - - return ®oExternalPolicyTestConfig{ - policy: policy, - }, nil -} - -type regoExternalPolicyTestConfig struct { - policy *regoEnforcer -} - -func setupPlan9MountTest(gc *generatedConstraints) (tc *regoPlan9MountTestConfig, err error) { - securityPolicy := gc.toPolicy() - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - - testContainer := selectContainerFromContainerList(gc.containers, testRand) - mountIndex := atMost(testRand, int32(len(testContainer.Mounts)-1)) - testMount := &testContainer.Mounts[mountIndex] - testMount.Source = plan9Prefix - testMount.Type = "secret" - - policy, err := newRegoPolicy(securityPolicy.marshalRego(), - toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) - if err != nil { - return nil, err - } - - containerID, err := mountImageForContainer(policy, testContainer) - if err != nil { - return nil, err - } - - uvmPathForShare := generateUVMPathForShare(testRand, containerID) - - envList := buildEnvironmentVariablesFromEnvRules(testContainer.EnvRules, testRand) - sandboxID := testDataGenerator.uniqueSandboxID() - - mounts := testContainer.Mounts - mounts = append(mounts, defaultMounts...) - - if testContainer.AllowElevated { - mounts = append(mounts, privilegedMounts...) - } - mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) - mountSpec.Mounts = append(mountSpec.Mounts, oci.Mount{ - Source: uvmPathForShare, - Destination: testMount.Destination, - Options: testMount.Options, - Type: testMount.Type, - }) - - user := buildIDNameFromConfig(testContainer.User.UserIDName, testRand) - groups := buildGroupIDNamesFromUser(testContainer.User, testRand) - umask := testContainer.User.Umask - - capabilities := testContainer.Capabilities.toExternal() - seccomp := testContainer.SeccompProfileSHA256 - - // see NOTE_TESTCOPY - return ®oPlan9MountTestConfig{ - envList: copyStrings(envList), - argList: copyStrings(testContainer.Command), - workingDir: testContainer.WorkingDir, - containerID: containerID, - sandboxID: sandboxID, - mounts: copyMounts(mountSpec.Mounts), - noNewPrivileges: testContainer.NoNewPrivileges, - user: user, - groups: groups, - umask: umask, - uvmPathForShare: uvmPathForShare, - policy: policy, - capabilities: &capabilities, - seccomp: seccomp, - }, nil -} - -type regoPlan9MountTestConfig struct { - envList []string - argList []string - workingDir string - containerID string - sandboxID string - mounts []oci.Mount - uvmPathForShare string - noNewPrivileges bool - user IDName - groups []IDName - umask string - policy *regoEnforcer - capabilities *oci.LinuxCapabilities - seccomp string -} - -func setupGetPropertiesTest(gc *generatedConstraints, allowPropertiesAccess bool) (tc *regoGetPropertiesTestConfig, err error) { - gc.allowGetProperties = allowPropertiesAccess - - securityPolicy := gc.toPolicy() - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - - policy, err := newRegoPolicy(securityPolicy.marshalRego(), - toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) - if err != nil { - return nil, err - } - - return ®oGetPropertiesTestConfig{ - policy: policy, - }, nil -} - -type regoGetPropertiesTestConfig struct { - policy *regoEnforcer -} - -func setupDumpStacksTest(constraints *generatedConstraints, allowDumpStacks bool) (tc *regoGetPropertiesTestConfig, err error) { - constraints.allowDumpStacks = allowDumpStacks - - securityPolicy := constraints.toPolicy() - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - - policy, err := newRegoPolicy(securityPolicy.marshalRego(), - toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) - if err != nil { - return nil, err - } - - return ®oGetPropertiesTestConfig{ - policy: policy, - }, nil -} - -type regoDumpStacksTestConfig struct { - policy *regoEnforcer -} - -func mountImageForContainer(policy *regoEnforcer, container *securityPolicyContainer) (string, error) { - ctx := context.Background() - containerID := testDataGenerator.uniqueContainerID() - - layerPaths, err := testDataGenerator.createValidOverlayForContainer(policy, container) - if err != nil { - return "", fmt.Errorf("error creating valid overlay: %w", err) - } - - // see NOTE_TESTCOPY - err = policy.EnforceOverlayMountPolicy(ctx, containerID, copyStrings(layerPaths), testDataGenerator.uniqueMountTarget()) - if err != nil { - return "", fmt.Errorf("error mounting filesystem: %w", err) - } - - return containerID, nil -} - -type regoPolicyOnlyTestConfig struct { - policy *regoEnforcer -} - -func setupRegoPolicyOnlyTest(gc *generatedConstraints) (tc *regoPolicyOnlyTestConfig, err error) { - securityPolicy := gc.toPolicy() - policy, err := newRegoPolicy(securityPolicy.marshalRego(), []oci.Mount{}, []oci.Mount{}) - if err != nil { - return nil, err - } - - // see NOTE_TESTCOPY - return ®oPolicyOnlyTestConfig{ - policy: policy, - }, nil -} - -type regoFragmentTestConfig struct { - fragments []*regoFragment - containers []*regoFragmentContainer - externalProcesses []*externalProcess - subFragments []*regoFragment - plan9Mounts []string - mountSpec []string - policy *regoEnforcer -} - -type regoFragmentContainer struct { - container *securityPolicyContainer - envList []string - sandboxID string - mounts []oci.Mount - user IDName - groups []IDName - capabilities *oci.LinuxCapabilities - seccomp string -} - -func setupSimpleRegoFragmentTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { - return setupRegoFragmentTestConfig(gc, 1, []string{"containers"}, []string{}, false, false, false, false) -} - -func setupRegoFragmentTestConfigWithIncludes(gc *generatedConstraints, includes []string) (*regoFragmentTestConfig, error) { - return setupRegoFragmentTestConfig(gc, 1, includes, []string{}, false, false, false, false) -} - -func setupRegoFragmentTestConfigWithExcludes(gc *generatedConstraints, excludes []string) (*regoFragmentTestConfig, error) { - return setupRegoFragmentTestConfig(gc, 1, []string{}, excludes, false, false, false, false) -} - -func setupRegoFragmentSVNErrorTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { - return setupRegoFragmentTestConfig(gc, 1, []string{"containers"}, []string{}, true, false, false, false) -} - -func setupRegoSubfragmentSVNErrorTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { - return setupRegoFragmentTestConfig(gc, 1, []string{"fragments"}, []string{}, true, false, false, false) -} - -func setupRegoFragmentTwoFeedTestConfig(gc *generatedConstraints, sameIssuer bool, sameFeed bool) (*regoFragmentTestConfig, error) { - return setupRegoFragmentTestConfig(gc, 2, []string{"containers"}, []string{}, false, sameIssuer, sameFeed, false) -} - -func setupRegoFragmentSVNMismatchTestConfig(gc *generatedConstraints) (*regoFragmentTestConfig, error) { - return setupRegoFragmentTestConfig(gc, 2, []string{"containers"}, []string{}, false, false, false, true) -} - -func compareSVNs(lhs string, rhs string) int { - lhs_int, err := strconv.Atoi(lhs) - if err == nil { - rhs_int, err := strconv.Atoi(rhs) - if err == nil { - return lhs_int - rhs_int - } - } - - panic("unable to compare SVNs") -} - -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)) - - if sameIssuer { - for _, fragment := range gc.fragments { - fragment.issuer = gc.fragments[0].issuer - if sameFeed { - fragment.feed = gc.fragments[0].feed - } - } - } - - subSVNError := svnError - if len(includes) > 0 && includes[0] == "fragments" { - svnError = false - } - fragments := selectFragmentsFromConstraints(gc, numFragments, includes, excludes, svnError, frameworkVersion, svnMismatch) - - containers := make([]*regoFragmentContainer, numFragments) - subFragments := make([]*regoFragment, numFragments) - 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, - } - - for _, include := range fragment.info.includes { - switch include { - case "fragments": - subFragments[i] = selectFragmentsFromConstraints(fragment.constraints, 1, []string{"containers"}, []string{}, subSVNError, frameworkVersion, false)[0] - break - - case "external_processes": - externalProcesses[i] = selectExternalProcessFromConstraints(fragment.constraints, testRand) - break - } - } - - // now that we've explicitly added the excluded items to the fragment - // we remove the include string so that the generated policy - // does not include them. - fragment.info.includes = removeStringsFromArray(fragment.info.includes, excludes) - - code := fragment.constraints.toFragment().marshalRego() - fragment.code = setFrameworkVersion(code, frameworkVersion) - } - - if sameFeed { - includeSet := make(map[string]bool) - minSVN := strconv.Itoa(maxGeneratedVersion) - for _, fragment := range gc.fragments { - svn := fragment.minimumSVN - if compareSVNs(svn, minSVN) < 0 { - minSVN = svn - } - for _, include := range fragment.includes { - includeSet[include] = true - } - } - frag := gc.fragments[0] - frag.minimumSVN = minSVN - frag.includes = make([]string, 0, len(includeSet)) - for include := range includeSet { - frag.includes = append(frag.includes, include) - } - - gc.fragments = []*fragment{frag} - - } - - securityPolicy := gc.toPolicy() - defaultMounts := toOCIMounts(generateMounts(testRand)) - privilegedMounts := toOCIMounts(generateMounts(testRand)) - policy, err := newRegoPolicy(securityPolicy.marshalRego(), defaultMounts, privilegedMounts) - - if err != nil { - return nil, err - } - - return ®oFragmentTestConfig{ - fragments: fragments, - containers: containers, - subFragments: subFragments, - externalProcesses: externalProcesses, - plan9Mounts: plan9Mounts, - policy: policy, - }, nil -} - -type regoDropEnvsTestConfig struct { - envList []string - expected []string - argList []string - workingDir string - containerID string - sandboxID string - mounts []oci.Mount - policy *regoEnforcer - capabilities oci.LinuxCapabilities -} - -func setupEnvRuleSets(count int) [][]EnvRuleConfig { - numEnvRules := []int{int(randMinMax(testRand, 1, 4)), - int(randMinMax(testRand, 1, 4)), - int(randMinMax(testRand, 1, 4))} - envRuleLookup := make(stringSet) - envRules := make([][]EnvRuleConfig, count) - - for i := 0; i < count; i++ { - rules := envRuleLookup.randUniqueArray(testRand, func(r *rand.Rand) string { - return randVariableString(r, 10) - }, int32(numEnvRules[i])) - - envRules[i] = make([]EnvRuleConfig, numEnvRules[i]) - for j, rule := range rules { - envRules[i][j] = EnvRuleConfig{ - Strategy: "string", - Rule: rule, - } - } - } - - return envRules -} - -func setupRegoDropEnvsTest(disjoint bool) (*regoContainerTestConfig, error) { - gc := generateConstraints(testRand, 1) - gc.allowEnvironmentVariableDropping = true - - const numContainers int = 3 - envRules := setupEnvRuleSets(numContainers) - containers := make([]*securityPolicyContainer, numContainers) - envs := make([][]string, numContainers) - - for i := 0; i < numContainers; i++ { - c, err := gc.containers[0].clone() - if err != nil { - return nil, err - } - containers[i] = c - envs[i] = buildEnvironmentVariablesFromEnvRules(envRules[i], testRand) - if i == 0 { - c.EnvRules = envRules[i] - } else if disjoint { - c.EnvRules = append(envRules[0], envRules[i]...) - } else { - c.EnvRules = append(containers[i-1].EnvRules, envRules[i]...) - } - } - - gc.containers = containers - securityPolicy := gc.toPolicy() - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - - policy, err := newRegoPolicy(securityPolicy.marshalRego(), - toOCIMounts(defaultMounts), - toOCIMounts(privilegedMounts)) - - if err != nil { - return nil, err - } - - containerIDs := make([]string, numContainers) - for i, c := range gc.containers { - containerID, err := mountImageForContainer(policy, c) - if err != nil { - return nil, err - } - - containerIDs[i] = containerID - } - - var envList []string - if disjoint { - var extraLen int - if len(envs[1]) < len(envs[2]) { - extraLen = len(envs[1]) - } else { - extraLen = len(envs[2]) - } - envList = append(envs[0], envs[1][:extraLen]...) - envList = append(envList, envs[2][:extraLen]...) - } else { - envList = append(envs[0], envs[1]...) - envList = append(envList, envs[2]...) - } - - user := buildIDNameFromConfig(containers[2].User.UserIDName, testRand) - groups := buildGroupIDNamesFromUser(containers[2].User, testRand) - umask := containers[2].User.Umask - - sandboxID := testDataGenerator.uniqueSandboxID() - - mounts := containers[2].Mounts - mounts = append(mounts, defaultMounts...) - if containers[2].AllowElevated { - mounts = append(mounts, privilegedMounts...) - } - - mountSpec := buildMountSpecFromMountArray(mounts, sandboxID, testRand) - capabilities := copyLinuxCapabilities(containers[2].Capabilities.toExternal()) - seccomp := containers[2].SeccompProfileSHA256 - - // see NOTE_TESTCOPY - return ®oContainerTestConfig{ - envList: copyStrings(envList), - argList: copyStrings(containers[2].Command), - workingDir: containers[2].WorkingDir, - containerID: containerIDs[2], - sandboxID: sandboxID, - mounts: copyMounts(mountSpec.Mounts), - noNewPrivileges: containers[2].NoNewPrivileges, - user: user, - groups: groups, - umask: umask, - policy: policy, - capabilities: &capabilities, - seccomp: seccomp, - ctx: gc.ctx, - }, nil -} - -type regoFrameworkVersionTestConfig struct { - policy *regoEnforcer - fragments []*regoFragment -} - -func setFrameworkVersion(code string, version string) string { - template := `framework_version := "%s"` - old := fmt.Sprintf(template, frameworkVersion) - if version == "" { - return strings.Replace(code, old, "", 1) - } - - new := fmt.Sprintf(template, version) - return strings.Replace(code, old, new, 1) -} - -func setupFrameworkVersionSimpleTest(gc *generatedConstraints, policyVersion string, version string) (*regoFrameworkVersionTestConfig, error) { - return setupFrameworkVersionTest(gc, policyVersion, version, 0, "", []string{}) -} - -func setupFrameworkVersionTest(gc *generatedConstraints, policyVersion string, version string, numFragments int, fragmentVersion string, includes []string) (*regoFrameworkVersionTestConfig, error) { - fragments := make([]*regoFragment, 0, numFragments) - if numFragments > 0 { - gc.fragments = generateFragments(testRand, int32(numFragments)) - fragments = selectFragmentsFromConstraints(gc, numFragments, includes, []string{}, false, fragmentVersion, false) - } - - securityPolicy := gc.toPolicy() - policy, err := newRegoPolicy(setFrameworkVersion(securityPolicy.marshalRego(), policyVersion), []oci.Mount{}, []oci.Mount{}) - if err != nil { - return nil, err - } - - code := strings.Replace(frameworkCodeTemplate, "@@FRAMEWORK_VERSION@@", version, 1) - policy.rego.RemoveModule("framework.rego") - policy.rego.AddModule("framework.rego", &rpi.RegoModule{Namespace: "framework", Code: code}) - err = policy.rego.Compile() - if err != nil { - return nil, err - } - - return ®oFrameworkVersionTestConfig{policy: policy, fragments: fragments}, nil -} - -type regoFragment struct { - info *fragment - constraints *generatedConstraints - code string -} - -func (f *regoFragment) selectContainer() *securityPolicyContainer { - return selectContainerFromContainerList(f.constraints.containers, testRand) -} - -func mustIncrementSVN(svn string) string { - svn_semver, err := semver.Parse(svn) - - if err == nil { - svn_semver.IncrementMajor() - return svn_semver.String() - } - - svn_int, err := strconv.Atoi(svn) - - if err == nil { - return strconv.Itoa(svn_int + 1) - } - - panic("Could not increment SVN") -} - -func selectFragmentsFromConstraints(gc *generatedConstraints, numFragments int, includes []string, excludes []string, svnError bool, frameworkVersion string, svnMismatch bool) []*regoFragment { - choices := randChoices(testRand, numFragments, len(gc.fragments)) - fragments := make([]*regoFragment, numFragments) - for i, choice := range choices { - config := gc.fragments[choice] - config.includes = addStringsToArray(config.includes, includes) - // since we want to test that the policy cannot include an excluded - // quantity, we must first ensure they are in the fragment - config.includes = addStringsToArray(config.includes, excludes) - - constraints := generateConstraints(testRand, maxContainersInGeneratedConstraints) - for _, include := range config.includes { - switch include { - case "fragments": - constraints.fragments = generateFragments(testRand, 1) - for _, fragment := range constraints.fragments { - fragment.includes = addStringsToArray(fragment.includes, []string{"containers"}) - } - break - - case "external_processes": - constraints.externalProcesses = generateExternalProcesses(testRand) - break - } - } - - svn := config.minimumSVN - if svnMismatch { - if randBool(testRand) { - svn = generateSemver(testRand) - } else { - config.minimumSVN = generateSemver(testRand) - } - } - - constraints.svn = svn - if svnError { - config.minimumSVN = mustIncrementSVN(config.minimumSVN) - } - - code := constraints.toFragment().marshalRego() - code = setFrameworkVersion(code, frameworkVersion) - - fragments[i] = ®oFragment{ - info: config, - constraints: constraints, - code: code, - } - } - - return fragments -} - -func generateSandboxID(r *rand.Rand) string { - return randVariableString(r, maxGeneratedSandboxIDLength) -} - -func generateEnforcementPoint(r *rand.Rand) string { - first := randChar(r) - return first + randString(r, atMost(r, maxGeneratedEnforcementPointLength)) -} - -func (gen *dataGenerator) uniqueSandboxID() string { - return gen.sandboxIDs.randUnique(gen.rng, generateSandboxID) -} - -func (gen *dataGenerator) uniqueEnforcementPoint() string { - return gen.enforcementPoints.randUnique(gen.rng, generateEnforcementPoint) -} - -func buildMountSpecFromMountArray(mounts []mountInternal, sandboxID string, r *rand.Rand) *oci.Spec { - mountSpec := new(oci.Spec) - - // Select some number of the valid, matching rules to be environment - // variable - numberOfMounts := int32(len(mounts)) - numberOfMatches := randMinMax(r, 1, numberOfMounts) - usedIndexes := map[int]struct{}{} - for numberOfMatches > 0 { - anIndex := -1 - if (numberOfMatches * 2) > numberOfMounts { - // if we have a lot of matches, randomly select - exists := true - - for exists { - anIndex = int(randMinMax(r, 0, numberOfMounts-1)) - _, exists = usedIndexes[anIndex] - } - } else { - // we have a "smaller set of rules. we'll just iterate and select from - // available - exists := true - - for exists { - anIndex++ - _, exists = usedIndexes[anIndex] - } - } - - mount := mounts[anIndex] - - source := substituteUVMPath(sandboxID, mount).Source - mountSpec.Mounts = append(mountSpec.Mounts, oci.Mount{ - Source: source, - Destination: mount.Destination, - Options: mount.Options, - Type: mount.Type, - }) - usedIndexes[anIndex] = struct{}{} - - numberOfMatches-- - } - - return mountSpec -} - -//go:embed api_test.rego -var apiTestCode string - -func (p *regoEnforcer) injectTestAPI() error { - p.rego.RemoveModule("api.rego") - p.rego.AddModule("api.rego", &rpi.RegoModule{Namespace: "api", Code: apiTestCode}) - - return p.rego.Compile() -} - -func selectContainerFromRunningContainers(containers []regoRunningContainer, r *rand.Rand) regoRunningContainer { - numContainers := len(containers) - return containers[r.Intn(numContainers)] -} - -func selectExecProcess(processes []containerExecProcess, r *rand.Rand) containerExecProcess { - numProcesses := len(processes) - return processes[r.Intn(numProcesses)] -} - -func idForRunningContainer(container *securityPolicyContainer, running []regoRunningContainer) (string, error) { - for _, c := range running { - if c.container == container { - return c.containerID, nil - } - } - - return "", errors.New("Container isn't running") -} - -func selectSignalFromSignals(r *rand.Rand, signals []syscall.Signal) syscall.Signal { - numSignals := len(signals) - return signals[r.Intn(numSignals)] -} - -func generateUVMPathForShare(r *rand.Rand, containerID string) string { - return fmt.Sprintf("%s/%s%s", - guestpath.LCOWRootPrefixInUVM, - containerID, - fmt.Sprintf(guestpath.LCOWMountPathPrefixFmt, atMost(r, maxPlan9MountIndex))) -} - -func generateFragments(r *rand.Rand, minFragments int32) []*fragment { - numFragments := randMinMax(r, minFragments, maxFragmentsInGeneratedConstraints) - - fragments := make([]*fragment, numFragments) - for i := 0; i < int(numFragments); i++ { - fragments[i] = generateFragment(r) - } - - return fragments -} - -func generateFragmentIssuer(r *rand.Rand) string { - return randString(r, maxGeneratedFragmentIssuerLength) -} - -func generateFragmentFeed(r *rand.Rand) string { - return randString(r, maxGeneratedFragmentFeedLength) -} - -func (gen *dataGenerator) uniqueFragmentNamespace() string { - return gen.fragmentNamespaces.randUnique(gen.rng, generateFragmentNamespace) -} - -func (gen *dataGenerator) uniqueFragmentIssuer() string { - return gen.fragmentIssuers.randUnique(gen.rng, generateFragmentIssuer) -} - -func (gen *dataGenerator) uniqueFragmentFeed() string { - return gen.fragmentFeeds.randUnique(gen.rng, generateFragmentFeed) -} - -func generateFragment(r *rand.Rand) *fragment { - possibleIncludes := []string{"containers", "fragments", "external_processes"} - numChoices := int(atLeastOneAtMost(r, int32(len(possibleIncludes)))) - includes := randChooseStrings(r, possibleIncludes, numChoices) - return &fragment{ - issuer: testDataGenerator.uniqueFragmentIssuer(), - feed: testDataGenerator.uniqueFragmentFeed(), - minimumSVN: generateSVN(r), - includes: includes, - } -} - -func generateLinuxID(r *rand.Rand) uint32 { - return r.Uint32() -} - -func addStringsToArray(values []string, valuesToAdd []string) []string { - toAdd := []string{} - for _, valueToAdd := range valuesToAdd { - add := true - for _, value := range values { - if value == valueToAdd { - add = false - break - } - } - if add { - toAdd = append(toAdd, valueToAdd) - } - } - - return append(values, toAdd...) -} - -func removeStringsFromArray(values []string, valuesToRemove []string) []string { - remain := make([]string, 0, len(values)) - for _, value := range values { - keep := true - for _, toRemove := range valuesToRemove { - if value == toRemove { - keep = false - break - } - } - if keep { - remain = append(remain, value) - } - } - - return remain -} - -func areStringArraysEqual(lhs []string, rhs []string) bool { - if len(lhs) != len(rhs) { - return false - } - - sort.Strings(lhs) - sort.Strings(rhs) - - for i, a := range lhs { - if a != rhs[i] { - return false - } - } - - return true -} - -func (c securityPolicyContainer) clone() (*securityPolicyContainer, error) { - contents, err := json.Marshal(c) - if err != nil { - return nil, err - } - - var clone securityPolicyContainer - err = json.Unmarshal(contents, &clone) - if err != nil { - return nil, err - } - - return &clone, nil -} - -func (p externalProcess) clone() *externalProcess { - envRules := make([]EnvRuleConfig, len(p.envRules)) - copy(envRules, p.envRules) - - return &externalProcess{ - command: copyStrings(p.command), - envRules: envRules, - workingDir: p.workingDir, - allowStdioAccess: p.allowStdioAccess, - } -} - -func (p containerExecProcess) clone() containerExecProcess { - return containerExecProcess{ - Command: copyStrings(p.Command), - Signals: p.Signals, - } -} - -func (c *securityPolicyContainer) toContainer() *Container { - execProcesses := make([]ExecProcessConfig, len(c.ExecProcesses)) - for i, ep := range c.ExecProcesses { - execProcesses[i] = ExecProcessConfig(ep) - } - - capabilities := CapabilitiesConfig{ - Bounding: c.Capabilities.Bounding, - Effective: c.Capabilities.Effective, - Inheritable: c.Capabilities.Inheritable, - Permitted: c.Capabilities.Permitted, - Ambient: c.Capabilities.Ambient, - } - - return &Container{ - Command: CommandArgs(stringArrayToStringMap(c.Command)), - EnvRules: envRuleArrayToEnvRules(c.EnvRules), - Layers: Layers(stringArrayToStringMap(c.Layers)), - WorkingDir: c.WorkingDir, - Mounts: mountArrayToMounts(c.Mounts), - AllowElevated: c.AllowElevated, - ExecProcesses: execProcesses, - Signals: c.Signals, - AllowStdioAccess: c.AllowStdioAccess, - NoNewPrivileges: c.NoNewPrivileges, - User: c.User, - Capabilities: &capabilities, - SeccompProfileSHA256: c.SeccompProfileSHA256, - } -} - -func envRuleArrayToEnvRules(envRules []EnvRuleConfig) EnvRules { - elements := make(map[string]EnvRuleConfig) - for i, envRule := range envRules { - elements[strconv.Itoa(i)] = envRule - } - return EnvRules{ - Elements: elements, - Length: len(envRules), - } -} - -func mountArrayToMounts(mounts []mountInternal) Mounts { - elements := make(map[string]Mount) - for i, mount := range mounts { - elements[strconv.Itoa(i)] = Mount{ - Source: mount.Source, - Destination: mount.Destination, - Type: mount.Type, - Options: Options(stringArrayToStringMap(mount.Options)), - } - } - - return Mounts{ - Elements: elements, - Length: len(mounts), - } -} - -func (p externalProcess) toConfig() ExternalProcessConfig { - return ExternalProcessConfig{ - Command: p.command, - WorkingDir: p.workingDir, - AllowStdioAccess: p.allowStdioAccess, - } -} - -func (f fragment) toConfig() FragmentConfig { - return FragmentConfig{ - Issuer: f.issuer, - Feed: f.feed, - MinimumSVN: f.minimumSVN, - Includes: f.includes, - } -} - -func stringArrayToStringMap(values []string) StringArrayMap { - elements := make(map[string]string) - for i, value := range values { - elements[strconv.Itoa(i)] = value - } - - return StringArrayMap{ - Elements: elements, - Length: len(values), - } -} - -func (s *stringSet) randUniqueArray(r *rand.Rand, generator func(*rand.Rand) string, numItems int32) []string { - items := make([]string, numItems) - for i := 0; i < int(numItems); i++ { - items[i] = s.randUnique(r, generator) - } - return items -} - -type regoScratchMountPolicyTestConfig struct { - policy *regoEnforcer -} - -func setupRegoScratchMountTest( - gc *generatedConstraints, - unencryptedScratch bool, -) (tc *regoScratchMountPolicyTestConfig, err error) { - securityPolicy := gc.toPolicy() - securityPolicy.AllowUnencryptedScratch = unencryptedScratch - - defaultMounts := generateMounts(testRand) - privilegedMounts := generateMounts(testRand) - policy, err := newRegoPolicy(securityPolicy.marshalRego(), toOCIMounts(defaultMounts), toOCIMounts(privilegedMounts)) - if err != nil { - return nil, err - } - return ®oScratchMountPolicyTestConfig{ - policy: policy, - }, nil -} - -func verifyPolicyRules(apiVersion string, enforcementPoints map[string]interface{}, policyCode string) error { - query := rego.New( - rego.Query("data.policy"), - rego.Module("policy.rego", policyCode), - rego.Module("framework.rego", FrameworkCode), - ) - - ctx := context.Background() - resultSet, err := query.Eval(ctx) - if err != nil { - return fmt.Errorf("unable to query policy template rules: %w", err) - } - - policyTemplateRules := resultSet[0].Expressions[0].Value.(map[string]interface{}) - policyTemplateAPIVersion := policyTemplateRules["api_version"].(string) - - if policyTemplateAPIVersion != apiVersion { - return fmt.Errorf("Policy template version != api version: %s != %s", apiVersion, policyTemplateAPIVersion) - } - - for rule := range enforcementPoints { - if _, ok := policyTemplateRules[rule]; !ok { - return fmt.Errorf("Rule %s in API is missing from policy template", rule) - } - } - - for rule := range policyTemplateRules { - if rule == "api_version" || rule == "framework_version" || rule == "reason" { - continue - } - - if _, ok := enforcementPoints[rule]; !ok { - return fmt.Errorf("Rule %s in policy template is missing from API", rule) - } - } - - return nil -} - -func buildIDNameFromConfig(config IDNameConfig, r *rand.Rand) IDName { - switch config.Strategy { - case IDNameStrategyName: - return IDName{ - ID: generateIDNameID(r), - Name: config.Rule, - } - - case IDNameStrategyID: - return IDName{ - ID: config.Rule, - Name: generateIDNameName(r), - } - - case IDNameStrategyAny: - return generateIDName(r) - - default: - panic(fmt.Sprintf("unsupported ID Name strategy: %v", config.Strategy)) - } -} - -func buildGroupIDNamesFromUser(user UserConfig, r *rand.Rand) []IDName { - groupIDNames := make([]IDName, 0) - - // Select some number of the valid, matching rules to be groups - numberOfGroups := int32(len(user.GroupIDNames)) - numberOfMatches := randMinMax(r, 1, numberOfGroups) - usedIndexes := map[int]struct{}{} - for numberOfMatches > 0 { - anIndex := -1 - if (numberOfMatches * 2) > numberOfGroups { - // if we have a lot of matches, randomly select - exists := true - - for exists { - anIndex = int(randMinMax(r, 0, numberOfGroups-1)) - _, exists = usedIndexes[anIndex] - } - } else { - // we have a "smaller set of rules. we'll just iterate and select from - // available - exists := true - - for exists { - anIndex++ - _, exists = usedIndexes[anIndex] - } - } - - if user.GroupIDNames[anIndex].Strategy == IDNameStrategyRegex { - // we don't match from regex groups or any groups - numberOfMatches-- - continue - } - - groupIDName := buildIDNameFromConfig(user.GroupIDNames[anIndex], r) - groupIDNames = append(groupIDNames, groupIDName) - usedIndexes[anIndex] = struct{}{} - - numberOfMatches-- - } - - return groupIDNames -} - -func generateIDNameName(r *rand.Rand) string { - return randVariableString(r, maxGeneratedNameLength) -} - -func generateIDNameID(r *rand.Rand) string { - id := r.Uint32() - return strconv.FormatUint(uint64(id), 10) -} - -func generateIDName(r *rand.Rand) IDName { - return IDName{ - ID: generateIDNameID(r), - Name: generateIDNameName(r), - } -} - -func generateCapabilities(r *rand.Rand) *oci.LinuxCapabilities { - return &oci.LinuxCapabilities{ - Bounding: generateCapabilitiesSet(r, 0), - Effective: generateCapabilitiesSet(r, 0), - Inheritable: generateCapabilitiesSet(r, 0), - Permitted: generateCapabilitiesSet(r, 0), - Ambient: generateCapabilitiesSet(r, 0), - } -} - -func generateSemver(r *rand.Rand) string { - major := randMinMax(r, 0, maxGeneratedVersion) - minor := randMinMax(r, 0, maxGeneratedVersion) - patch := randMinMax(r, 0, maxGeneratedVersion) - return fmt.Sprintf("%d.%d.%d", major, minor, patch) -} - -func alterCapabilitySet(r *rand.Rand, set []string) []string { - newSet := copyStrings(set) - - if len(newSet) == 0 { - return generateCapabilitiesSet(r, 1) - } - - alterations := atLeastNAtMostM(r, 1, 4) - for i := alterations; i > 0; i-- { - if len(newSet) == 0 { - newSet = generateCapabilitiesSet(r, 1) - } else { - action := atMost(r, 2) - if action == 0 { - newSet = superCapabilitySet(r, newSet) - } else if action == 1 { - newSet = subsetCapabilitySet(r, newSet) - } else { - replace := atMost(r, int32((len(newSet) - 1))) - newSet[replace] = generateCapability(r) - } - } - } - - return newSet -} - -func subsetCapabilitySet(r *rand.Rand, set []string) []string { - newSet := make([]string, 0) - - setSize := int32(len(set)) - if setSize == 0 { - // no subset is possible - return newSet - } else if setSize == 1 { - // only one possibility - return newSet - } - - // We need to remove at least 1 item, potentially all - numberOfMatches := randMinMax(r, 0, setSize-1) - usedIndexes := map[int]struct{}{} - for i := numberOfMatches; i > 0; i-- { - anIndex := -1 - if ((setSize - int32(len(usedIndexes))) * 2) > i { - // the set is pretty large compared to our number to select, - // we will gran randomly - exists := true - - for exists { - anIndex = int(randMinMax(r, 0, setSize-1)) - _, exists = usedIndexes[anIndex] - } - } else { - // we have a "smaller set of capabilities. we'll just iterate and - // select from available - exists := true - - for exists { - anIndex++ - _, exists = usedIndexes[anIndex] - } - } - - newSet = append(newSet, set[anIndex]) - usedIndexes[anIndex] = struct{}{} - } - - return newSet -} - -func superCapabilitySet(r *rand.Rand, set []string) []string { - newSet := copyStrings(set) - - additions := atLeastNAtMostM(r, 1, 12) - for i := additions; i > 0; i-- { - newSet = append(newSet, generateCapability(r)) - } - - return newSet -} - -func (c capabilitiesInternal) toExternal() oci.LinuxCapabilities { - return oci.LinuxCapabilities{ - Bounding: c.Bounding, - Effective: c.Effective, - Inheritable: c.Inheritable, - Permitted: c.Permitted, - Ambient: c.Ambient, - } -} - -func assertKeyValue(object map[string]interface{}, key string, expectedValue interface{}) error { - if actualValue, ok := object[key]; ok { - if actualValue != expectedValue { - return fmt.Errorf("incorrect value for no_new_privileges: %t != %t (expected)", actualValue, expectedValue) - } - } else { - return fmt.Errorf("missing value for %s", key) - } - - return nil -} - -func assertDecisionJSONContains(t *testing.T, err error, expectedValues ...string) bool { - if err == nil { - t.Errorf("expected error to contain %v but got nil", expectedValues) - return false - } - - policyDecision, err := ExtractPolicyDecision(err.Error()) - if err != nil { - t.Errorf("unable to extract policy decision from error: %v", err) - return false - } - - for _, expected := range expectedValues { - if !strings.Contains(policyDecision, expected) { - t.Errorf("expected error to contain %q", expected) - return false - } - } - - return true -} - -func assertDecisionJSONDoesNotContain(t *testing.T, err error, expectedValues ...string) bool { - if err == nil { - t.Errorf("expected error to contain %v but got nil", expectedValues) - return false - } - - policyDecision, err := ExtractPolicyDecision(err.Error()) - if err != nil { - t.Errorf("unable to extract policy decision from error: %v", err) - return false - } - - for _, expected := range expectedValues { - if strings.Contains(policyDecision, expected) { - t.Errorf("expected error to not contain %q", expected) - return false - } - } - - return true -} - type getUserInfoTestCase struct { userStrs []string additionalGIDs []uint32 diff --git a/pkg/securitypolicy/regopolicy_windows_test.go b/pkg/securitypolicy/regopolicy_windows_test.go new file mode 100644 index 0000000000..2fc8158a14 --- /dev/null +++ b/pkg/securitypolicy/regopolicy_windows_test.go @@ -0,0 +1,1331 @@ +//go:build windows && rego +// +build windows,rego + +package securitypolicy + +import ( + "context" + _ "embed" + "fmt" + "math/rand" + "strings" + "testing" + "testing/quick" + + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" + oci "github.com/opencontainers/runtime-spec/specs-go" +) + +const testOSType = "windows" + +func Test_Rego_EnforceCommandPolicy_NoMatches_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupSimpleRegoCreateContainerTestWindows(p) + if err != nil { + t.Error(err) + return false + } + + //_, _, _, err = tc.policy.EnforceCreateContainerPolicy(p.ctx, tc.sandboxID, tc.containerID, generateCommand(testRand), tc.envList, tc.workingDir, tc.mounts, false, tc.noNewPrivileges, tc.user, tc.groups, tc.umask, tc.capabilities, tc.seccomp) + + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(p.ctx, tc.containerID, generateCommand(testRand), tc.envList, tc.workingDir, tc.mounts, tc.user, nil) + + if err == nil { + return false + } + + //t.Logf("Error value: %v", err) + + return assertDecisionJSONContains(t, err, "invalid command") + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_EnforceCommandPolicy_NoMatches: %v", err) + } +} + +func Test_Rego_EnforceEnvironmentVariablePolicy_Re2Match_Windows(t *testing.T) { + testFunc := func(gc *generatedWindowsConstraints) bool { + container := selectWindowsContainerFromContainerList(gc.containers, testRand) + // add a rule to re2 match + re2MatchRule := EnvRuleConfig{ + Strategy: EnvVarRuleRegex, + Rule: "PREFIX_.+=.+", + } + + container.EnvRules = append(container.EnvRules, re2MatchRule) + + tc, err := setupRegoCreateContainerTestWindows(gc, container, false) + if err != nil { + t.Error(err) + return false + } + + envList := append(tc.envList, "PREFIX_FOO=BAR") + + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(gc.ctx, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, tc.user, nil) + + // getting an error means something is broken + if err != nil { + t.Errorf("Expected container setup to be allowed. It wasn't: %v", err) + return false + } + + return true + } + + if err := quick.Check(testFunc, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceEnvironmentVariablePolicy_Re2Match: %v", err) + } +} + +func Test_Rego_EnforceEnvironmentVariablePolicy_NotAllMatches_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupSimpleRegoCreateContainerTestWindows(p) + if err != nil { + t.Error(err) + return false + } + + envList := append(tc.envList, generateNeverMatchingEnvironmentVariable(testRand)) + + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(p.ctx, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, tc.user, nil) + + // not getting an error means something is broken + if err == nil { + return false + } + + return assertDecisionJSONContains(t, err, "invalid env list", envList[0]) + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceEnvironmentVariablePolicy_NotAllMatches: %v", err) + } +} + +func Test_Rego_EnforceEnvironmentVariablePolicy_DropEnvs_Windows(t *testing.T) { + testFunc := func(gc *generatedWindowsConstraints) bool { + gc.allowEnvironmentVariableDropping = true + container := selectWindowsContainerFromContainerList(gc.containers, testRand) + + tc, err := setupRegoCreateContainerTestWindows(gc, container, false) + if err != nil { + t.Error(err) + return false + } + + extraRules := generateEnvironmentVariableRules(testRand) + extraEnvs := buildEnvironmentVariablesFromEnvRules(extraRules, testRand) + + envList := append(tc.envList, extraEnvs...) + actual, _, _, err := tc.policy.EnforceCreateContainerPolicyV2(gc.ctx, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, tc.user, nil) + + // getting an error means something is broken + if err != nil { + t.Errorf("Expected container creation to be allowed. It wasn't: %v", err) + return false + } + + if !areStringArraysEqual(actual, tc.envList) { + t.Errorf("environment variables were not dropped correctly.") + return false + } + + return true + } + + if err := quick.Check(testFunc, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceEnvironmentVariablePolicy_DropEnvs: %v", err) + } +} + +func Test_Rego_EnforceEnvironmentVariablePolicy_DropEnvs_Multiple_Windows(t *testing.T) { + tc, err := setupRegoDropEnvsTestWindows(false) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + + extraRules := generateEnvironmentVariableRules(testRand) + extraEnvs := buildEnvironmentVariablesFromEnvRules(extraRules, testRand) + + envList := append(tc.envList, extraEnvs...) + actual, _, _, err := tc.policy.EnforceCreateContainerPolicyV2(tc.ctx, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, tc.user, nil) + + // getting an error means something is broken + if err != nil { + t.Errorf("Expected container creation to be allowed. It wasn't: %v", err) + } + + if !areStringArraysEqual(actual, tc.envList) { + t.Error("environment variables were not dropped correctly.") + } +} + +func Test_Rego_EnforceEnvironmentVariablePolicy_DropEnvs_Multiple_NoMatch_Windows(t *testing.T) { + tc, err := setupRegoDropEnvsTestWindows(true) + if err != nil { + t.Fatalf("error setting up test: %v", err) + } + + extraRules := generateEnvironmentVariableRules(testRand) + extraEnvs := buildEnvironmentVariablesFromEnvRules(extraRules, testRand) + + envList := append(tc.envList, extraEnvs...) + actual, _, _, err := tc.policy.EnforceCreateContainerPolicyV2(tc.ctx, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, tc.user, nil) + + // not getting an error means something is broken + if err == nil { + t.Error("expected container creation not to be allowed.") + } + + if actual != nil { + t.Error("envList should be nil") + } +} + +func Test_Rego_WorkingDirectoryPolicy_NoMatches_Windows(t *testing.T) { + testFunc := func(gc *generatedWindowsConstraints) bool { + tc, err := setupSimpleRegoCreateContainerTestWindows(gc) + if err != nil { + t.Error(err) + return false + } + + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(tc.ctx, tc.containerID, tc.argList, tc.envList, randString(testRand, 20), tc.mounts, tc.user, nil) + // not getting an error means something is broken + if err == nil { + return false + } + + return assertDecisionJSONContains(t, err, "invalid working directory") + } + + if err := quick.Check(testFunc, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_WorkingDirectoryPolicy_NoMatches: %v", err) + } +} +func Test_Rego_EnforceCreateContainer_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupSimpleRegoCreateContainerTestWindows(p) + if err != nil { + t.Errorf("Setup failed: %v", err) + return false + } + + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(p.ctx, tc.containerID, tc.argList, tc.envList, tc.workingDir, tc.mounts, tc.user, nil) + + if err != nil { + t.Errorf("Policy enforcement failed: %v", err) + } + // getting an error means something is broken + return err == nil + } + + if err := quick.Check(f, &quick.Config{MaxCount: 10, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceCreateContainer: %v", err) + } +} + +func Test_Rego_EnforceCreateContainer_Start_All_Containers(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + securityPolicy := p.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), testOSType) + if err != nil { + t.Error(err) + return false + } + + for _, container := range p.containers { + containerID, err := mountImageForWindowsContainer(policy, container) + if err != nil { + t.Error(err) + return false + } + + envList := buildEnvironmentVariablesFromEnvRules(container.EnvRules, testRand) + user := IDName{Name: container.User} + + _, _, _, err = policy.EnforceCreateContainerPolicyV2(p.ctx, containerID, container.Command, envList, container.WorkingDir, nil, user, nil) + + // getting an error means something is broken + if err != nil { + t.Error(err) + return false + } + } + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 10, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceCreateContainer_Start_All_Containers: %v", err) + } +} + +func Test_Rego_EnforceCreateContainer_Invalid_ContainerID_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupSimpleRegoCreateContainerTestWindows(p) + if err != nil { + t.Error(err) + return false + } + + containerID := testDataGenerator.uniqueContainerID() + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(p.ctx, containerID, tc.argList, tc.envList, tc.workingDir, nil, tc.user, nil) + + // not getting an error means something is broken + return err != nil + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceCreateContainer_Invalid_ContainerID: %v", err) + } +} + +func Test_Rego_EnforceCreateContainer_Same_Container_Twice_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupSimpleRegoCreateContainerTestWindows(p) + if err != nil { + t.Error(err) + return false + } + + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(p.ctx, tc.containerID, tc.argList, tc.envList, tc.workingDir, nil, tc.user, nil) + if err != nil { + t.Error("Unable to start valid container.") + return false + } + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(p.ctx, tc.containerID, tc.argList, tc.envList, tc.workingDir, nil, tc.user, nil) + + if err == nil { + t.Error("Able to start a container with already used id.") + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceCreateContainer_Same_Container_Twice: %v", err) + } +} + +// -- Capabilities/Mount/Rego version tests are removed -- Add back Rego versions test// +func Test_Rego_ExecInContainerPolicy_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + container := selectContainerFromRunningContainers(tc.runningContainers, testRand) + + process := selectWindowsExecProcess(container.windowsContainer.ExecProcesses, testRand) + envList := buildEnvironmentVariablesFromEnvRules(container.windowsContainer.EnvRules, testRand) + user := IDName{Name: container.windowsContainer.User} + + commandLine := []string{process.Command} + + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, container.containerID, commandLine, envList, container.windowsContainer.WorkingDir, user, nil) + + // getting an error means something is broken + if err != nil { + t.Error(err) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecInContainerPolicy: %v", err) + } +} + +func Test_Rego_ExecInContainerPolicy_No_Matches_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + container := selectContainerFromRunningContainers(tc.runningContainers, testRand) + + process := generateWindowsContainerExecProcess(testRand) + envList := buildEnvironmentVariablesFromEnvRules(container.windowsContainer.EnvRules, testRand) + user := IDName{Name: container.windowsContainer.User} + commandLine := []string{process.Command} + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, container.containerID, commandLine, envList, container.windowsContainer.WorkingDir, user, nil) + if err == nil { + t.Error("Test unexpectedly passed") + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecInContainerPolicy_No_Matches: %v", err) + } +} + +func Test_Rego_ExecInContainerPolicy_Command_No_Match_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + container := selectContainerFromRunningContainers(tc.runningContainers, testRand) + envList := buildEnvironmentVariablesFromEnvRules(container.windowsContainer.EnvRules, testRand) + user := IDName{Name: container.windowsContainer.User} + + command := generateCommand(testRand) + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, container.containerID, command, envList, container.windowsContainer.WorkingDir, user, nil) + + // not getting an error means something is broken + if err == nil { + t.Error("Unexpected success when enforcing policy") + return false + } + + return assertDecisionJSONContains(t, err, "invalid command") + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecInContainerPolicy_Command_No_Match: %v", err) + } +} + +func Test_Rego_ExecInContainerPolicy_Some_Env_Not_Allowed_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + container := selectContainerFromRunningContainers(tc.runningContainers, testRand) + process := selectWindowsExecProcess(container.windowsContainer.ExecProcesses, testRand) + envList := generateEnvironmentVariables(testRand) + user := IDName{Name: container.windowsContainer.User} + commandLine := []string{process.Command} + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, container.containerID, commandLine, envList, container.windowsContainer.WorkingDir, user, nil) + + // not getting an error means something is broken + if err == nil { + t.Error("Unexpected success when enforcing policy") + return false + } + + return assertDecisionJSONContains(t, err, "invalid env list") + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecInContainerPolicy_Some_Env_Not_Allowed: %v", err) + } +} + +func Test_Rego_ExecInContainerPolicy_WorkingDir_No_Match_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + container := selectContainerFromRunningContainers(tc.runningContainers, testRand) + process := selectWindowsExecProcess(container.windowsContainer.ExecProcesses, testRand) + envList := buildEnvironmentVariablesFromEnvRules(container.windowsContainer.EnvRules, testRand) + workingDir := generateWorkingDir(testRand) + user := IDName{Name: container.windowsContainer.User} + commandLine := []string{process.Command} + + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, container.containerID, commandLine, envList, workingDir, user, nil) + + // not getting an error means something is broken + if err == nil { + t.Error("Unexpected success when enforcing policy") + return false + } + + return assertDecisionJSONContains(t, err, "invalid working directory") + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecInContainerPolicy_WorkingDir_No_Match: %v", err) + } +} + +// -- capabilities tests are removed --// +func Test_Rego_ExecInContainerPolicy_DropEnvs_Windows(t *testing.T) { + testFunc := func(gc *generatedWindowsConstraints) bool { + gc.allowEnvironmentVariableDropping = true + tc, err := setupRegoRunningWindowsContainerTest(gc) + if err != nil { + t.Error(err) + return false + } + + container := selectContainerFromRunningContainers(tc.runningContainers, testRand) + + process := selectWindowsExecProcess(container.windowsContainer.ExecProcesses, testRand) + expected := buildEnvironmentVariablesFromEnvRules(container.windowsContainer.EnvRules, testRand) + + extraRules := generateEnvironmentVariableRules(testRand) + extraEnvs := buildEnvironmentVariablesFromEnvRules(extraRules, testRand) + + envList := append(expected, extraEnvs...) + user := IDName{Name: container.windowsContainer.User} + commandLine := []string{process.Command} + + actual, _, _, err := tc.policy.EnforceExecInContainerPolicyV2(gc.ctx, container.containerID, commandLine, envList, container.windowsContainer.WorkingDir, user, nil) + + if err != nil { + t.Errorf("expected exec in container process to be allowed. It wasn't: %v", err) + return false + } + + if !areStringArraysEqual(actual, expected) { + t.Errorf("environment variables were not dropped correctly.") + return false + } + + return true + } + + if err := quick.Check(testFunc, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecInContainerPolicy_DropEnvs: %v", err) + } +} + +func Test_Rego_MaliciousEnvList_Windows(t *testing.T) { + template := `package policy +create_container := { + "allowed": true, + "env_list": ["%s"] +} + +exec_in_container := { + "allowed": true, + "env_list": ["%s"] +} + +exec_external := { + "allowed": true, + "env_list": ["%s"] +}` + + generateEnv := func(r *rand.Rand) string { + return randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) + } + + generateEnvs := func(envSet stringSet) []string { + numVars := atLeastOneAtMost(testRand, maxGeneratedEnvironmentVariableRules) + return envSet.randUniqueArray(testRand, generateEnv, numVars) + } + + testFunc := func(gc *generatedWindowsConstraints) bool { + envSet := make(stringSet) + rego := fmt.Sprintf( + template, + strings.Join(generateEnvs(envSet), `","`), + strings.Join(generateEnvs(envSet), `","`), + strings.Join(generateEnvs(envSet), `","`)) + + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + + if err != nil { + t.Errorf("error creating policy: %v", err) + return false + } + + user := generateIDName(testRand) + + envList := generateEnvs(envSet) + toKeep, _, _, err := policy.EnforceCreateContainerPolicyV2(gc.ctx, "", []string{}, envList, "", []oci.Mount{}, user, nil) + if len(toKeep) > 0 { + t.Error("invalid environment variables not filtered from list returned from create_container") + return false + } + + envList = generateEnvs(envSet) + toKeep, _, _, err = policy.EnforceExecInContainerPolicyV2(gc.ctx, "", []string{}, envList, "", user, nil) + if len(toKeep) > 0 { + t.Error("invalid environment variables not filtered from list returned from exec_in_container") + return false + } + + envList = generateEnvs(envSet) + toKeep, _, err = policy.EnforceExecExternalProcessPolicy(gc.ctx, []string{}, envList, "") + if len(toKeep) > 0 { + t.Error("invalid environment variables not filtered from list returned from exec_external") + return false + } + + return true + } + + if err := quick.Check(testFunc, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_MaliciousEnvList: %v", err) + } +} + +func Test_Rego_InvalidEnvList_Windows(t *testing.T) { + rego := fmt.Sprintf(`package policy + api_version := "%s" + framework_version := "%s" + + create_container := { + "allowed": true, + "env_list": {"an_object": 1} + } + exec_in_container := { + "allowed": true, + "env_list": "string" + } + exec_external := { + "allowed": true, + "env_list": true + }`, apiVersion, frameworkVersion) + + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + + if err != nil { + t.Fatalf("error creating policy: %v", err) + } + + ctx := context.Background() + user := generateIDName(testRand) + + _, _, _, err = policy.EnforceCreateContainerPolicyV2(ctx, "", []string{}, []string{}, "", []oci.Mount{}, user, nil) + if err == nil { + t.Errorf("expected call to create_container to fail") + } else if err.Error() != "policy returned incorrect type for 'env_list', expected []interface{}, received map[string]interface {}" { + t.Errorf("incorrected error message from call to create_container: %v", err) + } + + _, _, _, err = policy.EnforceExecInContainerPolicyV2(ctx, "", []string{}, []string{}, "", user, nil) + if err == nil { + t.Errorf("expected call to exec_in_container to fail") + } else if err.Error() != "policy returned incorrect type for 'env_list', expected []interface{}, received string" { + t.Errorf("incorrected error message from call to exec_in_container: %v", err) + } + + _, _, err = policy.EnforceExecExternalProcessPolicy(ctx, []string{}, []string{}, "") + if err == nil { + t.Errorf("expected call to exec_external to fail") + } else if err.Error() != "policy returned incorrect type for 'env_list', expected []interface{}, received bool" { + t.Errorf("incorrected error message from call to exec_external: %v", err) + } +} + +func Test_Rego_InvalidEnvList_Member_Windows(t *testing.T) { + rego := fmt.Sprintf(`package policy + api_version := "%s" + framework_version := "%s" + + create_container := { + "allowed": true, + "env_list": ["one", "two", 3] + } + exec_in_container := { + "allowed": true, + "env_list": ["one", true, "three"] + } + exec_external := { + "allowed": true, + "env_list": ["one", ["two"], "three"] + }`, apiVersion, frameworkVersion) + + policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType) + + if err != nil { + t.Fatalf("error creating policy: %v", err) + } + + ctx := context.Background() + user := generateIDName(testRand) + + _, _, _, err = policy.EnforceCreateContainerPolicyV2(ctx, "", []string{}, []string{}, "", []oci.Mount{}, user, nil) + if err == nil { + t.Errorf("expected call to create_container to fail") + } else if err.Error() != "members of env_list from policy must be strings, received json.Number" { + t.Errorf("incorrected error message from call to create_container: %v", err) + } + + _, _, _, err = policy.EnforceExecInContainerPolicyV2(ctx, "", []string{}, []string{}, "", user, nil) + if err == nil { + t.Errorf("expected call to exec_in_container to fail") + } else if err.Error() != "members of env_list from policy must be strings, received bool" { + t.Errorf("incorrected error message from call to exec_in_container: %v", err) + } + + _, _, err = policy.EnforceExecExternalProcessPolicy(ctx, []string{}, []string{}, "") + if err == nil { + t.Errorf("expected call to exec_external to fail") + } else if err.Error() != "members of env_list from policy must be strings, received []interface {}" { + t.Errorf("incorrected error message from call to exec_external: %v", err) + } +} + +func Test_Rego_EnforceEnvironmentVariablePolicy_MissingRequired_Windows(t *testing.T) { + testFunc := func(gc *generatedWindowsConstraints) bool { + container := selectWindowsContainerFromContainerList(gc.containers, testRand) + // add a rule to re2 match + requiredRule := EnvRuleConfig{ + Strategy: "string", + Rule: randVariableString(testRand, maxGeneratedEnvironmentVariableRuleLength), + Required: true, + } + + container.EnvRules = append(container.EnvRules, requiredRule) + + tc, err := setupRegoCreateContainerTestWindows(gc, container, false) + if err != nil { + t.Error(err) + return false + } + + envList := make([]string, 0, len(container.EnvRules)) + for _, env := range tc.envList { + if env != requiredRule.Rule { + envList = append(envList, env) + } + } + + _, _, _, err = tc.policy.EnforceCreateContainerPolicyV2(gc.ctx, tc.containerID, tc.argList, envList, tc.workingDir, tc.mounts, tc.user, nil) + + // not getting an error means something is broken + if err == nil { + t.Errorf("Expected container setup to fail.") + return false + } + + return true + } + + if err := quick.Check(testFunc, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_EnforceEnvironmentVariablePolicy_MissingRequired: %v", err) + } +} + +func Test_Rego_ExecExternalProcessPolicy_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupWindowsExternalProcessTest(p) + if err != nil { + t.Error(err) + return false + } + + process := selectWindowsExternalProcessFromConstraints(p, testRand) + envList := buildEnvironmentVariablesFromEnvRules(process.envRules, testRand) + + _, _, err = tc.policy.EnforceExecExternalProcessPolicy(p.ctx, process.command, envList, process.workingDir) + if err != nil { + t.Error("Policy enforcement unexpectedly was denied") + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecExternalProcessPolicy: %v", err) + } +} + +func Test_Rego_ExecExternalProcessPolicy_No_Matches_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupWindowsExternalProcessTest(p) + if err != nil { + t.Error(err) + return false + } + + process := generateExternalProcess(testRand) + envList := buildEnvironmentVariablesFromEnvRules(process.envRules, testRand) + + _, _, err = tc.policy.EnforceExecExternalProcessPolicy(p.ctx, process.command, envList, process.workingDir) + if err == nil { + t.Error("Policy was unexpectedly not enforced") + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecExternalProcessPolicy_No_Matches: %v", err) + } +} + +func Test_Rego_ExecExternalProcessPolicy_Command_No_Match_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupWindowsExternalProcessTest(p) + if err != nil { + t.Error(err) + return false + } + + process := selectWindowsExternalProcessFromConstraints(p, testRand) + envList := buildEnvironmentVariablesFromEnvRules(process.envRules, testRand) + command := generateCommand(testRand) + + _, _, err = tc.policy.EnforceExecExternalProcessPolicy(p.ctx, command, envList, process.workingDir) + if err == nil { + t.Error("Policy was unexpectedly not enforced") + return false + } + + return assertDecisionJSONContains(t, err, "invalid command") + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecExternalProcessPolicy_Command_No_Match: %v", err) + } +} + +func Test_Rego_ExecExternalProcessPolicy_Some_Env_Not_Allowed_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupWindowsExternalProcessTest(p) + if err != nil { + t.Error(err) + return false + } + + process := selectWindowsExternalProcessFromConstraints(p, testRand) + envList := generateEnvironmentVariables(testRand) + + _, _, err = tc.policy.EnforceExecExternalProcessPolicy(p.ctx, process.command, envList, process.workingDir) + if err == nil { + t.Error("Policy was unexpectedly not enforced") + return false + } + + return assertDecisionJSONContains(t, err, "invalid env list") + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecExternalProcessPolicy_Some_Env_Not_Allowed: %v", err) + } +} + +func Test_Rego_ExecExternalProcessPolicy_WorkingDir_No_Match_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + tc, err := setupWindowsExternalProcessTest(p) + if err != nil { + t.Error(err) + return false + } + + process := selectWindowsExternalProcessFromConstraints(p, testRand) + envList := buildEnvironmentVariablesFromEnvRules(process.envRules, testRand) + workingDir := generateWorkingDir(testRand) + + _, _, err = tc.policy.EnforceExecExternalProcessPolicy(p.ctx, process.command, envList, workingDir) + if err == nil { + t.Error("Policy was unexpectedly not enforced") + return false + } + + return assertDecisionJSONContains(t, err, "invalid working directory") + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecExternalProcessPolicy_WorkingDir_No_Match: %v", err) + } +} + +func Test_Rego_ExecExternalProcessPolicy_DropEnvs_Windows(t *testing.T) { + testFunc := func(gc *generatedWindowsConstraints) bool { + gc.allowEnvironmentVariableDropping = true + tc, err := setupWindowsExternalProcessTest(gc) + if err != nil { + t.Error(err) + return false + } + + process := selectWindowsExternalProcessFromConstraints(gc, testRand) + expected := buildEnvironmentVariablesFromEnvRules(process.envRules, testRand) + + extraRules := generateEnvironmentVariableRules(testRand) + extraEnvs := buildEnvironmentVariablesFromEnvRules(extraRules, testRand) + + envList := append(expected, extraEnvs...) + + actual, _, err := tc.policy.EnforceExecExternalProcessPolicy(gc.ctx, process.command, envList, process.workingDir) + + if err != nil { + t.Errorf("expected exec in container process to be allowed. It wasn't: %v", err) + return false + } + + if !areStringArraysEqual(actual, expected) { + t.Errorf("environment variables were not dropped correctly.") + return false + } + + return true + } + + if err := quick.Check(testFunc, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_ExecExternalProcessPolicy_DropEnvs: %v", err) + } +} + +func Test_Rego_ExecExternalProcessPolicy_DropEnvs_Multiple_Windows(t *testing.T) { + envRules := setupEnvRuleSets(3) + + gc := generateWindowsConstraints(testRand, 1) + gc.allowEnvironmentVariableDropping = true + process0 := generateExternalProcess(testRand) + + process1 := process0.clone() + process1.envRules = append(envRules[0], envRules[1]...) + + process2 := process0.clone() + process2.envRules = append(process1.envRules, envRules[2]...) + + gc.externalProcesses = []*externalProcess{process0, process1, process2} + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + t.Fatal(err) + } + + envs0 := buildEnvironmentVariablesFromEnvRules(envRules[0], testRand) + envs1 := buildEnvironmentVariablesFromEnvRules(envRules[1], testRand) + envs2 := buildEnvironmentVariablesFromEnvRules(envRules[2], testRand) + envList := append(envs0, envs1...) + envList = append(envList, envs2...) + + actual, _, err := policy.EnforceExecExternalProcessPolicy(gc.ctx, process2.command, envList, process2.workingDir) + + // getting an error means something is broken + if err != nil { + t.Errorf("Expected container creation to be allowed. It wasn't: %v", err) + } + + if !areStringArraysEqual(actual, envList) { + t.Error("environment variables were not dropped correctly.") + } +} + +func Test_Rego_ExecExternalProcessPolicy_DropEnvs_Multiple_NoMatch_Windows(t *testing.T) { + envRules := setupEnvRuleSets(3) + + gc := generateWindowsConstraints(testRand, 1) + gc.allowEnvironmentVariableDropping = true + + process0 := generateExternalProcess(testRand) + + process1 := process0.clone() + process1.envRules = append(envRules[0], envRules[1]...) + + process2 := process0.clone() + process2.envRules = append(envRules[0], envRules[2]...) + + gc.externalProcesses = []*externalProcess{process0, process1, process2} + securityPolicy := gc.toPolicy() + defaultMounts := generateMounts(testRand) + privilegedMounts := generateMounts(testRand) + + policy, err := newRegoPolicy(securityPolicy.marshalWindowsRego(), + toOCIMounts(defaultMounts), + toOCIMounts(privilegedMounts), + testOSType) + if err != nil { + t.Fatal(err) + } + + envs0 := buildEnvironmentVariablesFromEnvRules(envRules[0], testRand) + envs1 := buildEnvironmentVariablesFromEnvRules(envRules[1], testRand) + envs2 := buildEnvironmentVariablesFromEnvRules(envRules[2], testRand) + var extraLen int + if len(envs1) > len(envs2) { + extraLen = len(envs2) + } else { + extraLen = len(envs1) + } + envList := append(envs0, envs1[:extraLen]...) + envList = append(envList, envs2[:extraLen]...) + + actual, _, err := policy.EnforceExecExternalProcessPolicy(gc.ctx, process2.command, envList, process2.workingDir) + + // not getting an error means something is broken + if err == nil { + t.Error("expected container creation to not be allowed.") + } + + if actual != nil { + t.Error("envList should be nil.") + } +} + +func Test_Rego_ShutdownContainerPolicy_Running_Container_Windows(t *testing.T) { + p := generateWindowsConstraints(testRand, maxContainersInGeneratedConstraints) + + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Fatalf("Unable to set up test: %v", err) + } + + container := selectContainerFromRunningContainers(tc.runningContainers, testRand) + + err = tc.policy.EnforceShutdownContainerPolicy(p.ctx, container.containerID) + if err != nil { + t.Fatal("Expected shutdown of running container to be allowed, it wasn't") + } +} + +func Test_Rego_ShutdownContainerPolicy_Not_Running_Container_Windows(t *testing.T) { + p := generateWindowsConstraints(testRand, maxContainersInGeneratedConstraints) + + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Fatalf("Unable to set up test: %v", err) + } + + notRunningContainerID := testDataGenerator.uniqueContainerID() + + err = tc.policy.EnforceShutdownContainerPolicy(p.ctx, notRunningContainerID) + if err == nil { + t.Fatal("Expected shutdown of not running container to be denied, it wasn't") + } +} + +func Test_Rego_SignalContainerProcessPolicy_ExecProcess_Allowed_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + containerUnderTest := generateConstraintsWindowsContainer(testRand, 1, maxLayersInGeneratedContainer) + + ep := generateWindowsExecProcesses(testRand) + ep[0].Signals = generateListOfWindowsSignals(testRand, 1, 4) + containerUnderTest.ExecProcesses = ep + processUnderTest := ep[0] + + p.containers = append(p.containers, containerUnderTest) + + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + containerID, err := idForRunningWindowsContainer(containerUnderTest, tc.runningContainers) + if err != nil { + r, err := runWindowsContainer(tc.policy, containerUnderTest) + if err != nil { + t.Errorf("Unable to setup test running container: %v", err) + return false + } + containerID = r.containerID + } + + envList := buildEnvironmentVariablesFromEnvRules(containerUnderTest.EnvRules, testRand) + user := IDName{Name: containerUnderTest.User} + commandLine := []string{processUnderTest.Command} + + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, containerID, commandLine, envList, containerUnderTest.WorkingDir, user, nil) + if err != nil { + t.Errorf("Unable to exec process for test: %v", err) + return false + } + + signal := selectSignalFromWindowsSignals(testRand, processUnderTest.Signals) + opts := &SignalContainerOptions{ + WindowsSignal: signal, + WindowsCommand: commandLine, + } + + err = tc.policy.EnforceSignalContainerProcessPolicyV2(p.ctx, containerID, opts) + if err != nil { + t.Errorf("Signal init process unexpectedly failed: %v", err) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 5, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_SignalContainerProcessPolicy_ExecProcess_Allowed: %v", err) + } +} + +func Test_Rego_SignalContainerProcessPolicy_ExecProcess_Not_Allowed_Windows(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + containerUnderTest := generateConstraintsWindowsContainer(testRand, 1, maxLayersInGeneratedContainer) + + ep := generateWindowsExecProcesses(testRand) + ep[0].Signals = make([]guestrequest.SignalValueWCOW, 0) + containerUnderTest.ExecProcesses = ep + processUnderTest := ep[0] + + p.containers = append(p.containers, containerUnderTest) + + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + containerID, err := idForRunningWindowsContainer(containerUnderTest, tc.runningContainers) + if err != nil { + r, err := runWindowsContainer(tc.policy, containerUnderTest) + if err != nil { + t.Errorf("Unable to setup test running container: %v", err) + return false + } + containerID = r.containerID + } + + envList := buildEnvironmentVariablesFromEnvRules(containerUnderTest.EnvRules, testRand) + user := IDName{Name: containerUnderTest.User} + commandLine := []string{processUnderTest.Command} + + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, containerID, commandLine, envList, containerUnderTest.WorkingDir, user, nil) + if err != nil { + t.Errorf("Unable to exec process for test: %v", err) + return false + } + + signal := generateWindowsSignal(testRand) + + opts := &SignalContainerOptions{ + WindowsSignal: signal, + WindowsCommand: commandLine, + } + + err = tc.policy.EnforceSignalContainerProcessPolicyV2(p.ctx, containerID, opts) + if err == nil { + t.Errorf("Signal init process unexpectedly succeeded: %v", err) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_SignalContainerProcessPolicy_ExecProcess_Not_Allowed: %v", err) + } +} + +func Test_Rego_SignalContainerProcessPolicy_ExecProcess_Bad_Command(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + containerUnderTest := generateConstraintsWindowsContainer(testRand, 1, maxLayersInGeneratedContainer) + + ep := generateWindowsExecProcesses(testRand) + ep[0].Signals = generateListOfWindowsSignals(testRand, 1, 4) + containerUnderTest.ExecProcesses = ep + processUnderTest := ep[0] + + p.containers = append(p.containers, containerUnderTest) + + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + containerID, err := idForRunningWindowsContainer(containerUnderTest, tc.runningContainers) + if err != nil { + r, err := runWindowsContainer(tc.policy, containerUnderTest) + if err != nil { + t.Errorf("Unable to setup test running container: %v", err) + return false + } + containerID = r.containerID + } + + envList := buildEnvironmentVariablesFromEnvRules(containerUnderTest.EnvRules, testRand) + user := IDName{Name: containerUnderTest.User} + commandLine := []string{processUnderTest.Command} + + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, containerID, commandLine, envList, containerUnderTest.WorkingDir, user, nil) + if err != nil { + t.Errorf("Unable to exec process for test: %v", err) + return false + } + + signal := selectSignalFromWindowsSignals(testRand, processUnderTest.Signals) + badCommand := generateCommand(testRand) + + opts := &SignalContainerOptions{ + WindowsSignal: signal, + WindowsCommand: badCommand, + } + + err = tc.policy.EnforceSignalContainerProcessPolicyV2(p.ctx, containerID, opts) + if err == nil { + t.Errorf("Signal init process unexpectedly succeeded: %v", err) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_SignalContainerProcessPolicy_ExecProcess_Bad_Command: %v", err) + } +} + +func Test_Rego_SignalContainerProcessPolicy_ExecProcess_Bad_ContainerID(t *testing.T) { + f := func(p *generatedWindowsConstraints) bool { + containerUnderTest := generateConstraintsWindowsContainer(testRand, 1, maxLayersInGeneratedContainer) + + ep := generateWindowsExecProcesses(testRand) + ep[0].Signals = generateListOfWindowsSignals(testRand, 1, 4) + containerUnderTest.ExecProcesses = ep + processUnderTest := ep[0] + + p.containers = append(p.containers, containerUnderTest) + + tc, err := setupRegoRunningWindowsContainerTest(p) + if err != nil { + t.Error(err) + return false + } + + containerID, err := idForRunningWindowsContainer(containerUnderTest, tc.runningContainers) + if err != nil { + r, err := runWindowsContainer(tc.policy, containerUnderTest) + if err != nil { + t.Errorf("Unable to setup test running container: %v", err) + return false + } + containerID = r.containerID + } + + envList := buildEnvironmentVariablesFromEnvRules(containerUnderTest.EnvRules, testRand) + user := IDName{Name: containerUnderTest.User} + commandLine := []string{processUnderTest.Command} + + _, _, _, err = tc.policy.EnforceExecInContainerPolicyV2(p.ctx, containerID, commandLine, envList, containerUnderTest.WorkingDir, user, nil) + if err != nil { + t.Errorf("Unable to exec process for test: %v", err) + return false + } + + signal := selectSignalFromWindowsSignals(testRand, processUnderTest.Signals) + badContainerID := generateContainerID(testRand) + + opts := &SignalContainerOptions{ + WindowsSignal: signal, + WindowsCommand: commandLine, + } + + err = tc.policy.EnforceSignalContainerProcessPolicyV2(p.ctx, badContainerID, opts) + if err == nil { + t.Errorf("Signal init process unexpectedly succeeded: %v", err) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 25, Rand: testRand}); err != nil { + t.Errorf("Test_Rego_SignalContainerProcessPolicy_ExecProcess_Bad_ContainerID: %v", err) + } +} + +func Test_Rego_GetPropertiesPolicy_On(t *testing.T) { + f := func(constraints *generatedWindowsConstraints) bool { + tc, err := setupGetPropertiesTestWindows(constraints, true) + if err != nil { + t.Error(err) + return false + } + + err = tc.policy.EnforceGetPropertiesPolicy(constraints.ctx) + if err != nil { + t.Error("Policy enforcement unexpectedly was denied") + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50}); err != nil { + t.Errorf("Test_Rego_GetPropertiesPolicy_On: %v", err) + } +} + +func Test_Rego_GetPropertiesPolicy_Off(t *testing.T) { + f := func(constraints *generatedWindowsConstraints) bool { + tc, err := setupGetPropertiesTestWindows(constraints, false) + if err != nil { + t.Error(err) + return false + } + + err = tc.policy.EnforceGetPropertiesPolicy(constraints.ctx) + if err == nil { + t.Error("Policy enforcement unexpectedly was allowed") + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50}); err != nil { + t.Errorf("Test_Rego_GetPropertiesPolicy_Off: %v", err) + } +} + +func Test_Rego_DumpStacksPolicy_On(t *testing.T) { + f := func(constraints *generatedWindowsConstraints) bool { + tc, err := setupDumpStacksTestWindows(constraints, true) + if err != nil { + t.Error(err) + return false + } + + err = tc.policy.EnforceDumpStacksPolicy(constraints.ctx) + if err != nil { + t.Errorf("Policy enforcement unexpectedly was denied: %v", err) + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50}); err != nil { + t.Errorf("Test_Rego_DumpStacksPolicy_On: %v", err) + } +} + +func Test_Rego_DumpStacksPolicy_Off(t *testing.T) { + f := func(constraints *generatedWindowsConstraints) bool { + tc, err := setupDumpStacksTestWindows(constraints, false) + if err != nil { + t.Error(err) + return false + } + + err = tc.policy.EnforceDumpStacksPolicy(constraints.ctx) + if err == nil { + t.Error("Policy enforcement unexpectedly was allowed") + return false + } + + return true + } + + if err := quick.Check(f, &quick.Config{MaxCount: 50}); err != nil { + t.Errorf("Test_Rego_DumpStacksPolicy_Off: %v", err) + } +} diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index 2385fe3665..2bb8b0e1d9 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -12,7 +12,8 @@ import ( "syscall" "github.com/Microsoft/hcsshim/internal/guestpath" - "github.com/opencontainers/runtime-spec/specs-go" + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" + specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) @@ -188,6 +189,11 @@ type ExecProcessConfig struct { Signals []syscall.Signal `json:"signals" toml:"signals"` } +type WindowsExecProcessConfig struct { + Command string `json:"command" toml:"command"` + Signals []guestrequest.SignalValueWCOW `json:"signals" toml:"signals"` +} + // CapabilitiesConfig contains the toml or JSON config for capabilies security // polict constraint description type CapabilitiesConfig struct { @@ -255,7 +261,8 @@ type SecurityPolicy struct { // everything". AllowAll bool `json:"allow_all"` // One or more containers that are allowed to run - Containers Containers `json:"containers"` + Containers Containers `json:"containers"` + WindowsContainers WindowsContainers `json:"windows_containers"` } // EncodeToString returns base64 encoded string representation of SecurityPolicy. @@ -272,6 +279,11 @@ type Containers struct { Elements map[string]Container `json:"elements"` } +type WindowsContainers struct { + Length int `json:"length"` + Elements map[string]WindowsContainer `json:"elements"` +} + type Container struct { Command CommandArgs `json:"command"` EnvRules EnvRules `json:"env_rules"` @@ -288,6 +300,17 @@ type Container struct { SeccompProfileSHA256 string `json:"-"` } +type WindowsContainer struct { + Command CommandArgs `json:"command"` + EnvRules EnvRules `json:"env_rules"` + Layers Layers `json:"layers"` + WorkingDir string `json:"working_dir"` + ExecProcesses []WindowsExecProcessConfig `json:"-"` + Signals []guestrequest.SignalValueWCOW `json:"-"` + AllowStdioAccess bool `json:"-"` + User string `json:"-"` +} + // StringArrayMap wraps an array of strings as a string map. type StringArrayMap struct { Length int `json:"length"` diff --git a/pkg/securitypolicy/securitypolicy_internal.go b/pkg/securitypolicy/securitypolicy_internal.go index 4dba1c7242..0057de5077 100644 --- a/pkg/securitypolicy/securitypolicy_internal.go +++ b/pkg/securitypolicy/securitypolicy_internal.go @@ -4,6 +4,8 @@ import ( "fmt" "strconv" "syscall" + + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" ) // Internal version of SecurityPolicy @@ -19,6 +21,19 @@ type securityPolicyInternal struct { AllowCapabilityDropping bool } +// Internal version of Windows SecurityPolicy +type securityPolicyWindowsInternal struct { + Containers []*securityPolicyWindowsContainer + ExternalProcesses []*externalProcess + Fragments []*fragment + AllowPropertiesAccess bool + AllowDumpStacks bool + AllowRuntimeLogging bool + AllowEnvironmentVariableDropping bool + AllowUnencryptedScratch bool + AllowCapabilityDropping bool +} + type securityPolicyFragment struct { Namespace string SVN string @@ -40,6 +55,18 @@ func containersToInternal(containers []*Container) ([]*securityPolicyContainer, return result, nil } +func windowsContainersToInternal(containers []*WindowsContainer) ([]*securityPolicyWindowsContainer, error) { + result := make([]*securityPolicyWindowsContainer, len(containers)) + for i, cConf := range containers { + cInternal, err := cConf.toInternal() + if err != nil { + return nil, err + } + result[i] = cInternal + } + return result, nil +} + func externalProcessToInternal(externalProcesses []ExternalProcessConfig) []*externalProcess { result := make([]*externalProcess, len(externalProcesses)) for i, pConf := range externalProcesses { @@ -94,7 +121,8 @@ func newSecurityPolicyFragment( svn string, containers []*Container, externalProcesses []ExternalProcessConfig, - fragments []FragmentConfig) (*securityPolicyFragment, error) { + fragments []FragmentConfig, +) (*securityPolicyFragment, error) { containersInternal, err := containersToInternal(containers) if err != nil { return nil, err @@ -124,30 +152,63 @@ type securityPolicyContainer struct { // will default to. WorkingDir string `json:"working_dir"` // A list of constraints for determining if a given mount is allowed. - Mounts []mountInternal `json:"mounts"` + Mounts []mountInternal `json:"mounts,omitempty"` AllowElevated bool `json:"allow_elevated"` // A list of lists of commands that can be used to execute additional // processes within the container ExecProcesses []containerExecProcess `json:"exec_processes"` // A list of signals that are allowed to be sent to the container's init // process. - Signals []syscall.Signal `json:"signals"` + Signals []syscall.Signal `json:"signals,omitempty"` + // Whether to allow the capture of init process standard out and standard error AllowStdioAccess bool `json:"allow_stdio_access"` // Whether to deny new privileges NoNewPrivileges bool `json:"no_new_privileges"` // The user that the container will run as - User UserConfig `json:"user"` + User UserConfig `json:"user,omitempty"` // Capability sets for the container Capabilities *capabilitiesInternal `json:"capabilities"` // Seccomp configuration for the container SeccompProfileSHA256 string `json:"seccomp_profile_sha256"` } +// Internal version of Container +type securityPolicyWindowsContainer struct { + // The command that we will allow the container to execute + Command []string `json:"command"` + // The rules for determining if a given environment variable is allowed + EnvRules []EnvRuleConfig `json:"env_rules"` + // An ordered list of dm-verity root hashes for each layer that makes up + // "a container". Containers are constructed as an overlay file system. The + // order that the layers are overlayed is important and needs to be enforced + // as part of policy. + Layers []string `json:"layers"` + // WorkingDir is a path to container's working directory, which all the processes + // will default to. + WorkingDir string `json:"working_dir"` + // A list of lists of commands that can be used to execute additional + // processes within the container + ExecProcesses []windowsContainerExecProcess `json:"exec_processes"` + // A list of signals that are allowed to be sent to the container's init + // process + Signals []guestrequest.SignalValueWCOW `json:"signals,omitempty"` + // Whether to allow the capture of init process standard out and standard error + AllowStdioAccess bool `json:"allow_stdio_access"` + // The user that the container will run as + User string `json:"user,omitempty"` +} + type containerExecProcess struct { Command []string `json:"command"` // A list of signals that are allowed to be sent to this process - Signals []syscall.Signal `json:"signals"` + Signals []syscall.Signal `json:"signals,omitempty"` +} + +type windowsContainerExecProcess struct { + Command string `json:"command"` + // A list of signals that are allowed to be sent to this process + Signals []guestrequest.SignalValueWCOW `json:"signals,omitempty"` } type externalProcess struct { @@ -197,16 +258,15 @@ func (c *Container) toInternal() (*securityPolicyContainer, error) { return nil, err } - mounts, err := c.Mounts.toInternal() - if err != nil { - return nil, err - } - execProcesses := make([]containerExecProcess, len(c.ExecProcesses)) for i, ep := range c.ExecProcesses { execProcesses[i] = containerExecProcess(ep) } + mounts, err := c.Mounts.toInternal() + if err != nil { + return nil, err + } var capabilities *capabilitiesInternal if c.Capabilities != nil { c := c.Capabilities.toInternal() @@ -230,6 +290,40 @@ func (c *Container) toInternal() (*securityPolicyContainer, error) { Capabilities: capabilities, SeccompProfileSHA256: c.SeccompProfileSHA256, }, nil + +} + +func (c *WindowsContainer) toInternal() (*securityPolicyWindowsContainer, error) { + command, err := c.Command.toInternal() + if err != nil { + return nil, err + } + + envRules, err := c.EnvRules.toInternal() + if err != nil { + return nil, err + } + + layers, err := c.Layers.toInternal() + if err != nil { + return nil, err + } + + execProcesses := make([]windowsContainerExecProcess, len(c.ExecProcesses)) + for i, ep := range c.ExecProcesses { + execProcesses[i] = windowsContainerExecProcess(ep) + } + + return &securityPolicyWindowsContainer{ + Command: command, + EnvRules: envRules, + Layers: layers, + WorkingDir: c.WorkingDir, + ExecProcesses: execProcesses, + Signals: c.Signals, + AllowStdioAccess: c.AllowStdioAccess, + User: c.User, + }, nil } func (c CommandArgs) toInternal() ([]string, error) { diff --git a/pkg/securitypolicy/securitypolicy_linux.go b/pkg/securitypolicy/securitypolicy_linux.go new file mode 100644 index 0000000000..6296c33a17 --- /dev/null +++ b/pkg/securitypolicy/securitypolicy_linux.go @@ -0,0 +1,139 @@ +//go:build linux +// +build linux + +package securitypolicy + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + specInternal "github.com/Microsoft/hcsshim/internal/guest/spec" + "github.com/Microsoft/hcsshim/internal/guestpath" + "github.com/moby/sys/user" + oci "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +//nolint:unused +const osType = "linux" + +// This is being used by StandEnforcer. +// substituteUVMPath substitutes mount prefix to an appropriate path inside +// UVM. At policy generation time, it's impossible to tell what the sandboxID +// will be, so the prefix substitution needs to happen during runtime. +func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { + if strings.HasPrefix(m.Source, guestpath.SandboxMountPrefix) { + m.Source = specInternal.SandboxMountSource(sandboxID, m.Source) + } else if strings.HasPrefix(m.Source, guestpath.HugePagesMountPrefix) { + m.Source = specInternal.HugePagesMountSource(sandboxID, m.Source) + } + return m +} + +// SandboxMountsDir returns sandbox mounts directory inside UVM/host. +func SandboxMountsDir(sandboxID string) string { + return specInternal.SandboxMountsDir((sandboxID)) +} + +// HugePagesMountsDir returns hugepages mounts directory inside UVM. +func HugePagesMountsDir(sandboxID string) string { + return specInternal.HugePagesMountsDir(sandboxID) +} + +func GetAllUserInfo(process *oci.Process, rootPath string) ( + userIDName IDName, + groupIDNames []IDName, + umask string, + err error, +) { + passwdPath := filepath.Join(rootPath, "/etc/passwd") + groupPath := filepath.Join(rootPath, "/etc/group") + + if process == nil { + err = errors.New("spec.Process is nil") + return + } + + // this default value is used in the Linux kernel if no umask is specified + umask = "0022" + if process.User.Umask != nil { + umask = fmt.Sprintf("%04o", *process.User.Umask) + } + + parsedUserStr := false + checkGroup := true + + if process.User.Username != "" { + var usrInfo specInternal.ParseUserStrResult + usrInfo, err = specInternal.ParseUserStr(rootPath, process.User.Username) + if err != nil { + err = errors.Errorf("failed to parse user str %q: %v", process.User.Username, err) + return + } + userIDName = IDName{ID: strconv.FormatUint(uint64(usrInfo.UID), 10), Name: usrInfo.Username} + groupIDNames = []IDName{ + {ID: strconv.FormatUint(uint64(usrInfo.GID), 10), Name: usrInfo.Groupname}, + } + parsedUserStr = true + } + + if !parsedUserStr { + // fallback UID/GID lookup + uid := process.User.UID + userIDName = IDName{ID: strconv.FormatUint(uint64(uid), 10), Name: ""} + if _, err = os.Stat(passwdPath); err == nil { + var userInfo user.User + userInfo, err = specInternal.GetUser(rootPath, func(user user.User) bool { + return uint32(user.Uid) == uid + }) + + if err != nil { + return userIDName, nil, "", err + } + + userIDName.Name = userInfo.Name + } + + gid := process.User.GID + groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} + + if _, err = os.Stat(groupPath); err == nil { + var groupInfo user.Group + groupInfo, err = specInternal.GetGroup(rootPath, func(group user.Group) bool { + return uint32(group.Gid) == gid + }) + + if err != nil { + return userIDName, nil, "", err + } + groupIDName.Name = groupInfo.Name + } else { + checkGroup = false + } + + groupIDNames = []IDName{groupIDName} + } + + additionalGIDs := process.User.AdditionalGids + if len(additionalGIDs) > 0 { + for _, gid := range additionalGIDs { + groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} + if checkGroup { + var groupInfo user.Group + groupInfo, err = specInternal.GetGroup(rootPath, func(group user.Group) bool { + return uint32(group.Gid) == gid + }) + if err != nil { + return userIDName, nil, "", err + } + groupIDName.Name = groupInfo.Name + } + groupIDNames = append(groupIDNames, groupIDName) + } + } + + return userIDName, groupIDNames, umask, nil +} diff --git a/pkg/securitypolicy/securitypolicy_marshal.go b/pkg/securitypolicy/securitypolicy_marshal.go index f9ca3a3013..47836bd5ab 100644 --- a/pkg/securitypolicy/securitypolicy_marshal.go +++ b/pkg/securitypolicy/securitypolicy_marshal.go @@ -11,20 +11,9 @@ import ( "fmt" "strings" "syscall" -) -type marshalFunc func( - allowAll bool, - containers []*Container, - externalProcesses []ExternalProcessConfig, - fragments []FragmentConfig, - allowPropertiesAccess bool, - allowDumpStacks bool, - allowRuntimeLogging bool, - allowEnvironmentVariableDropping bool, - allowUnencryptedScratch bool, - allowCapabilityDropping bool, -) (string, error) + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" +) const ( jsonMarshaller = "json" @@ -32,13 +21,13 @@ const ( ) var ( - registeredMarshallers = map[string]marshalFunc{} + registeredMarshallers = map[string]OSAwareMarshalFunc{} defaultMarshaller = jsonMarshaller ) func init() { registeredMarshallers[jsonMarshaller] = marshalJSON - registeredMarshallers[regoMarshaller] = marshalRego + registeredMarshallers[regoMarshaller] = osAwareMarshalRego } //go:embed policy.rego @@ -49,9 +38,127 @@ var openDoorRegoTemplate string var openDoorRego = strings.Replace(openDoorRegoTemplate, "@@API_VERSION@@", apiVersion, 1) +// ContainerInterface represents either Container or WindowsContainer +type ContainerInterface interface { + ToInternalContainer() (interface{}, error) +} + +// Implement ContainerInterface for Container +func (c *Container) ToInternalContainer() (interface{}, error) { + return c.toInternal() +} + +// Implement ContainerInterface for WindowsContainer +func (c *WindowsContainer) ToInternalContainer() (interface{}, error) { + return c.toInternal() +} + +// OSAwareMarshalFunc is like marshalFunc but works with mixed container types +type OSAwareMarshalFunc func( + allowAll bool, + linuxContainers []*Container, + windowsContainers []*WindowsContainer, + osType string, + externalProcesses []ExternalProcessConfig, + fragments []FragmentConfig, + allowPropertiesAccess bool, + allowDumpStacks bool, + allowRuntimeLogging bool, + allowEnvironmentVariableDropping bool, + allowUnencryptedScratch bool, + allowCapabilityDropping bool, +) (string, error) + +// osAwareMarshalRego handles both Linux and Windows containers +func osAwareMarshalRego( + allowAll bool, + linuxContainers []*Container, + windowsContainers []*WindowsContainer, + osType string, + externalProcesses []ExternalProcessConfig, + fragments []FragmentConfig, + allowPropertiesAccess bool, + allowDumpStacks bool, + allowRuntimeLogging bool, + allowEnvironmentVariableDropping bool, + allowUnencryptedScratch bool, + allowCapabilityDropping bool, +) (string, error) { + if allowAll { + if len(linuxContainers) > 0 || len(windowsContainers) > 0 { + return "", ErrInvalidOpenDoorPolicy + } + return openDoorRego, nil + } + + switch osType { + case "linux": + if len(windowsContainers) > 0 { + return "", fmt.Errorf("cannot marshal Windows containers on Linux OS") + } + return marshalRego(allowAll, linuxContainers, externalProcesses, fragments, + allowPropertiesAccess, allowDumpStacks, allowRuntimeLogging, + allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapabilityDropping) + + case "windows": + if len(linuxContainers) > 0 { + return "", fmt.Errorf("cannot marshal Linux containers on Windows OS") + } + return marshalWindowsRego(allowAll, windowsContainers, externalProcesses, fragments, + allowPropertiesAccess, allowDumpStacks, allowRuntimeLogging, + allowEnvironmentVariableDropping, allowUnencryptedScratch, allowCapabilityDropping) + + default: + return "", fmt.Errorf("unsupported OS type: %s", osType) + } +} + +// marshalWindowsRego creates Rego policy for Windows containers +func marshalWindowsRego( + allowAll bool, + containers []*WindowsContainer, + externalProcesses []ExternalProcessConfig, + fragments []FragmentConfig, + allowPropertiesAccess bool, + allowDumpStacks bool, + allowRuntimeLogging bool, + allowEnvironmentVariableDropping bool, + allowUnencryptedScratch bool, + allowCapabilityDropping bool, +) (string, error) { + if allowAll { + if len(containers) > 0 { + return "", ErrInvalidOpenDoorPolicy + } + return openDoorRego, nil + } + + // Convert WindowsContainer to internal Windows container representation + windowsContainersInternal, err := windowsContainersToInternal(containers) + if err != nil { + return "", err + } + + policy := &securityPolicyWindowsInternal{ + Containers: windowsContainersInternal, + ExternalProcesses: externalProcessToInternal(externalProcesses), + Fragments: fragmentsToInternal(fragments), + AllowPropertiesAccess: allowPropertiesAccess, + AllowDumpStacks: allowDumpStacks, + AllowRuntimeLogging: allowRuntimeLogging, + AllowEnvironmentVariableDropping: allowEnvironmentVariableDropping, + AllowUnencryptedScratch: allowUnencryptedScratch, + AllowCapabilityDropping: allowCapabilityDropping, + } + + return policy.marshalWindowsRego(), nil +} + func marshalJSON( allowAll bool, containers []*Container, + windowsContainers []*WindowsContainer, + osType string, _ []ExternalProcessConfig, _ []FragmentConfig, _ bool, @@ -155,6 +262,8 @@ func MarshalPolicy( return marshal( allowAll, containers, + nil, + "linux", externalProcesses, fragments, allowPropertiesAccess, @@ -311,6 +420,52 @@ func writeMounts(builder *strings.Builder, mounts []mountInternal, indent string writeLine(builder, `%s"mounts": [%s],`, indent, strings.Join(values, ",")) } +// Windows-specific marshal functions +func writeWindowsSignals(builder *strings.Builder, signals []guestrequest.SignalValueWCOW, indent string) { + signalsArray := make([]string, len(signals)) + for i, s := range signals { + signalsArray[i] = string(s) + } + array := (stringArray(signalsArray)).marshalRego() + writeLine(builder, `%s"signals": %s,`, indent, array) +} + +func writeWindowsUser(builder *strings.Builder, user string, indent string) { + writeLine(builder, `%s"user": "%s",`, indent, user) +} + +func (p windowsContainerExecProcess) marshalRego() string { + commandLine := []string{p.Command} + command := stringArray(commandLine).marshalRego() + signalsArray := make([]string, len(p.Signals)) + for i, s := range p.Signals { + signalsArray[i] = string(s) + } + signals := stringArray(signalsArray).marshalRego() + return fmt.Sprintf(`{"command": %s, "signals": %s}`, command, signals) +} + +func writeWindowsExecProcesses(builder *strings.Builder, execProcesses []windowsContainerExecProcess, indent string) { + values := make([]string, len(execProcesses)) + for i, process := range execProcesses { + values[i] = process.marshalRego() + } + writeLine(builder, `%s"exec_processes": [%s],`, indent, strings.Join(values, ",")) +} + +func writeWindowsContainer(builder *strings.Builder, container *securityPolicyWindowsContainer, indent string) { + writeLine(builder, "%s{", indent) + writeCommand(builder, container.Command, indent+indentUsing) + writeEnvRules(builder, container.EnvRules, indent+indentUsing) + writeLayers(builder, container.Layers, indent+indentUsing) + writeWindowsExecProcesses(builder, container.ExecProcesses, indent+indentUsing) + writeWindowsSignals(builder, container.Signals, indent+indentUsing) + writeWindowsUser(builder, container.User, indent+indentUsing) + writeLine(builder, `%s"working_dir": "%s",`, indent+indentUsing, container.WorkingDir) + writeLine(builder, `%s"allow_stdio_access": %t,`, indent+indentUsing, container.AllowStdioAccess) + writeLine(builder, "%s},", indent) +} + func (p containerExecProcess) marshalRego() string { command := stringArray(p.Command).marshalRego() signals := signalArray(p.Signals).marshalRego() @@ -449,3 +604,32 @@ func (p securityPolicyFragment) marshalRego() string { addExternalProcesses(builder, p.ExternalProcesses) return fmt.Sprintf("package %s\n\nsvn := \"%s\"\nframework_version := \"%s\"\n\n%s", p.Namespace, p.SVN, frameworkVersion, builder.String()) } + +func (p securityPolicyWindowsInternal) marshalWindowsRego() string { + builder := new(strings.Builder) + addFragments(builder, p.Fragments) + addWindowsContainers(builder, p.Containers) + addExternalProcesses(builder, p.ExternalProcesses) + writeLine(builder, `allow_properties_access := %t`, p.AllowPropertiesAccess) + writeLine(builder, `allow_dump_stacks := %t`, p.AllowDumpStacks) + writeLine(builder, `allow_runtime_logging := %t`, p.AllowRuntimeLogging) + 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) + result := strings.Replace(policyRegoTemplate, "@@OBJECTS@@", builder.String(), 1) + result = strings.Replace(result, "@@API_VERSION@@", apiVersion, 1) + result = strings.Replace(result, "@@FRAMEWORK_VERSION@@", frameworkVersion, 1) + return result +} + +func addWindowsContainers(builder *strings.Builder, containers []*securityPolicyWindowsContainer) { + if len(containers) == 0 { + return + } + + writeLine(builder, "containers := [") + for _, container := range containers { + writeWindowsContainer(builder, container, indentUsing) + } + writeLine(builder, "]") +} diff --git a/pkg/securitypolicy/securitypolicy_uvmpath_linux.go b/pkg/securitypolicy/securitypolicy_uvmpath_linux.go deleted file mode 100644 index 4fdfaafdcc..0000000000 --- a/pkg/securitypolicy/securitypolicy_uvmpath_linux.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build linux -// +build linux - -package securitypolicy - -import ( - "strings" - - specInternal "github.com/Microsoft/hcsshim/internal/guest/spec" - "github.com/Microsoft/hcsshim/internal/guestpath" -) - -// This is being used by StandEnforcer. -// substituteUVMPath substitutes mount prefix to an appropriate path inside -// UVM. At policy generation time, it's impossible to tell what the sandboxID -// will be, so the prefix substitution needs to happen during runtime. -func substituteUVMPath(sandboxID string, m mountInternal) mountInternal { - if strings.HasPrefix(m.Source, guestpath.SandboxMountPrefix) { - m.Source = specInternal.SandboxMountSource(sandboxID, m.Source) - } else if strings.HasPrefix(m.Source, guestpath.HugePagesMountPrefix) { - m.Source = specInternal.HugePagesMountSource(sandboxID, m.Source) - } - return m -} - -// SandboxMountsDir returns sandbox mounts directory inside UVM/host. -func SandboxMountsDir(sandboxID string) string { - return specInternal.SandboxMountsDir((sandboxID)) -} - -// HugePagesMountsDir returns hugepages mounts directory inside UVM. -func HugePagesMountsDir(sandboxID string) string { - return specInternal.HugePagesMountsDir(sandboxID) -} diff --git a/pkg/securitypolicy/securitypolicy_uvmpath_windows.go b/pkg/securitypolicy/securitypolicy_windows.go similarity index 75% rename from pkg/securitypolicy/securitypolicy_uvmpath_windows.go rename to pkg/securitypolicy/securitypolicy_windows.go index cc6949fa68..26b3515a33 100644 --- a/pkg/securitypolicy/securitypolicy_uvmpath_windows.go +++ b/pkg/securitypolicy/securitypolicy_windows.go @@ -3,6 +3,11 @@ package securitypolicy +import oci "github.com/opencontainers/runtime-spec/specs-go" + +//nolint:unused +const osType = "windows" + // This is being used by StandEnforcer and is a no-op for windows. // substituteUVMPath substitutes mount prefix to an appropriate path inside // UVM. At policy generation time, it's impossible to tell what the sandboxID @@ -22,3 +27,7 @@ func SandboxMountsDir(sandboxID string) string { func HugePagesMountsDir(sandboxID string) string { return "" } + +func GetAllUserInfo(process *oci.Process, rootPath string) (IDName, []IDName, string, error) { + return IDName{}, []IDName{}, "", nil +} diff --git a/pkg/securitypolicy/securitypolicyenforcer.go b/pkg/securitypolicy/securitypolicyenforcer.go index 1dae5412ce..b951a607d4 100644 --- a/pkg/securitypolicy/securitypolicyenforcer.go +++ b/pkg/securitypolicy/securitypolicyenforcer.go @@ -10,6 +10,7 @@ import ( "sync" "syscall" + "github.com/Microsoft/hcsshim/internal/protocol/guestrequest" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) @@ -18,6 +19,33 @@ type createEnforcerFunc func(base64EncodedPolicy string, criMounts, criPrivilege type EnvList []string +type ExecOptions struct { + Groups []IDName // optional: empty slice or nil + Umask string // optional: "" means unspecified + Capabilities *oci.LinuxCapabilities // optional: nil means "none" + NoNewPrivileges *bool // optional: nil means "not set" +} + +type CreateContainerOptions struct { + SandboxID string + Privileged *bool + NoNewPrivileges *bool + Groups []IDName + Umask string + Capabilities *oci.LinuxCapabilities + SeccompProfileSHA256 string +} + +type SignalContainerOptions struct { + IsInitProcess bool + // One of these will be set depending on platform + LinuxSignal syscall.Signal + WindowsSignal guestrequest.SignalValueWCOW + + LinuxStartupArgs []string + WindowsCommand []string +} + const ( openDoorEnforcer = "open_door" standardEnforcer = "standard" @@ -54,6 +82,16 @@ type SecurityPolicyEnforcer interface { capabilities *oci.LinuxCapabilities, seccompProfileSHA256 string, ) (EnvList, *oci.LinuxCapabilities, bool, error) + EnforceCreateContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + mounts []oci.Mount, + user IDName, + opts *CreateContainerOptions, + ) (EnvList, *oci.LinuxCapabilities, bool, error) ExtendDefaultMounts([]oci.Mount) error EncodedSecurityPolicy() string EnforceExecInContainerPolicy( @@ -68,9 +106,19 @@ type SecurityPolicyEnforcer interface { umask string, capabilities *oci.LinuxCapabilities, ) (EnvList, *oci.LinuxCapabilities, bool, error) + EnforceExecInContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + user IDName, + opts *ExecOptions, + ) (EnvList, *oci.LinuxCapabilities, bool, error) EnforceExecExternalProcessPolicy(ctx context.Context, argList []string, envList []string, workingDir string) (EnvList, bool, error) EnforceShutdownContainerPolicy(ctx context.Context, containerID string) error EnforceSignalContainerProcessPolicy(ctx context.Context, containerID string, signal syscall.Signal, isInitProcess bool, startupArgList []string) error + EnforceSignalContainerProcessPolicyV2(ctx context.Context, containerID string, opts *SignalContainerOptions) error EnforcePlan9MountPolicy(ctx context.Context, target string) (err error) EnforcePlan9UnmountPolicy(ctx context.Context, target string) (err error) EnforceGetPropertiesPolicy(ctx context.Context) error @@ -80,17 +128,18 @@ type SecurityPolicyEnforcer interface { EnforceScratchMountPolicy(ctx context.Context, scratchPath string, encrypted bool) (err error) EnforceScratchUnmountPolicy(ctx context.Context, scratchPath string) (err error) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) + EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string) (err error) } -//nolint +//nolint:unused type stringSet map[string]struct{} -//nolint +//nolint:unused func (s stringSet) add(item string) { s[item] = struct{}{} } -//nolint +//nolint:unused func (s stringSet) contains(item string) bool { _, contains := s[item] return contains @@ -212,6 +261,7 @@ func CreateSecurityPolicyEnforcer( enforcer = openDoorEnforcer } } + if createEnforcer, ok := registeredEnforcers[enforcer]; !ok { return nil, fmt.Errorf("unknown enforcer: %q", enforcer) } else { @@ -508,12 +558,37 @@ func (pe *StandardSecurityPolicyEnforcer) EnforceCreateContainerPolicy( return envList, caps, true, nil } +func (*StandardSecurityPolicyEnforcer) EnforceCreateContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + mounts []oci.Mount, + user IDName, + opts *CreateContainerOptions, +) (EnvList, *oci.LinuxCapabilities, bool, error) { + return envList, opts.Capabilities, true, nil +} + // Stub. We are deprecating the standard enforcer. Newly added enforcement // points are simply allowed. func (*StandardSecurityPolicyEnforcer) EnforceExecInContainerPolicy(_ context.Context, _ string, _ []string, envList []string, _ string, _ bool, _ IDName, _ []IDName, _ string, caps *oci.LinuxCapabilities) (EnvList, *oci.LinuxCapabilities, bool, error) { return envList, caps, true, nil } +func (*StandardSecurityPolicyEnforcer) EnforceExecInContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + user IDName, + opts *ExecOptions, +) (EnvList, *oci.LinuxCapabilities, bool, error) { + return envList, opts.Capabilities, true, nil +} + // Stub. We are deprecating the standard enforcer. Newly added enforcement // points are simply allowed. func (*StandardSecurityPolicyEnforcer) EnforceExecExternalProcessPolicy(_ context.Context, _ []string, envList []string, _ string) (EnvList, bool, error) { @@ -532,6 +607,10 @@ func (*StandardSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicy(conte return nil } +func (*StandardSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicyV2(ctx context.Context, containerID string, opts *SignalContainerOptions) error { + return nil +} + // Stub. We are deprecating the standard enforcer. Newly added enforcement // points are simply allowed. func (*StandardSecurityPolicyEnforcer) EnforcePlan9MountPolicy(context.Context, string) error { @@ -586,6 +665,10 @@ func (StandardSecurityPolicyEnforcer) EnforceScratchUnmountPolicy(context.Contex return nil } +func (StandardSecurityPolicyEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string) error { + return nil +} + // Stub. We are deprecating the standard enforcer. func (StandardSecurityPolicyEnforcer) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) { return IDName{}, nil, "", nil @@ -895,10 +978,35 @@ func (OpenDoorSecurityPolicyEnforcer) EnforceCreateContainerPolicy(_ context.Con return envList, caps, true, nil } +func (OpenDoorSecurityPolicyEnforcer) EnforceCreateContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + mounts []oci.Mount, + user IDName, + opts *CreateContainerOptions, +) (EnvList, *oci.LinuxCapabilities, bool, error) { + return envList, opts.Capabilities, true, nil +} + func (OpenDoorSecurityPolicyEnforcer) EnforceExecInContainerPolicy(_ context.Context, _ string, _ []string, envList []string, _ string, _ bool, _ IDName, _ []IDName, _ string, caps *oci.LinuxCapabilities) (EnvList, *oci.LinuxCapabilities, bool, error) { return envList, caps, true, nil } +func (OpenDoorSecurityPolicyEnforcer) EnforceExecInContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + user IDName, + opts *ExecOptions, +) (EnvList, *oci.LinuxCapabilities, bool, error) { + return envList, opts.Capabilities, true, nil +} + func (OpenDoorSecurityPolicyEnforcer) EnforceExecExternalProcessPolicy(_ context.Context, _ []string, envList []string, _ string) (EnvList, bool, error) { return envList, true, nil } @@ -911,6 +1019,10 @@ func (*OpenDoorSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicy(conte return nil } +func (*OpenDoorSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicyV2(ctx context.Context, containerID string, opts *SignalContainerOptions) error { + return nil +} + func (*OpenDoorSecurityPolicyEnforcer) EnforcePlan9MountPolicy(context.Context, string) error { return nil } @@ -955,6 +1067,10 @@ func (OpenDoorSecurityPolicyEnforcer) GetUserInfo(spec *oci.Process, rootPath st return IDName{}, nil, "", nil } +func (OpenDoorSecurityPolicyEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string) error { + return nil +} + type ClosedDoorSecurityPolicyEnforcer struct { encodedSecurityPolicy string //nolint:unused } @@ -981,10 +1097,35 @@ func (ClosedDoorSecurityPolicyEnforcer) EnforceCreateContainerPolicy(context.Con return nil, nil, false, errors.New("running commands is denied by policy") } +func (ClosedDoorSecurityPolicyEnforcer) EnforceCreateContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + mounts []oci.Mount, + user IDName, + opts *CreateContainerOptions, +) (EnvList, *oci.LinuxCapabilities, bool, error) { + return nil, nil, false, errors.New("running commands is denied by policy") +} + func (ClosedDoorSecurityPolicyEnforcer) EnforceExecInContainerPolicy(context.Context, string, []string, []string, string, bool, IDName, []IDName, string, *oci.LinuxCapabilities) (EnvList, *oci.LinuxCapabilities, bool, error) { return nil, nil, false, errors.New("starting additional processes in a container is denied by policy") } +func (ClosedDoorSecurityPolicyEnforcer) EnforceExecInContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + user IDName, + opts *ExecOptions, +) (EnvList, *oci.LinuxCapabilities, bool, error) { + return nil, nil, false, errors.New("starting additional processes in a container is denied by policy") +} + func (ClosedDoorSecurityPolicyEnforcer) EnforceExecExternalProcessPolicy(context.Context, []string, []string, string) (EnvList, bool, error) { return nil, false, errors.New("starting additional processes in uvm is denied by policy") } @@ -997,6 +1138,10 @@ func (*ClosedDoorSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicy(con return errors.New("signalling container processes is denied by policy") } +func (*ClosedDoorSecurityPolicyEnforcer) EnforceSignalContainerProcessPolicyV2(ctx context.Context, containerID string, opts *SignalContainerOptions) error { + return errors.New("signalling container processes is denied by policy") +} + func (*ClosedDoorSecurityPolicyEnforcer) EnforcePlan9MountPolicy(context.Context, string) error { return errors.New("mounting is denied by policy") } @@ -1040,3 +1185,7 @@ func (ClosedDoorSecurityPolicyEnforcer) EnforceScratchUnmountPolicy(context.Cont func (ClosedDoorSecurityPolicyEnforcer) GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error) { return IDName{}, nil, "", nil } + +func (ClosedDoorSecurityPolicyEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string) error { + return nil +} diff --git a/pkg/securitypolicy/securitypolicyenforcer_rego.go b/pkg/securitypolicy/securitypolicyenforcer_rego.go index c9567734ab..7bafd5e91a 100644 --- a/pkg/securitypolicy/securitypolicyenforcer_rego.go +++ b/pkg/securitypolicy/securitypolicyenforcer_rego.go @@ -1,5 +1,5 @@ -//go:build linux && rego -// +build linux,rego +//go:build rego +// +build rego package securitypolicy @@ -9,20 +9,15 @@ import ( "encoding/base64" "encoding/json" "fmt" - "os" - "path/filepath" "strconv" "strings" "syscall" - "github.com/moby/sys/user" - oci "github.com/opencontainers/runtime-spec/specs-go" - "github.com/pkg/errors" - - specGuest "github.com/Microsoft/hcsshim/internal/guest/spec" "github.com/Microsoft/hcsshim/internal/guestpath" "github.com/Microsoft/hcsshim/internal/log" rpi "github.com/Microsoft/hcsshim/internal/regopolicyinterpreter" + oci "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" ) const regoEnforcerName = "rego" @@ -59,12 +54,14 @@ type regoEnforcer struct { stdio map[string]bool // Maximum error message length maxErrorMessageLength int + // OS type + osType string } var _ SecurityPolicyEnforcer = (*regoEnforcer)(nil) //nolint:unused -func (sp SecurityPolicy) toInternal() (*securityPolicyInternal, error) { +/*func (sp SecurityPolicy) toInternal() (*securityPolicyInternal, error) { policy := new(securityPolicyInternal) var err error if policy.Containers, err = sp.Containers.toInternal(); err != nil { @@ -72,7 +69,7 @@ func (sp SecurityPolicy) toInternal() (*securityPolicyInternal, error) { } return policy, nil -} +}*/ func toStringSet(items []string) stringSet { s := make(stringSet) @@ -118,6 +115,7 @@ func createRegoEnforcer(base64EncodedPolicy string, } // Try to unmarshal the JSON + var code string securityPolicy := new(SecurityPolicy) err = json.Unmarshal(rawPolicy, securityPolicy) @@ -126,46 +124,78 @@ func createRegoEnforcer(base64EncodedPolicy string, return createOpenDoorEnforcer(base64EncodedPolicy, defaultMounts, privilegedMounts, maxErrorMessageLength) } - containers := make([]*Container, securityPolicy.Containers.Length) + if osType == "linux" { + containers := make([]*Container, securityPolicy.Containers.Length) + for i := 0; i < securityPolicy.Containers.Length; i++ { + index := strconv.Itoa(i) + cConf, ok := securityPolicy.Containers.Elements[index] + if !ok { + return nil, fmt.Errorf("container constraint with index %q not found", index) + } + cConf.AllowStdioAccess = true + cConf.NoNewPrivileges = false + cConf.User = UserConfig{ + UserIDName: IDNameConfig{Strategy: IDNameStrategyAny}, + GroupIDNames: []IDNameConfig{{Strategy: IDNameStrategyAny}}, + Umask: "0022", + } + cConf.SeccompProfileSHA256 = "" + containers[i] = &cConf + } - for i := 0; i < securityPolicy.Containers.Length; i++ { - index := strconv.Itoa(i) - cConf, ok := securityPolicy.Containers.Elements[index] - if !ok { - return nil, fmt.Errorf("container constraint with index %q not found", index) + code, err = osAwareMarshalRego( + securityPolicy.AllowAll, + containers, + nil, + osType, + []ExternalProcessConfig{}, + []FragmentConfig{}, + true, + true, + true, + false, + true, + false, + ) + if err != nil { + return nil, fmt.Errorf("error marshaling the policy to Rego: %w", err) } - cConf.AllowStdioAccess = true - cConf.NoNewPrivileges = false - cConf.User = UserConfig{ - UserIDName: IDNameConfig{Strategy: IDNameStrategyAny}, - GroupIDNames: []IDNameConfig{{Strategy: IDNameStrategyAny}}, - Umask: "0022", + } else if osType == "windows" { + windows_containers := make([]*WindowsContainer, securityPolicy.Containers.Length) + for i := 0; i < securityPolicy.Containers.Length; i++ { + index := strconv.Itoa(i) + cConf, ok := securityPolicy.WindowsContainers.Elements[index] + if !ok { + return nil, fmt.Errorf("container constraint with index %q not found", index) + } + cConf.AllowStdioAccess = true + windows_containers[i] = &cConf } - cConf.SeccompProfileSHA256 = "" - containers[i] = &cConf - } - code, err = marshalRego( - securityPolicy.AllowAll, - containers, - []ExternalProcessConfig{}, - []FragmentConfig{}, - true, - true, - true, - false, - true, - false, - ) - if err != nil { - return nil, fmt.Errorf("error marshaling the policy to Rego: %w", err) + code, err = osAwareMarshalRego( + securityPolicy.AllowAll, + nil, + windows_containers, + osType, + []ExternalProcessConfig{}, + []FragmentConfig{}, + true, + true, + true, + false, + true, + false, + ) + if err != nil { + return nil, fmt.Errorf("error marshaling the policy to Rego: %w", err) + } } } else { // this is either a Rego policy or malformed JSON code = string(rawPolicy) } - regoPolicy, err := newRegoPolicy(code, defaultMounts, privilegedMounts) + regoPolicy, err := newRegoPolicy(code, defaultMounts, privilegedMounts, osType) if err != nil { return nil, fmt.Errorf("error creating Rego policy: %w", err) } @@ -178,9 +208,14 @@ func (policy *regoEnforcer) enableLogging(path string, logLevel rpi.LogLevel) { policy.rego.EnableLogging(path, logLevel) } -func newRegoPolicy(code string, defaultMounts []oci.Mount, privilegedMounts []oci.Mount) (policy *regoEnforcer, err error) { +func newRegoPolicy(code string, defaultMounts []oci.Mount, privilegedMounts []oci.Mount, osType string) (policy *regoEnforcer, err error) { policy = new(regoEnforcer) + if osType != "windows" && osType != "linux" { + return nil, fmt.Errorf("unsupported operating system: %q", osType) + } + + policy.osType = osType policy.defaultMounts = make([]oci.Mount, len(defaultMounts)) copy(policy.defaultMounts, defaultMounts) @@ -197,6 +232,7 @@ func newRegoPolicy(code string, defaultMounts []oci.Mount, privilegedMounts []oc } policy.rego, err = rpi.NewRegoPolicyInterpreter(code, data) + policy.rego.UpdateOSType(osType) if err != nil { return nil, err } @@ -208,6 +244,7 @@ func newRegoPolicy(code string, defaultMounts []oci.Mount, privilegedMounts []oc err = policy.rego.Compile() if err != nil { + fmt.Printf("print rego compilation failed: %v\n", err) return nil, fmt.Errorf("rego compilation failed: %w", err) } @@ -711,25 +748,69 @@ func (policy *regoEnforcer) EnforceCreateContainerPolicy( capsToKeep *oci.LinuxCapabilities, stdioAccessAllowed bool, err error) { - if capabilities == nil { + opts := &CreateContainerOptions{ + SandboxID: sandboxID, + Privileged: &privileged, + NoNewPrivileges: &noNewPrivileges, + Groups: groups, + Umask: umask, + Capabilities: capabilities, + SeccompProfileSHA256: seccompProfileSHA256, + } + return policy.EnforceCreateContainerPolicyV2(ctx, containerID, argList, envList, workingDir, mounts, user, opts) +} + +func (policy *regoEnforcer) EnforceCreateContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + mounts []oci.Mount, + user IDName, + opts *CreateContainerOptions, +) (envToKeep EnvList, + capsToKeep *oci.LinuxCapabilities, + stdioAccessAllowed bool, + err error) { + + if policy.osType == "linux" && opts.Capabilities == nil { return nil, nil, false, errors.New(capabilitiesNilError) } - input := inputData{ - "containerID": containerID, - "argList": argList, - "envList": envList, - "workingDir": workingDir, - "sandboxDir": specGuest.SandboxMountsDir(sandboxID), - "hugePagesDir": specGuest.HugePagesMountsDir(sandboxID), - "mounts": appendMountData([]interface{}{}, mounts), - "privileged": privileged, - "noNewPrivileges": noNewPrivileges, - "user": user.toInput(), - "groups": groupsToInputs(groups), - "umask": umask, - "capabilities": mapifyCapabilities(capabilities), - "seccompProfileSHA256": seccompProfileSHA256, + var input inputData + + if envList == nil { + envList = []string{} + } + switch policy.osType { + case "linux": + input = inputData{ + "containerID": containerID, + "argList": argList, + "envList": envList, + "workingDir": workingDir, + "sandboxDir": SandboxMountsDir(opts.SandboxID), + "hugePagesDir": HugePagesMountsDir(opts.SandboxID), + "mounts": appendMountData([]interface{}{}, mounts), + "privileged": opts.Privileged, + "noNewPrivileges": opts.NoNewPrivileges, + "user": user.toInput(), + "groups": groupsToInputs(opts.Groups), + "umask": opts.Umask, + "capabilities": mapifyCapabilities(opts.Capabilities), + "seccompProfileSHA256": opts.SeccompProfileSHA256, + } + case "windows": + input = inputData{ + "containerID": containerID, + "argList": argList, + "envList": envList, + "workingDir": workingDir, + "user": user.Name, + } + default: + return nil, nil, false, errors.Errorf("unsupported OS value in options: %q", policy.osType) } results, err := policy.enforce(ctx, "create_container", input) @@ -742,9 +823,11 @@ func (policy *regoEnforcer) EnforceCreateContainerPolicy( return nil, nil, false, err } - capsToKeep, err = getCapsToKeep(capabilities, results) - if err != nil { - return nil, nil, false, err + if policy.osType == "linux" { + capsToKeep, err = getCapsToKeep(opts.Capabilities, results) + if err != nil { + return nil, nil, false, err + } } stdioAccessAllowed, err = results.Bool("allow_stdio_access") @@ -782,6 +865,18 @@ func appendMountData(mountData []interface{}, mounts []oci.Mount) []interface{} return mountData } +func appendMountDataWindows(mountData []interface{}, mounts []oci.Mount) []interface{} { + for _, mount := range mounts { + mountData = append(mountData, inputData{ + "destination": mount.Destination, + "source": mount.Source, + "options": mount.Options, + }) + } + + return mountData +} + func (policy *regoEnforcer) ExtendDefaultMounts(mounts []oci.Mount) error { policy.defaultMounts = append(policy.defaultMounts, mounts...) defaultMounts := appendMountData([]interface{}{}, policy.defaultMounts) @@ -807,20 +902,57 @@ func (policy *regoEnforcer) EnforceExecInContainerPolicy( capsToKeep *oci.LinuxCapabilities, stdioAccessAllowed bool, err error) { - if capabilities == nil { + opts := &ExecOptions{ + Groups: groups, + Umask: umask, + Capabilities: capabilities, + NoNewPrivileges: &noNewPrivileges, + } + return policy.EnforceExecInContainerPolicyV2(ctx, containerID, argList, envList, workingDir, user, opts) +} + +func (policy *regoEnforcer) EnforceExecInContainerPolicyV2( + ctx context.Context, + containerID string, + argList []string, + envList []string, + workingDir string, + user IDName, + opts *ExecOptions, +) (envToKeep EnvList, + capsToKeep *oci.LinuxCapabilities, + stdioAccessAllowed bool, + err error) { + + if policy.osType == "linux" && opts.Capabilities == nil { return nil, nil, false, errors.New(capabilitiesNilError) } - input := inputData{ - "containerID": containerID, - "argList": argList, - "envList": envList, - "workingDir": workingDir, - "noNewPrivileges": noNewPrivileges, - "user": user.toInput(), - "groups": groupsToInputs(groups), - "umask": umask, - "capabilities": mapifyCapabilities(capabilities), + var input inputData + + switch policy.osType { + case "linux": + input = inputData{ + "containerID": containerID, + "argList": argList, + "envList": envList, + "workingDir": workingDir, + "noNewPrivileges": opts.NoNewPrivileges, + "user": user.toInput(), + "groups": groupsToInputs(opts.Groups), + "umask": opts.Umask, + "capabilities": mapifyCapabilities(opts.Capabilities), + } + case "windows": + input = inputData{ + "containerID": containerID, + "argList": argList, + "envList": envList, + "workingDir": workingDir, + "user": user.Name, + } + default: + return nil, nil, false, errors.Errorf("unsupported OS value in options: %q", policy.osType) } results, err := policy.enforce(ctx, "exec_in_container", input) @@ -833,11 +965,12 @@ func (policy *regoEnforcer) EnforceExecInContainerPolicy( return nil, nil, false, err } - capsToKeep, err = getCapsToKeep(capabilities, results) - if err != nil { - return nil, nil, false, err + if policy.osType == "linux" { + capsToKeep, err = getCapsToKeep(opts.Capabilities, results) + if err != nil { + return nil, nil, false, err + } } - return envToKeep, capsToKeep, policy.stdio[containerID], nil } @@ -876,11 +1009,34 @@ func (policy *regoEnforcer) EnforceShutdownContainerPolicy(ctx context.Context, } func (policy *regoEnforcer) EnforceSignalContainerProcessPolicy(ctx context.Context, containerID string, signal syscall.Signal, isInitProcess bool, startupArgList []string) error { - input := inputData{ - "containerID": containerID, - "signal": signal, - "isInitProcess": isInitProcess, - "argList": startupArgList, + opts := &SignalContainerOptions{ + LinuxSignal: signal, + IsInitProcess: isInitProcess, + LinuxStartupArgs: startupArgList, + } + return policy.EnforceSignalContainerProcessPolicyV2(ctx, containerID, opts) +} + +func (policy *regoEnforcer) EnforceSignalContainerProcessPolicyV2(ctx context.Context, containerID string, opts *SignalContainerOptions) error { + var input inputData + + switch policy.osType { + case "linux": + input = inputData{ + "containerID": containerID, + "signal": opts.LinuxSignal, + "isInitProcess": opts.IsInitProcess, + "argList": opts.LinuxStartupArgs, + } + case "windows": + input = inputData{ + "containerID": containerID, + "signal": opts.WindowsSignal, + "isInitProcess": opts.IsInitProcess, + "argList": opts.WindowsCommand, + } + default: + return errors.Errorf("unsupported OS value in options: %q", policy.osType) } _, err := policy.enforce(ctx, "signal_container_process", input) @@ -992,98 +1148,17 @@ func (policy *regoEnforcer) EnforceScratchUnmountPolicy(ctx context.Context, scr return nil } -func (policy *regoEnforcer) GetUserInfo(process *oci.Process, rootPath string) ( - userIDName IDName, - groupIDNames []IDName, - umask string, - err error, -) { - passwdPath := filepath.Join(rootPath, "/etc/passwd") - groupPath := filepath.Join(rootPath, "/etc/group") - - if process == nil { - err = errors.New("spec.Process is nil") - return - } - - // this default value is used in the Linux kernel if no umask is specified - umask = "0022" - if process.User.Umask != nil { - umask = fmt.Sprintf("%04o", *process.User.Umask) - } - - parsedUserStr := false - checkGroup := true - - if process.User.Username != "" { - var usrInfo specGuest.ParseUserStrResult - usrInfo, err = specGuest.ParseUserStr(rootPath, process.User.Username) - if err != nil { - err = errors.Errorf("failed to parse user str %q: %v", process.User.Username, err) - return - } - userIDName = IDName{ID: strconv.FormatUint(uint64(usrInfo.UID), 10), Name: usrInfo.Username} - groupIDNames = []IDName{ - {ID: strconv.FormatUint(uint64(usrInfo.GID), 10), Name: usrInfo.Groupname}, - } - parsedUserStr = true - } - - if !parsedUserStr { - // fallback UID/GID lookup - uid := process.User.UID - userIDName = IDName{ID: strconv.FormatUint(uint64(uid), 10), Name: ""} - if _, err = os.Stat(passwdPath); err == nil { - var userInfo user.User - userInfo, err = specGuest.GetUser(rootPath, func(user user.User) bool { - return uint32(user.Uid) == uid - }) - - if err != nil { - return userIDName, nil, "", err - } - - userIDName.Name = userInfo.Name - } - - gid := process.User.GID - groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} - - if _, err = os.Stat(groupPath); err == nil { - var groupInfo user.Group - groupInfo, err = specGuest.GetGroup(rootPath, func(group user.Group) bool { - return uint32(group.Gid) == gid - }) - - if err != nil { - return userIDName, nil, "", err - } - groupIDName.Name = groupInfo.Name - } else { - checkGroup = false - err = nil - } - - groupIDNames = []IDName{groupIDName} +func (policy *regoEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string) error { + log.G(ctx).Tracef("Enforcing verified cims in securitypolicy pkg %+v", layerHashes) + input := inputData{ + "containerID": containerID, + "layerHashes": layerHashes, } - additionalGIDs := process.User.AdditionalGids - if len(additionalGIDs) > 0 { - for _, gid := range additionalGIDs { - groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} - if checkGroup { - var groupInfo user.Group - groupInfo, err = specGuest.GetGroup(rootPath, func(group user.Group) bool { - return uint32(group.Gid) == gid - }) - if err != nil { - return userIDName, nil, "", err - } - groupIDName.Name = groupInfo.Name - } - groupIDNames = append(groupIDNames, groupIDName) - } - } + _, err := policy.enforce(ctx, "mount_cims", input) + return err +} - return userIDName, groupIDNames, umask, nil +func (policy *regoEnforcer) GetUserInfo(process *oci.Process, rootPath string) (IDName, []IDName, string, error) { + return GetAllUserInfo(process, rootPath) } diff --git a/pkg/securitypolicy/securitypolicy_test.go b/pkg/securitypolicy/standardpolicy_test.go similarity index 57% rename from pkg/securitypolicy/securitypolicy_test.go rename to pkg/securitypolicy/standardpolicy_test.go index 4cd3b25a28..9a88b90831 100644 --- a/pkg/securitypolicy/securitypolicy_test.go +++ b/pkg/securitypolicy/standardpolicy_test.go @@ -1,76 +1,19 @@ -//go:build linux -// +build linux +//go:build linux && rego +// +build linux,rego package securitypolicy import ( "context" - "fmt" - "math/rand" - "os" - "path/filepath" - "reflect" + "strconv" - "strings" - "syscall" + "testing" "testing/quick" - "time" - "github.com/Microsoft/hcsshim/internal/guestpath" "github.com/google/go-cmp/cmp" ) -const ( - // variables that influence generated test fixtures - minStringLength = 10 - maxContainersInGeneratedConstraints = 32 - maxLayersInGeneratedContainer = 32 - maxGeneratedContainerID = 1000000 - maxGeneratedCommandLength = 128 - maxGeneratedCommandArgs = 12 - maxGeneratedEnvironmentVariables = 16 - maxGeneratedEnvironmentVariableRuleLength = 64 - maxGeneratedEnvironmentVariableRules = 8 - maxGeneratedFragmentNamespaceLength = 32 - maxGeneratedMountTargetLength = 256 - maxGeneratedVersion = 10 - rootHashLength = 64 - maxGeneratedMounts = 4 - maxGeneratedMountSourceLength = 32 - maxGeneratedMountDestinationLength = 32 - maxGeneratedMountOptions = 5 - maxGeneratedMountOptionLength = 32 - maxGeneratedExecProcesses = 4 - maxGeneratedWorkingDirLength = 128 - maxSignalNumber = 64 - maxGeneratedNameLength = 8 - maxGeneratedGroupNames = 4 - maxGeneratedCapabilities = 12 - maxGeneratedCapabilitesLength = 24 - // additional consts - // the standard enforcer tests don't do anything with the encoded policy - // string. this const exists to make that explicit - ignoredEncodedPolicyString = "" -) - -var testRand *rand.Rand -var testDataGenerator *dataGenerator - -func init() { - seed := time.Now().Unix() - if seedStr, ok := os.LookupEnv("SEED"); ok { - if parsedSeed, err := strconv.ParseInt(seedStr, 10, 64); err != nil { - fmt.Fprintf(os.Stderr, "failed to parse seed: %d\n", seed) - } else { - seed = parsedSeed - } - } - testRand = rand.New(rand.NewSource(seed)) - fmt.Fprintf(os.Stdout, "securitypolicy_test seed: %d\n", seed) - testDataGenerator = newDataGenerator(testRand) -} - // Validate that our conversion from the external SecurityPolicy representation // to our internal format is done correctly. func Test_StandardSecurityPolicyEnforcer_From_Security_Policy_Conversion(t *testing.T) { @@ -894,654 +837,3 @@ func Test_EnforcePrivileged_NoAllowElevatedAllowsUnprivilegedContainer(t *testin t.Fatalf("expected lack of escalation to be fine: %s", err) } } - -// -// Setup and "fixtures" follow... -// - -func (*SecurityPolicy) Generate(r *rand.Rand, _ int) reflect.Value { - // This fixture setup is used from 1 test. Given the limited scope it is - // used from, all functionality is in this single function. That saves having - // confusing fixture name functions where we have generate* for both internal - // and external versions - p := &SecurityPolicy{ - Containers: Containers{ - Elements: map[string]Container{}, - }, - } - p.AllowAll = false - numContainers := int(atLeastOneAtMost(r, maxContainersInGeneratedConstraints)) - for i := 0; i < numContainers; i++ { - c := Container{ - Command: CommandArgs{ - Elements: map[string]string{}, - }, - EnvRules: EnvRules{ - Elements: map[string]EnvRuleConfig{}, - }, - Layers: Layers{ - Elements: map[string]string{}, - }, - } - - // command - numArgs := int(atLeastOneAtMost(r, maxGeneratedCommandArgs)) - for j := 0; j < numArgs; j++ { - c.Command.Elements[strconv.Itoa(j)] = randVariableString(r, maxGeneratedCommandLength) - } - c.Command.Length = numArgs - - // layers - numLayers := int(atLeastOneAtMost(r, maxLayersInGeneratedContainer)) - for j := 0; j < numLayers; j++ { - c.Layers.Elements[strconv.Itoa(j)] = generateRootHash(r) - } - c.Layers.Length = numLayers - - // env variable rules - numEnvRules := int(atMost(r, maxGeneratedEnvironmentVariableRules)) - for j := 0; j < numEnvRules; j++ { - rule := EnvRuleConfig{ - Strategy: "string", - Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), - Required: false, - } - c.EnvRules.Elements[strconv.Itoa(j)] = rule - } - c.EnvRules.Length = numEnvRules - - p.Containers.Elements[strconv.Itoa(i)] = c - } - - p.Containers.Length = numContainers - - return reflect.ValueOf(p) -} - -func (*generatedConstraints) Generate(r *rand.Rand, _ int) reflect.Value { - c := generateConstraints(r, maxContainersInGeneratedConstraints) - return reflect.ValueOf(c) -} - -type testConfig struct { - container *securityPolicyContainer - layers []string - containerID string - policy *StandardSecurityPolicyEnforcer -} - -func setupContainerWithOverlay(gc *generatedConstraints, valid bool) (tc *testConfig, err error) { - sp := NewStandardSecurityPolicyEnforcer(gc.containers, ignoredEncodedPolicyString) - - containerID := testDataGenerator.uniqueContainerID() - c := selectContainerFromContainerList(gc.containers, testRand) - - var layerPaths []string - if valid { - layerPaths, err = testDataGenerator.createValidOverlayForContainer(sp, c) - if err != nil { - return nil, fmt.Errorf("error creating valid overlay: %w", err) - } - } else { - layerPaths, err = testDataGenerator.createInvalidOverlayForContainer(sp, c) - if err != nil { - return nil, fmt.Errorf("error creating invalid overlay: %w", err) - } - } - - return &testConfig{ - container: c, - layers: layerPaths, - containerID: containerID, - policy: sp, - }, nil -} - -func generateConstraints(r *rand.Rand, maxContainers int32) *generatedConstraints { - var containers []*securityPolicyContainer - - numContainers := (int)(atLeastOneAtMost(r, maxContainers)) - for i := 0; i < numContainers; i++ { - containers = append(containers, generateConstraintsContainer(r, 1, maxLayersInGeneratedContainer)) - } - - return &generatedConstraints{ - containers: containers, - externalProcesses: make([]*externalProcess, 0), - fragments: make([]*fragment, 0), - allowGetProperties: randBool(r), - allowDumpStacks: randBool(r), - allowRuntimeLogging: false, - allowEnvironmentVariableDropping: false, - allowUnencryptedScratch: randBool(r), - namespace: generateFragmentNamespace(testRand), - svn: generateSVN(testRand), - allowCapabilityDropping: false, - ctx: context.Background(), - } -} - -func generateConstraintsContainer(r *rand.Rand, minNumberOfLayers, maxNumberOfLayers int32) *securityPolicyContainer { - c := securityPolicyContainer{} - p := generateContainerInitProcess(r) - c.Command = p.Command - c.EnvRules = p.EnvRules - c.WorkingDir = p.WorkingDir - c.Mounts = generateMounts(r) - numLayers := int(atLeastNAtMostM(r, minNumberOfLayers, maxNumberOfLayers)) - for i := 0; i < numLayers; i++ { - c.Layers = append(c.Layers, generateRootHash(r)) - } - c.ExecProcesses = generateExecProcesses(r) - c.Signals = generateListOfSignals(r, 0, maxSignalNumber) - c.AllowStdioAccess = randBool(r) - c.NoNewPrivileges = randBool(r) - c.User = generateUser(r) - c.Capabilities = generateInternalCapabilities(r) - c.SeccompProfileSHA256 = generateSeccomp(r) - - return &c -} - -func generateSeccomp(r *rand.Rand) string { - if randBool(r) { - // 50% chance of no seccomp profile - return "" - } - - return generateRootHash(r) -} - -func generateInternalCapabilities(r *rand.Rand) *capabilitiesInternal { - return &capabilitiesInternal{ - Bounding: generateCapabilitiesSet(r, 0), - Effective: generateCapabilitiesSet(r, 0), - Inheritable: generateCapabilitiesSet(r, 0), - Permitted: generateCapabilitiesSet(r, 0), - Ambient: generateCapabilitiesSet(r, 0), - } -} - -func generateCapabilitiesSet(r *rand.Rand, minSize int32) []string { - capabilities := make([]string, 0) - - numArgs := atLeastNAtMostM(r, minSize, maxGeneratedCapabilities) - for i := 0; i < int(numArgs); i++ { - capabilities = append(capabilities, generateCapability(r)) - } - - return capabilities -} - -func generateCapability(r *rand.Rand) string { - return randVariableString(r, maxGeneratedCapabilitesLength) -} - -func generateContainerInitProcess(r *rand.Rand) containerInitProcess { - return containerInitProcess{ - Command: generateCommand(r), - EnvRules: generateEnvironmentVariableRules(r), - WorkingDir: generateWorkingDir(r), - AllowStdioAccess: randBool(r), - } -} - -func generateContainerExecProcess(r *rand.Rand) containerExecProcess { - return containerExecProcess{ - Command: generateCommand(r), - Signals: generateListOfSignals(r, 0, maxSignalNumber), - } -} - -func generateRootHash(r *rand.Rand) string { - return randString(r, rootHashLength) -} - -func generateWorkingDir(r *rand.Rand) string { - return randVariableString(r, maxGeneratedWorkingDirLength) -} - -func generateCommand(r *rand.Rand) []string { - var args []string - - numArgs := atLeastOneAtMost(r, maxGeneratedCommandArgs) - for i := 0; i < int(numArgs); i++ { - args = append(args, randVariableString(r, maxGeneratedCommandLength)) - } - - return args -} - -func generateEnvironmentVariableRules(r *rand.Rand) []EnvRuleConfig { - var rules []EnvRuleConfig - - numArgs := atLeastOneAtMost(r, maxGeneratedEnvironmentVariableRules) - for i := 0; i < int(numArgs); i++ { - rule := EnvRuleConfig{ - Strategy: "string", - Rule: randVariableString(r, maxGeneratedEnvironmentVariableRuleLength), - } - rules = append(rules, rule) - } - - return rules -} - -func generateExecProcesses(r *rand.Rand) []containerExecProcess { - var processes []containerExecProcess - - numProcesses := atLeastOneAtMost(r, maxGeneratedExecProcesses) - for i := 0; i < int(numProcesses); i++ { - processes = append(processes, generateContainerExecProcess(r)) - } - - return processes -} - -func generateUmask(r *rand.Rand) string { - // we are generating values from 000 to 777 as decimal values - // to ensure we cover the full range of umask values - // and so the resulting string will be a 4 digit octal representation - // even though we are using decimal values - return fmt.Sprintf("%04d", randMinMax(r, 0, 777)) -} - -func generateIDNameConfig(r *rand.Rand) IDNameConfig { - strategies := []IDNameStrategy{IDNameStrategyName, IDNameStrategyID} - strategy := strategies[randMinMax(r, 0, int32(len(strategies)-1))] - switch strategy { - case IDNameStrategyName: - return IDNameConfig{ - Strategy: IDNameStrategyName, - Rule: randVariableString(r, maxGeneratedNameLength), - } - - case IDNameStrategyID: - return IDNameConfig{ - Strategy: IDNameStrategyID, - Rule: fmt.Sprintf("%d", r.Uint32()), - } - } - panic("unreachable") -} - -func generateUser(r *rand.Rand) UserConfig { - numGroups := int(atLeastOneAtMost(r, maxGeneratedGroupNames)) - groupIDs := make([]IDNameConfig, numGroups) - for i := 0; i < numGroups; i++ { - groupIDs[i] = generateIDNameConfig(r) - } - - return UserConfig{ - UserIDName: generateIDNameConfig(r), - GroupIDNames: groupIDs, - Umask: generateUmask(r), - } -} - -func generateEnvironmentVariables(r *rand.Rand) []string { - var envVars []string - - numVars := atLeastOneAtMost(r, maxGeneratedEnvironmentVariables) - for i := 0; i < int(numVars); i++ { - variable := randVariableString(r, maxGeneratedEnvironmentVariableRuleLength) - envVars = append(envVars, variable) - } - - return envVars -} - -func generateNeverMatchingEnvironmentVariable(r *rand.Rand) string { - return randString(r, maxGeneratedEnvironmentVariableRuleLength+1) -} - -func buildEnvironmentVariablesFromEnvRules(rules []EnvRuleConfig, r *rand.Rand) []string { - vars := make([]string, 0) - - // Select some number of the valid, matching rules to be environment - // variable - numberOfRules := int32(len(rules)) - numberOfMatches := randMinMax(r, 1, numberOfRules) - - // Build in all required rules, this isn't a setup method of "missing item" - // tests - for _, rule := range rules { - - if rule.Required { - if rule.Strategy != EnvVarRuleRegex { - vars = append(vars, rule.Rule) - } - numberOfMatches-- - } - } - - usedIndexes := map[int]struct{}{} - for numberOfMatches > 0 { - anIndex := -1 - if (numberOfMatches * 2) > numberOfRules { - // if we have a lot of matches, randomly select - exists := true - - for exists { - anIndex = int(randMinMax(r, 0, numberOfRules-1)) - _, exists = usedIndexes[anIndex] - } - } else { - // we have a "smaller set of rules. we'll just iterate and select from - // available - exists := true - - for exists { - anIndex++ - _, exists = usedIndexes[anIndex] - } - } - - // include it if it's not regex - if rules[anIndex].Strategy != EnvVarRuleRegex { - vars = append(vars, rules[anIndex].Rule) - usedIndexes[anIndex] = struct{}{} - } - numberOfMatches-- - - } - - return vars -} - -func generateMountTarget(r *rand.Rand) string { - return randVariableString(r, maxGeneratedMountTargetLength) -} - -func generateInvalidRootHash(r *rand.Rand) string { - // Guaranteed to be an incorrect size as it maxes out in size at one less - // than the correct length. If this ever creates a hash that passes, we - // have a seriously weird bug - return randVariableString(r, rootHashLength-1) -} - -func generateFragmentNamespace(r *rand.Rand) string { - return randChar(r) + randVariableString(r, maxGeneratedFragmentNamespaceLength) -} - -func generateSVN(r *rand.Rand) string { - return strconv.FormatInt(int64(randMinMax(r, 0, maxGeneratedVersion)), 10) -} - -func selectRootHashFromConstraints(constraints *generatedConstraints, r *rand.Rand) string { - numberOfContainersInConstraints := len(constraints.containers) - container := constraints.containers[r.Intn(numberOfContainersInConstraints)] - numberOfLayersInContainer := len(container.Layers) - - return container.Layers[r.Intn(numberOfLayersInContainer)] -} - -func generateContainerID(r *rand.Rand) string { - id := atLeastOneAtMost(r, maxGeneratedContainerID) - return strconv.FormatInt(int64(id), 10) -} - -func generateMounts(r *rand.Rand) []mountInternal { - numberOfMounts := atLeastOneAtMost(r, maxGeneratedMounts) - mounts := make([]mountInternal, numberOfMounts) - - for i := 0; i < int(numberOfMounts); i++ { - numberOfOptions := atLeastOneAtMost(r, maxGeneratedMountOptions) - options := make([]string, numberOfOptions) - for j := 0; j < int(numberOfOptions); j++ { - options[j] = randVariableString(r, maxGeneratedMountOptionLength) - } - - sourcePrefix := "" - // select a "source type". our default is "no special prefix" ie a - // "standard source". - prefixType := randMinMax(r, 1, 3) - switch prefixType { - case 2: - // sandbox mount, gets special handling - sourcePrefix = guestpath.SandboxMountPrefix - case 3: - // huge page mount, gets special handling - sourcePrefix = guestpath.HugePagesMountPrefix - } - - source := filepath.Join(sourcePrefix, randVariableString(r, maxGeneratedMountSourceLength)) - destination := randVariableString(r, maxGeneratedMountDestinationLength) - - mounts[i] = mountInternal{ - Source: source, - Destination: destination, - Options: options, - Type: "bind", - } - } - - return mounts -} - -func generateListOfSignals(r *rand.Rand, atLeast int32, atMost int32) []syscall.Signal { - numSignals := int(atLeastNAtMostM(r, atLeast, atMost)) - signalSet := make(map[syscall.Signal]struct{}) - - for i := 0; i < numSignals; i++ { - signal := generateSignal(r) - signalSet[signal] = struct{}{} - } - - var signals []syscall.Signal - for k := range signalSet { - signals = append(signals, k) - } - - return signals -} - -func generateSignal(r *rand.Rand) syscall.Signal { - return syscall.Signal(atLeastOneAtMost(r, maxSignalNumber)) -} - -func selectContainerFromContainerList(containers []*securityPolicyContainer, r *rand.Rand) *securityPolicyContainer { - return containers[r.Intn(len(containers))] -} - -type dataGenerator struct { - rng *rand.Rand - mountTargets stringSet - containerIDs stringSet - sandboxIDs stringSet - enforcementPoints stringSet - fragmentIssuers stringSet - fragmentFeeds stringSet - fragmentNamespaces stringSet -} - -func newDataGenerator(rng *rand.Rand) *dataGenerator { - return &dataGenerator{ - rng: rng, - mountTargets: make(stringSet), - containerIDs: make(stringSet), - sandboxIDs: make(stringSet), - enforcementPoints: make(stringSet), - fragmentIssuers: make(stringSet), - fragmentFeeds: make(stringSet), - fragmentNamespaces: make(stringSet), - } -} - -func (s *stringSet) randUnique(r *rand.Rand, generator func(*rand.Rand) string) string { - for { - item := generator(r) - if !s.contains(item) { - s.add(item) - return item - } - } -} - -func (gen *dataGenerator) uniqueMountTarget() string { - return gen.mountTargets.randUnique(gen.rng, generateMountTarget) -} - -func (gen *dataGenerator) uniqueContainerID() string { - return gen.containerIDs.randUnique(gen.rng, generateContainerID) -} - -func (gen *dataGenerator) createValidOverlayForContainer(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { - ctx := context.Background() - // storage for our mount paths - overlay := make([]string, len(container.Layers)) - - for i := 0; i < len(container.Layers); i++ { - mount := gen.uniqueMountTarget() - err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) - if err != nil { - return overlay, err - } - - overlay[len(overlay)-i-1] = mount - } - - return overlay, nil -} - -func (gen *dataGenerator) createInvalidOverlayForContainer(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { - method := gen.rng.Intn(3) - switch method { - case 0: - return gen.invalidOverlaySameSizeWrongMounts(enforcer, container) - case 1: - return gen.invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer, container) - default: - return gen.invalidOverlayRandomJunk(enforcer, container) - } -} - -func (gen *dataGenerator) invalidOverlaySameSizeWrongMounts(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { - ctx := context.Background() - // storage for our mount paths - overlay := make([]string, len(container.Layers)) - - for i := 0; i < len(container.Layers); i++ { - mount := gen.uniqueMountTarget() - err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) - if err != nil { - return overlay, err - } - - // generate a random new mount point to cause an error - overlay[len(overlay)-i-1] = gen.uniqueMountTarget() - } - - return overlay, nil -} - -func (gen *dataGenerator) invalidOverlayCorrectDevicesWrongOrderSomeMissing(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { - ctx := context.Background() - if len(container.Layers) == 1 { - // won't work with only 1, we need to bail out to another method - return gen.invalidOverlayRandomJunk(enforcer, container) - } - // storage for our mount paths - var overlay []string - - for i := 0; i < len(container.Layers); i++ { - mount := gen.uniqueMountTarget() - err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) - if err != nil { - return overlay, err - } - - if gen.rng.Intn(10) != 0 { - overlay = append(overlay, mount) - } - } - - return overlay, nil -} - -func (gen *dataGenerator) invalidOverlayRandomJunk(enforcer SecurityPolicyEnforcer, container *securityPolicyContainer) ([]string, error) { - ctx := context.Background() - // create "junk" for entry - layersToCreate := gen.rng.Int31n(maxLayersInGeneratedContainer) - overlay := make([]string, layersToCreate) - - for i := 0; i < int(layersToCreate); i++ { - overlay[i] = gen.uniqueMountTarget() - } - - // setup entirely different and "correct" expected mounting - for i := 0; i < len(container.Layers); i++ { - mount := gen.uniqueMountTarget() - err := enforcer.EnforceDeviceMountPolicy(ctx, mount, container.Layers[i]) - if err != nil { - return overlay, err - } - } - - return overlay, nil -} - -func randVariableString(r *rand.Rand, maxLen int32) string { - return randString(r, atLeastOneAtMost(r, maxLen)) -} - -func randString(r *rand.Rand, length int32) string { - if length < minStringLength { - length = minStringLength - } - charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - sb := strings.Builder{} - sb.Grow(int(length)) - for i := 0; i < (int)(length); i++ { - sb.WriteByte(charset[r.Intn(len(charset))]) - } - - return sb.String() -} - -func randChar(r *rand.Rand) string { - charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - return string(charset[r.Intn(len(charset))]) -} - -func randBool(r *rand.Rand) bool { - return randMinMax(r, 0, 1) == 0 -} - -func randMinMax(r *rand.Rand, min int32, max int32) int32 { - return r.Int31n(max-min+1) + min -} - -func atLeastNAtMostM(r *rand.Rand, min, max int32) int32 { - return randMinMax(r, min, max) -} - -func atLeastOneAtMost(r *rand.Rand, most int32) int32 { - return atLeastNAtMostM(r, 1, most) -} - -func atMost(r *rand.Rand, most int32) int32 { - return randMinMax(r, 0, most) -} - -type generatedConstraints struct { - containers []*securityPolicyContainer - externalProcesses []*externalProcess - fragments []*fragment - allowGetProperties bool - allowDumpStacks bool - allowRuntimeLogging bool - allowEnvironmentVariableDropping bool - allowUnencryptedScratch bool - namespace string - svn string - allowCapabilityDropping bool - ctx context.Context -} - -type containerInitProcess struct { - Command []string - EnvRules []EnvRuleConfig - WorkingDir string - AllowStdioAccess bool -} diff --git a/pkg/securitypolicy/version_api b/pkg/securitypolicy/version_api index 2774f8587f..142464bf22 100644 --- a/pkg/securitypolicy/version_api +++ b/pkg/securitypolicy/version_api @@ -1 +1 @@ -0.10.0 \ No newline at end of file +0.11.0 \ No newline at end of file diff --git a/pkg/securitypolicy/version_framework b/pkg/securitypolicy/version_framework index 9325c3ccda..60a2d3e96c 100644 --- a/pkg/securitypolicy/version_framework +++ b/pkg/securitypolicy/version_framework @@ -1 +1 @@ -0.3.0 \ No newline at end of file +0.4.0 \ No newline at end of file