github.com/CycloneDX/sbom-utility@v0.16.0/cmd/validate_custom.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  	"github.com/CycloneDX/sbom-utility/schema"
    23  	"github.com/CycloneDX/sbom-utility/utils"
    24  	"github.com/jwangsadinata/go-multimap/slicemultimap"
    25  )
    26  
    27  // Validate all custom requirements that cannot be found be schema validation
    28  // These custom requirements are categorized by the following areas:
    29  // 1. Composition - document elements are organized as required (even though allowed by schema)
    30  // 2. Metadata - Top-level, document metadata includes specific fields and/or values that match required criteria (e.g., regex)
    31  // 3. License data - Components, Services (or any object that carries a License) meets specified requirements
    32  func validateCustomCDXDocument(document *schema.BOM, policyConfig *schema.LicensePolicyConfig) (innerError error) {
    33  	getLogger().Enter()
    34  	defer getLogger().Exit(innerError)
    35  
    36  	// Load custom validation file
    37  	errCfg := schema.LoadCustomValidationConfig(utils.GlobalFlags.ConfigCustomValidationFile)
    38  	if errCfg != nil {
    39  		getLogger().Warningf("custom validation not possible: %s", errCfg.Error())
    40  		innerError = errCfg
    41  		return
    42  	}
    43  
    44  	// Validate all custom composition requirements for overall CDX SBOM are met
    45  	if innerError = validateCustomDocumentComposition(document); innerError != nil {
    46  		return
    47  	}
    48  
    49  	// Validate that at least required (e.g., valid, approved) "License" data exists
    50  	if innerError = validateLicenseData(document, policyConfig); innerError != nil {
    51  		return
    52  	}
    53  
    54  	// Validate all custom requirements for the CDX metadata structure
    55  	// TODO: move up, as second test, once all custom test files have
    56  	// required metadata
    57  	if innerError = validateCustomMetadata(document); innerError != nil {
    58  		return
    59  	}
    60  	return
    61  }
    62  
    63  // This validation function checks for custom composition requirements as follows:
    64  // 1. Assure that the "metadata.component" does NOT have child Components
    65  // 2. TODO: Assure that the "components" list is a "flat" list
    66  func validateCustomDocumentComposition(document *schema.BOM) (innerError error) {
    67  	getLogger().Enter()
    68  	defer getLogger().Exit(innerError)
    69  
    70  	// retrieve top-level component data from metadata
    71  	component := document.GetCdxMetadataComponent()
    72  
    73  	// NOTE: The absence of a top-level component in the metadata
    74  	// SHOULD be a composition error
    75  	if component == nil {
    76  		return
    77  	}
    78  
    79  	// Generate a (composition) validation error
    80  	pComponent := component.Components
    81  	if pComponent != nil && len(*pComponent) > 0 {
    82  		var fields = []string{"metadata", "component", "components"}
    83  		innerError = NewSBOMCompositionError(
    84  			MSG_INVALID_METADATA_COMPONENT_COMPONENTS,
    85  			document,
    86  			fields)
    87  		return
    88  	}
    89  
    90  	return
    91  }
    92  
    93  // This validation function checks for custom metadata requirements are as follows:
    94  // 1. required "Properties" exist and have valid values (against supplied regex)
    95  // 2. Supplier field is filled out according to custom requirements
    96  // 3. Manufacturer field is filled out according to custom requirements
    97  // TODO: test for custom values in other metadata/fields:
    98  func validateCustomMetadata(document *schema.BOM) (err error) {
    99  	getLogger().Enter()
   100  	defer getLogger().Exit(err)
   101  
   102  	// validate that the top-level pComponent is declared with all required values
   103  	if pComponent := document.GetCdxMetadataComponent(); pComponent == nil {
   104  		err := NewSBOMMetadataError(
   105  			document,
   106  			MSG_INVALID_METADATA_COMPONENT,
   107  			*document.GetCdxMetadata())
   108  		return err
   109  	}
   110  
   111  	// Validate required custom properties (by `name`) exist with appropriate values
   112  	err = validateCustomMetadataProperties(document)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	return err
   118  }
   119  
   120  // This validation function checks for custom metadata property requirements (i.e., names, values)
   121  // TODO: Evaluate need for this given new means to do this with JSON Schema v6 and 7
   122  func validateCustomMetadataProperties(document *schema.BOM) (err error) {
   123  	getLogger().Enter()
   124  	defer getLogger().Exit(err)
   125  
   126  	validationProps := schema.CustomValidationChecks.GetCustomValidationMetadataProperties()
   127  	if len(validationProps) == 0 {
   128  		getLogger().Infof("No properties to validate")
   129  		return
   130  	}
   131  
   132  	// TODO: move map to BOM object
   133  	hashmap := slicemultimap.New()
   134  	pProperties := document.GetCdxMetadataProperties()
   135  	if pProperties != nil {
   136  		err = hashMetadataProperties(hashmap, *pProperties)
   137  		if err != nil {
   138  			return
   139  		}
   140  	}
   141  
   142  	for _, checks := range validationProps {
   143  		getLogger().Tracef("Running validation checks: Property name: `%s`, checks(s): `%v`...", checks.Name, checks)
   144  		values, found := hashmap.Get(checks.Name)
   145  		if !found {
   146  			err = NewSbomMetadataPropertyError(
   147  				document,
   148  				MSG_PROPERTY_NOT_FOUND,
   149  				&checks, nil)
   150  			return err
   151  		}
   152  
   153  		// Check: (key) uniqueness
   154  		// i.e., Multiple values with same "key" (specified), not provided
   155  		// TODO: currently hashmap assumes "name" as the key; this could be dynamic (using reflect)
   156  		if checks.CheckUnique != "" {
   157  			getLogger().Tracef("CheckUnique: key: `%s`, `%s`, value(s): `%v`...", checks.Key, checks.CheckUnique, values)
   158  			// if multi-hashmap has more than one value, property is NOT unique
   159  			if len(values) > 1 {
   160  				err := NewSbomMetadataPropertyError(
   161  					document,
   162  					MSG_PROPERTY_NOT_UNIQUE,
   163  					&checks, nil)
   164  				return err
   165  			}
   166  		}
   167  
   168  		if checks.CheckRegex != "" {
   169  			getLogger().Tracef("CheckRegex: field: `%s`, regex: `%v`...", checks.CheckRegex, checks.Value)
   170  			compiledRegex, errCompile := utils.CompileRegex(checks.Value)
   171  			if errCompile != nil {
   172  				return errCompile
   173  			}
   174  
   175  			// TODO: check multiple values if provided
   176  			value := values[0]
   177  			if stringValue, ok := value.(string); ok {
   178  				getLogger().Debugf(">> Testing value: `%s`...", stringValue)
   179  				matched := compiledRegex.Match([]byte(stringValue))
   180  				if !matched {
   181  					err = NewSbomMetadataPropertyError(
   182  						document,
   183  						MSG_PROPERTY_REGEX_FAILED,
   184  						&checks, nil)
   185  					return err
   186  				} else {
   187  					getLogger().Debugf("matched:  ")
   188  				}
   189  
   190  			} else {
   191  				err = NewSbomMetadataPropertyError(
   192  					document,
   193  					MSG_PROPERTY_NOT_UNIQUE,
   194  					&checks, nil)
   195  				return err
   196  			}
   197  
   198  		}
   199  	}
   200  
   201  	return err
   202  }
   203  
   204  func hashMetadataProperties(hashmap *slicemultimap.MultiMap, properties []schema.CDXProperty) (err error) {
   205  	getLogger().Enter()
   206  	defer getLogger().Exit()
   207  
   208  	if hashmap == nil {
   209  		return getLogger().Errorf("invalid hashmap: %v", hashmap)
   210  	}
   211  
   212  	for _, prop := range properties {
   213  		hashmap.Put(prop.Name, prop.Value)
   214  	}
   215  
   216  	return
   217  }
   218  
   219  // TODO: Assure that after hashing "license" data within the "components" array
   220  // that at least one valid license is found
   221  // TODO: Assure top-level "metadata.component"
   222  // TODO support []WhereFilter
   223  func validateLicenseData(document *schema.BOM, policyConfig *schema.LicensePolicyConfig) (err error) {
   224  	getLogger().Enter()
   225  	defer getLogger().Exit(err)
   226  
   227  	// Now we need to validate that the input file contains licenses
   228  	// the license "hash" function does this validation checking for us...
   229  	// TODO support []WhereFilter
   230  	// NOTE: licenseFlags will be all defaults (should not matter for simple true/false validation)
   231  	err = loadDocumentLicenses(document, policyConfig, nil, utils.GlobalFlags.LicenseFlags)
   232  
   233  	if err != nil {
   234  		return
   235  	}
   236  
   237  	// TODO: verify that the input file contained valid license data
   238  
   239  	return
   240  }