github.com/jenkins-x/jx/v2@v2.1.155/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/jenkins-x/jx/v2/pkg/cmd/helper" 12 "github.com/jenkins-x/jx/v2/pkg/cmd/opts" 13 "github.com/jenkins-x/jx/v2/pkg/kube" 14 "github.com/jenkins-x/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 }