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 }