github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/formats/formats.go (about)

     1  package formats
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/nextlinux/gosbom/gosbom/formats/cyclonedxjson"
    12  	"github.com/nextlinux/gosbom/gosbom/formats/cyclonedxxml"
    13  	"github.com/nextlinux/gosbom/gosbom/formats/github"
    14  	"github.com/nextlinux/gosbom/gosbom/formats/gosbomjson"
    15  	"github.com/nextlinux/gosbom/gosbom/formats/spdxjson"
    16  	"github.com/nextlinux/gosbom/gosbom/formats/spdxtagvalue"
    17  	"github.com/nextlinux/gosbom/gosbom/formats/table"
    18  	"github.com/nextlinux/gosbom/gosbom/formats/template"
    19  	"github.com/nextlinux/gosbom/gosbom/formats/text"
    20  	"github.com/nextlinux/gosbom/gosbom/sbom"
    21  	"github.com/nextlinux/gosbom/internal/log"
    22  	"golang.org/x/exp/slices"
    23  )
    24  
    25  func Formats() []sbom.Format {
    26  	return []sbom.Format{
    27  		gosbomjson.Format(),
    28  		cyclonedxxml.Format(),
    29  		cyclonedxjson.Format(),
    30  		github.Format(),
    31  		spdxtagvalue.Format2_1(),
    32  		spdxtagvalue.Format2_2(),
    33  		spdxtagvalue.Format2_3(),
    34  		spdxjson.Format2_2(),
    35  		spdxjson.Format2_3(),
    36  		table.Format(),
    37  		text.Format(),
    38  		template.Format(),
    39  	}
    40  }
    41  
    42  func Identify(by []byte) sbom.Format {
    43  	for _, f := range Formats() {
    44  		if err := f.Validate(bytes.NewReader(by)); err != nil {
    45  			if !errors.Is(err, sbom.ErrValidationNotSupported) {
    46  				log.WithFields("error", err).Tracef("format validation for %s failed", f.ID())
    47  			}
    48  			continue
    49  		}
    50  		return f
    51  	}
    52  	return nil
    53  }
    54  
    55  // ByName accepts a name@version string, such as:
    56  //
    57  //	spdx-json@2.1 or cyclonedx@2
    58  func ByName(name string) sbom.Format {
    59  	parts := strings.SplitN(name, "@", 2)
    60  	version := sbom.AnyVersion
    61  	if len(parts) > 1 {
    62  		version = parts[1]
    63  	}
    64  	return ByNameAndVersion(parts[0], version)
    65  }
    66  
    67  func ByNameAndVersion(name string, version string) sbom.Format {
    68  	name = cleanFormatName(name)
    69  	var mostRecentFormat sbom.Format
    70  	for _, f := range Formats() {
    71  		for _, n := range f.IDs() {
    72  			if cleanFormatName(string(n)) == name && versionMatches(f.Version(), version) {
    73  				if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
    74  					mostRecentFormat = f
    75  				}
    76  			}
    77  		}
    78  	}
    79  	return mostRecentFormat
    80  }
    81  
    82  func versionMatches(version string, match string) bool {
    83  	if version == sbom.AnyVersion || match == sbom.AnyVersion {
    84  		return true
    85  	}
    86  
    87  	match = strings.ReplaceAll(match, ".", "\\.")
    88  	match = strings.ReplaceAll(match, "*", ".*")
    89  	match = fmt.Sprintf("^%s(\\..*)*$", match)
    90  	matcher, err := regexp.Compile(match)
    91  	if err != nil {
    92  		return false
    93  	}
    94  	return matcher.MatchString(version)
    95  }
    96  
    97  func cleanFormatName(name string) string {
    98  	r := strings.NewReplacer("-", "", "_", "")
    99  	return strings.ToLower(r.Replace(name))
   100  }
   101  
   102  // Encode takes all SBOM elements and a format option and encodes an SBOM document.
   103  func Encode(s sbom.SBOM, f sbom.Format) ([]byte, error) {
   104  	buff := bytes.Buffer{}
   105  
   106  	if err := f.Encode(&buff, s); err != nil {
   107  		return nil, fmt.Errorf("unable to encode sbom: %w", err)
   108  	}
   109  
   110  	return buff.Bytes(), nil
   111  }
   112  
   113  // Decode takes a reader for an SBOM and generates all internal SBOM elements.
   114  func Decode(reader io.Reader) (*sbom.SBOM, sbom.Format, error) {
   115  	by, err := io.ReadAll(reader)
   116  	if err != nil {
   117  		return nil, nil, fmt.Errorf("unable to read sbom: %w", err)
   118  	}
   119  
   120  	f := Identify(by)
   121  	if f == nil {
   122  		return nil, nil, fmt.Errorf("unable to identify format")
   123  	}
   124  
   125  	s, err := f.Decode(bytes.NewReader(by))
   126  	return s, f, err
   127  }
   128  
   129  func AllIDs() (ids []sbom.FormatID) {
   130  	for _, f := range Formats() {
   131  		if slices.Contains(ids, f.ID()) {
   132  			continue
   133  		}
   134  		ids = append(ids, f.ID())
   135  	}
   136  	return ids
   137  }