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  }