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  // }