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