github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/k8s/scanner/scanner.go (about) 1 package scanner 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "sort" 8 "strings" 9 10 cdx "github.com/CycloneDX/cyclonedx-go" 11 ms "github.com/mitchellh/mapstructure" 12 "github.com/package-url/packageurl-go" 13 "github.com/samber/lo" 14 "golang.org/x/xerrors" 15 16 "github.com/aquasecurity/go-version/pkg/version" 17 "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" 18 "github.com/aquasecurity/trivy-kubernetes/pkg/bom" 19 cmd "github.com/devseccon/trivy/pkg/commands/artifact" 20 "github.com/devseccon/trivy/pkg/digest" 21 ftypes "github.com/devseccon/trivy/pkg/fanal/types" 22 "github.com/devseccon/trivy/pkg/flag" 23 "github.com/devseccon/trivy/pkg/k8s" 24 "github.com/devseccon/trivy/pkg/k8s/report" 25 "github.com/devseccon/trivy/pkg/log" 26 "github.com/devseccon/trivy/pkg/parallel" 27 "github.com/devseccon/trivy/pkg/purl" 28 cyc "github.com/devseccon/trivy/pkg/sbom/cyclonedx" 29 "github.com/devseccon/trivy/pkg/sbom/cyclonedx/core" 30 "github.com/devseccon/trivy/pkg/scanner/local" 31 "github.com/devseccon/trivy/pkg/types" 32 ) 33 34 const ( 35 k8sCoreComponentNamespace = core.Namespace + "resource:" 36 k8sComponentType = "Type" 37 k8sComponentName = "Name" 38 k8sComponentNode = "node" 39 ) 40 41 type Scanner struct { 42 cluster string 43 runner cmd.Runner 44 opts flag.Options 45 } 46 47 func NewScanner(cluster string, runner cmd.Runner, opts flag.Options) *Scanner { 48 return &Scanner{ 49 cluster, 50 runner, 51 opts, 52 } 53 } 54 55 func (s *Scanner) Scan(ctx context.Context, artifactsData []*artifacts.Artifact) (report.Report, error) { 56 // disable logs before scanning 57 err := log.InitLogger(s.opts.Debug, true) 58 if err != nil { 59 return report.Report{}, xerrors.Errorf("logger error: %w", err) 60 } 61 62 // enable log, this is done in a defer function, 63 // to enable logs even when the function returns earlier 64 // due to an error 65 defer func() { 66 err = log.InitLogger(s.opts.Debug, false) 67 if err != nil { 68 // we use log.Fatal here because the error was to enable the logger 69 log.Fatal(xerrors.Errorf("can't enable logger error: %w", err)) 70 } 71 }() 72 73 if s.opts.Format == types.FormatCycloneDX { 74 rootComponent, err := clusterInfoToReportResources(artifactsData) 75 if err != nil { 76 return report.Report{}, err 77 } 78 return report.Report{ 79 SchemaVersion: 0, 80 RootComponent: rootComponent, 81 }, nil 82 } 83 var resourceArtifacts []*artifacts.Artifact 84 var k8sCoreArtifacts []*artifacts.Artifact 85 for _, artifact := range artifactsData { 86 if strings.HasSuffix(artifact.Kind, "Components") || strings.HasSuffix(artifact.Kind, "Cluster") { 87 k8sCoreArtifacts = append(k8sCoreArtifacts, artifact) 88 continue 89 } 90 resourceArtifacts = append(resourceArtifacts, artifact) 91 } 92 93 var resources []report.Resource 94 95 type scanResult struct { 96 vulns []report.Resource 97 misconfig report.Resource 98 } 99 100 onItem := func(ctx context.Context, artifact *artifacts.Artifact) (scanResult, error) { 101 scanResults := scanResult{} 102 if s.opts.Scanners.AnyEnabled(types.VulnerabilityScanner, types.SecretScanner) { 103 opts := s.opts 104 opts.Credentials = make([]ftypes.Credential, len(s.opts.Credentials)) 105 copy(opts.Credentials, s.opts.Credentials) 106 // add image private registry credential auto detected from workload imagePullsecret / serviceAccount 107 if len(artifact.Credentials) > 0 { 108 for _, cred := range artifact.Credentials { 109 opts.RegistryOptions.Credentials = append(opts.RegistryOptions.Credentials, 110 ftypes.Credential{ 111 Username: cred.Username, 112 Password: cred.Password, 113 }, 114 ) 115 } 116 } 117 vulns, err := s.scanVulns(ctx, artifact, opts) 118 if err != nil { 119 return scanResult{}, xerrors.Errorf("scanning vulnerabilities error: %w", err) 120 } 121 scanResults.vulns = vulns 122 } 123 if local.ShouldScanMisconfigOrRbac(s.opts.Scanners) { 124 misconfig, err := s.scanMisconfigs(ctx, artifact) 125 if err != nil { 126 return scanResult{}, xerrors.Errorf("scanning misconfigurations error: %w", err) 127 } 128 scanResults.misconfig = misconfig 129 } 130 return scanResults, nil 131 } 132 133 onResult := func(result scanResult) error { 134 resources = append(resources, result.vulns...) 135 // don't add empty misconfig results to resources slice to avoid an empty resource 136 if result.misconfig.Results != nil { 137 resources = append(resources, result.misconfig) 138 } 139 return nil 140 } 141 142 p := parallel.NewPipeline(s.opts.Parallel, !s.opts.Quiet, resourceArtifacts, onItem, onResult) 143 err = p.Do(ctx) 144 if err != nil { 145 return report.Report{}, err 146 } 147 if s.opts.Scanners.AnyEnabled(types.VulnerabilityScanner) { 148 k8sResource, err := s.scanK8sVulns(ctx, k8sCoreArtifacts) 149 if err != nil { 150 return report.Report{}, err 151 } 152 resources = append(resources, k8sResource...) 153 } 154 return report.Report{ 155 SchemaVersion: 0, 156 ClusterName: s.cluster, 157 Resources: resources, 158 }, nil 159 160 } 161 162 func (s *Scanner) scanVulns(ctx context.Context, artifact *artifacts.Artifact, opts flag.Options) ([]report.Resource, error) { 163 resources := make([]report.Resource, 0, len(artifact.Images)) 164 165 for _, image := range artifact.Images { 166 167 opts.Target = image 168 169 imageReport, err := s.runner.ScanImage(ctx, opts) 170 171 if err != nil { 172 log.Logger.Warnf("failed to scan image %s: %s", image, err) 173 resources = append(resources, report.CreateResource(artifact, imageReport, err)) 174 continue 175 } 176 177 resource, err := s.filter(ctx, imageReport, artifact) 178 if err != nil { 179 return nil, xerrors.Errorf("filter error: %w", err) 180 } 181 182 resources = append(resources, resource) 183 } 184 185 return resources, nil 186 } 187 188 func (s *Scanner) scanMisconfigs(ctx context.Context, artifact *artifacts.Artifact) (report.Resource, error) { 189 configFile, err := createTempFile(artifact) 190 if err != nil { 191 return report.Resource{}, xerrors.Errorf("scan error: %w", err) 192 } 193 194 s.opts.Target = configFile 195 196 configReport, err := s.runner.ScanFilesystem(ctx, s.opts) 197 // remove config file after scanning 198 removeFile(configFile) 199 if err != nil { 200 log.Logger.Debugf("failed to scan config %s/%s: %s", artifact.Kind, artifact.Name, err) 201 return report.CreateResource(artifact, configReport, err), err 202 } 203 204 return s.filter(ctx, configReport, artifact) 205 } 206 func (s *Scanner) filter(ctx context.Context, r types.Report, artifact *artifacts.Artifact) (report.Resource, error) { 207 var err error 208 r, err = s.runner.Filter(ctx, s.opts, r) 209 if err != nil { 210 return report.Resource{}, xerrors.Errorf("filter error: %w", err) 211 } 212 return report.CreateResource(artifact, r, nil), nil 213 } 214 215 const ( 216 golang = "golang" 217 oci = "oci" 218 kubelet = "k8s.io/kubelet" 219 controlPlaneComponents = "ControlPlaneComponents" 220 clusterInfo = "Cluster" 221 nodeComponents = "NodeComponents" 222 nodeCoreComponents = "node-core-components" 223 ) 224 225 func (s *Scanner) scanK8sVulns(ctx context.Context, artifactsData []*artifacts.Artifact) ([]report.Resource, error) { 226 var resources []report.Resource 227 var nodeName string 228 if nodeName = findNodeName(artifactsData); nodeName == "" { 229 return resources, nil 230 } 231 232 k8sScanner := k8s.NewKubenetesScanner() 233 scanOptions := types.ScanOptions{ 234 Scanners: s.opts.Scanners, 235 VulnType: s.opts.VulnType, 236 } 237 for _, artifact := range artifactsData { 238 switch artifact.Kind { 239 case controlPlaneComponents: 240 var comp bom.Component 241 err := ms.Decode(artifact.RawResource, &comp) 242 if err != nil { 243 return nil, err 244 } 245 246 lang := k8sNamespace(comp.Version, nodeName) 247 results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{ 248 Applications: []ftypes.Application{ 249 { 250 Type: ftypes.LangType(lang), 251 FilePath: artifact.Name, 252 Libraries: []ftypes.Package{ 253 { 254 Name: comp.Name, 255 Version: comp.Version, 256 }, 257 }, 258 }, 259 }, 260 }, scanOptions) 261 if err != nil { 262 return nil, err 263 } 264 if results != nil { 265 resources = append(resources, report.CreateK8sResource(artifact, results)) 266 } 267 case nodeComponents: 268 var nf bom.NodeInfo 269 err := ms.Decode(artifact.RawResource, &nf) 270 if err != nil { 271 return nil, err 272 } 273 kubeletVersion := sanitizedVersion(nf.KubeletVersion) 274 lang := k8sNamespace(kubeletVersion, nodeName) 275 runtimeName, runtimeVersion := runtimeNameVersion(nf.ContainerRuntimeVersion) 276 results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{ 277 Applications: []ftypes.Application{ 278 { 279 Type: ftypes.LangType(lang), 280 FilePath: artifact.Name, 281 Libraries: []ftypes.Package{ 282 { 283 Name: kubelet, 284 Version: kubeletVersion, 285 }, 286 }, 287 }, 288 { 289 Type: ftypes.GoBinary, 290 FilePath: artifact.Name, 291 Libraries: []ftypes.Package{ 292 { 293 Name: runtimeName, 294 Version: runtimeVersion, 295 }, 296 }, 297 }, 298 }, 299 }, scanOptions) 300 if err != nil { 301 return nil, err 302 } 303 if results != nil { 304 resources = append(resources, report.CreateK8sResource(artifact, results)) 305 } 306 case clusterInfo: 307 var cf bom.ClusterInfo 308 err := ms.Decode(artifact.RawResource, &cf) 309 if err != nil { 310 return nil, err 311 } 312 lang := k8sNamespace(cf.Version, nodeName) 313 314 results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{ 315 Applications: []ftypes.Application{ 316 { 317 Type: ftypes.LangType(lang), 318 FilePath: artifact.Name, 319 Libraries: []ftypes.Package{ 320 { 321 Name: cf.Name, 322 Version: cf.Version, 323 }, 324 }, 325 }, 326 }, 327 }, scanOptions) 328 if err != nil { 329 return nil, err 330 } 331 if results != nil { 332 resources = append(resources, report.CreateK8sResource(artifact, results)) 333 } 334 } 335 } 336 return resources, nil 337 } 338 339 func findNodeName(allArtifact []*artifacts.Artifact) string { 340 for _, artifact := range allArtifact { 341 if artifact.Kind != nodeComponents { 342 continue 343 } 344 return artifact.Name 345 } 346 return "" 347 } 348 349 func clusterInfoToReportResources(allArtifact []*artifacts.Artifact) (*core.Component, error) { 350 var coreComponents []*core.Component 351 var cInfo *core.Component 352 353 // Find the first node name to identify AKS cluster 354 var nodeName string 355 if nodeName = findNodeName(allArtifact); nodeName == "" { 356 return nil, fmt.Errorf("failed to find node name") 357 } 358 359 for _, artifact := range allArtifact { 360 switch artifact.Kind { 361 case controlPlaneComponents: 362 var comp bom.Component 363 err := ms.Decode(artifact.RawResource, &comp) 364 if err != nil { 365 return nil, err 366 } 367 var imageComponents []*core.Component 368 for _, c := range comp.Containers { 369 name := fmt.Sprintf("%s/%s", c.Registry, c.Repository) 370 cDigest := c.Digest 371 if !strings.Contains(c.Digest, string(digest.SHA256)) { 372 cDigest = fmt.Sprintf("%s:%s", string(digest.SHA256), cDigest) 373 } 374 ver := sanitizedVersion(c.Version) 375 376 imagePURL, err := purl.NewPackageURL(purl.TypeOCI, types.Metadata{ 377 RepoDigests: []string{ 378 fmt.Sprintf("%s@%s", name, cDigest), 379 }, 380 }, ftypes.Package{}) 381 382 if err != nil { 383 return nil, xerrors.Errorf("failed to create PURL: %w", err) 384 } 385 imageComponents = append(imageComponents, &core.Component{ 386 PackageURL: imagePURL, 387 Type: cdx.ComponentTypeContainer, 388 Name: name, 389 Version: cDigest, 390 Properties: []core.Property{ 391 { 392 Name: cyc.PropertyPkgID, 393 Value: fmt.Sprintf("%s:%s", name, ver), 394 }, 395 { 396 Name: cyc.PropertyPkgType, 397 Value: oci, 398 }, 399 }, 400 }) 401 } 402 rootComponent := &core.Component{ 403 Name: comp.Name, 404 Version: comp.Version, 405 Type: cdx.ComponentTypeApplication, 406 Properties: toProperties(comp.Properties, k8sCoreComponentNamespace), 407 Components: imageComponents, 408 PackageURL: generatePURL(comp.Name, comp.Version, nodeName), 409 } 410 coreComponents = append(coreComponents, rootComponent) 411 case nodeComponents: 412 var nf bom.NodeInfo 413 err := ms.Decode(artifact.RawResource, &nf) 414 if err != nil { 415 return nil, err 416 } 417 coreComponents = append(coreComponents, nodeComponent(nf)) 418 case clusterInfo: 419 var cf bom.ClusterInfo 420 err := ms.Decode(artifact.RawResource, &cf) 421 if err != nil { 422 return nil, err 423 } 424 cInfo = &core.Component{ 425 Name: cf.Name, 426 Version: cf.Version, 427 Properties: toProperties(cf.Properties, k8sCoreComponentNamespace), 428 } 429 default: 430 return nil, fmt.Errorf("resource kind %s is not supported", artifact.Kind) 431 } 432 } 433 rootComponent := &core.Component{ 434 Name: cInfo.Name, 435 Version: cInfo.Version, 436 Type: cdx.ComponentTypePlatform, 437 Properties: cInfo.Properties, 438 Components: coreComponents, 439 PackageURL: generatePURL(cInfo.Name, cInfo.Version, nodeName), 440 } 441 return rootComponent, nil 442 } 443 444 func sanitizedVersion(ver string) string { 445 return strings.TrimPrefix(ver, "v") 446 } 447 448 func osNameVersion(name string) (string, string) { 449 var buffer bytes.Buffer 450 var v string 451 var err error 452 parts := strings.Split(name, " ") 453 for _, p := range parts { 454 _, err = version.Parse(p) 455 if err != nil { 456 buffer.WriteString(p + " ") 457 continue 458 } 459 v = p 460 break 461 } 462 return strings.ToLower(strings.TrimSpace(buffer.String())), v 463 } 464 465 func runtimeNameVersion(name string) (string, string) { 466 runtime, ver, ok := strings.Cut(name, "://") 467 if !ok { 468 return "", "" 469 } 470 471 switch runtime { 472 case "cri-o": 473 name = "github.com/cri-o/cri-o" 474 case "containerd": 475 name = "github.com/containerd/containerd" 476 case "cri-dockerd": 477 name = "github.com/Mirantis/cri-dockerd" 478 } 479 return name, ver 480 } 481 482 func nodeComponent(nf bom.NodeInfo) *core.Component { 483 osName, osVersion := osNameVersion(nf.OsImage) 484 runtimeName, runtimeVersion := runtimeNameVersion(nf.ContainerRuntimeVersion) 485 kubeletVersion := sanitizedVersion(nf.KubeletVersion) 486 properties := toProperties(nf.Properties, "") 487 properties = append(properties, toProperties(map[string]string{ 488 k8sComponentType: k8sComponentNode, 489 k8sComponentName: nf.NodeName, 490 }, k8sCoreComponentNamespace)...) 491 return &core.Component{ 492 Type: cdx.ComponentTypePlatform, 493 Name: nf.NodeName, 494 Properties: properties, 495 Components: []*core.Component{ 496 { 497 Type: cdx.ComponentTypeOS, 498 Name: osName, 499 Version: osVersion, 500 Properties: []core.Property{ 501 { 502 Name: "Class", 503 Value: string(types.ClassOSPkg), 504 }, 505 { 506 Name: "Type", 507 Value: osName, 508 }, 509 }, 510 }, 511 { 512 Type: cdx.ComponentTypeApplication, 513 Name: nodeCoreComponents, 514 Properties: []core.Property{ 515 { 516 Name: "Class", 517 Value: string(types.ClassLangPkg), 518 }, 519 { 520 Name: "Type", 521 Value: golang, 522 }, 523 }, 524 Components: []*core.Component{ 525 { 526 Type: cdx.ComponentTypeApplication, 527 Name: kubelet, 528 Version: kubeletVersion, 529 Properties: []core.Property{ 530 { 531 Name: k8sComponentType, 532 Value: k8sComponentNode, 533 Namespace: k8sCoreComponentNamespace, 534 }, 535 { 536 Name: k8sComponentName, 537 Value: kubelet, 538 Namespace: k8sCoreComponentNamespace, 539 }, 540 }, 541 PackageURL: generatePURL(kubelet, kubeletVersion, nf.NodeName), 542 }, 543 { 544 Type: cdx.ComponentTypeApplication, 545 Name: runtimeName, 546 Version: runtimeVersion, 547 Properties: []core.Property{ 548 { 549 Name: k8sComponentType, 550 Value: k8sComponentNode, 551 Namespace: k8sCoreComponentNamespace, 552 }, 553 { 554 Name: k8sComponentName, 555 Value: runtimeName, 556 Namespace: k8sCoreComponentNamespace, 557 }, 558 }, 559 PackageURL: &purl.PackageURL{ 560 PackageURL: *packageurl.NewPackageURL(golang, "", runtimeName, runtimeVersion, packageurl.Qualifiers{}, ""), 561 }, 562 }, 563 }, 564 }, 565 }, 566 } 567 } 568 569 func toProperties(props map[string]string, namespace string) []core.Property { 570 properties := lo.MapToSlice(props, func(k, v string) core.Property { 571 return core.Property{ 572 Name: k, 573 Value: v, 574 Namespace: namespace, 575 } 576 }) 577 sort.Slice(properties, func(i, j int) bool { 578 return properties[i].Name < properties[j].Name 579 }) 580 return properties 581 } 582 583 func generatePURL(name, ver, nodeName string) *purl.PackageURL { 584 585 var namespace string 586 // Identify k8s distribution. An empty namespace means upstream. 587 if namespace = k8sNamespace(ver, nodeName); namespace == "" { 588 return nil 589 } else if namespace == "kubernetes" { 590 namespace = "" 591 } 592 593 return &purl.PackageURL{ 594 PackageURL: *packageurl.NewPackageURL(purl.TypeK8s, namespace, name, ver, nil, ""), 595 } 596 } 597 598 func k8sNamespace(ver, nodeName string) string { 599 namespace := "kubernetes" 600 switch { 601 case strings.Contains(ver, "eks"): 602 namespace = purl.NamespaceEKS 603 case strings.Contains(ver, "gke"): 604 namespace = purl.NamespaceGKE 605 case strings.Contains(ver, "rke2"): 606 namespace = purl.NamespaceRKE 607 case strings.Contains(ver, "hotfix"): 608 if !strings.Contains(nodeName, "aks") { 609 // Unknown k8s distribution 610 return "" 611 } 612 namespace = purl.NamespaceAKS 613 case strings.Contains(nodeName, "ocp"): 614 namespace = purl.NamespaceOCP 615 } 616 return namespace 617 }