mirror of
https://github.com/aquasecurity/kube-bench.git
synced 2024-11-23 00:28:07 +00:00
Merge pull request #47 from aquasecurity/auto-detect
Auto-detect executables and config files
This commit is contained in:
commit
516343eb06
202
cfg/config.yaml
202
cfg/config.yaml
@ -7,106 +7,112 @@
|
|||||||
# nodeControls: ./cfg/node.yaml
|
# nodeControls: ./cfg/node.yaml
|
||||||
# federatedControls: ./cfg/federated.yaml
|
# federatedControls: ./cfg/federated.yaml
|
||||||
|
|
||||||
## Support components
|
master:
|
||||||
etcd:
|
components:
|
||||||
bin: etcd
|
- apiserver
|
||||||
conf: /etc/etcd/etcd.conf
|
- scheduler
|
||||||
|
- controllermanager
|
||||||
|
- etcd
|
||||||
|
- flanneld
|
||||||
|
# kubernetes is a component to cover the config file /etc/kubernetes/config that is referred to in the benchmark
|
||||||
|
- kubernetes
|
||||||
|
|
||||||
flanneld:
|
kubernetes:
|
||||||
bin: flanneld
|
defaultconf: /etc/kubernetes/config
|
||||||
conf: /etc/sysconfig/flanneld
|
|
||||||
|
|
||||||
# Installation
|
apiserver:
|
||||||
# Configure kubernetes component binaries and paths to their configuration files.
|
bins:
|
||||||
installation:
|
- "kube-apiserver"
|
||||||
default:
|
- "hyperkube apiserver"
|
||||||
config: /etc/kubernetes/config
|
- "apiserver"
|
||||||
master:
|
confs:
|
||||||
bin:
|
- /etc/kubernetes/manifests/kube-apiserver.yaml
|
||||||
apiserver: apiserver
|
- /etc/kubernetes/apiserver.conf
|
||||||
scheduler: scheduler
|
- /etc/kubernetes/apiserver
|
||||||
controller-manager: controller-manager
|
defaultconf: /etc/kubernetes/apiserver
|
||||||
conf:
|
|
||||||
apiserver: /etc/kubernetes/apiserver
|
|
||||||
scheduler: /etc/kubernetes/scheduler
|
|
||||||
controller-manager: /etc/kubernetes/controller-manager
|
|
||||||
node:
|
|
||||||
bin:
|
|
||||||
kubelet: kubelet
|
|
||||||
proxy: proxy
|
|
||||||
conf:
|
|
||||||
kubelet: /etc/kubernetes/kubelet
|
|
||||||
proxy: /etc/kubernetes/proxy
|
|
||||||
federated:
|
|
||||||
bin:
|
|
||||||
apiserver: federation-apiserver
|
|
||||||
controller-manager: federation-controller-manager
|
|
||||||
|
|
||||||
kops:
|
scheduler:
|
||||||
config: /etc/kubernetes/config
|
bins:
|
||||||
master:
|
- "kube-scheduler"
|
||||||
bin:
|
- "hyperkube scheduler"
|
||||||
apiserver: apiserver
|
- "scheduler"
|
||||||
scheduler: scheduler
|
confs:
|
||||||
controller-manager: controller-manager
|
- /etc/kubernetes/manifests/kube-scheduler.yaml
|
||||||
conf:
|
- /etc/kubernetes/scheduler.conf
|
||||||
apiserver: /etc/kubernetes/apiserver
|
- /etc/kubernetes/scheduler
|
||||||
scheduler: /etc/kubernetes/scheduler
|
defaultconf: /etc/kubernetes/scheduler
|
||||||
controller-manager: /etc/kubernetes/apiserver
|
|
||||||
node:
|
controllermanager:
|
||||||
bin:
|
bins:
|
||||||
kubelet: kubelet
|
- "kube-controller-manager"
|
||||||
proxy: proxy
|
- "hyperkube controller-manager"
|
||||||
conf:
|
- "controller-manager"
|
||||||
kubelet: /etc/kubernetes/kubelet
|
confs:
|
||||||
proxy: /etc/kubernetes/proxy
|
- /etc/kubernetes/manifests/kube-controller-manager.yaml
|
||||||
federated:
|
- /etc/kubernetes/controller-manager.conf
|
||||||
bin:
|
- /etc/kubernetes/controller-manager
|
||||||
apiserver: federation-apiserver
|
defaultconf: /etc/kubernetes/controller-manager
|
||||||
controller-manager: federation-controller-manager
|
|
||||||
|
etcd:
|
||||||
|
optional: true
|
||||||
|
bins:
|
||||||
|
- "etcd"
|
||||||
|
confs:
|
||||||
|
- /etc/kubernetes/manifests/etcd.yaml
|
||||||
|
- /etc/etcd/etcd.conf
|
||||||
|
defaultconf: /etc/etcd/etcd.conf
|
||||||
|
|
||||||
|
flanneld:
|
||||||
|
optional: true
|
||||||
|
bins:
|
||||||
|
- flanneld
|
||||||
|
defaultconf: /etc/sysconfig/flanneld
|
||||||
|
|
||||||
|
|
||||||
|
node:
|
||||||
|
components:
|
||||||
|
- kubelet
|
||||||
|
- proxy
|
||||||
|
# kubernetes is a component to cover the config file /etc/kubernetes/config that is referred to in the benchmark
|
||||||
|
- kubernetes
|
||||||
|
|
||||||
|
kubernetes:
|
||||||
|
defaultconf: /etc/kubernetes/config
|
||||||
|
|
||||||
|
kubelet:
|
||||||
|
bins:
|
||||||
|
- "hyperkube kubelet"
|
||||||
|
- "kubelet"
|
||||||
|
confs:
|
||||||
|
- /etc/kubernetes/kubelet.conf
|
||||||
|
- /etc/kubernetes/kubelet
|
||||||
|
defaultconf: "/etc/kubernetes/kubelet.conf"
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
bins:
|
||||||
|
- "kube-proxy"
|
||||||
|
- "hyperkube proxy"
|
||||||
|
- "proxy"
|
||||||
|
confs:
|
||||||
|
- /etc/kubernetes/proxy.conf
|
||||||
|
- /etc/kubernetes/proxy
|
||||||
|
- /etc/kubernetes/addons/kube-proxy-daemonset.yaml
|
||||||
|
|
||||||
|
federated:
|
||||||
|
components:
|
||||||
|
- fedapiserver
|
||||||
|
- fedcontrollermanager
|
||||||
|
|
||||||
|
fedapiserver:
|
||||||
|
bins:
|
||||||
|
- "hyperkube federation-apiserver"
|
||||||
|
- "kube-federation-apiserver"
|
||||||
|
- "federation-apiserver"
|
||||||
|
|
||||||
|
fedcontrollermanager:
|
||||||
|
bins:
|
||||||
|
- "hyperkube federation-controller-manager"
|
||||||
|
- "kube-federation-controller-manager"
|
||||||
|
- "federation-controller-manager"
|
||||||
|
|
||||||
hyperkube:
|
|
||||||
config: /etc/kubernetes/config
|
|
||||||
master:
|
|
||||||
bin:
|
|
||||||
apiserver: hyperkube apiserver
|
|
||||||
scheduler: hyperkube scheduler
|
|
||||||
controller-manager: hyperkube controller-manager
|
|
||||||
conf:
|
|
||||||
apiserver: /etc/kubernetes/manifests/kube-apiserver.yaml
|
|
||||||
scheduler: /etc/kubernetes/manifests/kube-scheduler.yaml
|
|
||||||
controller-manager: /etc/kubernetes/manifests/kube-controller-manager.yaml
|
|
||||||
node:
|
|
||||||
bin:
|
|
||||||
kubelet: hyperkube kubelet
|
|
||||||
proxy: hyperkube proxy
|
|
||||||
conf:
|
|
||||||
kubelet: /etc/kubernetes/kubelet
|
|
||||||
proxy: /etc/kubernetes/addons/kube-proxy-daemonset.yaml
|
|
||||||
federated:
|
|
||||||
bin:
|
|
||||||
apiserver: hyperkube federation-apiserver
|
|
||||||
controller-manager: hyperkube federation-controller-manager
|
|
||||||
|
|
||||||
kubeadm:
|
|
||||||
config: /etc/kubernetes/config
|
|
||||||
master:
|
|
||||||
bin:
|
|
||||||
apiserver: kube-apiserver
|
|
||||||
scheduler: kube-scheduler
|
|
||||||
controller-manager: kube-controller-manager
|
|
||||||
conf:
|
|
||||||
apiserver: /etc/kubernetes/admin.conf
|
|
||||||
scheduler: /etc/kubernetes/scheduler.conf
|
|
||||||
controller-manager: /etc/kubernetes/controller-manager.conf
|
|
||||||
node:
|
|
||||||
bin:
|
|
||||||
kubelet: kubelet
|
|
||||||
proxy: kube-proxy
|
|
||||||
conf:
|
|
||||||
kubelet: /etc/kubernetes/kubelet.conf
|
|
||||||
proxy: /etc/kubernetes/proxy.conf
|
|
||||||
federated:
|
|
||||||
bin:
|
|
||||||
apiserver: kube-federation-apiserver
|
|
||||||
controller-manager: kube-federation-controller-manager
|
|
||||||
|
@ -636,7 +636,7 @@ groups:
|
|||||||
|
|
||||||
- id: 1.4.3
|
- id: 1.4.3
|
||||||
text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)"
|
text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)"
|
||||||
audit: "/bin/sh -c 'if test -e $config; then stat -c %a $config; fi'"
|
audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %a $kubernetesconf; fi'"
|
||||||
tests:
|
tests:
|
||||||
bin_op: or
|
bin_op: or
|
||||||
test_items:
|
test_items:
|
||||||
@ -656,12 +656,12 @@ groups:
|
|||||||
value: "600"
|
value: "600"
|
||||||
set: true
|
set: true
|
||||||
remediation: "Run the below command (based on the file location on your system) on the master node.
|
remediation: "Run the below command (based on the file location on your system) on the master node.
|
||||||
\nFor example, chmod 644 $config"
|
\nFor example, chmod 644 $kubernetesconf"
|
||||||
scored: true
|
scored: true
|
||||||
|
|
||||||
- id: 1.4.4
|
- id: 1.4.4
|
||||||
text: "Ensure that the config file ownership is set to root:root (Scored)"
|
text: "Ensure that the config file ownership is set to root:root (Scored)"
|
||||||
audit: "/bin/sh -c 'if test -e $config; then stat -c %U:%G $config; fi'"
|
audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %U:%G $kubernetesconf; fi'"
|
||||||
tests:
|
tests:
|
||||||
test_items:
|
test_items:
|
||||||
- flag: "root:root"
|
- flag: "root:root"
|
||||||
@ -670,7 +670,7 @@ groups:
|
|||||||
value: "root:root"
|
value: "root:root"
|
||||||
set: true
|
set: true
|
||||||
remediation: "Run the below command (based on the file location on your system) on the master node.
|
remediation: "Run the below command (based on the file location on your system) on the master node.
|
||||||
\nFor example, chown root:root $config"
|
\nFor example, chown root:root $kubernetesconf"
|
||||||
scored: true
|
scored: true
|
||||||
|
|
||||||
- id: 1.4.5
|
- id: 1.4.5
|
||||||
|
@ -17,7 +17,7 @@ groups:
|
|||||||
op: eq
|
op: eq
|
||||||
value: false
|
value: false
|
||||||
set: true
|
set: true
|
||||||
remediation: "Edit the $config file on each node and set the KUBE_ALLOW_PRIV
|
remediation: "Edit the $kubeletconf file on each node and set the KUBE_ALLOW_PRIV
|
||||||
parameter to \"--allow-privileged=false\""
|
parameter to \"--allow-privileged=false\""
|
||||||
scored: true
|
scored: true
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ groups:
|
|||||||
op: eq
|
op: eq
|
||||||
value: true
|
value: true
|
||||||
set: true
|
set: true
|
||||||
remediation: "Edit the /etc/kubernetes/kubelet file on each node and set the KUBELET_ARGS parameter
|
remediation: "Edit the $kubeletconf file on each node and set the KUBELET_ARGS parameter
|
||||||
to a value to include \"--feature-gates=RotateKubeletClientCertificate=true\"."
|
to a value to include \"--feature-gates=RotateKubeletClientCertificate=true\"."
|
||||||
scored: true
|
scored: true
|
||||||
|
|
||||||
@ -213,7 +213,7 @@ groups:
|
|||||||
op: eq
|
op: eq
|
||||||
value: true
|
value: true
|
||||||
set: true
|
set: true
|
||||||
remediation: "Edit the /etc/kubernetes/kubelet file on each node and set the KUBELET_ARGS parameter
|
remediation: "Edit the $kubeletconf file on each node and set the KUBELET_ARGS parameter
|
||||||
to a value to include \"--feature-gates=RotateKubeletServerCertificate=true\"."
|
to a value to include \"--feature-gates=RotateKubeletServerCertificate=true\"."
|
||||||
scored: true
|
scored: true
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ groups:
|
|||||||
checks:
|
checks:
|
||||||
- id: 2.2.1
|
- id: 2.2.1
|
||||||
text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)"
|
text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)"
|
||||||
audit: "/bin/sh -c 'if test -e $config; then stat -c %a $config; fi'"
|
audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %a $kubernetesconf; fi'"
|
||||||
tests:
|
tests:
|
||||||
bin_op: or
|
bin_op: or
|
||||||
test_items:
|
test_items:
|
||||||
@ -242,12 +242,12 @@ groups:
|
|||||||
value: "600"
|
value: "600"
|
||||||
set: true
|
set: true
|
||||||
remediation: "Run the below command (based on the file location on your system) on the each worker node.
|
remediation: "Run the below command (based on the file location on your system) on the each worker node.
|
||||||
\nFor example, chmod 644 $config"
|
\nFor example, chmod 644 $kubernetesconf"
|
||||||
scored: true
|
scored: true
|
||||||
|
|
||||||
- id: 2.2.2
|
- id: 2.2.2
|
||||||
text: "Ensure that the config file ownership is set to root:root (Scored)"
|
text: "Ensure that the config file ownership is set to root:root (Scored)"
|
||||||
audit: "/bin/sh -c 'if test -e $config; then stat -c %U:%G $config; fi'"
|
audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %U:%G $kubernetesconf; fi'"
|
||||||
tests:
|
tests:
|
||||||
test_items:
|
test_items:
|
||||||
- flag: "root:root"
|
- flag: "root:root"
|
||||||
@ -256,7 +256,7 @@ groups:
|
|||||||
value: root:root
|
value: root:root
|
||||||
set: true
|
set: true
|
||||||
remediation: "Run the below command (based on the file location on your system) on the each worker node.
|
remediation: "Run the below command (based on the file location on your system) on the each worker node.
|
||||||
\nFor example, chown root:root $config"
|
\nFor example, chown root:root $kubernetesconf"
|
||||||
scored: true
|
scored: true
|
||||||
|
|
||||||
- id: 2.2.3
|
- id: 2.2.3
|
||||||
|
@ -156,7 +156,9 @@ func (c *Check) Run() {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.V(2).Info("%s\n", errmsgs)
|
if errmsgs != "" {
|
||||||
|
glog.V(2).Info(errmsgs)
|
||||||
|
}
|
||||||
|
|
||||||
res := c.Tests.execute(out.String())
|
res := c.Tests.execute(out.String())
|
||||||
if res {
|
if res {
|
||||||
|
102
cmd/common.go
102
cmd/common.go
@ -17,9 +17,9 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/aquasecurity/kube-bench/check"
|
"github.com/aquasecurity/kube-bench/check"
|
||||||
|
"github.com/golang/glog"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,34 +52,30 @@ var (
|
|||||||
func runChecks(t check.NodeType) {
|
func runChecks(t check.NodeType) {
|
||||||
var summary check.Summary
|
var summary check.Summary
|
||||||
var file string
|
var file string
|
||||||
|
var err error
|
||||||
|
var typeConf *viper.Viper
|
||||||
|
|
||||||
// Master variables
|
glog.V(1).Info(fmt.Sprintf("Using config file: %s\n", viper.ConfigFileUsed()))
|
||||||
apiserverBin = viper.GetString("installation." + installation + ".master.bin.apiserver")
|
|
||||||
apiserverConf = viper.GetString("installation." + installation + ".master.conf.apiserver")
|
|
||||||
schedulerBin = viper.GetString("installation." + installation + ".master.bin.scheduler")
|
|
||||||
schedulerConf = viper.GetString("installation." + installation + ".master.conf.scheduler")
|
|
||||||
controllerManagerBin = viper.GetString("installation." + installation + ".master.bin.controller-manager")
|
|
||||||
controllerManagerConf = viper.GetString("installation." + installation + ".master.conf.controller-manager")
|
|
||||||
config = viper.GetString("installation." + installation + ".config")
|
|
||||||
|
|
||||||
etcdBin = viper.GetString("etcd.bin")
|
switch t {
|
||||||
etcdConf = viper.GetString("etcd.conf")
|
case check.MASTER:
|
||||||
flanneldBin = viper.GetString("flanneld.bin")
|
file = masterFile
|
||||||
flanneldConf = viper.GetString("flanneld.conf")
|
typeConf = viper.Sub("master")
|
||||||
|
case check.NODE:
|
||||||
|
file = nodeFile
|
||||||
|
typeConf = viper.Sub("node")
|
||||||
|
case check.FEDERATED:
|
||||||
|
file = federatedFile
|
||||||
|
typeConf = viper.Sub("federated")
|
||||||
|
}
|
||||||
|
|
||||||
// Node variables
|
// Get the set of exectuables and config files we care about on this type of node. This also
|
||||||
kubeletBin = viper.GetString("installation." + installation + ".node.bin.kubelet")
|
// checks that the executables we need for the node type are running.
|
||||||
kubeletConf = viper.GetString("installation." + installation + ".node.conf.kubelet")
|
binmap := getBinaries(typeConf)
|
||||||
proxyBin = viper.GetString("installation." + installation + ".node.bin.proxy")
|
confmap := getConfigFiles(typeConf)
|
||||||
proxyConf = viper.GetString("installation." + installation + ".node.conf.proxy")
|
|
||||||
|
|
||||||
// Federated
|
|
||||||
fedApiserverBin = viper.GetString("installation." + installation + ".federated.bin.apiserver")
|
|
||||||
fedControllerManagerBin = viper.GetString("installation." + installation + ".federated.bin.controller-manager")
|
|
||||||
|
|
||||||
// Run kubernetes installation validation checks.
|
// Run kubernetes installation validation checks.
|
||||||
verifyKubeVersion(kubeMajorVersion, kubeMinorVersion)
|
verifyKubeVersion(kubeMajorVersion, kubeMinorVersion)
|
||||||
verifyNodeType(t)
|
|
||||||
|
|
||||||
switch t {
|
switch t {
|
||||||
case check.MASTER:
|
case check.MASTER:
|
||||||
@ -96,26 +92,9 @@ func runChecks(t check.NodeType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Variable substitutions. Replace all occurrences of variables in controls files.
|
// Variable substitutions. Replace all occurrences of variables in controls files.
|
||||||
s := multiWordReplace(string(in), "$apiserverbin", apiserverBin)
|
s := string(in)
|
||||||
s = multiWordReplace(s, "$apiserverconf", apiserverConf)
|
s = makeSubstitutions(s, "bin", binmap)
|
||||||
s = multiWordReplace(s, "$schedulerbin", schedulerBin)
|
s = makeSubstitutions(s, "conf", confmap)
|
||||||
s = multiWordReplace(s, "$schedulerconf", schedulerConf)
|
|
||||||
s = multiWordReplace(s, "$controllermanagerbin", controllerManagerBin)
|
|
||||||
s = multiWordReplace(s, "$controllermanagerconf", controllerManagerConf)
|
|
||||||
s = multiWordReplace(s, "$config", config)
|
|
||||||
|
|
||||||
s = multiWordReplace(s, "$etcdbin", etcdBin)
|
|
||||||
s = multiWordReplace(s, "$etcdconf", etcdConf)
|
|
||||||
s = multiWordReplace(s, "$flanneldbin", flanneldBin)
|
|
||||||
s = multiWordReplace(s, "$flanneldconf", flanneldConf)
|
|
||||||
|
|
||||||
s = multiWordReplace(s, "$kubeletbin", kubeletBin)
|
|
||||||
s = multiWordReplace(s, "$kubeletconf", kubeletConf)
|
|
||||||
s = multiWordReplace(s, "$proxybin", proxyBin)
|
|
||||||
s = multiWordReplace(s, "$proxyconf", proxyConf)
|
|
||||||
|
|
||||||
s = multiWordReplace(s, "$fedapiserverbin", fedApiserverBin)
|
|
||||||
s = multiWordReplace(s, "$fedcontrollermanagerbin", fedControllerManagerBin)
|
|
||||||
|
|
||||||
controls, err := check.NewControls(t, []byte(s))
|
controls, err := check.NewControls(t, []byte(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -147,41 +126,6 @@ func runChecks(t check.NodeType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyNodeType checks the executables and config files are as expected
|
|
||||||
// for the specified tests (master, node or federated).
|
|
||||||
func verifyNodeType(t check.NodeType) {
|
|
||||||
var bins []string
|
|
||||||
var confs []string
|
|
||||||
|
|
||||||
switch t {
|
|
||||||
case check.MASTER:
|
|
||||||
bins = []string{apiserverBin, schedulerBin, controllerManagerBin}
|
|
||||||
confs = []string{apiserverConf, schedulerConf, controllerManagerConf}
|
|
||||||
case check.NODE:
|
|
||||||
bins = []string{kubeletBin, proxyBin}
|
|
||||||
confs = []string{kubeletConf, proxyConf}
|
|
||||||
case check.FEDERATED:
|
|
||||||
bins = []string{fedApiserverBin, fedControllerManagerBin}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bin := range bins {
|
|
||||||
if !verifyBin(bin, ps) {
|
|
||||||
printlnWarn(fmt.Sprintf("%s is not running", bin))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, conf := range confs {
|
|
||||||
_, err := os.Stat(conf)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
printlnWarn(fmt.Sprintf("Missing kubernetes config file: %s", conf))
|
|
||||||
} else {
|
|
||||||
exitWithError(fmt.Errorf("error looking for file %s: %v", conf, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// colorPrint outputs the state in a specific colour, along with a message string
|
// colorPrint outputs the state in a specific colour, along with a message string
|
||||||
func colorPrint(state check.State, s string) {
|
func colorPrint(state check.State, s string) {
|
||||||
colors[state].Printf("[%s] ", state)
|
colors[state].Printf("[%s] ", state)
|
||||||
@ -190,8 +134,6 @@ func colorPrint(state check.State, s string) {
|
|||||||
|
|
||||||
// prettyPrint outputs the results to stdout in human-readable format
|
// prettyPrint outputs the results to stdout in human-readable format
|
||||||
func prettyPrint(r *check.Controls, summary check.Summary) {
|
func prettyPrint(r *check.Controls, summary check.Summary) {
|
||||||
colorPrint(check.INFO, fmt.Sprintf("Using config file: %s\n", viper.ConfigFileUsed()))
|
|
||||||
|
|
||||||
colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text))
|
colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text))
|
||||||
for _, g := range r.Groups {
|
for _, g := range r.Groups {
|
||||||
colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text))
|
colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text))
|
||||||
|
14
cmd/root.go
14
cmd/root.go
@ -34,14 +34,6 @@ var (
|
|||||||
masterFile string
|
masterFile string
|
||||||
nodeFile string
|
nodeFile string
|
||||||
federatedFile string
|
federatedFile string
|
||||||
|
|
||||||
loud bool
|
|
||||||
|
|
||||||
kubeConfDir string
|
|
||||||
etcdConfDir string
|
|
||||||
flanneldConfDir string
|
|
||||||
|
|
||||||
installation string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RootCmd represents the base command when called without any subcommands
|
// RootCmd represents the base command when called without any subcommands
|
||||||
@ -67,12 +59,6 @@ func init() {
|
|||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
|
|
||||||
RootCmd.PersistentFlags().BoolVar(&jsonFmt, "json", false, "Prints the results as JSON")
|
RootCmd.PersistentFlags().BoolVar(&jsonFmt, "json", false, "Prints the results as JSON")
|
||||||
RootCmd.PersistentFlags().StringVar(
|
|
||||||
&installation,
|
|
||||||
"installation",
|
|
||||||
"default",
|
|
||||||
"Specify how kubernetes cluster was installed. Possible values are default,hyperkube,kops,kubeadm",
|
|
||||||
)
|
|
||||||
RootCmd.PersistentFlags().StringVarP(
|
RootCmd.PersistentFlags().StringVarP(
|
||||||
&checkList,
|
&checkList,
|
||||||
"check",
|
"check",
|
||||||
|
132
cmd/util.go
132
cmd/util.go
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/aquasecurity/kube-bench/check"
|
"github.com/aquasecurity/kube-bench/check"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -22,6 +23,14 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var psFunc func(string) string
|
||||||
|
var statFunc func(string) (os.FileInfo, error)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
psFunc = ps
|
||||||
|
statFunc = os.Stat
|
||||||
|
}
|
||||||
|
|
||||||
func printlnWarn(msg string) {
|
func printlnWarn(msg string) {
|
||||||
fmt.Fprintf(os.Stderr, "[%s] %s\n",
|
fmt.Fprintf(os.Stderr, "[%s] %s\n",
|
||||||
colors[check.WARN].Sprintf("%s", check.WARN),
|
colors[check.WARN].Sprintf("%s", check.WARN),
|
||||||
@ -43,7 +52,7 @@ func exitWithError(err error) {
|
|||||||
|
|
||||||
func continueWithError(err error, msg string) string {
|
func continueWithError(err error, msg string) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.V(1).Info(err)
|
glog.V(2).Info(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
@ -75,8 +84,71 @@ func ps(proc string) string {
|
|||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getBinaries finds which of the set of candidate executables are running
|
||||||
|
func getBinaries(v *viper.Viper) map[string]string {
|
||||||
|
binmap := make(map[string]string)
|
||||||
|
|
||||||
|
for _, component := range v.GetStringSlice("components") {
|
||||||
|
s := v.Sub(component)
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return binmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigFiles finds which of the set of candidate config files exist
|
||||||
|
func getConfigFiles(v *viper.Viper) map[string]string {
|
||||||
|
confmap := make(map[string]string)
|
||||||
|
|
||||||
|
for _, component := range v.GetStringSlice("components") {
|
||||||
|
s := v.Sub(component)
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if any of the candidate config files exist
|
||||||
|
conf := findConfigFile(s.GetStringSlice("confs"))
|
||||||
|
if conf == "" {
|
||||||
|
if s.IsSet("defaultconf") {
|
||||||
|
conf = s.GetString("defaultconf")
|
||||||
|
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
|
||||||
|
printlnWarn(fmt.Sprintf("Missing config file for %s", component))
|
||||||
|
conf = component
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
glog.V(2).Info(fmt.Sprintf("Component %s uses config file '%s'", component, conf))
|
||||||
|
}
|
||||||
|
|
||||||
|
confmap[component] = conf
|
||||||
|
}
|
||||||
|
|
||||||
|
return confmap
|
||||||
|
}
|
||||||
|
|
||||||
// verifyBin checks that the binary specified is running
|
// verifyBin checks that the binary specified is running
|
||||||
func verifyBin(bin string, psFunc func(string) string) bool {
|
func verifyBin(bin string) bool {
|
||||||
|
|
||||||
// Strip any quotes
|
// Strip any quotes
|
||||||
bin = strings.Trim(bin, "'\"")
|
bin = strings.Trim(bin, "'\"")
|
||||||
@ -87,7 +159,47 @@ func verifyBin(bin string, psFunc func(string) string) bool {
|
|||||||
proc := strings.Fields(bin)[0]
|
proc := strings.Fields(bin)[0]
|
||||||
out := psFunc(proc)
|
out := psFunc(proc)
|
||||||
|
|
||||||
return strings.Contains(out, bin)
|
// 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)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// findExecutable looks through a list of possible executable names and finds the first one that's running
|
||||||
|
func findExecutable(candidates []string) (string, error) {
|
||||||
|
for _, c := range candidates {
|
||||||
|
if verifyBin(c) {
|
||||||
|
return c, nil
|
||||||
|
} else {
|
||||||
|
glog.V(1).Info(fmt.Sprintf("executable '%s' not running", c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("no candidates running")
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyKubeVersion(major string, minor string) {
|
func verifyKubeVersion(major string, minor string) {
|
||||||
@ -159,3 +271,17 @@ func multiWordReplace(s string, subname string, sub string) string {
|
|||||||
|
|
||||||
return strings.Replace(s, subname, sub, -1)
|
return strings.Replace(s, subname, sub, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSubstitutions(s string, ext string, m map[string]string) string {
|
||||||
|
for k, v := range m {
|
||||||
|
subst := "$" + k + ext
|
||||||
|
if v == "" {
|
||||||
|
glog.V(2).Info(fmt.Sprintf("No subsitution for '%s'\n", subst))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
glog.V(1).Info(fmt.Sprintf("Substituting %s with '%s'\n", subst, v))
|
||||||
|
s = multiWordReplace(s, subst, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
227
cmd/util_test.go
227
cmd/util_test.go
@ -15,9 +15,13 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckVersion(t *testing.T) {
|
func TestCheckVersion(t *testing.T) {
|
||||||
@ -78,10 +82,19 @@ func TestVersionMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var g string
|
var g string
|
||||||
|
var e []error
|
||||||
|
var eIndex int
|
||||||
|
|
||||||
func fakeps(proc string) string {
|
func fakeps(proc string) string {
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fakestat(file string) (os.FileInfo, error) {
|
||||||
|
err := e[eIndex]
|
||||||
|
eIndex++
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
func TestVerifyBin(t *testing.T) {
|
func TestVerifyBin(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
proc string
|
proc string
|
||||||
@ -95,14 +108,110 @@ func TestVerifyBin(t *testing.T) {
|
|||||||
{proc: "cmd", psOut: "cmd param1 param2", exp: true},
|
{proc: "cmd", psOut: "cmd param1 param2", exp: true},
|
||||||
{proc: "cmd param", psOut: "cmd param1 param2", exp: true},
|
{proc: "cmd param", psOut: "cmd param1 param2", exp: true},
|
||||||
{proc: "cmd param", psOut: "cmd", exp: false},
|
{proc: "cmd param", psOut: "cmd", exp: false},
|
||||||
|
{proc: "cmd", psOut: "cmd x \ncmd y", exp: true},
|
||||||
|
{proc: "cmd y", psOut: "cmd x \ncmd y", exp: true},
|
||||||
|
{proc: "cmd", psOut: "/usr/bin/cmd", exp: true},
|
||||||
|
{proc: "cmd", psOut: "kube-cmd", exp: false},
|
||||||
|
{proc: "cmd", psOut: "/usr/bin/kube-cmd", exp: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
psFunc = fakeps
|
||||||
|
for id, c := range cases {
|
||||||
|
t.Run(strconv.Itoa(id), func(t *testing.T) {
|
||||||
|
g = c.psOut
|
||||||
|
v := verifyBin(c.proc)
|
||||||
|
if v != c.exp {
|
||||||
|
t.Fatalf("Expected %v got %v", c.exp, v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindExecutable(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
candidates []string // list of executables we'd consider
|
||||||
|
psOut string // fake output from ps
|
||||||
|
exp string // the one we expect to find in the (fake) ps output
|
||||||
|
expErr bool
|
||||||
|
}{
|
||||||
|
{candidates: []string{"one", "two", "three"}, psOut: "two", exp: "two"},
|
||||||
|
{candidates: []string{"one", "two", "three"}, psOut: "two three", exp: "two"},
|
||||||
|
{candidates: []string{"one double", "two double", "three double"}, psOut: "two double is running", exp: "two double"},
|
||||||
|
{candidates: []string{"one", "two", "three"}, psOut: "blah", expErr: true},
|
||||||
|
{candidates: []string{"one double", "two double", "three double"}, psOut: "two", expErr: true},
|
||||||
|
{candidates: []string{"apiserver", "kube-apiserver"}, psOut: "kube-apiserver", exp: "kube-apiserver"},
|
||||||
|
{candidates: []string{"apiserver", "kube-apiserver", "hyperkube-apiserver"}, psOut: "kube-apiserver", exp: "kube-apiserver"},
|
||||||
|
}
|
||||||
|
|
||||||
|
psFunc = fakeps
|
||||||
|
for id, c := range cases {
|
||||||
|
t.Run(strconv.Itoa(id), func(t *testing.T) {
|
||||||
|
g = c.psOut
|
||||||
|
e, err := findExecutable(c.candidates)
|
||||||
|
if e != c.exp {
|
||||||
|
t.Fatalf("Expected %v got %v", c.exp, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && c.expErr {
|
||||||
|
t.Fatalf("Expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !c.expErr {
|
||||||
|
t.Fatalf("Didn't expect error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBinaries(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
config map[string]interface{}
|
||||||
|
psOut string
|
||||||
|
exp map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}},
|
||||||
|
psOut: "kube-apiserver",
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// "thing" is not in the list of components
|
||||||
|
config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}},
|
||||||
|
psOut: "kube-apiserver thing",
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// "anotherthing" in list of components but doesn't have a defintion
|
||||||
|
config: map[string]interface{}{"components": []string{"apiserver", "anotherthing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}},
|
||||||
|
psOut: "kube-apiserver thing",
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// more than one component
|
||||||
|
config: map[string]interface{}{"components": []string{"apiserver", "thing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}},
|
||||||
|
psOut: "kube-apiserver \nthing",
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// default binary to component name
|
||||||
|
config: map[string]interface{}{"components": []string{"apiserver", "thing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}, "optional": true}},
|
||||||
|
psOut: "kube-apiserver \notherthing some params",
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := viper.New()
|
||||||
|
psFunc = fakeps
|
||||||
|
|
||||||
for id, c := range cases {
|
for id, c := range cases {
|
||||||
t.Run(strconv.Itoa(id), func(t *testing.T) {
|
t.Run(strconv.Itoa(id), func(t *testing.T) {
|
||||||
g = c.psOut
|
g = c.psOut
|
||||||
v := verifyBin(c.proc, fakeps)
|
for k, val := range c.config {
|
||||||
if v != c.exp {
|
v.Set(k, val)
|
||||||
t.Fatalf("Expected %v got %v", c.exp, v)
|
}
|
||||||
|
m := getBinaries(v)
|
||||||
|
if !reflect.DeepEqual(m, c.exp) {
|
||||||
|
t.Fatalf("Got %v\nExpected %v", m, c.exp)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -129,3 +238,115 @@ func TestMultiWordReplace(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindConfigFile(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input []string
|
||||||
|
statResults []error
|
||||||
|
exp string
|
||||||
|
}{
|
||||||
|
{input: []string{"myfile"}, statResults: []error{nil}, exp: "myfile"},
|
||||||
|
{input: []string{"thisfile", "thatfile"}, statResults: []error{os.ErrNotExist, nil}, exp: "thatfile"},
|
||||||
|
{input: []string{"thisfile", "thatfile"}, statResults: []error{os.ErrNotExist, os.ErrNotExist}, exp: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
statFunc = fakestat
|
||||||
|
for id, c := range cases {
|
||||||
|
t.Run(strconv.Itoa(id), func(t *testing.T) {
|
||||||
|
e = c.statResults
|
||||||
|
eIndex = 0
|
||||||
|
conf := findConfigFile(c.input)
|
||||||
|
if conf != c.exp {
|
||||||
|
t.Fatalf("Got %s expected %s", conf, c.exp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConfigFiles(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
config map[string]interface{}
|
||||||
|
exp map[string]string
|
||||||
|
statResults []error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}}},
|
||||||
|
statResults: []error{os.ErrNotExist, nil},
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Component "thing" isn't included in the list of components
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"components": []string{"apiserver"},
|
||||||
|
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
|
||||||
|
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}},
|
||||||
|
statResults: []error{os.ErrNotExist, nil},
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// More than one component
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"components": []string{"apiserver", "thing"},
|
||||||
|
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
|
||||||
|
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}},
|
||||||
|
statResults: []error{os.ErrNotExist, nil, nil},
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "/my/file/thing"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Default thing to specified default config
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"components": []string{"apiserver", "thing"},
|
||||||
|
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
|
||||||
|
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}, "defaultconf": "another/thing"}},
|
||||||
|
statResults: []error{os.ErrNotExist, nil, os.ErrNotExist},
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "another/thing"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Default thing to component name
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"components": []string{"apiserver", "thing"},
|
||||||
|
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
|
||||||
|
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}},
|
||||||
|
statResults: []error{os.ErrNotExist, nil, os.ErrNotExist},
|
||||||
|
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := viper.New()
|
||||||
|
statFunc = fakestat
|
||||||
|
|
||||||
|
for id, c := range cases {
|
||||||
|
t.Run(strconv.Itoa(id), func(t *testing.T) {
|
||||||
|
for k, val := range c.config {
|
||||||
|
v.Set(k, val)
|
||||||
|
}
|
||||||
|
e = c.statResults
|
||||||
|
eIndex = 0
|
||||||
|
|
||||||
|
m := getConfigFiles(v)
|
||||||
|
if !reflect.DeepEqual(m, c.exp) {
|
||||||
|
t.Fatalf("Got %v\nExpected %v", m, c.exp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeSubsitutions(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
subst map[string]string
|
||||||
|
exp string
|
||||||
|
}{
|
||||||
|
{input: "Replace $thisbin", subst: map[string]string{"this": "that"}, exp: "Replace that"},
|
||||||
|
{input: "Replace $thisbin", subst: map[string]string{"this": "that", "here": "there"}, exp: "Replace that"},
|
||||||
|
{input: "Replace $thisbin and $herebin", subst: map[string]string{"this": "that", "here": "there"}, exp: "Replace that and there"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.input, func(t *testing.T) {
|
||||||
|
s := makeSubstitutions(c.input, "bin", c.subst)
|
||||||
|
if s != c.exp {
|
||||||
|
t.Fatalf("Got %s expected %s", s, c.exp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user