2017-07-13 00:24:09 +00:00
package cmd
import (
"fmt"
"os"
"os/exec"
2018-06-29 11:19:34 +00:00
"path/filepath"
2017-08-11 16:59:57 +00:00
"regexp"
2018-06-29 11:19:34 +00:00
"strconv"
2017-07-13 00:24:09 +00:00
"strings"
"github.com/aquasecurity/kube-bench/check"
"github.com/fatih/color"
2017-07-25 00:34:07 +00:00
"github.com/golang/glog"
2017-08-30 17:01:53 +00:00
"github.com/spf13/viper"
2017-07-13 00:24:09 +00:00
)
var (
// Print colors
colors = map [ check . State ] * color . Color {
check . PASS : color . New ( color . FgGreen ) ,
check . FAIL : color . New ( color . FgRed ) ,
check . WARN : color . New ( color . FgYellow ) ,
check . INFO : color . New ( color . FgBlue ) ,
}
)
2017-08-30 16:51:28 +00:00
var psFunc func ( string ) string
var statFunc func ( string ) ( os . FileInfo , error )
func init ( ) {
psFunc = ps
statFunc = os . Stat
}
2017-08-06 16:59:03 +00:00
func printlnWarn ( msg string ) {
2017-07-25 00:34:07 +00:00
fmt . Fprintf ( os . Stderr , "[%s] %s\n" ,
colors [ check . WARN ] . Sprintf ( "%s" , check . WARN ) ,
msg ,
)
}
2017-08-06 16:59:03 +00:00
func sprintlnWarn ( msg string ) string {
2017-07-25 00:34:07 +00:00
return fmt . Sprintf ( "[%s] %s" ,
colors [ check . WARN ] . Sprintf ( "%s" , check . WARN ) ,
msg ,
)
}
func exitWithError ( err error ) {
fmt . Fprintf ( os . Stderr , "\n%v\n" , err )
os . Exit ( 1 )
}
func continueWithError ( err error , msg string ) string {
2017-07-13 00:24:09 +00:00
if err != nil {
2017-08-30 17:37:01 +00:00
glog . V ( 2 ) . Info ( err )
2017-07-13 00:24:09 +00:00
}
2017-07-25 00:34:07 +00:00
if msg != "" {
fmt . Fprintf ( os . Stderr , "%s\n" , msg )
}
return ""
2017-07-13 00:24:09 +00:00
}
func cleanIDs ( list string ) [ ] string {
list = strings . Trim ( list , "," )
ids := strings . Split ( list , "," )
for _ , id := range ids {
id = strings . Trim ( id , " " )
}
return ids
}
2017-08-15 15:44:40 +00:00
// ps execs out to the ps command; it's separated into a function so we can write tests
func ps ( proc string ) string {
cmd := exec . Command ( "ps" , "-C" , proc , "-o" , "cmd" , "--no-headers" )
2017-07-13 00:24:09 +00:00
out , err := cmd . Output ( )
2017-07-25 00:34:07 +00:00
if err != nil {
continueWithError ( fmt . Errorf ( "%s: %s" , cmd . Args , err ) , "" )
}
2017-07-13 00:24:09 +00:00
2017-08-15 15:44:40 +00:00
return string ( out )
}
2017-07-13 00:24:09 +00:00
2017-08-30 17:01:53 +00:00
// getBinaries finds which of the set of candidate executables are running
2017-08-31 13:45:16 +00:00
func getBinaries ( v * viper . Viper ) map [ string ] string {
2017-08-30 17:01:53 +00:00
binmap := make ( map [ string ] string )
2017-08-31 13:45:16 +00:00
for _ , component := range v . GetStringSlice ( "components" ) {
s := v . Sub ( component )
if s == nil {
continue
2017-08-30 17:01:53 +00:00
}
2017-08-31 13:45:16 +00:00
optional := s . GetBool ( "optional" )
bins := s . GetStringSlice ( "bins" )
if len ( bins ) > 0 {
bin , err := findExecutable ( bins )
if err != nil && ! optional {
exitWithError ( fmt . Errorf ( "need %s executable but none of the candidates are running" , component ) )
}
// Default the executable name that we'll substitute to the name of the component
if bin == "" {
bin = component
glog . V ( 2 ) . Info ( fmt . Sprintf ( "Component %s not running" , component ) )
} else {
glog . V ( 2 ) . Info ( fmt . Sprintf ( "Component %s uses running binary %s" , component , bin ) )
}
binmap [ component ] = bin
}
2017-08-30 17:01:53 +00:00
}
2017-08-31 13:45:16 +00:00
2017-08-30 17:01:53 +00:00
return binmap
}
2018-06-29 11:19:34 +00:00
// getConfigFilePath locates the config files we should be using based on either the specified
// version, or the running version of kubernetes if not specified
func getConfigFilePath ( specifiedVersion string , runningVersion string , filename string ) ( path string , err error ) {
var fileVersion string
if specifiedVersion != "" {
fileVersion = specifiedVersion
} else {
fileVersion = runningVersion
}
for {
path = filepath . Join ( cfgDir , fileVersion )
file := filepath . Join ( path , string ( filename ) )
glog . V ( 2 ) . Info ( fmt . Sprintf ( "Looking for config file: %s\n" , file ) )
if _ , err = os . Stat ( file ) ; ! os . IsNotExist ( err ) {
if specifiedVersion == "" && fileVersion != runningVersion {
glog . V ( 1 ) . Info ( fmt . Sprintf ( "No test file found for %s - using tests for Kubernetes %s\n" , runningVersion , fileVersion ) )
}
return path , nil
}
// If we were given an explicit version to look for, don't look for any others
if specifiedVersion != "" {
return "" , err
}
fileVersion = decrementVersion ( fileVersion )
if fileVersion == "" {
return "" , fmt . Errorf ( "no test files found <= runningVersion" )
}
}
}
// decrementVersion decrements the version number
// We want to decrement individually even through versions where we don't supply test files
// just in case someone wants to specify their own test files for that version
func decrementVersion ( version string ) string {
split := strings . Split ( version , "." )
minor , err := strconv . Atoi ( split [ 1 ] )
if err != nil {
return ""
}
if minor <= 1 {
return ""
}
split [ 1 ] = strconv . Itoa ( minor - 1 )
return strings . Join ( split , "." )
}
2017-08-30 17:01:53 +00:00
// getConfigFiles finds which of the set of candidate config files exist
2017-10-24 12:01:02 +00:00
// accepts a string 't' which indicates the type of config file, conf,
// podspec or untifile.
2017-11-28 17:33:42 +00:00
func getConfigFiles ( v * viper . Viper ) map [ string ] string {
2017-08-30 17:01:53 +00:00
confmap := make ( map [ string ] string )
2017-08-31 13:45:16 +00:00
for _ , component := range v . GetStringSlice ( "components" ) {
s := v . Sub ( component )
if s == nil {
continue
}
// See if any of the candidate config files exist
2017-11-28 17:33:42 +00:00
conf := findConfigFile ( s . GetStringSlice ( "confs" ) )
2017-08-30 17:01:53 +00:00
if conf == "" {
2017-11-28 17:33:42 +00:00
if s . IsSet ( "defaultconf" ) {
conf = s . GetString ( "defaultconf" )
2017-08-31 13:45:16 +00:00
glog . V ( 2 ) . Info ( fmt . Sprintf ( "Using default config file name '%s' for component %s" , conf , component ) )
} else {
// Default the config file name that we'll substitute to the name of the component
2017-11-03 10:41:01 +00:00
glog . V ( 2 ) . Info ( fmt . Sprintf ( "Missing config file for %s" , component ) )
2017-08-31 13:45:16 +00:00
conf = component
}
2017-08-30 17:01:53 +00:00
} else {
2017-08-31 13:45:16 +00:00
glog . V ( 2 ) . Info ( fmt . Sprintf ( "Component %s uses config file '%s'" , component , conf ) )
2017-08-30 17:01:53 +00:00
}
2017-08-31 13:45:16 +00:00
confmap [ component ] = conf
2017-08-30 17:01:53 +00:00
}
return confmap
}
2017-08-15 15:44:40 +00:00
// verifyBin checks that the binary specified is running
2017-08-30 16:51:28 +00:00
func verifyBin ( bin string ) bool {
2017-07-13 00:24:09 +00:00
2017-08-15 15:44:40 +00:00
// Strip any quotes
bin = strings . Trim ( bin , "'\"" )
// bin could consist of more than one word
// We'll search for running processes with the first word, and then check the whole
// proc as supplied is included in the results
proc := strings . Fields ( bin ) [ 0 ]
out := psFunc ( proc )
2017-08-31 15:01:31 +00:00
// There could be multiple lines in the ps output
// The binary needs to be the first word in the ps output, except that it could be preceded by a path
// e.g. /usr/bin/kubelet is a match for kubelet
// but apiserver is not a match for kube-apiserver
reFirstWord := regexp . MustCompile ( ` ^(\S*\/)* ` + bin )
lines := strings . Split ( out , "\n" )
for _ , l := range lines {
if reFirstWord . Match ( [ ] byte ( l ) ) {
2017-08-30 16:48:12 +00:00
return true
}
}
return false
2017-07-13 00:24:09 +00:00
}
2017-08-30 17:01:53 +00:00
// fundConfigFile looks through a list of possible config files and finds the first one that exists
func findConfigFile ( candidates [ ] string ) string {
for _ , c := range candidates {
_ , err := statFunc ( c )
if err == nil {
return c
}
if ! os . IsNotExist ( err ) {
exitWithError ( fmt . Errorf ( "error looking for file %s: %v" , c , err ) )
}
}
return ""
}
2017-08-30 11:07:46 +00:00
// findExecutable looks through a list of possible executable names and finds the first one that's running
2017-08-30 16:51:28 +00:00
func findExecutable ( candidates [ ] string ) ( string , error ) {
2017-08-30 11:07:46 +00:00
for _ , c := range candidates {
2017-08-30 16:51:28 +00:00
if verifyBin ( c ) {
2017-08-30 11:07:46 +00:00
return c , nil
}
2018-01-11 18:01:58 +00:00
glog . V ( 1 ) . Info ( fmt . Sprintf ( "executable '%s' not running" , c ) )
2017-08-30 11:07:46 +00:00
}
return "" , fmt . Errorf ( "no candidates running" )
2017-07-13 00:24:09 +00:00
}
2017-08-15 16:00:35 +00:00
func multiWordReplace ( s string , subname string , sub string ) string {
f := strings . Fields ( sub )
if len ( f ) > 1 {
sub = "'" + sub + "'"
}
return strings . Replace ( s , subname , sub , - 1 )
}
2017-09-17 14:35:25 +00:00
2017-11-03 12:59:35 +00:00
func getKubeVersion ( ) string {
2017-09-17 14:35:25 +00:00
// These executables might not be on the user's path.
_ , err := exec . LookPath ( "kubectl" )
2018-03-23 08:29:17 +00:00
2017-09-17 14:35:25 +00:00
if err != nil {
2018-03-23 08:29:17 +00:00
_ , err = exec . LookPath ( "kubelet" )
if err != nil {
2018-05-11 17:58:24 +00:00
exitWithError ( fmt . Errorf ( "Version check failed: need kubectl or kubelet binaries to get kubernetes version.\nAlternately, you can specify the version with --version" ) )
2018-03-23 08:29:17 +00:00
}
return getKubeVersionFromKubelet ( )
2017-09-17 14:35:25 +00:00
}
2018-03-23 08:29:17 +00:00
return getKubeVersionFromKubectl ( )
}
func getKubeVersionFromKubectl ( ) string {
2017-11-03 12:59:35 +00:00
cmd := exec . Command ( "kubectl" , "version" , "--short" )
out , err := cmd . CombinedOutput ( )
2017-09-17 14:35:25 +00:00
if err != nil {
2017-11-13 15:25:34 +00:00
continueWithError ( fmt . Errorf ( "%s" , out ) , "" )
2017-09-17 14:35:25 +00:00
}
2017-11-21 13:19:09 +00:00
return getVersionFromKubectlOutput ( string ( out ) )
}
2017-09-17 14:35:25 +00:00
2018-03-23 08:29:17 +00:00
func getKubeVersionFromKubelet ( ) string {
cmd := exec . Command ( "kubelet" , "--version" )
out , err := cmd . CombinedOutput ( )
2018-05-11 17:58:24 +00:00
2018-03-23 08:29:17 +00:00
if err != nil {
continueWithError ( fmt . Errorf ( "%s" , out ) , "" )
}
return getVersionFromKubeletOutput ( string ( out ) )
}
2017-11-21 13:19:09 +00:00
func getVersionFromKubectlOutput ( s string ) string {
serverVersionRe := regexp . MustCompile ( ` Server Version: v(\d+.\d+) ` )
subs := serverVersionRe . FindStringSubmatch ( s )
if len ( subs ) < 2 {
2017-11-13 15:25:34 +00:00
printlnWarn ( fmt . Sprintf ( "Unable to get kubectl version, using default version: %s" , defaultKubeVersion ) )
2017-11-21 13:19:09 +00:00
return defaultKubeVersion
2017-11-03 12:59:35 +00:00
}
2017-11-21 13:19:09 +00:00
return subs [ 1 ]
2017-09-17 14:35:25 +00:00
}
2017-09-20 00:39:30 +00:00
2018-03-23 08:29:17 +00:00
func getVersionFromKubeletOutput ( s string ) string {
serverVersionRe := regexp . MustCompile ( ` Kubernetes v(\d+.\d+) ` )
subs := serverVersionRe . FindStringSubmatch ( s )
if len ( subs ) < 2 {
printlnWarn ( fmt . Sprintf ( "Unable to get kubelet version, using default version: %s" , defaultKubeVersion ) )
return defaultKubeVersion
}
return subs [ 1 ]
}
2017-08-30 17:37:01 +00:00
func makeSubstitutions ( s string , ext string , m map [ string ] string ) string {
for k , v := range m {
subst := "$" + k + ext
2017-08-31 13:43:59 +00:00
if v == "" {
glog . V ( 2 ) . Info ( fmt . Sprintf ( "No subsitution for '%s'\n" , subst ) )
continue
}
2018-06-29 11:19:00 +00:00
glog . V ( 2 ) . Info ( fmt . Sprintf ( "Substituting %s with '%s'\n" , subst , v ) )
2017-08-30 17:37:01 +00:00
s = multiWordReplace ( s , subst , v )
}
return s
}