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