github.com/CycloneDX/sbom-utility@v0.16.0/cmd/license.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 cmd
    20  
    21  import (
    22  	"fmt"
    23  
    24  	"github.com/CycloneDX/sbom-utility/common"
    25  	"github.com/CycloneDX/sbom-utility/schema"
    26  	"github.com/CycloneDX/sbom-utility/utils"
    27  	"github.com/spf13/cobra"
    28  )
    29  
    30  const (
    31  	SUBCOMMAND_LICENSE_LIST   = "list"
    32  	SUBCOMMAND_LICENSE_POLICY = "policy"
    33  )
    34  
    35  var VALID_SUBCOMMANDS_LICENSE = []string{SUBCOMMAND_LICENSE_LIST, SUBCOMMAND_LICENSE_POLICY}
    36  
    37  // License list default values
    38  const (
    39  	LICENSE_LIST_NOT_APPLICABLE = "N/A"
    40  	LICENSE_NO_ASSERTION        = "NOASSERTION"
    41  )
    42  
    43  func NewCommandLicense() *cobra.Command {
    44  	var command = new(cobra.Command)
    45  	command.Use = "license"
    46  	command.Short = "Process licenses found in the BOM input file"
    47  	command.Long = "Process licenses found in the BOM input file"
    48  	command.RunE = licenseCmdImpl
    49  	command.ValidArgs = VALID_SUBCOMMANDS_LICENSE
    50  	command.PreRunE = func(cmd *cobra.Command, args []string) (err error) {
    51  		// the license command requires at least 1 valid subcommand (argument)
    52  		getLogger().Tracef("args: %v\n", args)
    53  		if len(args) == 0 {
    54  			return getLogger().Errorf("Missing required argument(s).")
    55  		} else if len(args) > 1 {
    56  			return getLogger().Errorf("Too many arguments provided: %v", args)
    57  		}
    58  		// Make sure subcommand is known
    59  		if !preRunTestForSubcommand(VALID_SUBCOMMANDS_LICENSE, args[0]) {
    60  			return getLogger().Errorf("Subcommand provided is not valid: `%v`", args[0])
    61  		}
    62  		return
    63  	}
    64  	return command
    65  }
    66  
    67  func licenseCmdImpl(cmd *cobra.Command, args []string) error {
    68  	getLogger().Enter(args)
    69  	defer getLogger().Exit()
    70  	return nil
    71  }
    72  
    73  //------------------------------------
    74  // CDX License hashing functions
    75  //------------------------------------
    76  
    77  // Hash ALL licenses found in the SBOM document
    78  // Note: CDX spec. allows for licenses to be declared in the following places:
    79  // 1. (root).metadata.licenses[]
    80  // 2. (root).metadata.component.licenses[] + all "nested" components
    81  // 3. (root).components[](.license[]) (each component + all "nested" components)
    82  // 4. (root).services[](.license[]) (each service + all "nested" services)
    83  func loadDocumentLicenses(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (err error) {
    84  	getLogger().Enter()
    85  	defer getLogger().Exit(err)
    86  
    87  	// NOTE: DEBUG: use this to debug license policy hashmaps have appropriate # of entries
    88  	//licensePolicyConfig.Debug()
    89  
    90  	// At this time, fail SPDX format SBOMs as "unsupported" (for "any" format)
    91  	if !bom.FormatInfo.IsCycloneDx() {
    92  		err = schema.NewUnsupportedFormatForCommandError(
    93  			bom.GetFilename(),
    94  			bom.FormatInfo.CanonicalName,
    95  			CMD_LICENSE, FORMAT_ANY)
    96  		return
    97  	}
    98  
    99  	// Before looking for license data, fully unmarshal the SBOM
   100  	// into named structures
   101  	if err = bom.UnmarshalCycloneDXBOM(); err != nil {
   102  		return
   103  	}
   104  
   105  	// 1. Hash all licenses in the SBOM metadata (i.e., (root).metadata.component)
   106  	// Note: this SHOULD represent a summary of all licenses that apply
   107  	// to the component being described in the SBOM
   108  	if err = hashMetadataLicenses(bom, policyConfig, schema.LC_LOC_METADATA, whereFilters, licenseFlags); err != nil {
   109  		return
   110  	}
   111  
   112  	// 2. Hash all licenses in (root).metadata.component (+ "nested" components)
   113  	if err = hashMetadataComponentLicenses(bom, policyConfig, schema.LC_LOC_METADATA_COMPONENT, whereFilters, licenseFlags); err != nil {
   114  		return
   115  	}
   116  
   117  	// 3. Hash all component licenses found in the (root).components[] (+ "nested" components)
   118  	pComponents := bom.GetCdxComponents()
   119  	if pComponents != nil && len(*pComponents) > 0 {
   120  		if err = hashComponentsLicenses(bom, policyConfig, pComponents, schema.LC_LOC_COMPONENTS, whereFilters, licenseFlags); err != nil {
   121  			return
   122  		}
   123  	}
   124  
   125  	// 4. Hash all service licenses found in the (root).services[] (array) (+ "nested" services)
   126  	pServices := bom.GetCdxServices()
   127  	if pServices != nil && len(*pServices) > 0 {
   128  		if err = hashServicesLicenses(bom, policyConfig, pServices, schema.LC_LOC_SERVICES, whereFilters, licenseFlags); err != nil {
   129  			return
   130  		}
   131  	}
   132  	return
   133  }
   134  
   135  // Note: An actual error SHOULD ONLY be returned by the custom validation code.
   136  func warnNoLicenseFound(bom *schema.BOM, location int) {
   137  	message := fmt.Sprintf("%s (%s)",
   138  		MSG_LICENSES_NOT_FOUND, // "licenses not found"
   139  		schema.GetLicenseChoiceLocationName(location))
   140  	sbomError := NewInvalidSBOMError(bom, message, nil, nil)
   141  	getLogger().Warning(sbomError)
   142  }
   143  
   144  // Note: An actual error SHOULD ONLY be returned by the custom validation code.
   145  func warnInvalidResourceLicense(resourceType string, bomRef string, name string, version string) {
   146  	getLogger().Warningf("%s. resourceType: `%s`: bomRef: `%s`, name:`%s`, version: `%s`",
   147  		MSG_LICENSE_NOT_FOUND,
   148  		resourceType, bomRef, name, version)
   149  }
   150  
   151  // Hash the license found in the (root).metadata.licenses[] array
   152  func hashMetadataLicenses(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, location int, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (err error) {
   153  	getLogger().Enter()
   154  	defer getLogger().Exit(err)
   155  
   156  	pLicenses := bom.GetCdxMetadataLicenses()
   157  	// Issue a warning that the SBOM does not declare at least one, top-level component license.
   158  	if pLicenses == nil {
   159  		warnNoLicenseFound(bom, location)
   160  		return
   161  	}
   162  
   163  	var licenseInfo schema.LicenseInfo
   164  	for _, pLicenseChoice := range *pLicenses {
   165  		getLogger().Tracef("hashing license: id: `%s`, name: `%s`",
   166  			pLicenseChoice.License.Id, pLicenseChoice.License.Name)
   167  
   168  		licenseInfo.LicenseChoice = pLicenseChoice
   169  		licenseInfo.BOMLocationValue = location
   170  		licenseInfo.ResourceName = LICENSE_LIST_NOT_APPLICABLE
   171  		licenseInfo.BOMRef = LICENSE_LIST_NOT_APPLICABLE
   172  		err = hashLicenseInfoByLicenseType(bom, policyConfig, licenseInfo, whereFilters, licenseFlags)
   173  		if err != nil {
   174  			return
   175  		}
   176  	}
   177  	return
   178  }
   179  
   180  // Hash the license found in the (root).metadata.component object (and any "nested" components)
   181  func hashMetadataComponentLicenses(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, location int, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (err error) {
   182  	getLogger().Enter()
   183  	defer getLogger().Exit(err)
   184  
   185  	pComponent := bom.GetCdxMetadataComponent()
   186  	if pComponent == nil {
   187  		warnNoLicenseFound(bom, location)
   188  		return
   189  	}
   190  	_, err = hashComponentLicense(bom, policyConfig, *pComponent, location, whereFilters, licenseFlags)
   191  	return
   192  }
   193  
   194  // Hash all licenses found in an array of CDX Components
   195  func hashComponentsLicenses(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, pComponents *[]schema.CDXComponent, location int, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (err error) {
   196  	getLogger().Enter()
   197  	defer getLogger().Exit(err)
   198  
   199  	if pComponents != nil {
   200  		for _, cdxComponent := range *pComponents {
   201  			_, err = hashComponentLicense(bom, policyConfig, cdxComponent, location, whereFilters, licenseFlags)
   202  			if err != nil {
   203  				return
   204  			}
   205  		}
   206  	}
   207  	return
   208  }
   209  
   210  // Hash all licenses found in an array of CDX Services
   211  func hashServicesLicenses(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, pServices *[]schema.CDXService, location int, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (err error) {
   212  	getLogger().Enter()
   213  	defer getLogger().Exit(err)
   214  
   215  	if pServices != nil {
   216  		for _, cdxServices := range *pServices {
   217  			err = hashServiceLicense(bom, policyConfig, cdxServices, location, whereFilters, licenseFlags)
   218  			if err != nil {
   219  				return
   220  			}
   221  		}
   222  	}
   223  	return
   224  }
   225  
   226  // Hash a CDX Component's licenses and recursively those of any "nested" components
   227  func hashComponentLicense(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, cdxComponent schema.CDXComponent, location int, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (li *schema.LicenseInfo, err error) {
   228  	getLogger().Enter()
   229  	defer getLogger().Exit(err)
   230  	var licenseInfo schema.LicenseInfo
   231  
   232  	pLicenses := cdxComponent.Licenses
   233  	if pLicenses != nil && len(*pLicenses) > 0 {
   234  		for _, licenseChoice := range *pLicenses {
   235  			getLogger().Debugf("licenseChoice: %s", getLogger().FormatStruct(licenseChoice))
   236  			getLogger().Tracef("hashing license for component=`%s`", cdxComponent.Name)
   237  
   238  			licenseInfo = *schema.NewLicenseInfoFromComponent(cdxComponent, licenseChoice, location)
   239  			err = hashLicenseInfoByLicenseType(bom, policyConfig, licenseInfo, whereFilters, licenseFlags)
   240  
   241  			if err != nil {
   242  				// Show intent to not check for error returns as there no intent to recover
   243  				_ = getLogger().Errorf("%s. license: %+v", MSG_LICENSE_HASH_ERROR, licenseInfo)
   244  				return
   245  			}
   246  		}
   247  	} else {
   248  		// Account for component with no license with an "UNDEFINED" entry
   249  		licenseInfo = *schema.NewLicenseInfoFromComponent(cdxComponent, schema.CDXLicenseChoice{}, location)
   250  		_, err = bom.HashmapLicenseInfo(policyConfig, LICENSE_NO_ASSERTION, licenseInfo, whereFilters, licenseFlags)
   251  
   252  		// Issue a warning that the component had no license; use "safe" BOMRef string value
   253  		warnInvalidResourceLicense(schema.RESOURCE_TYPE_COMPONENT, licenseInfo.BOMRef.String(), cdxComponent.Name, cdxComponent.Version)
   254  		// No actual licenses to process
   255  		return
   256  	}
   257  
   258  	// Recursively hash licenses for all child components (i.e., hierarchical composition)
   259  	pComponents := cdxComponent.Components
   260  	if pComponents != nil && len(*pComponents) > 0 {
   261  		err = hashComponentsLicenses(bom, policyConfig, pComponents, location, whereFilters, licenseFlags)
   262  		if err != nil {
   263  			return
   264  		}
   265  	}
   266  	return
   267  }
   268  
   269  // Hash all licenses found in a CDX Service
   270  func hashServiceLicense(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, cdxService schema.CDXService, location int, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (err error) {
   271  	getLogger().Enter()
   272  	defer getLogger().Exit(err)
   273  
   274  	var licenseInfo schema.LicenseInfo
   275  
   276  	pLicenses := cdxService.Licenses
   277  	if pLicenses != nil && len(*pLicenses) > 0 {
   278  		for _, licenseChoice := range *pLicenses {
   279  			getLogger().Debugf("licenseChoice: %s", getLogger().FormatStruct(licenseChoice))
   280  			getLogger().Tracef("Hashing license for service=`%s`", cdxService.Name)
   281  			licenseInfo = *schema.NewLicenseInfoFromService(cdxService, licenseChoice, location)
   282  			err = hashLicenseInfoByLicenseType(bom, policyConfig, licenseInfo, whereFilters, licenseFlags)
   283  			if err != nil {
   284  				// Show intent to not check for error returns as there no intent to recover
   285  				_ = getLogger().Errorf("%s. license: %+v", MSG_LICENSE_HASH_ERROR, licenseInfo)
   286  				return
   287  			}
   288  		}
   289  	} else {
   290  		// Account for service with no license with an "UNDEFINED" entry
   291  		// hash any service w/o a license using special key name
   292  		licenseInfo = *schema.NewLicenseInfoFromService(cdxService, schema.CDXLicenseChoice{}, location)
   293  		_, err = bom.HashmapLicenseInfo(policyConfig, LICENSE_NO_ASSERTION, licenseInfo, whereFilters, licenseFlags)
   294  
   295  		// Issue a warning that the service had no license; use "safe" BOMRef string value
   296  		warnInvalidResourceLicense(schema.RESOURCE_TYPE_SERVICE, licenseInfo.BOMRef.String(), cdxService.Name, cdxService.Version)
   297  
   298  		// No actual licenses to process
   299  		return
   300  	}
   301  
   302  	// Recursively hash licenses for all child components (i.e., hierarchical composition)
   303  	pServices := cdxService.Services
   304  	if pServices != nil && len(*pServices) > 0 {
   305  		err = hashServicesLicenses(bom, policyConfig, pServices, location, whereFilters, licenseFlags)
   306  		if err != nil {
   307  			// Show intent to not check for error returns as there no intent to recover
   308  			_ = getLogger().Errorf("%s. license: %+v", MSG_LICENSE_HASH_ERROR, licenseInfo)
   309  			return
   310  		}
   311  	}
   312  	return
   313  }
   314  
   315  // Wrap the license data itself in a "licenseInfo" object which tracks:
   316  // 1. What type of information do we have about the license (i.e., SPDX ID, Name or expression)
   317  // 2. Where the license was found within the SBOM
   318  // 3. The entity name (e.g., service or component name) that declared the license
   319  // 4. The entity local BOM reference (i.e., "bomRef")
   320  func hashLicenseInfoByLicenseType(bom *schema.BOM, policyConfig *schema.LicensePolicyConfig, licenseInfo schema.LicenseInfo, whereFilters []common.WhereFilter, licenseFlags utils.LicenseCommandFlags) (err error) {
   321  	getLogger().Enter()
   322  	defer getLogger().Exit(err)
   323  
   324  	licenseChoice := licenseInfo.LicenseChoice
   325  	pLicense := licenseChoice.License
   326  
   327  	if pLicense != nil && pLicense.Id != "" {
   328  		licenseInfo.LicenseChoiceTypeValue = schema.LC_TYPE_ID
   329  		_, err = bom.HashmapLicenseInfo(policyConfig, pLicense.Id, licenseInfo, whereFilters, licenseFlags)
   330  	} else if pLicense != nil && pLicense.Name != "" {
   331  		licenseInfo.LicenseChoiceTypeValue = schema.LC_TYPE_NAME
   332  		_, err = bom.HashmapLicenseInfo(policyConfig, pLicense.Name, licenseInfo, whereFilters, licenseFlags)
   333  	} else if licenseChoice.Expression != "" {
   334  		licenseInfo.LicenseChoiceTypeValue = schema.LC_TYPE_EXPRESSION
   335  		_, err = bom.HashmapLicenseInfo(policyConfig, licenseChoice.Expression, licenseInfo, whereFilters, licenseFlags)
   336  	} else {
   337  		// Note: This code path only executes if hashing is performed
   338  		// without schema validation (which would find this as an error)
   339  		// Note: licenseInfo.LicenseChoiceType = 0 // default, invalid
   340  		baseError := NewSbomLicenseDataError()
   341  		baseError.AppendMessage(fmt.Sprintf(": for entity: `%s` (%s)",
   342  			licenseInfo.BOMRef,
   343  			licenseInfo.ResourceName))
   344  		err = baseError
   345  		return
   346  	}
   347  
   348  	if err != nil {
   349  		baseError := NewSbomLicenseDataError()
   350  		baseError.AppendMessage(fmt.Sprintf(": for entity: `%s` (%s)",
   351  			licenseInfo.BOMRef,
   352  			licenseInfo.ResourceName))
   353  		err = baseError
   354  	}
   355  	return
   356  }