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