github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/scan_cluster.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"time"
     9  
    10  	"github.com/jenkins-x/jx-logging/pkg/log"
    11  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    12  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    13  	"github.com/olli-ai/jx/v2/pkg/kube"
    14  	"github.com/olli-ai/jx/v2/pkg/util"
    15  	"github.com/pkg/errors"
    16  	"github.com/spf13/cobra"
    17  	batchv1 "k8s.io/api/batch/v1"
    18  	v1 "k8s.io/api/core/v1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"sigs.k8s.io/yaml"
    21  )
    22  
    23  const (
    24  	kubeHunterImage         = "aquasec/kube-hunter"
    25  	kubeHunterContainerName = "jx-kube-hunter"
    26  	kubeHunterNamespace     = "jx-kube-hunter"
    27  	kubeHunterJobName       = "jx-kube-hunter-job"
    28  
    29  	outputFormatYAML = "yaml"
    30  )
    31  
    32  // ScanClusterOptions the options for 'scan cluster' command
    33  type ScanClusterOptions struct {
    34  	ScanOptions
    35  
    36  	Output string
    37  }
    38  
    39  type node struct {
    40  	Type     string `json:"type" yaml:"type"`
    41  	Location string `json:"location" yaml:"location"`
    42  }
    43  
    44  type service struct {
    45  	Service     string `json:"service" yaml:"service"`
    46  	Location    string `json:"location" yaml:"location"`
    47  	Description string `json:"description" yaml:"description"`
    48  }
    49  
    50  type vulnerability struct {
    51  	Vulnerability string `json:"vulnerability" yaml:"vulnerability"`
    52  	Location      string `json:"location" yaml:"location"`
    53  	Category      string `json:"category" yaml:"category"`
    54  	Description   string `json:"description" yaml:"description"`
    55  	Evidence      string `json:"evidence" yaml:"evidence"`
    56  }
    57  
    58  type scanResult struct {
    59  	Nodes           []node          `json:"nodes" yaml:"nodes"`
    60  	Services        []service       `json:"services" yaml:"services"`
    61  	Vulnerabilities []vulnerability `json:"vulnerabilities" yaml:"vulnerabilities"`
    62  }
    63  
    64  // NewCmdScanCluster creates a command object for "scan cluster" command
    65  func NewCmdScanCluster(commonOpts *opts.CommonOptions) *cobra.Command {
    66  	options := &ScanClusterOptions{
    67  		ScanOptions: ScanOptions{
    68  			CommonOptions: commonOpts,
    69  		},
    70  	}
    71  
    72  	cmd := &cobra.Command{
    73  		Use:   "cluster",
    74  		Short: "Performs a cluster security scan",
    75  		Run: func(cmd *cobra.Command, args []string) {
    76  			options.Cmd = cmd
    77  			options.Args = args
    78  			err := options.Run()
    79  			helper.CheckErr(err)
    80  		},
    81  	}
    82  
    83  	cmd.Flags().StringVarP(&options.Output, "output", "o", "plain", "output format is one of: yaml|plain")
    84  
    85  	return cmd
    86  }
    87  
    88  // Run executes the "scan cluster" command
    89  func (o *ScanClusterOptions) Run() error {
    90  	kubeClient, err := o.KubeClient()
    91  	if err != nil {
    92  		return errors.Wrap(err, "creating kube client")
    93  	}
    94  
    95  	// Create a dedicated namespace for kube-hunter scan
    96  	ns := kubeHunterNamespace
    97  	namespace := &v1.Namespace{
    98  		ObjectMeta: metav1.ObjectMeta{
    99  			Name: ns,
   100  		},
   101  	}
   102  	_, err = kubeClient.CoreV1().Namespaces().Create(namespace)
   103  	if err != nil {
   104  		return errors.Wrapf(err, "creating namespace '%s'", ns)
   105  	}
   106  
   107  	// Start the kube-hunter scanning
   108  	container := o.hunterContainer()
   109  	job := o.createScanJob(kubeHunterJobName, ns, container)
   110  	job, err = kubeClient.BatchV1().Jobs(ns).Create(job)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	// Wait for scanning to complete successfully
   116  	err = kube.WaitForJobToSucceeded(kubeClient, ns, kubeHunterJobName, 3*time.Minute)
   117  	if err != nil {
   118  		return errors.Wrap(err, "waiting for kube hunter job to complete the scanning")
   119  	}
   120  
   121  	result, err := o.retriveScanResult(ns)
   122  	if err != nil {
   123  		return errors.Wrap(err, "retrieving scan result")
   124  	}
   125  
   126  	// Clean up the kube-hunter namespace
   127  	err = kubeClient.CoreV1().Namespaces().Delete(ns, &metav1.DeleteOptions{})
   128  	if err != nil {
   129  		return errors.Wrapf(err, "cleaning up the scanning namespace '%s'", ns)
   130  	}
   131  
   132  	scanResult, err := o.parseResult(result)
   133  	if err != nil {
   134  		return errors.Wrap(err, "parsing the scan result")
   135  	}
   136  
   137  	err = o.printResult(scanResult)
   138  	if err != nil {
   139  		return errors.Wrap(err, "printing the result")
   140  	}
   141  
   142  	// Signal the error in the exit code if there are any vulnerabilities
   143  	foundVulns := len(scanResult.Vulnerabilities)
   144  	if foundVulns > 0 {
   145  		os.Exit(2)
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  func (o *ScanClusterOptions) hunterContainer() *v1.Container {
   152  	return &v1.Container{
   153  		Name:            kubeHunterContainerName,
   154  		Image:           o.hunterImage(),
   155  		ImagePullPolicy: v1.PullAlways,
   156  		Command:         []string{"python", "kube-hunter.py"},
   157  		Args:            []string{"--pod", "--report=yaml", "--log=none"},
   158  	}
   159  }
   160  
   161  func (o *ScanClusterOptions) hunterImage() string {
   162  	defaultImage := kubeHunterImage + ":latest"
   163  	resolver, err := o.CreateVersionResolver("", "")
   164  	if err != nil {
   165  		return defaultImage
   166  	}
   167  	versionedImage, err := resolver.ResolveDockerImage(kubeHunterImage)
   168  	if err != nil {
   169  		return defaultImage
   170  	}
   171  	return versionedImage
   172  }
   173  
   174  func (o *ScanClusterOptions) createScanJob(name string, namespace string, container *v1.Container) *batchv1.Job {
   175  	podTmpl := v1.PodTemplateSpec{
   176  		ObjectMeta: metav1.ObjectMeta{
   177  			Name:      name,
   178  			Namespace: namespace,
   179  		},
   180  		Spec: v1.PodSpec{
   181  			Containers:    []v1.Container{*container},
   182  			RestartPolicy: v1.RestartPolicyNever,
   183  		},
   184  	}
   185  
   186  	return &batchv1.Job{
   187  		TypeMeta: metav1.TypeMeta{
   188  			Kind:       "Job",
   189  			APIVersion: "batch/v1",
   190  		},
   191  		ObjectMeta: metav1.ObjectMeta{
   192  			Name:      name,
   193  			Namespace: namespace,
   194  		},
   195  		Spec: batchv1.JobSpec{
   196  			Template: podTmpl,
   197  		},
   198  	}
   199  }
   200  
   201  func (o *ScanClusterOptions) retriveScanResult(namespace string) (string, error) {
   202  	kubeClient, err := o.KubeClient()
   203  	if err != nil {
   204  		return "", errors.Wrap(err, "creating kube client")
   205  	}
   206  
   207  	labels := map[string]string{"job-name": kubeHunterJobName}
   208  	selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: labels})
   209  	podList, err := kubeClient.CoreV1().Pods(namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
   210  	if err != nil {
   211  		return "", errors.Wrap(err, "listing the scan job PODs")
   212  	}
   213  	foundPods := len(podList.Items)
   214  	if foundPods != 1 {
   215  		return "", fmt.Errorf("one POD expected for security scan job '%s'. Found: %d PODs.", kubeHunterJobName, foundPods)
   216  	}
   217  
   218  	podName := podList.Items[0].Name
   219  	logOpts := &v1.PodLogOptions{
   220  		Container: kubeHunterContainerName,
   221  		Follow:    false,
   222  	}
   223  	req := kubeClient.CoreV1().Pods(namespace).GetLogs(podName, logOpts)
   224  	readCloser, err := req.Stream()
   225  	if err != nil {
   226  		return "", errors.Wrap(err, "creating the logs stream reader")
   227  	}
   228  	defer readCloser.Close()
   229  
   230  	var result []byte
   231  	reader := bufio.NewReader(readCloser)
   232  	for {
   233  		line, _, err := reader.ReadLine()
   234  		if err != nil && err != io.EOF {
   235  			return "", errors.Wrapf(err, "reading logs from POD  '%s'", podName)
   236  		}
   237  		if err == io.EOF {
   238  			break
   239  		}
   240  		line = append(line, '\n')
   241  		result = append(result, line...)
   242  	}
   243  
   244  	return string(result), nil
   245  }
   246  
   247  func (o *ScanClusterOptions) parseResult(result string) (*scanResult, error) {
   248  	r := scanResult{}
   249  	err := yaml.Unmarshal([]byte(result), &r)
   250  	if err != nil {
   251  		return nil, errors.Wrap(err, "parsing the YAML result")
   252  	}
   253  	return &r, nil
   254  }
   255  
   256  func (o *ScanClusterOptions) printResult(result *scanResult) error {
   257  	if o.Output == outputFormatYAML {
   258  		var output []byte
   259  		output, err := yaml.Marshal(result)
   260  		if err != nil {
   261  			return errors.Wrap(err, "converting scan result to YAML")
   262  		}
   263  		log.Logger().Info(string(output))
   264  	} else {
   265  		nodeTable := o.CreateTable()
   266  		nodeTable.SetColumnAlign(1, util.ALIGN_LEFT)
   267  		nodeTable.SetColumnAlign(2, util.ALIGN_LEFT)
   268  		nodeTable.AddRow("NODE", "LOCATION")
   269  		for _, n := range result.Nodes {
   270  			nodeTable.AddRow(n.Type, n.Location)
   271  		}
   272  		nodeTable.Render()
   273  		log.Logger().Info("")
   274  
   275  		serviceTable := o.CreateTable()
   276  		serviceTable.SetColumnAlign(1, util.ALIGN_LEFT)
   277  		serviceTable.SetColumnAlign(2, util.ALIGN_LEFT)
   278  		serviceTable.SetColumnAlign(3, util.ALIGN_LEFT)
   279  		serviceTable.AddRow("SERVICE", "LOCATION", "DESCRIPTION")
   280  		for _, s := range result.Services {
   281  			serviceTable.AddRow(s.Service, s.Location, s.Description)
   282  		}
   283  		serviceTable.Render()
   284  		log.Logger().Info("")
   285  
   286  		vulnTable := o.CreateTable()
   287  		vulnTable.SetColumnAlign(1, util.ALIGN_LEFT)
   288  		vulnTable.SetColumnAlign(2, util.ALIGN_LEFT)
   289  		vulnTable.SetColumnAlign(3, util.ALIGN_LEFT)
   290  		vulnTable.SetColumnAlign(4, util.ALIGN_LEFT)
   291  		vulnTable.SetColumnAlign(5, util.ALIGN_LEFT)
   292  		vulnTable.AddRow("VULNERABILITY", "LOCATION", "CATEGORY", "DESCRIPTION", "EVIDENCE")
   293  		for _, vuln := range result.Vulnerabilities {
   294  			vulnTable.AddRow(vuln.Vulnerability, vuln.Location, vuln.Category, vuln.Description, vuln.Evidence)
   295  		}
   296  		vulnTable.Render()
   297  		log.Logger().Info("")
   298  	}
   299  	return nil
   300  }