github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/linter/kubebench/check/check.go (about)

     1  // Copyright © 2017 Aqua Security Software Ltd. <info@aquasec.com>
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package check
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"os/exec"
    21  	"strings"
    22  
    23  	"github.com/golang/glog"
    24  )
    25  
    26  // NodeType indicates the type of node (master, node).
    27  type NodeType string
    28  
    29  // State is the state of a control check.
    30  type State string
    31  
    32  const (
    33  	// PASS check passed.
    34  	PASS State = "PASS"
    35  	// FAIL check failed.
    36  	FAIL State = "FAIL"
    37  	// WARN could not carry out check.
    38  	WARN State = "WARN"
    39  	// INFO informational message
    40  	INFO State = "INFO"
    41  
    42  	// SKIP for when a check should be skipped.
    43  	SKIP = "skip"
    44  
    45  	// MASTER a master node
    46  	MASTER NodeType = "master"
    47  	// NODE a node
    48  	NODE NodeType = "node"
    49  	// FEDERATED a federated deployment.
    50  	FEDERATED NodeType = "federated"
    51  
    52  	// ETCD an etcd node
    53  	ETCD NodeType = "etcd"
    54  	// CONTROLPLANE a control plane node
    55  	CONTROLPLANE NodeType = "controlplane"
    56  	// POLICIES a node to run policies from
    57  	POLICIES NodeType = "policies"
    58  	// MANAGEDSERVICES a node to run managedservices from
    59  	MANAGEDSERVICES = "managedservices"
    60  
    61  	// MANUAL Check Type
    62  	MANUAL string = "manual"
    63  )
    64  
    65  // Check contains information about a recommendation in the
    66  // CIS Kubernetes document.
    67  type Check struct {
    68  	ID                string   `yaml:"id" json:"test_number"`
    69  	Text              string   `json:"test_desc"`
    70  	Audit             string   `json:"audit"`
    71  	AuditEnv          string   `yaml:"audit_env"`
    72  	AuditConfig       string   `yaml:"audit_config"`
    73  	Type              string   `json:"type"`
    74  	Tests             *tests   `json:"-"`
    75  	Set               bool     `json:"-"`
    76  	Remediation       string   `json:"remediation"`
    77  	TestInfo          []string `json:"test_info"`
    78  	State             `json:"status"`
    79  	ActualValue       string `json:"actual_value"`
    80  	Scored            bool   `json:"scored"`
    81  	IsMultiple        bool   `yaml:"use_multiple_values"`
    82  	ExpectedResult    string `json:"expected_result"`
    83  	Reason            string `json:"reason,omitempty"`
    84  	AuditOutput       string `json:"-"`
    85  	AuditEnvOutput    string `json:"-"`
    86  	AuditConfigOutput string `json:"-"`
    87  	DisableEnvTesting bool   `json:"-"`
    88  }
    89  
    90  // Runner wraps the basic Run method.
    91  type Runner interface {
    92  	// Run runs a given check and returns the execution state.
    93  	Run(c *Check) State
    94  }
    95  
    96  // NewRunner constructs a default Runner.
    97  func NewRunner() Runner {
    98  	return &defaultRunner{}
    99  }
   100  
   101  type defaultRunner struct{}
   102  
   103  func (r *defaultRunner) Run(c *Check) State {
   104  	return c.run()
   105  }
   106  
   107  // Run executes the audit commands specified in a check and outputs
   108  // the results.
   109  func (c *Check) run() State {
   110  	glog.V(3).Infof("-----   Running check %v   -----", c.ID)
   111  	// Since this is an Scored check
   112  	// without tests return a 'WARN' to alert
   113  	// the user that this check needs attention
   114  	if c.Scored && strings.TrimSpace(c.Type) == "" && c.Tests == nil {
   115  		c.Reason = "There are no tests"
   116  		c.State = WARN
   117  		glog.V(3).Info(c.Reason)
   118  		return c.State
   119  	}
   120  
   121  	// If check type is skip, force result to INFO
   122  	if c.Type == SKIP {
   123  		c.Reason = "Test marked as skip"
   124  		c.State = INFO
   125  		glog.V(3).Info(c.Reason)
   126  		return c.State
   127  	}
   128  
   129  	// If check type is manual force result to WARN
   130  	if c.Type == MANUAL {
   131  		c.Reason = "Test marked as a manual test"
   132  		c.State = WARN
   133  		glog.V(3).Info(c.Reason)
   134  		return c.State
   135  	}
   136  
   137  	// If there aren't any tests defined this is a FAIL or WARN
   138  	if c.Tests == nil || len(c.Tests.TestItems) == 0 {
   139  		c.Reason = "No tests defined"
   140  		if c.Scored {
   141  			c.State = FAIL
   142  		} else {
   143  			c.State = WARN
   144  		}
   145  		glog.V(3).Info(c.Reason)
   146  		return c.State
   147  	}
   148  
   149  	// Command line parameters override the setting in the config file, so if we get a good result from the Audit command that's all we need to run
   150  	var finalOutput *testOutput
   151  	var lastCommand string
   152  
   153  	lastCommand, err := c.runAuditCommands()
   154  	if err == nil {
   155  		finalOutput, err = c.execute()
   156  	}
   157  
   158  	if finalOutput != nil {
   159  		if finalOutput.testResult {
   160  			c.State = PASS
   161  		} else {
   162  			if c.Scored {
   163  				c.State = FAIL
   164  			} else {
   165  				c.State = WARN
   166  			}
   167  		}
   168  
   169  		c.ActualValue = finalOutput.actualResult
   170  		c.ExpectedResult = finalOutput.ExpectedResult
   171  	}
   172  
   173  	if err != nil {
   174  		c.Reason = err.Error()
   175  		if c.Scored {
   176  			c.State = FAIL
   177  		} else {
   178  			c.State = WARN
   179  		}
   180  		glog.V(3).Info(c.Reason)
   181  	}
   182  
   183  	if finalOutput != nil {
   184  		glog.V(3).Infof("Command: %q TestResult: %t State: %q \n", lastCommand, finalOutput.testResult, c.State)
   185  	} else {
   186  		glog.V(3).Infof("Command: %q TestResult: <<EMPTY>> \n", lastCommand)
   187  	}
   188  
   189  	if c.Reason != "" {
   190  		glog.V(2).Info(c.Reason)
   191  	}
   192  	return c.State
   193  }
   194  
   195  func (c *Check) runAuditCommands() (lastCommand string, err error) {
   196  	// Always run auditEnvOutput if needed
   197  	if c.AuditEnv != "" {
   198  		c.AuditEnvOutput, err = runAudit(c.AuditEnv)
   199  		if err != nil {
   200  			return c.AuditEnv, err
   201  		}
   202  	}
   203  
   204  	// Run the audit command and auditConfig commands, if present
   205  	c.AuditOutput, err = runAudit(c.Audit)
   206  	if err != nil {
   207  		return c.Audit, err
   208  	}
   209  
   210  	c.AuditConfigOutput, err = runAudit(c.AuditConfig)
   211  	// when file not found then error comes as exit status 127
   212  	// in some env same error comes as exit status 1
   213  	if err != nil && (strings.Contains(err.Error(), "exit status 127") ||
   214  		strings.Contains(err.Error(), "No such file or directory")) &&
   215  		(c.AuditEnvOutput != "" || c.AuditOutput != "") {
   216  		// suppress file not found error when there is Audit OR auditEnv output present
   217  		glog.V(3).Info(err)
   218  		err = nil
   219  		c.AuditConfigOutput = ""
   220  	}
   221  	return c.AuditConfig, err
   222  }
   223  
   224  func (c *Check) execute() (finalOutput *testOutput, err error) {
   225  	finalOutput = &testOutput{}
   226  
   227  	ts := c.Tests
   228  	res := make([]testOutput, len(ts.TestItems))
   229  	expectedResultArr := make([]string, len(res))
   230  
   231  	glog.V(3).Infof("Running %d test_items", len(ts.TestItems))
   232  	for i, t := range ts.TestItems {
   233  
   234  		t.isMultipleOutput = c.IsMultiple
   235  
   236  		// Try with the auditOutput first, and if that's not found, try the auditConfigOutput
   237  		t.auditUsed = AuditCommand
   238  		result := *(t.execute(c.AuditOutput))
   239  
   240  		// Check for AuditConfigOutput only if AuditConfig is set and auditConfigOutput is not empty
   241  		if !result.flagFound && c.AuditConfig != "" && c.AuditConfigOutput != "" {
   242  			// t.isConfigSetting = true
   243  			t.auditUsed = AuditConfig
   244  			result = *(t.execute(c.AuditConfigOutput))
   245  			if !result.flagFound && t.Env != "" {
   246  				t.auditUsed = AuditEnv
   247  				result = *(t.execute(c.AuditEnvOutput))
   248  			}
   249  		}
   250  
   251  		if !result.flagFound && t.Env != "" {
   252  			t.auditUsed = AuditEnv
   253  			result = *(t.execute(c.AuditEnvOutput))
   254  		}
   255  		glog.V(2).Infof("Used %s", t.auditUsed)
   256  		res[i] = result
   257  		expectedResultArr[i] = res[i].ExpectedResult
   258  	}
   259  
   260  	var result bool
   261  	// If no binary operation is specified, default to AND
   262  	switch ts.BinOp {
   263  	default:
   264  		glog.V(2).Info(fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp))
   265  		finalOutput.actualResult = fmt.Sprintf("unknown binary operator for tests %s\n", ts.BinOp)
   266  		return finalOutput, fmt.Errorf("unknown binary operator for tests %s", ts.BinOp)
   267  	case and, "":
   268  		result = true
   269  		for i := range res {
   270  			result = result && res[i].testResult
   271  		}
   272  		// Generate an AND expected result
   273  		finalOutput.ExpectedResult = strings.Join(expectedResultArr, " AND ")
   274  
   275  	case or:
   276  		result = false
   277  		for i := range res {
   278  			result = result || res[i].testResult
   279  		}
   280  		// Generate an OR expected result
   281  		finalOutput.ExpectedResult = strings.Join(expectedResultArr, " OR ")
   282  	}
   283  
   284  	finalOutput.testResult = result
   285  	finalOutput.actualResult = res[0].actualResult
   286  
   287  	glog.V(3).Infof("Returning from execute on tests: finalOutput %#v", finalOutput)
   288  	return finalOutput, nil
   289  }
   290  
   291  func runAudit(audit string) (output string, err error) {
   292  	var out bytes.Buffer
   293  
   294  	audit = strings.TrimSpace(audit)
   295  	if len(audit) == 0 {
   296  		return output, err
   297  	}
   298  
   299  	cmd := exec.Command("/bin/sh")
   300  	cmd.Stdin = strings.NewReader(audit)
   301  	cmd.Stdout = &out
   302  	cmd.Stderr = &out
   303  	err = cmd.Run()
   304  	output = out.String()
   305  
   306  	if err != nil {
   307  		err = fmt.Errorf("failed to run: %q, output: %q, error: %s", audit, output, err)
   308  	} else {
   309  		glog.V(3).Infof("Command: %q", audit)
   310  		glog.V(3).Infof("Output:\n %q", output)
   311  	}
   312  	return output, err
   313  }