github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/linter/kubebench/common.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 kubebench 16 17 import ( 18 "bufio" 19 "encoding/json" 20 "fmt" 21 "io/ioutil" 22 "os" 23 "path/filepath" 24 "sort" 25 "strconv" 26 "strings" 27 28 check2 "github.com/castai/kvisor/cmd/linter/kubebench/check" 29 "github.com/golang/glog" 30 "github.com/spf13/viper" 31 ) 32 33 // NewRunFilter constructs a Predicate based on FilterOpts which determines whether tested Checks should be run or not. 34 func NewRunFilter(opts FilterOpts) (check2.Predicate, error) { 35 if opts.CheckList != "" && opts.GroupList != "" { 36 return nil, fmt.Errorf("group option and check option can't be used together") 37 } 38 39 var groupIDs map[string]bool 40 if opts.GroupList != "" { 41 groupIDs = cleanIDs(opts.GroupList) 42 } 43 44 var checkIDs map[string]bool 45 if opts.CheckList != "" { 46 checkIDs = cleanIDs(opts.CheckList) 47 } 48 49 return func(g *check2.Group, c *check2.Check) bool { 50 test := true 51 if len(groupIDs) > 0 { 52 _, ok := groupIDs[g.ID] 53 test = test && ok 54 } 55 56 if len(checkIDs) > 0 { 57 _, ok := checkIDs[c.ID] 58 test = test && ok 59 } 60 61 test = test && (opts.Scored && c.Scored || opts.Unscored && !c.Scored) 62 63 return test 64 }, nil 65 } 66 67 func runChecks(nodetype check2.NodeType, testYamlFile, detectedVersion string) { 68 // Verify config file was loaded into Viper during Cobra sub-command initialization. 69 if configFileError != nil { 70 colorPrint(check2.FAIL, fmt.Sprintf("Failed to read config file: %v\n", configFileError)) 71 os.Exit(1) 72 } 73 74 in, err := ioutil.ReadFile(testYamlFile) 75 if err != nil { 76 exitWithError(fmt.Errorf("error opening %s test file: %v", testYamlFile, err)) 77 } 78 79 glog.V(1).Info(fmt.Sprintf("Using test file: %s\n", testYamlFile)) 80 81 // Get the viper config for this section of tests 82 typeConf := viper.Sub(string(nodetype)) 83 if typeConf == nil { 84 colorPrint(check2.FAIL, fmt.Sprintf("No config settings for %s\n", string(nodetype))) 85 os.Exit(1) 86 } 87 88 // Get the set of executables we need for this section of the tests 89 binmap, err := getBinaries(typeConf, nodetype) 90 // Checks that the executables we need for the section are running. 91 if err != nil { 92 glog.V(1).Info(fmt.Sprintf("failed to get a set of executables needed for tests: %v", err)) 93 } 94 95 confmap := getFiles(typeConf, "config") 96 svcmap := getFiles(typeConf, "service") 97 kubeconfmap := getFiles(typeConf, "kubeconfig") 98 cafilemap := getFiles(typeConf, "ca") 99 datadirmap := getFiles(typeConf, "datadir") 100 101 // Variable substitutions. Replace all occurrences of variables in controls files. 102 s := string(in) 103 s, binSubs := makeSubstitutions(s, "bin", binmap) 104 s, _ = makeSubstitutions(s, "conf", confmap) 105 s, _ = makeSubstitutions(s, "svc", svcmap) 106 s, _ = makeSubstitutions(s, "kubeconfig", kubeconfmap) 107 s, _ = makeSubstitutions(s, "cafile", cafilemap) 108 s, _ = makeSubstitutions(s, "datadir", datadirmap) 109 110 controls, err := check2.NewControls(nodetype, []byte(s), detectedVersion) 111 if err != nil { 112 exitWithError(fmt.Errorf("error setting up %s controls: %v", nodetype, err)) 113 } 114 115 runner := check2.NewRunner() 116 filter, err := NewRunFilter(filterOpts) 117 if err != nil { 118 exitWithError(fmt.Errorf("error setting up run filter: %v", err)) 119 } 120 121 generateDefaultEnvAudit(controls, binSubs) 122 123 controls.RunChecks(runner, filter, parseSkipIds(skipIds)) 124 controlsCollection = append(controlsCollection, controls) 125 } 126 127 func generateDefaultEnvAudit(controls *check2.Controls, binSubs []string) { 128 for _, group := range controls.Groups { 129 for _, checkItem := range group.Checks { 130 if checkItem.Tests != nil && !checkItem.DisableEnvTesting { 131 for _, test := range checkItem.Tests.TestItems { 132 if test.Env != "" && checkItem.AuditEnv == "" { 133 binPath := "" 134 135 if len(binSubs) == 1 { 136 binPath = binSubs[0] 137 } else { 138 glog.V(1).Infof("AuditEnv not explicit for check (%s), where bin path cannot be determined", checkItem.ID) 139 } 140 141 if test.Env != "" && checkItem.AuditEnv == "" { 142 checkItem.AuditEnv = fmt.Sprintf("cat \"/proc/$(/bin/ps -C %s -o pid= | tr -d ' ')/environ\" | tr '\\0' '\\n'", binPath) 143 } 144 } 145 } 146 } 147 } 148 } 149 } 150 151 func parseSkipIds(skipIds string) map[string]bool { 152 skipIdMap := make(map[string]bool, 0) 153 if skipIds != "" { 154 for _, id := range strings.Split(skipIds, ",") { 155 skipIdMap[strings.Trim(id, " ")] = true 156 } 157 } 158 return skipIdMap 159 } 160 161 // colorPrint outputs the state in a specific colour, along with a message string 162 func colorPrint(state check2.State, s string) { 163 colors[state].Printf("[%s] ", state) 164 fmt.Printf("%s", s) 165 } 166 167 // prettyPrint outputs the results to stdout in human-readable format 168 func prettyPrint(r *check2.Controls, summary check2.Summary) { 169 // Print check results. 170 if !noResults { 171 colorPrint(check2.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text)) 172 for _, g := range r.Groups { 173 colorPrint(check2.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text)) 174 for _, c := range g.Checks { 175 colorPrint(c.State, fmt.Sprintf("%s %s\n", c.ID, c.Text)) 176 177 if includeTestOutput && c.State == check2.FAIL && len(c.ActualValue) > 0 { 178 printRawOutput(c.ActualValue) 179 } 180 } 181 } 182 183 fmt.Println() 184 } 185 186 // Print remediations. 187 if !noRemediations { 188 if summary.Fail > 0 || summary.Warn > 0 { 189 colors[check2.WARN].Printf("== Remediations %s ==\n", r.Type) 190 for _, g := range r.Groups { 191 for _, c := range g.Checks { 192 if c.State == check2.FAIL { 193 fmt.Printf("%s %s\n", c.ID, c.Remediation) 194 } 195 if c.State == check2.WARN { 196 // Print the error if test failed due to problem with the audit command 197 if c.Reason != "" && c.Type != "manual" { 198 fmt.Printf("%s audit test did not run: %s\n", c.ID, c.Reason) 199 } else { 200 fmt.Printf("%s %s\n", c.ID, c.Remediation) 201 } 202 } 203 } 204 } 205 fmt.Println() 206 } 207 } 208 209 // Print summary setting output color to highest severity. 210 if !noSummary { 211 printSummary(summary, string(r.Type)) 212 } 213 } 214 215 func printSummary(summary check2.Summary, sectionName string) { 216 var res check2.State 217 if summary.Fail > 0 { 218 res = check2.FAIL 219 } else if summary.Warn > 0 { 220 res = check2.WARN 221 } else { 222 res = check2.PASS 223 } 224 225 colors[res].Printf("== Summary %s ==\n", sectionName) 226 fmt.Printf("%d checks PASS\n%d checks FAIL\n%d checks WARN\n%d checks INFO\n\n", 227 summary.Pass, summary.Fail, summary.Warn, summary.Info, 228 ) 229 } 230 231 // loadConfig finds the correct config dir based on the kubernetes version, 232 // merges any specific config.yaml file found with the main config 233 // and returns the benchmark file to use. 234 func loadConfig(nodetype check2.NodeType, benchmarkVersion string) string { 235 var file string 236 var err error 237 238 switch nodetype { 239 case check2.MASTER: 240 file = masterFile 241 case check2.NODE: 242 file = nodeFile 243 case check2.CONTROLPLANE: 244 file = controlplaneFile 245 case check2.ETCD: 246 file = etcdFile 247 case check2.POLICIES: 248 file = policiesFile 249 case check2.MANAGEDSERVICES: 250 file = managedservicesFile 251 } 252 253 path, err := getConfigFilePath(benchmarkVersion, file) 254 if err != nil { 255 exitWithError(fmt.Errorf("can't find %s controls file in %s: %v", nodetype, cfgDir, err)) 256 } 257 258 // Merge version-specific config if any. 259 mergeConfig(path) 260 261 return filepath.Join(path, file) 262 } 263 264 func mergeConfig(path string) error { 265 viper.SetConfigFile(path + "/config.yaml") 266 err := viper.MergeInConfig() 267 if err != nil { 268 if os.IsNotExist(err) { 269 glog.V(2).Info(fmt.Sprintf("No version-specific config.yaml file in %s", path)) 270 } else { 271 return fmt.Errorf("couldn't read config file %s: %v", path+"/config.yaml", err) 272 } 273 } 274 275 glog.V(1).Info(fmt.Sprintf("Using config file: %s\n", viper.ConfigFileUsed())) 276 277 return nil 278 } 279 280 func mapToBenchmarkVersion(kubeToBenchmarkMap map[string]string, kv string) (string, error) { 281 kvOriginal := kv 282 cisVersion, found := kubeToBenchmarkMap[kv] 283 glog.V(2).Info(fmt.Sprintf("mapToBenchmarkVersion for k8sVersion: %q cisVersion: %q found: %t\n", kv, cisVersion, found)) 284 for !found && (kv != defaultKubeVersion && !isEmpty(kv)) { 285 kv = decrementVersion(kv) 286 cisVersion, found = kubeToBenchmarkMap[kv] 287 glog.V(2).Info(fmt.Sprintf("mapToBenchmarkVersion for k8sVersion: %q cisVersion: %q found: %t\n", kv, cisVersion, found)) 288 } 289 290 if !found { 291 glog.V(1).Info(fmt.Sprintf("mapToBenchmarkVersion unable to find a match for: %q", kvOriginal)) 292 glog.V(3).Info(fmt.Sprintf("mapToBenchmarkVersion kubeToBenchmarkMap: %#v", kubeToBenchmarkMap)) 293 return "", fmt.Errorf("unable to find a matching Benchmark Version match for kubernetes version: %s", kvOriginal) 294 } 295 296 return cisVersion, nil 297 } 298 299 func loadVersionMapping(v *viper.Viper) (map[string]string, error) { 300 kubeToBenchmarkMap := v.GetStringMapString("version_mapping") 301 if kubeToBenchmarkMap == nil || (len(kubeToBenchmarkMap) == 0) { 302 return nil, fmt.Errorf("config file is missing 'version_mapping' section") 303 } 304 305 return kubeToBenchmarkMap, nil 306 } 307 308 func loadTargetMapping(v *viper.Viper) (map[string][]string, error) { 309 benchmarkVersionToTargetsMap := v.GetStringMapStringSlice("target_mapping") 310 if len(benchmarkVersionToTargetsMap) == 0 { 311 return nil, fmt.Errorf("config file is missing 'target_mapping' section") 312 } 313 314 return benchmarkVersionToTargetsMap, nil 315 } 316 317 func getBenchmarkVersion(kubeVersion, benchmarkVersion string, platform Platform, v *viper.Viper) (bv string, err error) { 318 detecetedKubeVersion = "none" 319 if !isEmpty(kubeVersion) && !isEmpty(benchmarkVersion) { 320 return "", fmt.Errorf("It is an error to specify both --version and --benchmark flags") 321 } 322 if isEmpty(benchmarkVersion) && isEmpty(kubeVersion) && !isEmpty(platform.Name) { 323 benchmarkVersion = getPlatformBenchmarkVersion(platform) 324 if !isEmpty(benchmarkVersion) { 325 detecetedKubeVersion = benchmarkVersion 326 } 327 } 328 329 if isEmpty(benchmarkVersion) { 330 if isEmpty(kubeVersion) { 331 kv, err := getKubeVersion() 332 if err != nil { 333 return "", fmt.Errorf("Version check failed: %s\nAlternatively, you can specify the version with --version", err) 334 } 335 kubeVersion = kv.BaseVersion() 336 detecetedKubeVersion = kubeVersion 337 } 338 339 kubeToBenchmarkMap, err := loadVersionMapping(v) 340 if err != nil { 341 return "", err 342 } 343 344 benchmarkVersion, err = mapToBenchmarkVersion(kubeToBenchmarkMap, kubeVersion) 345 if err != nil { 346 return "", err 347 } 348 349 glog.V(2).Info(fmt.Sprintf("Mapped Kubernetes version: %s to Benchmark version: %s", kubeVersion, benchmarkVersion)) 350 } 351 352 glog.V(1).Info(fmt.Sprintf("Kubernetes version: %q to Benchmark version: %q", kubeVersion, benchmarkVersion)) 353 return benchmarkVersion, nil 354 } 355 356 // isMaster verify if master components are running on the node. 357 func isMaster() bool { 358 return isThisNodeRunning(check2.MASTER) 359 } 360 361 // isEtcd verify if etcd components are running on the node. 362 func isEtcd() bool { 363 return isThisNodeRunning(check2.ETCD) 364 } 365 366 func isThisNodeRunning(nodeType check2.NodeType) bool { 367 glog.V(3).Infof("Checking if the current node is running %s components", nodeType) 368 nodeTypeConf := viper.Sub(string(nodeType)) 369 if nodeTypeConf == nil { 370 glog.V(2).Infof("No config for %s components found", nodeType) 371 return false 372 } 373 374 components, err := getBinariesFunc(nodeTypeConf, nodeType) 375 if err != nil { 376 glog.V(2).Infof("Failed to find %s binaries: %v", nodeType, err) 377 return false 378 } 379 if len(components) == 0 { 380 glog.V(2).Infof("No %s binaries specified", nodeType) 381 return false 382 } 383 384 glog.V(2).Infof("Node is running %s components", nodeType) 385 return true 386 } 387 388 func exitCodeSelection(controlsCollection []*check2.Controls) int { 389 for _, control := range controlsCollection { 390 if control.Fail > 0 { 391 return exitCode 392 } 393 } 394 395 return 0 396 } 397 398 func writeOutput(controlsCollection []*check2.Controls) { 399 sort.Slice(controlsCollection, func(i, j int) bool { 400 iid, _ := strconv.Atoi(controlsCollection[i].ID) 401 jid, _ := strconv.Atoi(controlsCollection[j].ID) 402 return iid < jid 403 }) 404 if junitFmt { 405 writeJunitOutput(controlsCollection) 406 return 407 } 408 if jsonFmt { 409 writeJSONOutput(controlsCollection) 410 return 411 } 412 writeStdoutOutput(controlsCollection) 413 } 414 415 func writeJSONOutput(controlsCollection []*check2.Controls) { 416 var out []byte 417 var err error 418 if !noTotals { 419 var totals check2.OverallControls 420 totals.Controls = controlsCollection 421 totals.Totals = getSummaryTotals(controlsCollection) 422 out, err = json.Marshal(totals) 423 } else { 424 out, err = json.Marshal(controlsCollection) 425 } 426 if err != nil { 427 exitWithError(fmt.Errorf("failed to output in JSON format: %v", err)) 428 } 429 printOutput(string(out), outputFile) 430 } 431 432 func writeJunitOutput(controlsCollection []*check2.Controls) { 433 // QuickFix for issue https://github.com/aquasecurity/kube-bench/issues/883 434 // Should consider to deprecate of switch to using Junit template 435 prefix := "<testsuites>\n" 436 suffix := "\n</testsuites>" 437 var outputAllControls []byte 438 for _, controls := range controlsCollection { 439 tempOut, err := controls.JUnit() 440 outputAllControls = append(outputAllControls[:], tempOut[:]...) 441 if err != nil { 442 exitWithError(fmt.Errorf("failed to output in JUnit format: %v", err)) 443 } 444 } 445 printOutput(prefix+string(outputAllControls)+suffix, outputFile) 446 } 447 448 func writeStdoutOutput(controlsCollection []*check2.Controls) { 449 for _, controls := range controlsCollection { 450 summary := controls.Summary 451 prettyPrint(controls, summary) 452 } 453 if !noTotals { 454 printSummary(getSummaryTotals(controlsCollection), "total") 455 } 456 } 457 458 func getSummaryTotals(controlsCollection []*check2.Controls) check2.Summary { 459 var totalSummary check2.Summary 460 for _, controls := range controlsCollection { 461 summary := controls.Summary 462 totalSummary.Fail = totalSummary.Fail + summary.Fail 463 totalSummary.Warn = totalSummary.Warn + summary.Warn 464 totalSummary.Pass = totalSummary.Pass + summary.Pass 465 totalSummary.Info = totalSummary.Info + summary.Info 466 } 467 return totalSummary 468 } 469 470 func printRawOutput(output string) { 471 for _, row := range strings.Split(output, "\n") { 472 fmt.Println(fmt.Sprintf("\t %s", row)) 473 } 474 } 475 476 func writeOutputToFile(output string, outputFile string) error { 477 file, err := os.Create(outputFile) 478 if err != nil { 479 return err 480 } 481 defer file.Close() 482 483 w := bufio.NewWriter(file) 484 fmt.Fprintln(w, output) 485 return w.Flush() 486 } 487 488 func printOutput(output string, outputFile string) { 489 if outputFile == "" { 490 fmt.Println(output) 491 } else { 492 err := writeOutputToFile(output, outputFile) 493 if err != nil { 494 exitWithError(fmt.Errorf("Failed to write to output file %s: %v", outputFile, err)) 495 } 496 } 497 } 498 499 // validTargets helps determine if the targets 500 // are legitimate for the benchmarkVersion. 501 func validTargets(benchmarkVersion string, targets []string, v *viper.Viper) (bool, error) { 502 benchmarkVersionToTargetsMap, err := loadTargetMapping(v) 503 if err != nil { 504 return false, err 505 } 506 providedTargets, found := benchmarkVersionToTargetsMap[benchmarkVersion] 507 if !found { 508 return false, fmt.Errorf("No targets configured for %s", benchmarkVersion) 509 } 510 511 for _, pt := range targets { 512 f := false 513 for _, t := range providedTargets { 514 if pt == strings.ToLower(t) { 515 f = true 516 break 517 } 518 } 519 520 if !f { 521 return false, nil 522 } 523 } 524 525 return true, nil 526 }