github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/k8s/report/report.go (about) 1 package report 2 3 import ( 4 "fmt" 5 "io" 6 "strings" 7 8 "golang.org/x/exp/maps" 9 "golang.org/x/exp/slices" 10 11 dbTypes "github.com/aquasecurity/trivy-db/pkg/types" 12 "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" 13 ftypes "github.com/devseccon/trivy/pkg/fanal/types" 14 "github.com/devseccon/trivy/pkg/log" 15 "github.com/devseccon/trivy/pkg/sbom/cyclonedx/core" 16 "github.com/devseccon/trivy/pkg/types" 17 ) 18 19 const ( 20 AllReport = "all" 21 SummaryReport = "summary" 22 23 workloadComponent = "workload" 24 infraComponent = "infra" 25 ) 26 27 type Option struct { 28 Format types.Format 29 Report string 30 Output io.Writer 31 Severities []dbTypes.Severity 32 ColumnHeading []string 33 Scanners types.Scanners 34 Components []string 35 APIVersion string 36 } 37 38 // Report represents a kubernetes scan report 39 type Report struct { 40 SchemaVersion int `json:",omitempty"` 41 ClusterName string 42 Resources []Resource `json:",omitempty"` 43 RootComponent *core.Component `json:"-"` 44 name string 45 } 46 47 // ConsolidatedReport represents a kubernetes scan report with consolidated findings 48 type ConsolidatedReport struct { 49 SchemaVersion int `json:",omitempty"` 50 ClusterName string 51 Findings []Resource `json:",omitempty"` 52 } 53 54 // Resource represents a kubernetes resource report 55 type Resource struct { 56 Namespace string `json:",omitempty"` 57 Kind string 58 Name string 59 Metadata types.Metadata `json:",omitempty"` 60 Results types.Results `json:",omitempty"` 61 Error string `json:",omitempty"` 62 63 // original report 64 Report types.Report `json:"-"` 65 } 66 67 func (r Resource) fullname() string { 68 return strings.ToLower(fmt.Sprintf("%s/%s/%s", r.Namespace, r.Kind, r.Name)) 69 } 70 71 // Failed returns whether the k8s report includes any vulnerabilities or misconfigurations 72 func (r Report) Failed() bool { 73 for _, v := range r.Resources { 74 if v.Results.Failed() { 75 return true 76 } 77 } 78 return false 79 } 80 81 func (r Report) consolidate() ConsolidatedReport { 82 consolidated := ConsolidatedReport{ 83 SchemaVersion: r.SchemaVersion, 84 ClusterName: r.ClusterName, 85 } 86 87 index := make(map[string]Resource) 88 var vulnerabilities []Resource 89 for _, m := range r.Resources { 90 if vulnerabilitiesOrSecretResource(m) { 91 vulnerabilities = append(vulnerabilities, m) 92 } else { 93 index[m.fullname()] = m 94 } 95 } 96 97 for _, v := range vulnerabilities { 98 key := v.fullname() 99 100 if res, ok := index[key]; ok { 101 index[key] = Resource{ 102 Namespace: res.Namespace, 103 Kind: res.Kind, 104 Name: res.Name, 105 Metadata: res.Metadata, 106 Results: append(res.Results, v.Results...), 107 Error: res.Error, 108 } 109 110 continue 111 } 112 113 index[key] = v 114 } 115 116 consolidated.Findings = maps.Values(index) 117 118 return consolidated 119 } 120 121 // Writer defines the result write operation 122 type Writer interface { 123 Write(Report) error 124 } 125 126 type reports struct { 127 Report Report 128 Columns []string 129 } 130 131 // SeparateMisconfigReports returns 3 reports based on scanners and components flags, 132 // - misconfiguration report 133 // - rbac report 134 // - infra checks report 135 func SeparateMisconfigReports(k8sReport Report, scanners types.Scanners, components []string) []reports { 136 137 var workloadMisconfig, infraMisconfig, rbacAssessment, workloadVulnerabilities, workloadResource []Resource 138 for _, resource := range k8sReport.Resources { 139 if vulnerabilitiesOrSecretResource(resource) { 140 workloadVulnerabilities = append(workloadVulnerabilities, resource) 141 continue 142 } 143 144 switch { 145 case scanners.Enabled(types.RBACScanner) && rbacResource(resource): 146 rbacAssessment = append(rbacAssessment, resource) 147 case infraResource(resource): 148 workload, infra := splitInfraAndWorkloadResources(resource) 149 150 if slices.Contains(components, infraComponent) { 151 infraMisconfig = append(infraMisconfig, infra) 152 } 153 154 if slices.Contains(components, workloadComponent) { 155 workloadMisconfig = append(workloadMisconfig, workload) 156 } 157 158 case scanners.Enabled(types.MisconfigScanner) && !rbacResource(resource): 159 if slices.Contains(components, workloadComponent) { 160 workloadMisconfig = append(workloadMisconfig, resource) 161 } 162 } 163 } 164 165 var r []reports 166 workloadResource = append(workloadResource, workloadVulnerabilities...) 167 workloadResource = append(workloadResource, workloadMisconfig...) 168 if shouldAddWorkloadReport(scanners) { 169 workloadReport := Report{ 170 SchemaVersion: 0, 171 ClusterName: k8sReport.ClusterName, 172 Resources: workloadResource, 173 name: "Workload Assessment", 174 } 175 176 if (slices.Contains(components, workloadComponent) && 177 len(workloadMisconfig) > 0) || 178 len(workloadVulnerabilities) > 0 { 179 r = append(r, reports{ 180 Report: workloadReport, 181 Columns: WorkloadColumns(), 182 }) 183 } 184 } 185 186 if scanners.Enabled(types.RBACScanner) && len(rbacAssessment) > 0 { 187 r = append(r, reports{ 188 Report: Report{ 189 SchemaVersion: 0, 190 ClusterName: k8sReport.ClusterName, 191 Resources: rbacAssessment, 192 name: "RBAC Assessment", 193 }, 194 Columns: RoleColumns(), 195 }) 196 } 197 198 if scanners.Enabled(types.MisconfigScanner) && 199 slices.Contains(components, infraComponent) && 200 len(infraMisconfig) > 0 { 201 202 r = append(r, reports{ 203 Report: Report{ 204 SchemaVersion: 0, 205 ClusterName: k8sReport.ClusterName, 206 Resources: infraMisconfig, 207 name: "Infra Assessment", 208 }, 209 Columns: InfraColumns(), 210 }) 211 } 212 213 return r 214 } 215 216 func rbacResource(misConfig Resource) bool { 217 return misConfig.Kind == "Role" || misConfig.Kind == "RoleBinding" || misConfig.Kind == "ClusterRole" || misConfig.Kind == "ClusterRoleBinding" 218 } 219 220 func infraResource(misConfig Resource) bool { 221 return (misConfig.Kind == "Pod" && misConfig.Namespace == "kube-system") || misConfig.Kind == "NodeInfo" 222 } 223 224 func CreateResource(artifact *artifacts.Artifact, report types.Report, err error) Resource { 225 r := CreateK8sResource(artifact, report.Results) 226 227 r.Metadata = report.Metadata 228 r.Report = report 229 // if there was any error during the scan 230 if err != nil { 231 r.Error = err.Error() 232 } 233 234 return r 235 } 236 237 func CreateK8sResource(artifact *artifacts.Artifact, scanResults types.Results) Resource { 238 results := make([]types.Result, 0, len(scanResults)) 239 // fix target name 240 for _, result := range scanResults { 241 // if resource is a kubernetes file fix the target name, 242 // to avoid showing the temp file that was removed. 243 if result.Type == ftypes.Kubernetes { 244 result.Target = fmt.Sprintf("%s/%s", artifact.Kind, artifact.Name) 245 } 246 results = append(results, result) 247 } 248 249 r := Resource{ 250 Namespace: artifact.Namespace, 251 Kind: artifact.Kind, 252 Name: artifact.Name, 253 Metadata: types.Metadata{}, 254 Results: results, 255 Report: types.Report{ 256 Results: results, 257 ArtifactName: artifact.Name, 258 }, 259 } 260 261 return r 262 } 263 264 func (r Report) PrintErrors() { 265 for _, resource := range r.Resources { 266 if resource.Error != "" { 267 log.Logger.Errorf("Error during vulnerabilities or misconfiguration scan: %s", resource.Error) 268 } 269 } 270 } 271 272 func splitInfraAndWorkloadResources(misconfig Resource) (Resource, Resource) { 273 workload := copyResource(misconfig) 274 infra := copyResource(misconfig) 275 276 workloadResults := make(types.Results, 0) 277 infraResults := make(types.Results, 0) 278 279 for _, result := range misconfig.Results { 280 var workloadMisconfigs, infraMisconfigs []types.DetectedMisconfiguration 281 282 for _, m := range result.Misconfigurations { 283 if strings.HasPrefix(m.ID, "KCV") { 284 infraMisconfigs = append(infraMisconfigs, m) 285 continue 286 } 287 288 workloadMisconfigs = append(workloadMisconfigs, m) 289 } 290 291 if len(workloadMisconfigs) > 0 { 292 workloadResults = append(workloadResults, copyResult(result, workloadMisconfigs)) 293 } 294 295 if len(infraMisconfigs) > 0 { 296 infraResults = append(infraResults, copyResult(result, infraMisconfigs)) 297 } 298 } 299 300 workload.Results = workloadResults 301 workload.Report.Results = workloadResults 302 303 infra.Results = infraResults 304 infra.Report.Results = infraResults 305 306 return workload, infra 307 } 308 309 func copyResource(r Resource) Resource { 310 return Resource{ 311 Namespace: r.Namespace, 312 Kind: r.Kind, 313 Name: r.Name, 314 Metadata: r.Metadata, 315 Error: r.Error, 316 Report: r.Report, 317 } 318 } 319 320 func copyResult(r types.Result, misconfigs []types.DetectedMisconfiguration) types.Result { 321 return types.Result{ 322 Target: r.Target, 323 Class: r.Class, 324 Type: r.Type, 325 MisconfSummary: r.MisconfSummary, 326 Misconfigurations: misconfigs, 327 } 328 } 329 330 func shouldAddWorkloadReport(scanners types.Scanners) bool { 331 return scanners.AnyEnabled(types.MisconfigScanner, types.VulnerabilityScanner, types.SecretScanner) 332 } 333 334 func vulnerabilitiesOrSecretResource(resource Resource) bool { 335 return len(resource.Results) > 0 && (len(resource.Results[0].Vulnerabilities) > 0 || len(resource.Results[0].Secrets) > 0) 336 }