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 }