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 }