github.com/CycloneDX/sbom-utility@v0.16.0/schema/bom_hash.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 /* 3 * Licensed to the Apache Software Foundation (ASF) under one or more 4 * contributor license agreements. See the NOTICE file distributed with 5 * this work for additional information regarding copyright ownership. 6 * The ASF licenses this file to You under the Apache License, Version 2.0 7 * (the "License"); you may not use this file except in compliance with 8 * the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package schema 20 21 import ( 22 "fmt" 23 "reflect" 24 "runtime/debug" 25 "strings" 26 27 "github.com/CycloneDX/sbom-utility/common" 28 "github.com/CycloneDX/sbom-utility/utils" 29 ) 30 31 // ------------------- 32 // Components 33 // ------------------- 34 35 // This hashes all components regardless where in the BOM document structure 36 // they are declared. This includes both the top-level metadata component 37 // (i.e., the subject of the BOM) as well as the components array. 38 func (bom *BOM) HashmapComponentResources(whereFilters []common.WhereFilter) (err error) { 39 getLogger().Enter() 40 defer func() { 41 if panicInfo := recover(); panicInfo != nil { 42 fmt.Printf("%v, %s", panicInfo, string(debug.Stack())) 43 } 44 }() 45 defer getLogger().Exit(err) 46 47 // Hash the top-level component declared in the BOM metadata 48 pMetadataComponent := bom.GetCdxMetadataComponent() 49 if pMetadataComponent != nil { 50 _, err = bom.HashmapComponent(*pMetadataComponent, whereFilters, true) 51 if err != nil { 52 return 53 } 54 } 55 56 // Hash all components found in the (root).components[] (+ "nested" components) 57 pComponents := bom.GetCdxComponents() 58 if pComponents != nil && len(*pComponents) > 0 { 59 if err = bom.HashmapComponents(*pComponents, whereFilters, false); err != nil { 60 return 61 } 62 } 63 return 64 } 65 66 func (bom *BOM) HashmapComponents(components []CDXComponent, whereFilters []common.WhereFilter, root bool) (err error) { 67 getLogger().Enter() 68 defer getLogger().Exit(err) 69 for _, cdxComponent := range components { 70 _, err = bom.HashmapComponent(cdxComponent, whereFilters, root) 71 if err != nil { 72 return 73 } 74 } 75 return 76 } 77 78 // Hash a CDX Component and recursively those of any "nested" components 79 // TODO: we should WARN if version is not a valid semver (e.g., examples/cyclonedx/BOM/laravel-7.12.0/bom.1.3.json) 80 // TODO: Use pointer for CDXComponent 81 func (bom *BOM) HashmapComponent(cdxComponent CDXComponent, whereFilters []common.WhereFilter, isRoot bool) (hashed bool, err error) { 82 getLogger().Enter() 83 defer getLogger().Exit(err) 84 85 if reflect.DeepEqual(cdxComponent, CDXComponent{}) { 86 getLogger().Warning("empty component object found") 87 return 88 } 89 90 if cdxComponent.Name == "" { 91 getLogger().Warningf("component missing required value `name` : %v ", cdxComponent) 92 } 93 94 if cdxComponent.Version == "" { 95 getLogger().Warningf("component named `%s` missing `version`", cdxComponent.Name) 96 } 97 98 if cdxComponent.BOMRef == nil || *cdxComponent.BOMRef == "" { 99 getLogger().Warningf("component named `%s` missing `bom-ref`", cdxComponent.Name) 100 } 101 102 // Map CDX data struct to our internal structure used for reporting/stats gathering 103 var componentInfo *CDXComponentInfo = NewComponentInfo(cdxComponent) 104 componentInfo.IsRoot = isRoot // mark BOM root component based upon location 105 106 var match bool = true 107 if len(whereFilters) > 0 { 108 mapResourceInfo, _ := utils.MarshalStructToJsonMap(componentInfo) 109 match, _ = whereFilterMatch(mapResourceInfo, whereFilters) 110 } 111 112 if match { 113 hashed = true 114 bom.ComponentMap.Put(componentInfo.BOMRef, componentInfo) 115 bom.ResourceMap.Put(componentInfo.BOMRef, componentInfo.CDXResourceInfo) 116 getLogger().Tracef("Hashmap Put() componentInfo: %+v", componentInfo) 117 } 118 119 // Recursively hash licenses for all child components (i.e., hierarchical composition) 120 pComponent := cdxComponent.Components 121 if pComponent != nil && len(*pComponent) > 0 { 122 err = bom.HashmapComponents(*cdxComponent.Components, whereFilters, isRoot) 123 if err != nil { 124 return 125 } 126 } 127 return 128 } 129 130 // ------------------- 131 // Services 132 // ------------------- 133 134 func (bom *BOM) HashmapServiceResources(whereFilters []common.WhereFilter) (err error) { 135 getLogger().Enter() 136 defer getLogger().Exit(err) 137 138 pServices := bom.GetCdxServices() 139 if pServices != nil && len(*pServices) > 0 { 140 if err = bom.HashmapServices(*pServices, whereFilters); err != nil { 141 return 142 } 143 } 144 return 145 } 146 147 // TODO: use pointer for []CDXService 148 func (bom *BOM) HashmapServices(services []CDXService, whereFilters []common.WhereFilter) (err error) { 149 getLogger().Enter() 150 defer getLogger().Exit(err) 151 152 for _, cdxService := range services { 153 _, err = bom.HashmapService(cdxService, whereFilters) 154 if err != nil { 155 return 156 } 157 } 158 return 159 } 160 161 // Hash a CDX Component and recursively those of any "nested" components 162 // TODO: use pointer for CDXService 163 func (bom *BOM) HashmapService(cdxService CDXService, whereFilters []common.WhereFilter) (hashed bool, err error) { 164 getLogger().Enter() 165 defer getLogger().Exit(err) 166 167 if reflect.DeepEqual(cdxService, CDXService{}) { 168 getLogger().Warning("empty service object found") 169 return 170 } 171 172 if cdxService.Name == "" { 173 getLogger().Warningf("service missing required value `name` : %v ", cdxService) 174 } 175 176 if cdxService.Version == "" { 177 getLogger().Warningf("service named `%s` missing `version`", cdxService.Name) 178 } 179 180 if cdxService.BOMRef == nil || *cdxService.BOMRef == "" { 181 getLogger().Warningf("service named `%s` missing `bom-ref`", cdxService.Name) 182 } 183 184 // Map CDX data struct to our internal structure used for reporting/stats gathering 185 var serviceInfo *CDXServiceInfo = NewServiceInfo(cdxService) 186 187 var match bool = true 188 if len(whereFilters) > 0 { 189 mapResourceInfo, _ := utils.MarshalStructToJsonMap(serviceInfo) 190 match, _ = whereFilterMatch(mapResourceInfo, whereFilters) 191 } 192 193 if match { 194 // TODO: AppendLicenseInfo(LICENSE_NONE, resourceInfo) 195 hashed = true 196 bom.ServiceMap.Put(serviceInfo.BOMRef, serviceInfo) 197 bom.ResourceMap.Put(serviceInfo.BOMRef, serviceInfo.CDXResourceInfo) 198 getLogger().Tracef("Hashmap Put() serviceInfo: %+v", serviceInfo) 199 } 200 201 // Recursively hash licenses for all child components (i.e., hierarchical composition) 202 pServices := cdxService.Services 203 if pServices != nil && len(*pServices) > 0 { 204 err = bom.HashmapServices(*pServices, whereFilters) 205 if err != nil { 206 return 207 } 208 } 209 return 210 } 211 212 // ------------------- 213 // Licenses 214 // ------------------- 215 216 func (bom *BOM) HashmapLicenseInfo(policyConfig *LicensePolicyConfig, key string, licenseInfo LicenseInfo, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (hashed bool, err error) { 217 if reflect.DeepEqual(licenseInfo, LicenseInfo{}) { 218 getLogger().Warning("empty license object found") 219 return 220 } 221 222 // Find license usage policy by either license Id, Name or Expression 223 if policyConfig != nil { 224 licenseInfo.Policy, err = policyConfig.FindPolicy(licenseInfo) 225 if err != nil { 226 return 227 } 228 // Note: FindPolicy(), at worst, will return an empty LicensePolicy object 229 licenseInfo.UsagePolicy = licenseInfo.Policy.UsagePolicy 230 } 231 licenseInfo.License = key 232 // Derive values for report filtering 233 licenseInfo.LicenseChoiceType = GetLicenseChoiceTypeName(licenseInfo.LicenseChoiceTypeValue) 234 licenseInfo.BOMLocation = GetLicenseChoiceLocationName(licenseInfo.BOMLocationValue) 235 236 // If we need to include all license fields, they need to be copied to from 237 // wherever they appear into base LicenseInfo struct (for JSON tag/where filtering) 238 if !licenseFlags.Summary { 239 copyExtendedLicenseChoiceFieldData(&licenseInfo) 240 } 241 242 var match bool = true 243 if len(whereFilters) > 0 { 244 mapInfo, _ := utils.MarshalStructToJsonMap(licenseInfo) 245 match, _ = whereFilterMatch(mapInfo, whereFilters) 246 } 247 248 if match { 249 hashed = true 250 // Hash LicenseInfo by license key (i.e., id|name|expression) 251 bom.LicenseMap.Put(key, licenseInfo) 252 getLogger().Tracef("Hashmap Put() licenseInfo: %+v", licenseInfo) 253 } 254 return 255 } 256 257 // TODO make this a method of *LicenseInfo (object) 258 func copyExtendedLicenseChoiceFieldData(pLicenseInfo *LicenseInfo) { 259 if pLicenseInfo == nil { 260 getLogger().Tracef("invalid *LicenseInfo: nil") 261 return 262 } 263 264 var lcType = pLicenseInfo.LicenseChoiceType 265 if lcType == LC_VALUE_ID || lcType == LC_VALUE_NAME { 266 if pLicenseInfo.LicenseChoice.License == nil { 267 getLogger().Tracef("invalid *CDXLicense: nil") 268 return 269 } 270 pLicenseInfo.LicenseId = pLicenseInfo.LicenseChoice.License.Id 271 pLicenseInfo.LicenseName = pLicenseInfo.LicenseChoice.License.Name 272 pLicenseInfo.LicenseUrl = pLicenseInfo.LicenseChoice.License.Url 273 274 if pLicenseInfo.LicenseChoice.License.Text != nil { 275 // NOTE: always copy full context text; downstream display functions 276 // can truncate later 277 pLicenseInfo.LicenseTextContent = pLicenseInfo.LicenseChoice.License.Text.Content 278 pLicenseInfo.LicenseTextContentType = pLicenseInfo.LicenseChoice.License.Text.ContentType 279 pLicenseInfo.LicenseTextEncoding = pLicenseInfo.LicenseChoice.License.Text.Encoding 280 } 281 } else if lcType == LC_VALUE_EXPRESSION { 282 pLicenseInfo.LicenseExpression = pLicenseInfo.LicenseChoice.Expression 283 } 284 } 285 286 // ------------------- 287 // Vulnerabilities 288 // ------------------- 289 290 func (bom *BOM) HashmapVulnerabilityResources(whereFilters []common.WhereFilter) (err error) { 291 getLogger().Enter() 292 defer getLogger().Exit(err) 293 294 pVulnerabilities := bom.GetCdxVulnerabilities() 295 296 if pVulnerabilities != nil && len(*pVulnerabilities) > 0 { 297 if err = bom.HashmapVulnerabilities(*pVulnerabilities, whereFilters); err != nil { 298 return 299 } 300 } 301 return 302 } 303 304 // We need to hash our own informational structure around the CDX data in order 305 // to simplify --where queries to command line users 306 func (bom *BOM) HashmapVulnerabilities(vulnerabilities []CDXVulnerability, whereFilters []common.WhereFilter) (err error) { 307 getLogger().Enter() 308 defer getLogger().Exit(err) 309 310 for _, cdxVulnerability := range vulnerabilities { 311 _, err = bom.HashmapVulnerability(cdxVulnerability, whereFilters) 312 if err != nil { 313 return 314 } 315 } 316 return 317 } 318 319 // Hash a CDX Component and recursively those of any "nested" components 320 // TODO we should WARN if version is not a valid semver (e.g., examples/cyclonedx/BOM/laravel-7.12.0/bom.1.3.json) 321 func (bom *BOM) HashmapVulnerability(cdxVulnerability CDXVulnerability, whereFilters []common.WhereFilter) (hashed bool, err error) { 322 getLogger().Enter() 323 defer getLogger().Exit(err) 324 var vulnInfo VulnerabilityInfo 325 326 // Note: the CDX Vulnerability type has no required fields 327 if reflect.DeepEqual(cdxVulnerability, CDXVulnerability{}) { 328 getLogger().Warning("empty vulnerability object found") 329 return 330 } 331 332 if cdxVulnerability.Id == "" { 333 getLogger().Warningf("vulnerability missing required value `id` : %v ", cdxVulnerability) 334 } 335 336 if cdxVulnerability.Published == "" { 337 getLogger().Warningf("vulnerability (`%s`) missing `published` date", cdxVulnerability.Id) 338 } 339 340 if cdxVulnerability.Created == "" { 341 getLogger().Warningf("vulnerability (`%s`) missing `created` date", cdxVulnerability.Id) 342 } 343 344 if cdxVulnerability.Ratings == nil || len(*cdxVulnerability.Ratings) == 0 { 345 getLogger().Warningf("vulnerability (`%s`) missing `ratings`", cdxVulnerability.Id) 346 } 347 348 // hash any component w/o a license using special key name 349 vulnInfo.Vulnerability = cdxVulnerability 350 if cdxVulnerability.BOMRef != nil && *cdxVulnerability.BOMRef != "" { 351 vulnInfo.BOMRef = cdxVulnerability.BOMRef.String() 352 } 353 vulnInfo.Id = cdxVulnerability.Id 354 355 // Truncate dates from 2023-02-02T00:00:00.000Z to 2023-02-02 356 // Note: if validation errors are found by the "truncate" function, 357 // it will emit an error and return the original (failing) value 358 dateTime, _ := utils.TruncateTimeStampISO8601Date(cdxVulnerability.Created) 359 vulnInfo.Created = dateTime 360 361 dateTime, _ = utils.TruncateTimeStampISO8601Date(cdxVulnerability.Published) 362 vulnInfo.Published = dateTime 363 364 dateTime, _ = utils.TruncateTimeStampISO8601Date(cdxVulnerability.Updated) 365 vulnInfo.Updated = dateTime 366 367 dateTime, _ = utils.TruncateTimeStampISO8601Date(cdxVulnerability.Rejected) 368 vulnInfo.Rejected = dateTime 369 370 vulnInfo.Description = cdxVulnerability.Description 371 372 // Source object: retrieve report fields from nested objects 373 if cdxVulnerability.Source != nil { 374 source := *cdxVulnerability.Source 375 vulnInfo.Source = source 376 vulnInfo.SourceName = source.Name 377 vulnInfo.SourceUrl = source.Url 378 } 379 380 // replace empty Analysis values with "UNDEFINED" 381 if cdxVulnerability.Analysis != nil { 382 vulnInfo.AnalysisState = cdxVulnerability.Analysis.State 383 if vulnInfo.AnalysisState == "" { 384 vulnInfo.AnalysisState = VULN_ANALYSIS_STATE_EMPTY 385 } 386 387 vulnInfo.AnalysisJustification = cdxVulnerability.Analysis.Justification 388 if vulnInfo.AnalysisJustification == "" { 389 vulnInfo.AnalysisJustification = VULN_ANALYSIS_STATE_EMPTY 390 } 391 392 vulnInfo.AnalysisResponse = *cdxVulnerability.Analysis.Response 393 if len(vulnInfo.AnalysisResponse) == 0 { 394 vulnInfo.AnalysisResponse = []string{VULN_ANALYSIS_STATE_EMPTY} 395 } 396 } else { 397 vulnInfo.AnalysisState = VULN_ANALYSIS_STATE_EMPTY 398 vulnInfo.AnalysisJustification = VULN_ANALYSIS_STATE_EMPTY 399 vulnInfo.AnalysisResponse = []string{VULN_ANALYSIS_STATE_EMPTY} 400 } 401 402 // Convert []int to []string for --where filter 403 // TODO: see if we can eliminate this conversion and handle while preparing report data 404 // as this SHOULD appear there as []interface{} 405 if cdxVulnerability.Cwes != nil && len(*cdxVulnerability.Cwes) > 0 { 406 // strip off slice/array brackets 407 vulnInfo.CweIds = strings.Fields(strings.Trim(fmt.Sprint(cdxVulnerability.Cwes), "[]")) 408 } 409 410 // CVSS Score Qualitative Rating 411 // 0.0 None 412 // 0.1 – 3.9 Low 413 // 4.0 – 6.9 Medium 414 // 7.0 – 8.9 High 415 // 9.0 – 10.0 Critical 416 417 // TODO: if summary report, see if more than one severity can be shown without clogging up column data 418 if cdxVulnerability.Ratings != nil && len(*cdxVulnerability.Ratings) > 0 { 419 //var sourceMatch int 420 for _, rating := range *cdxVulnerability.Ratings { 421 // defer to same source as the top-level vuln. declares 422 fSeverity := fmt.Sprintf("%s: %v (%s)", rating.Method, rating.Score, rating.Severity) 423 // give listing priority to ratings that matches top-level vuln. reporting source 424 if rating.Source.Name == cdxVulnerability.Source.Name { 425 // prepend to slice 426 vulnInfo.CvssSeverity = append([]string{fSeverity}, vulnInfo.CvssSeverity...) 427 continue 428 } 429 vulnInfo.CvssSeverity = append(vulnInfo.CvssSeverity, fSeverity) 430 } 431 } else { 432 // Set first entry to empty value (i.e., "none") 433 vulnInfo.CvssSeverity = append(vulnInfo.CvssSeverity, VULN_RATING_EMPTY) 434 } 435 436 var match bool = true 437 if len(whereFilters) > 0 { 438 mapVulnInfo, _ := utils.MarshalStructToJsonMap(vulnInfo) 439 match, _ = whereFilterMatch(mapVulnInfo, whereFilters) 440 } 441 442 if match { 443 hashed = true 444 bom.VulnerabilityMap.Put(vulnInfo.Id, vulnInfo) 445 getLogger().Tracef("Hashmap Put() vulnInfo: %+v", vulnInfo) 446 } 447 return 448 }