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 }