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