github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/common/cyclonedxhelpers/decoder.go (about) 1 package cyclonedxhelpers 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/format/common" 11 "github.com/anchore/syft/syft/linux" 12 "github.com/anchore/syft/syft/pkg" 13 "github.com/anchore/syft/syft/sbom" 14 "github.com/anchore/syft/syft/source" 15 ) 16 17 func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) { 18 if bom == nil { 19 return nil, fmt.Errorf("no content defined in CycloneDX BOM") 20 } 21 22 s := &sbom.SBOM{ 23 Artifacts: sbom.Artifacts{ 24 Packages: pkg.NewCollection(), 25 LinuxDistribution: linuxReleaseFromComponents(*bom.Components), 26 }, 27 Source: extractComponents(bom.Metadata), 28 Descriptor: extractDescriptor(bom.Metadata), 29 } 30 31 idMap := make(map[string]interface{}) 32 33 if err := collectBomPackages(bom, s, idMap); err != nil { 34 return nil, err 35 } 36 37 collectRelationships(bom, s, idMap) 38 39 return s, nil 40 } 41 42 func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error { 43 if bom.Components == nil { 44 return fmt.Errorf("no components are defined in the CycloneDX BOM") 45 } 46 for i := range *bom.Components { 47 collectPackages(&(*bom.Components)[i], s, idMap) 48 } 49 return nil 50 } 51 52 func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) { 53 switch component.Type { 54 case cyclonedx.ComponentTypeOS: 55 case cyclonedx.ComponentTypeContainer: 56 case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary: 57 p := decodeComponent(component) 58 idMap[component.BOMRef] = p 59 syftID := extractSyftPacakgeID(component.BOMRef) 60 if syftID != "" { 61 idMap[syftID] = p 62 } 63 // TODO there must be a better way than needing to call this manually: 64 p.SetID() 65 s.Artifacts.Packages.Add(*p) 66 } 67 68 if component.Components != nil { 69 for i := range *component.Components { 70 collectPackages(&(*component.Components)[i], s, idMap) 71 } 72 } 73 } 74 75 func extractSyftPacakgeID(i string) string { 76 instance, err := packageurl.FromString(i) 77 if err != nil { 78 return "" 79 } 80 for _, q := range instance.Qualifiers { 81 if q.Key == "package-id" { 82 return q.Value 83 } 84 } 85 return "" 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 common.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 := common.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 := common.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 switch c.Type { 215 case cyclonedx.ComponentTypeContainer: 216 var labels map[string]string 217 218 if meta.Properties != nil { 219 labels = decodeProperties(*meta.Properties, "syft:image:labels:") 220 } 221 222 return source.Description{ 223 ID: "", 224 // TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet) 225 226 Metadata: source.StereoscopeImageSourceMetadata{ 227 UserInput: c.Name, 228 ID: c.BOMRef, 229 ManifestDigest: c.Version, 230 Labels: labels, 231 }, 232 } 233 case cyclonedx.ComponentTypeFile: 234 // TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet) 235 236 // TODO: this is lossy... we can't know if this is a file or a directory 237 return source.Description{ 238 ID: "", 239 Metadata: source.FileSourceMetadata{Path: c.Name}, 240 } 241 } 242 return source.Description{} 243 } 244 245 // if there is more than one tool in meta.Tools' list the last item will be used 246 // as descriptor. If there is a way to know which tool to use here please fix it. 247 func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) { 248 if meta == nil || meta.Tools == nil { 249 return 250 } 251 252 for _, t := range *meta.Tools { 253 desc.Name = t.Name 254 desc.Version = t.Version 255 } 256 257 return 258 }