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