config/exec_resource.go (195 lines of code) (raw):

// Copyright 2020 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "context" "fmt" "io" "io/ioutil" "os" "os/exec" "path" "path/filepath" "strings" "github.com/GoogleCloudPlatform/osconfig/clog" "github.com/GoogleCloudPlatform/osconfig/util" "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" ) const maxExecOutputSize = 500 * 1024 var runner = util.CommandRunner(&util.DefaultRunner{}) type execResource struct { *agentendpointpb.OSPolicy_Resource_ExecResource validatePath, enforcePath, tempDir string enforceOutput []byte } // TODO: use a persistent cache for downloaded files so we dont need to redownload them each time func (e *execResource) download(ctx context.Context, execR *agentendpointpb.OSPolicy_Resource_ExecResource_Exec) (string, error) { tmpDir, err := ioutil.TempDir(e.tempDir, "") if err != nil { return "", fmt.Errorf("failed to create temp dir: %s", err) } // File extensions are important on Windows. var name string perms := os.FileMode(0644) switch execR.GetSource().(type) { case *agentendpointpb.OSPolicy_Resource_ExecResource_Exec_Script: switch execR.GetInterpreter() { case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_NONE: if goos == "windows" { name = "script.cmd" } else { name = "script" } perms = os.FileMode(0755) case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_SHELL: if goos == "windows" { name = "script.cmd" } else { name = "script.sh" } case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_POWERSHELL: name = "script.ps1" default: return "", fmt.Errorf("unsupported interpreter %q", execR.GetInterpreter()) } name = filepath.Join(tmpDir, name) if _, err := util.AtomicWriteFileStream(strings.NewReader(execR.GetScript()), "", name, perms); err != nil { return "", err } case *agentendpointpb.OSPolicy_Resource_ExecResource_Exec_File: if execR.GetFile().GetLocalPath() != "" { return execR.GetFile().GetLocalPath(), nil } switch { case execR.GetFile().GetGcs().GetObject() != "": name = path.Base(execR.GetFile().GetGcs().GetObject()) case execR.GetFile().GetRemote().GetUri() != "": name = path.Base(execR.GetFile().GetRemote().GetUri()) default: return "", fmt.Errorf("unsupported File %v", execR.GetFile()) } if execR.GetInterpreter() == agentendpointpb.OSPolicy_Resource_ExecResource_Exec_NONE { perms = os.FileMode(0755) } name = filepath.Join(tmpDir, name) if _, err := downloadFile(ctx, name, perms, execR.GetFile()); err != nil { return "", err } default: return "", fmt.Errorf("unrecognized Source type for ExecResource: %q", execR.GetSource()) } return name, nil } func (e *execResource) validate(ctx context.Context) (*ManagedResources, error) { tmpDir, err := ioutil.TempDir("", "osconfig_exec_resource_") if err != nil { return nil, fmt.Errorf("failed to create temp dir: %s", err) } e.tempDir = tmpDir e.validatePath, err = e.download(ctx, e.GetValidate()) if err != nil { return nil, err } // Assume lack of Enforce means policy is in VALIDATE mode. if e.GetEnforce() != nil { e.enforcePath, err = e.download(ctx, e.GetEnforce()) if err != nil { return nil, err } } return nil, nil } func (e *execResource) run(ctx context.Context, name string, execR *agentendpointpb.OSPolicy_Resource_ExecResource_Exec) ([]byte, []byte, int, error) { if execR == nil { return nil, nil, 0, fmt.Errorf("ExecResource Exec cannot be nil") } var cmd string var args []string switch execR.GetInterpreter() { case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_NONE: cmd = name case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_SHELL: if goos == "windows" { cmd = name } else { args = append(args, name) cmd = "/bin/sh" } case agentendpointpb.OSPolicy_Resource_ExecResource_Exec_POWERSHELL: if goos != "windows" { return nil, nil, 0, fmt.Errorf("interpreter %q can only be used on Windows systems", execR.GetInterpreter()) } args = append(args, "-File", name) cmd = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\PowerShell.exe" default: return nil, nil, 0, fmt.Errorf("unsupported interpreter %q", execR.GetInterpreter()) } args = append(args, execR.GetArgs()...) stdout, stderr, err := runner.Run(ctx, exec.CommandContext(ctx, cmd, args...)) code := 0 if err != nil { code = -1 if v, ok := err.(*exec.ExitError); ok { code = v.ExitCode() } } return stdout, stderr, code, err } func (e *execResource) checkState(ctx context.Context) (inDesiredState bool, err error) { // For validate we expect an exit code of 100 for "correct state" and 101 for "incorrect state". // 100 was chosen over 0 (and 101 vs 1) because we want an explicit indicator of // "correct" vs "incorrect" state and errors. Also Powershell will always exit 0 unless "exit" // is explicitly called. // A code of -1 indicates some other error, so we just return err. stdout, stderr, code, err := e.run(ctx, e.validatePath, e.GetValidate()) switch code { case -1: return false, err case 100: return true, nil case 101: return false, nil default: return false, fmt.Errorf("unexpected return code from validate: %d, stdout: %s, stderr: %s", code, stdout, stderr) } } func (e *execResource) enforceState(ctx context.Context) (inDesiredState bool, err error) { clog.Infof(ctx, `Running "Enforce" for ExecResource.`) // For enforce we expect an exit code of 100 for "success" and anything positive code is a failure". // 100 was chosen over 0 because we want an explicit indicator of "sucess" vs errors. // Also Powershell will always exit 0 unless "exit" is explicitly called. // A code of -1 indicates some other error, so we just return err. stdout, stderr, code, err := e.run(ctx, e.enforcePath, e.GetEnforce()) switch code { case -1: return false, err case 100: out, err := execOutput(ctx, e.GetEnforce().GetOutputFilePath()) e.enforceOutput = out return true, err default: return false, fmt.Errorf("unexpected return code from enforce: %d, stdout: %s, stderr: %s", code, stdout, stderr) } } func execOutput(ctx context.Context, outputFilePath string) ([]byte, error) { if outputFilePath == "" { return nil, nil } clog.Debugf(ctx, "Reading %q for ExecResource output", outputFilePath) f, err := os.Open(outputFilePath) if err != nil { return nil, fmt.Errorf("error opening OutputFilePath: %v", err) } defer f.Close() // Make a byte slice with a capacity of 1 over maxSize (for checking maxExecOutputSize). output := make([]byte, 0, maxExecOutputSize+1) // Read up to capactity. n, err := f.Read(output[:cap(output)]) output = output[:len(output)+n] if err != nil && err != io.EOF { return output, fmt.Errorf("error reading from OutputFilePath: %v", err) } // Return the output up to this point and an error if total size is greater than maxExecOutputSize. if n > maxExecOutputSize { return output[:maxExecOutputSize], fmt.Errorf("contents of OutputFilePath greater than %dK", maxExecOutputSize/1024) } return output, nil } func (e *execResource) populateOutput(rCompliance *agentendpointpb.OSPolicyResourceCompliance) { if e.enforceOutput != nil { rCompliance.Output = &agentendpointpb.OSPolicyResourceCompliance_ExecResourceOutput_{ ExecResourceOutput: &agentendpointpb.OSPolicyResourceCompliance_ExecResourceOutput{ EnforcementOutput: e.enforceOutput, }, } } } func (e *execResource) cleanup(ctx context.Context) error { if e.tempDir != "" { return os.RemoveAll(e.tempDir) } return nil }