github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/formats/common/cyclonedxhelpers/decoder.go (about) 1 package cyclonedxhelpers 2 3 import ( 4 "fmt" 5 "io" 6 7 "github.com/CycloneDX/cyclonedx-go" 8 "github.com/nextlinux/gosbom/gosbom/artifact" 9 "github.com/nextlinux/gosbom/gosbom/formats/common" 10 "github.com/nextlinux/gosbom/gosbom/linux" 11 "github.com/nextlinux/gosbom/gosbom/pkg" 12 "github.com/nextlinux/gosbom/gosbom/sbom" 13 "github.com/nextlinux/gosbom/gosbom/source" 14 15 "github.com/anchore/packageurl-go" 16 ) 17 18 func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator { 19 return func(reader io.Reader) error { 20 bom := &cyclonedx.BOM{} 21 err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom) 22 if err != nil { 23 return err 24 } 25 // random JSON does not necessarily cause an error (e.g. SPDX) 26 if (cyclonedx.BOM{} == *bom || bom.Components == nil) { 27 return fmt.Errorf("not a valid CycloneDX document") 28 } 29 return nil 30 } 31 } 32 33 func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder { 34 return func(reader io.Reader) (*sbom.SBOM, error) { 35 bom := &cyclonedx.BOM{ 36 Components: &[]cyclonedx.Component{}, 37 } 38 err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom) 39 if err != nil { 40 return nil, err 41 } 42 s, err := ToGosbomModel(bom) 43 if err != nil { 44 return nil, err 45 } 46 return s, nil 47 } 48 } 49 50 func ToGosbomModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) { 51 if bom == nil { 52 return nil, fmt.Errorf("no content defined in CycloneDX BOM") 53 } 54 55 s := &sbom.SBOM{ 56 Artifacts: sbom.Artifacts{ 57 Packages: pkg.NewCollection(), 58 LinuxDistribution: linuxReleaseFromComponents(*bom.Components), 59 }, 60 Source: extractComponents(bom.Metadata), 61 Descriptor: extractDescriptor(bom.Metadata), 62 } 63 64 idMap := make(map[string]interface{}) 65 66 if err := collectBomPackages(bom, s, idMap); err != nil { 67 return nil, err 68 } 69 70 collectRelationships(bom, s, idMap) 71 72 return s, nil 73 } 74 75 func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error { 76 if bom.Components == nil { 77 return fmt.Errorf("no components are defined in the CycloneDX BOM") 78 } 79 for i := range *bom.Components { 80 collectPackages(&(*bom.Components)[i], s, idMap) 81 } 82 return nil 83 } 84 85 func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) { 86 switch component.Type { 87 case cyclonedx.ComponentTypeOS: 88 case cyclonedx.ComponentTypeContainer: 89 case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary: 90 p := decodeComponent(component) 91 idMap[component.BOMRef] = p 92 gosbomID := extractGosbomPacakgeID(component.BOMRef) 93 if gosbomID != "" { 94 idMap[gosbomID] = p 95 } 96 // TODO there must be a better way than needing to call this manually: 97 p.SetID() 98 s.Artifacts.Packages.Add(*p) 99 } 100 101 if component.Components != nil { 102 for i := range *component.Components { 103 collectPackages(&(*component.Components)[i], s, idMap) 104 } 105 } 106 } 107 108 func extractGosbomPacakgeID(i string) string { 109 instance, err := packageurl.FromString(i) 110 if err != nil { 111 return "" 112 } 113 for _, q := range instance.Qualifiers { 114 if q.Key == "package-id" { 115 return q.Value 116 } 117 } 118 return "" 119 } 120 121 func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release { 122 for i := range components { 123 component := &components[i] 124 if component.Type == cyclonedx.ComponentTypeOS { 125 return linuxReleaseFromOSComponent(component) 126 } 127 } 128 return nil 129 } 130 131 func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release { 132 if component == nil { 133 return nil 134 } 135 136 var name string 137 var version string 138 if component.SWID != nil { 139 name = component.SWID.Name 140 version = component.SWID.Version 141 } 142 if name == "" { 143 name = component.Name 144 } 145 if name == "" { 146 name = getPropertyValue(component, "id") 147 } 148 if version == "" { 149 version = component.Version 150 } 151 if version == "" { 152 version = getPropertyValue(component, "versionID") 153 } 154 155 rel := &linux.Release{ 156 CPEName: component.CPE, 157 PrettyName: name, 158 Name: name, 159 ID: name, 160 IDLike: []string{name}, 161 Version: version, 162 VersionID: version, 163 } 164 if component.ExternalReferences != nil { 165 for _, ref := range *component.ExternalReferences { 166 switch ref.Type { 167 case cyclonedx.ERTypeIssueTracker: 168 rel.BugReportURL = ref.URL 169 case cyclonedx.ERTypeWebsite: 170 rel.HomeURL = ref.URL 171 case cyclonedx.ERTypeOther: 172 switch ref.Comment { 173 case "support": 174 rel.SupportURL = ref.URL 175 case "privacyPolicy": 176 rel.PrivacyPolicyURL = ref.URL 177 } 178 } 179 } 180 } 181 182 if component.Properties != nil { 183 values := map[string]string{} 184 for _, p := range *component.Properties { 185 values[p.Name] = p.Value 186 } 187 common.DecodeInto(&rel, values, "gosbom:distro", CycloneDXFields) 188 } 189 190 return rel 191 } 192 193 func getPropertyValue(component *cyclonedx.Component, name string) string { 194 if component.Properties != nil { 195 for _, p := range *component.Properties { 196 if p.Name == name { 197 return p.Value 198 } 199 } 200 } 201 return "" 202 } 203 204 func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) { 205 if bom.Dependencies == nil { 206 return 207 } 208 for _, d := range *bom.Dependencies { 209 to, fromExists := idMap[d.Ref].(artifact.Identifiable) 210 if !fromExists { 211 continue 212 } 213 214 if d.Dependencies == nil { 215 continue 216 } 217 218 for _, t := range *d.Dependencies { 219 from, toExists := idMap[t].(artifact.Identifiable) 220 if !toExists { 221 continue 222 } 223 s.Relationships = append(s.Relationships, artifact.Relationship{ 224 From: from, 225 To: to, 226 Type: artifact.DependencyOfRelationship, // FIXME this information is lost 227 }) 228 } 229 } 230 } 231 232 func extractComponents(meta *cyclonedx.Metadata) source.Metadata { 233 if meta == nil || meta.Component == nil { 234 return source.Metadata{} 235 } 236 c := meta.Component 237 238 image := source.ImageMetadata{ 239 UserInput: c.Name, 240 ID: c.BOMRef, 241 ManifestDigest: c.Version, 242 } 243 244 switch c.Type { 245 case cyclonedx.ComponentTypeContainer: 246 return source.Metadata{ 247 Scheme: source.ImageScheme, 248 ImageMetadata: image, 249 } 250 case cyclonedx.ComponentTypeFile: 251 return source.Metadata{ 252 Scheme: source.FileScheme, // or source.DirectoryScheme 253 Path: c.Name, 254 ImageMetadata: image, 255 } 256 } 257 return source.Metadata{} 258 } 259 260 // if there is more than one tool in meta.Tools' list the last item will be used 261 // as descriptor. If there is a way to know which tool to use here please fix it. 262 func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) { 263 if meta == nil || meta.Tools == nil { 264 return 265 } 266 267 for _, t := range *meta.Tools { 268 desc.Name = t.Name 269 desc.Version = t.Version 270 } 271 272 return 273 }