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 }