github.com/CycloneDX/sbom-utility@v0.16.0/schema/bom.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 schema 20 21 import ( 22 "bytes" 23 "crypto/sha256" 24 "encoding/gob" 25 "encoding/json" 26 "fmt" 27 "io" 28 "os" 29 "path/filepath" 30 "reflect" 31 "regexp" 32 33 "github.com/CycloneDX/sbom-utility/utils" 34 "github.com/jwangsadinata/go-multimap/slicemultimap" 35 ) 36 37 // Candidate BOM document (context) information 38 type BOM struct { 39 filename string 40 absFilename string 41 rawBytes []byte 42 JsonMap map[string]interface{} 43 FormatInfo FormatSchema 44 SchemaInfo FormatSchemaInstance 45 CdxBom *CDXBom 46 Statistics *StatisticsInfo 47 ResourceMap *slicemultimap.MultiMap 48 ComponentMap *slicemultimap.MultiMap 49 ServiceMap *slicemultimap.MultiMap 50 VulnerabilityMap *slicemultimap.MultiMap 51 LicenseMap *slicemultimap.MultiMap 52 GobDecodeBuffer bytes.Buffer 53 GobEncodeBuffer bytes.Buffer 54 GobDecoder *gob.Decoder 55 GobEncoder *gob.Encoder 56 } 57 58 const ( 59 COMPONENT_ID_NONE = "None" 60 COMPONENT_ID_NAME = "name" 61 COMPONENT_ID_BOMREF = "bom-ref" 62 COMPONENT_ID_PURL = "purl" 63 COMPONENT_ID_CPE = "cpe" 64 COMPONENT_ID_SWID = "swid" 65 ) 66 67 const ( 68 SERVICE_ID_NONE = "None" 69 SERVICE_ID_BOMREF = "bom-ref" 70 ) 71 72 // Note: the SPDX spec. does not provide regex for an SPDX ID, but provides the following in ABNF: 73 // 74 // string = 1*(ALPHA / DIGIT / "-" / "." ) 75 // 76 // Currently, the regex below tests composition of of only 77 // alphanum, "-", and "." characters and disallows empty strings 78 // TODO: 79 // - First and last chars are not "-" or "." 80 // - Enforce reasonable min/max lengths 81 // In theory, we can check overall length with positive lookahead 82 // (e.g., min 3 max 128): (?=.{3,128}$) 83 // However, this does not appear to be supported in `regexp` package 84 // or perhaps it must be a compiled expression TBD 85 const ( 86 REGEX_VALID_SPDX_ID = "^[a-zA-Z0-9.-]+$" 87 ) 88 89 // compiled regexp. to save time 90 var spdxIdRegexp *regexp.Regexp 91 92 func (bom *BOM) GetRawBytes() []byte { 93 return bom.rawBytes 94 } 95 96 func NewBOM(inputFile string) *BOM { 97 temp := BOM{ 98 filename: inputFile, 99 } 100 101 // TODO: only allocate multi-maps when get() method (to be created) is called (on-demand) 102 // NOTE: the CdxBom Map is allocated (i.e., using `make`) as part of `UnmarshalSBOM` method 103 temp.ResourceMap = slicemultimap.New() 104 temp.ComponentMap = slicemultimap.New() 105 temp.ServiceMap = slicemultimap.New() 106 temp.VulnerabilityMap = slicemultimap.New() 107 temp.LicenseMap = slicemultimap.New() 108 109 // Create a GOB Encoder/Decoder 110 temp.GobDecoder = gob.NewDecoder(&temp.GobDecodeBuffer) 111 temp.GobEncoder = gob.NewEncoder(&temp.GobEncodeBuffer) 112 113 // Stats 114 temp.Statistics = new(StatisticsInfo) 115 temp.Statistics.ComponentStats = new(BOMComponentStats) 116 117 return &temp 118 } 119 120 func (bom *BOM) GetFilename() string { 121 return bom.filename 122 } 123 124 func (bom *BOM) GetFilenameInterpolated() string { 125 if bom.filename == INPUT_TYPE_STDIN { 126 return "stdin" 127 } 128 return bom.filename 129 } 130 131 func (bom *BOM) GetJSONMap() map[string]interface{} { 132 return bom.JsonMap 133 } 134 135 func (bom *BOM) GetCdxBom() (pCdxBom *CDXBom) { 136 return bom.CdxBom 137 } 138 139 func (bom *BOM) GetCdxMetadata() (pMetadata *CDXMetadata) { 140 if bom := bom.GetCdxBom(); bom != nil { 141 pMetadata = bom.Metadata 142 } 143 return 144 } 145 146 func (bom *BOM) GetCdxMetadataComponent() (pComponent *CDXComponent) { 147 if metadata := bom.GetCdxMetadata(); metadata != nil { 148 pComponent = metadata.Component 149 } 150 return 151 } 152 153 func (bom *BOM) GetCdxMetadataLicenses() (licenses *[]CDXLicenseChoice) { 154 if metadata := bom.GetCdxMetadata(); metadata != nil { 155 licenses = metadata.Licenses 156 } 157 return licenses 158 } 159 160 func (bom *BOM) GetCdxMetadataProperties() (pProperties *[]CDXProperty) { 161 if metadata := bom.GetCdxMetadata(); metadata != nil { 162 pProperties = metadata.Properties 163 } 164 return pProperties 165 } 166 167 func (bom *BOM) GetCdxComponents() (pComponents *[]CDXComponent) { 168 if bom := bom.GetCdxBom(); bom != nil { 169 pComponents = bom.Components 170 } 171 return pComponents 172 } 173 174 func (bom *BOM) GetCdxServices() (pServices *[]CDXService) { 175 if bom := bom.GetCdxBom(); bom != nil { 176 pServices = bom.Services 177 } 178 return pServices 179 } 180 181 func (bom *BOM) GetCdxProperties() (pProperties *[]CDXProperty) { 182 if bom := bom.GetCdxBom(); bom != nil { 183 pProperties = bom.Properties 184 } 185 return pProperties 186 } 187 188 func (bom *BOM) GetCdxExternalReferences() (pReferences *[]CDXExternalReference) { 189 if bom := bom.GetCdxBom(); bom != nil { 190 pReferences = bom.ExternalReferences 191 } 192 return pReferences 193 } 194 195 func (bom *BOM) GetCdxDependencies() (pDependencies *[]CDXDependency) { 196 if bom := bom.GetCdxBom(); bom != nil { 197 pDependencies = bom.Dependencies 198 } 199 return pDependencies 200 } 201 202 func (bom *BOM) GetCdxCompositions() (pCompositions *[]CDXCompositions) { 203 if bom := bom.GetCdxBom(); bom != nil { 204 pCompositions = bom.Compositions 205 } 206 return pCompositions 207 } 208 209 func (bom *BOM) GetCdxAnnotations() (pAnnotations *[]CDXAnnotation) { 210 if bom := bom.GetCdxBom(); bom != nil { 211 pAnnotations = bom.Annotations 212 } 213 return pAnnotations 214 } 215 216 func (bom *BOM) GetCdxFormula() (pFormula *[]CDXFormula) { 217 if bom := bom.GetCdxBom(); bom != nil { 218 pFormula = bom.Formulation 219 } 220 return pFormula 221 } 222 223 func (bom *BOM) GetCdxSignature() (pSignature *JSFSignature) { 224 if bom := bom.GetCdxBom(); bom != nil { 225 pSignature = bom.Signature 226 } 227 return pSignature 228 } 229 230 func (bom *BOM) GetCdxVulnerabilities() (pVulnerabilities *[]CDXVulnerability) { 231 if bom := bom.GetCdxBom(); bom != nil { 232 pVulnerabilities = bom.Vulnerabilities 233 } 234 return pVulnerabilities 235 } 236 237 func (bom *BOM) GetKeyValueAsString(key string) (sValue string, err error) { 238 getLogger().Enter() 239 defer getLogger().Exit() 240 241 getLogger().Tracef("key: `%s`", key) 242 243 if (bom.JsonMap) == nil { 244 err := fmt.Errorf("document object does not have a Map allocated") 245 getLogger().Error(err) 246 return "", err 247 } 248 249 value := bom.JsonMap[key] 250 if value == nil { 251 getLogger().Tracef("key: `%s` not found in document map", key) 252 return "", nil 253 } 254 255 getLogger().Tracef("value: `%v` (%T)", value, value) 256 return value.(string), nil 257 } 258 259 func (bom *BOM) ReadRawBytes() (err error) { 260 // validate filename 261 if len(bom.filename) == 0 { 262 return fmt.Errorf("schema: invalid filename: `%s`", bom.filename) 263 } 264 265 // Check to see of stdin is the BOM source data 266 if bom.filename == INPUT_TYPE_STDIN { 267 if bom.rawBytes, err = io.ReadAll(os.Stdin); err != nil { 268 return 269 } 270 } else { // load the BOM data from relative filename 271 // Conditionally append working directory if no abs. path detected 272 if len(bom.filename) > 0 && !filepath.IsAbs(bom.filename) { 273 bom.absFilename = filepath.Join(utils.GlobalFlags.WorkingDir, bom.filename) 274 } else { 275 bom.absFilename = bom.filename 276 } 277 278 // Open our jsonFile 279 var jsonFile *os.File 280 if jsonFile, err = os.Open(bom.absFilename); err != nil { 281 return 282 } 283 284 // defer the closing of our jsonFile 285 defer jsonFile.Close() 286 287 // read our opened jsonFile as a byte array. 288 if bom.rawBytes, err = io.ReadAll(jsonFile); err != nil { 289 return 290 } 291 } 292 293 getLogger().Tracef("read data from: `%s`", bom.filename) 294 getLogger().Tracef("\n >> rawBytes[:100]=[%s]", bom.rawBytes[:100]) 295 return 296 } 297 298 func (bom *BOM) UnmarshalBOMAsJSONMap() (err error) { 299 getLogger().Enter() 300 defer getLogger().Exit() 301 302 if err = bom.ReadRawBytes(); err != nil { 303 return 304 } 305 306 // Attempt to unmarshal the prospective JSON document to a map 307 bom.JsonMap = make(map[string]interface{}) 308 errUnmarshal := json.Unmarshal(bom.rawBytes, &(bom.JsonMap)) 309 if errUnmarshal != nil { 310 getLogger().Trace(errUnmarshal) 311 if syntaxError, ok := errUnmarshal.(*json.SyntaxError); ok { 312 line, character := utils.CalcLineAndCharacterPos(bom.rawBytes, syntaxError.Offset) 313 getLogger().Tracef("syntax error found at line,char=[%d,%d]", line, character) 314 } 315 return errUnmarshal 316 } 317 318 // Print the data type of result variable 319 getLogger().Tracef("bom.jsonMap(%s)", reflect.TypeOf(bom.JsonMap)) 320 321 return nil 322 } 323 324 func (bom *BOM) UnmarshalCycloneDXBOM() (err error) { 325 getLogger().Enter() 326 defer getLogger().Exit() 327 328 // Unmarshal as a JSON Map, if not done already 329 if bom.JsonMap == nil { 330 if err = bom.UnmarshalBOMAsJSONMap(); err != nil { 331 return 332 } 333 } 334 335 // Use the JSON Map to unmarshal to CDX-specific types 336 if bom.CdxBom, err = UnMarshalDocument(bom.JsonMap); err != nil { 337 return 338 } 339 340 return 341 } 342 343 // NOTE: This method uses JSON Marshal() (i.e, from the json/encoding package) 344 // which, by default, encodes characters using Unicode for HTML transmission 345 // (assuming its primary use is for HTML servers). 346 // For example, this means the following characters are translated to Unicode 347 // if marshall() method is used: 348 // '&' is encoded as: \u0026 349 // '<' is encoded as: \u003c 350 // '>' is encoded as: \u003e 351 func (bom *BOM) MarshalCycloneDXBOM(writer io.Writer, prefix string, indent string) (err error) { 352 getLogger().Enter() 353 defer getLogger().Exit() 354 355 var jsonBytes []byte 356 jsonBytes, err = json.MarshalIndent(bom.CdxBom, prefix, indent) 357 if err != nil { 358 return 359 } 360 361 numBytes, errWrite := writer.Write(jsonBytes) 362 if errWrite != nil { 363 return errWrite 364 } 365 getLogger().Tracef("wrote [%v] bytes to output", numBytes) 366 367 return 368 } 369 370 // This method ensures the preservation of original characters (after any edits) 371 // 372 // It is needed because JSON Marshal() (i.e., the json/encoding package), by default, 373 // encodes chars (assumes JSON docs are being transmitted over HTML streams). 374 // This assumption by json/encoding is not true for BOM documents as stream (wire) 375 // transmission encodings are specified for both formats which do not use HTML encoding. 376 // 377 // For example, the following characters are lost using json/encoding: 378 // '&' is encoded as: \u0026 379 // '<' is encoded as: \u003c 380 // '>' is encoded as: \u003e 381 // Instead, this custom encoder method dutifully preserves the input byte values 382 // TODO: Support "--prefix string"; prefix parameter currently ignored 383 func (bom *BOM) WriteAsEncodedJSON(writer io.Writer, prefix string, indent string) (err error) { 384 getLogger().Enter() 385 defer getLogger().Exit() 386 387 var outputBuffer bytes.Buffer 388 outputBuffer, err = utils.EncodeAnyToIndentedJSONStr(bom.CdxBom, indent) 389 if err != nil { 390 return 391 } 392 393 numBytes, errWrite := writer.Write(outputBuffer.Bytes()) 394 if errWrite != nil { 395 return errWrite 396 } 397 getLogger().Tracef("wrote [%v] bytes to output", numBytes) 398 399 return 400 } 401 402 func (bom *BOM) WriteAsEncodedJSONInt(writer io.Writer, numSpaces int) (err error) { 403 _, err = utils.WriteAnyAsEncodedJSONInt(writer, bom.CdxBom, numSpaces) 404 return 405 } 406 407 // Approach 1 408 func (bom *BOM) HashEntity(entity interface{}) (sha string) { 409 bom.GobEncoder.Encode(entity) 410 sha256 := sha256.Sum256(bom.GobEncodeBuffer.Bytes()) 411 return fmt.Sprintf("%x\n", sha256) 412 } 413 414 func (bom *BOM) HashJsonMap(entity interface{}) (sha string, err error) { 415 bom.GobEncoder.Encode(entity) 416 417 // Verify entity is a JSON map type 418 if !utils.IsJsonMapType(entity) { 419 err = fmt.Errorf("invalid type. Cannot hash non-struct type (%T)", entity) 420 return 421 } 422 423 sha = bom.HashEntity(entity) 424 return 425 } 426 427 func (bom *BOM) HashStruct(entity interface{}) (sha string, err error) { 428 bom.GobEncoder.Encode(entity) 429 430 // Verify entity is a Go struct type 431 kind := reflect.ValueOf(entity).Kind() 432 if kind != reflect.Struct { 433 err = fmt.Errorf("invalid type. Cannot hash non-struct type (%T); kind: %v", entity, kind) 434 return 435 } 436 437 sha = bom.HashEntity(entity) 438 return 439 } 440 441 // Approach 2 442 // NOTE: Not yet needed 443 // TODO: allow multiple "writes" of a slice of entities 444 // func (bom *BOM) HashEntities(entities []interface{}) (sha []byte, err error) { 445 446 // bom.GobEncoder.Encode(entities) 447 // hasher := sha256.New() 448 // hasher.Write(bom.GobEncodeBuffer.Bytes()) 449 // fmt.Printf("%x\n", hasher.Sum(nil)) 450 // return 451 // }