Keith Zantow 621d21eb04
feat: Add PURL list input/output format (#3853)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
2025-05-12 13:33:24 -04:00

134 lines
3.1 KiB
Go

package cyclonedxjson
import (
"encoding/json"
"fmt"
"io"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
"github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers"
"github.com/anchore/syft/syft/format/internal/stream"
"github.com/anchore/syft/syft/sbom"
)
var _ sbom.FormatDecoder = (*decoder)(nil)
type decoder struct {
decoder cyclonedxutil.Decoder
}
func NewFormatDecoder() sbom.FormatDecoder {
return decoder{
decoder: cyclonedxutil.NewDecoder(cyclonedx.BOMFileFormatJSON),
}
}
func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error) {
reader, err := stream.SeekableReader(r)
if err != nil {
return nil, "", "", err
}
id, version := d.Identify(reader)
if id != ID {
return nil, "", "", fmt.Errorf("not a cyclonedx json document")
}
if version == "" {
return nil, "", "", fmt.Errorf("unsupported cyclonedx json document version")
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return nil, id, version, fmt.Errorf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
}
doc, err := d.decoder.Decode(reader)
if err != nil {
return nil, id, version, fmt.Errorf("unable to decode cyclonedx json document: %w", err)
}
s, err := helpers.ToSyftModel(doc)
if err != nil {
return nil, id, version, err
}
return s, id, version, nil
}
func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
reader, err := stream.SeekableReader(r)
if err != nil {
return "", ""
}
if _, err := reader.Seek(0, io.SeekStart); err != nil {
log.Debugf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
return "", ""
}
type Document struct {
JSONSchema string `json:"$schema"`
BOMFormat string `json:"bomFormat"`
SpecVersion string `json:"specVersion"`
}
dec := json.NewDecoder(reader)
var doc Document
if err = dec.Decode(&doc); err != nil {
// maybe not json? maybe not valid? doesn't matter, we won't process it.
return "", ""
}
id, version := getFormatInfo(doc.BOMFormat, doc.SpecVersion)
if version == "" || id != ID {
// not a cyclonedx json document that we support
return "", ""
}
return id, version
}
func getFormatInfo(bomFormat string, specVersion any) (sbom.FormatID, string) {
if bomFormat != "CycloneDX" {
// not a cyclonedx json document
return "", ""
}
// by this point, it looks to be cyclonedx json, but we need to know the version
var (
version string
spec cyclonedx.SpecVersion
err error
)
switch s := specVersion.(type) {
case string:
version = s
spec, err = cyclonedxutil.SpecVersionFromString(version)
if err != nil {
// not a supported version, but is cyclonedx json
return ID, ""
}
case cyclonedx.SpecVersion:
spec = s
version = cyclonedxutil.VersionFromSpecVersion(spec)
if version == "" {
// not a supported version, but is cyclonedx json
return ID, ""
}
default:
// bad input provided for version info
return ID, ""
}
if spec < 0 {
// not a supported version, but is cyclonedx json
return ID, ""
}
return ID, version
}