github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/sbom/sbom.go (about)

     1  package sbom
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"encoding/xml"
     7  	"io"
     8  	"strings"
     9  
    10  	"github.com/in-toto/in-toto-golang/in_toto"
    11  	"golang.org/x/xerrors"
    12  
    13  	"github.com/devseccon/trivy/pkg/attestation"
    14  	"github.com/devseccon/trivy/pkg/sbom/cyclonedx"
    15  	"github.com/devseccon/trivy/pkg/sbom/spdx"
    16  	"github.com/devseccon/trivy/pkg/types"
    17  )
    18  
    19  type Format string
    20  
    21  const (
    22  	FormatCycloneDXJSON       Format = "cyclonedx-json"
    23  	FormatCycloneDXXML        Format = "cyclonedx-xml"
    24  	FormatSPDXJSON            Format = "spdx-json"
    25  	FormatSPDXTV              Format = "spdx-tv"
    26  	FormatSPDXXML             Format = "spdx-xml"
    27  	FormatAttestCycloneDXJSON Format = "attest-cyclonedx-json"
    28  	FormatUnknown             Format = "unknown"
    29  
    30  	// FormatLegacyCosignAttestCycloneDXJSON is used to support the older format of CycloneDX JSON Attestation
    31  	// produced by the Cosign V1.
    32  	// ref. https://github.com/sigstore/cosign/pull/2718
    33  	FormatLegacyCosignAttestCycloneDXJSON Format = "legacy-cosign-attest-cyclonedx-json"
    34  
    35  	// PredicateCycloneDXBeforeV05 is the PredicateCycloneDX value defined in in-toto-golang before v0.5.0.
    36  	// This is necessary for backward-compatible SBOM detection.
    37  	// ref. https://github.com/in-toto/in-toto-golang/pull/188
    38  	PredicateCycloneDXBeforeV05 = "https://cyclonedx.org/schema"
    39  )
    40  
    41  var ErrUnknownFormat = xerrors.New("Unknown SBOM format")
    42  
    43  type cdxHeader struct {
    44  	// XML specific field
    45  	XMLNS string `json:"-" xml:"xmlns,attr"`
    46  
    47  	// JSON specific field
    48  	BOMFormat string `json:"bomFormat" xml:"-"`
    49  }
    50  
    51  type spdxHeader struct {
    52  	SpdxID string `json:"SPDXID"`
    53  }
    54  
    55  func IsCycloneDXJSON(r io.ReadSeeker) (bool, error) {
    56  	if _, err := r.Seek(0, io.SeekStart); err != nil {
    57  		return false, xerrors.Errorf("seek error: %w", err)
    58  	}
    59  
    60  	var cdxBom cdxHeader
    61  	if err := json.NewDecoder(r).Decode(&cdxBom); err == nil {
    62  		if cdxBom.BOMFormat == "CycloneDX" {
    63  			return true, nil
    64  		}
    65  	}
    66  	return false, nil
    67  }
    68  func IsCycloneDXXML(r io.ReadSeeker) (bool, error) {
    69  	if _, err := r.Seek(0, io.SeekStart); err != nil {
    70  		return false, xerrors.Errorf("seek error: %w", err)
    71  	}
    72  
    73  	var cdxBom cdxHeader
    74  	if err := xml.NewDecoder(r).Decode(&cdxBom); err == nil {
    75  		if strings.HasPrefix(cdxBom.XMLNS, "http://cyclonedx.org") {
    76  			return true, nil
    77  		}
    78  	}
    79  	return false, nil
    80  }
    81  
    82  func IsSPDXJSON(r io.ReadSeeker) (bool, error) {
    83  	if _, err := r.Seek(0, io.SeekStart); err != nil {
    84  		return false, xerrors.Errorf("seek error: %w", err)
    85  	}
    86  
    87  	var spdxBom spdxHeader
    88  	if err := json.NewDecoder(r).Decode(&spdxBom); err == nil {
    89  		if strings.HasPrefix(spdxBom.SpdxID, "SPDX") {
    90  			return true, nil
    91  		}
    92  	}
    93  	return false, nil
    94  }
    95  
    96  func IsSPDXTV(r io.ReadSeeker) (bool, error) {
    97  	if _, err := r.Seek(0, io.SeekStart); err != nil {
    98  		return false, xerrors.Errorf("seek error: %w", err)
    99  	}
   100  
   101  	if scanner := bufio.NewScanner(r); scanner.Scan() {
   102  		if strings.HasPrefix(scanner.Text(), "SPDX") {
   103  			return true, nil
   104  		}
   105  	}
   106  	return false, nil
   107  }
   108  
   109  func DetectFormat(r io.ReadSeeker) (Format, error) {
   110  	// Rewind the SBOM file at the end
   111  	defer r.Seek(0, io.SeekStart)
   112  
   113  	// Try CycloneDX JSON
   114  	if ok, err := IsCycloneDXJSON(r); err != nil {
   115  		return FormatUnknown, err
   116  	} else if ok {
   117  		return FormatCycloneDXJSON, nil
   118  	}
   119  
   120  	// Try CycloneDX XML
   121  	if ok, err := IsCycloneDXXML(r); err != nil {
   122  		return FormatUnknown, err
   123  	} else if ok {
   124  		return FormatCycloneDXXML, nil
   125  	}
   126  
   127  	// Try SPDX json
   128  	if ok, err := IsSPDXJSON(r); err != nil {
   129  		return FormatUnknown, err
   130  	} else if ok {
   131  		return FormatSPDXJSON, nil
   132  	}
   133  
   134  	// Try SPDX tag-value
   135  	if ok, err := IsSPDXTV(r); err != nil {
   136  		return FormatUnknown, err
   137  	} else if ok {
   138  		return FormatSPDXTV, nil
   139  	}
   140  
   141  	if _, err := r.Seek(0, io.SeekStart); err != nil {
   142  		return FormatUnknown, xerrors.Errorf("seek error: %w", err)
   143  	}
   144  
   145  	// Try in-toto attestation
   146  	format, ok := decodeAttestCycloneDXJSONFormat(r)
   147  	if ok {
   148  		return format, nil
   149  	}
   150  
   151  	return FormatUnknown, nil
   152  }
   153  
   154  func decodeAttestCycloneDXJSONFormat(r io.ReadSeeker) (Format, bool) {
   155  	var s attestation.Statement
   156  
   157  	if err := json.NewDecoder(r).Decode(&s); err != nil {
   158  		return "", false
   159  	}
   160  
   161  	if s.PredicateType != in_toto.PredicateCycloneDX && s.PredicateType != PredicateCycloneDXBeforeV05 {
   162  		return "", false
   163  	}
   164  
   165  	if s.Predicate == nil {
   166  		return "", false
   167  	}
   168  
   169  	m, ok := s.Predicate.(map[string]interface{})
   170  	if !ok {
   171  		return "", false
   172  	}
   173  
   174  	if _, ok := m["Data"]; ok {
   175  		return FormatLegacyCosignAttestCycloneDXJSON, true
   176  	}
   177  
   178  	return FormatAttestCycloneDXJSON, true
   179  }
   180  
   181  func Decode(f io.Reader, format Format) (types.SBOM, error) {
   182  	var (
   183  		v       interface{}
   184  		bom     types.SBOM
   185  		decoder interface{ Decode(any) error }
   186  	)
   187  
   188  	switch format {
   189  	case FormatCycloneDXJSON:
   190  		v = &cyclonedx.BOM{SBOM: &bom}
   191  		decoder = json.NewDecoder(f)
   192  	case FormatAttestCycloneDXJSON:
   193  		// dsse envelope
   194  		//   => in-toto attestation
   195  		//     => CycloneDX JSON
   196  		v = &attestation.Statement{
   197  			Predicate: &cyclonedx.BOM{SBOM: &bom},
   198  		}
   199  		decoder = json.NewDecoder(f)
   200  	case FormatLegacyCosignAttestCycloneDXJSON:
   201  		// dsse envelope
   202  		//   => in-toto attestation
   203  		//     => cosign predicate
   204  		//       => CycloneDX JSON
   205  		v = &attestation.Statement{
   206  			Predicate: &attestation.CosignPredicate{
   207  				Data: &cyclonedx.BOM{SBOM: &bom},
   208  			},
   209  		}
   210  		decoder = json.NewDecoder(f)
   211  	case FormatSPDXJSON:
   212  		v = &spdx.SPDX{SBOM: &bom}
   213  		decoder = json.NewDecoder(f)
   214  	case FormatSPDXTV:
   215  		v = &spdx.SPDX{SBOM: &bom}
   216  		decoder = spdx.NewTVDecoder(f)
   217  
   218  	default:
   219  		return types.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format)
   220  
   221  	}
   222  
   223  	// Decode a file content into sbom.SBOM
   224  	if err := decoder.Decode(v); err != nil {
   225  		return types.SBOM{}, xerrors.Errorf("failed to decode: %w", err)
   226  	}
   227  
   228  	return bom, nil
   229  }