github.com/anchore/syft@v1.38.2/syft/format/common/cyclonedxhelpers/to_format_model.go (about) 1 package cyclonedxhelpers 2 3 import ( 4 "fmt" 5 "slices" 6 "strings" 7 "time" 8 9 cyclonedx "github.com/CycloneDX/cyclonedx-go" 10 "github.com/google/uuid" 11 12 stfile "github.com/anchore/stereoscope/pkg/file" 13 "github.com/anchore/syft/internal/log" 14 "github.com/anchore/syft/syft/artifact" 15 "github.com/anchore/syft/syft/cpe" 16 "github.com/anchore/syft/syft/file" 17 "github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers" 18 "github.com/anchore/syft/syft/linux" 19 "github.com/anchore/syft/syft/pkg" 20 "github.com/anchore/syft/syft/sbom" 21 "github.com/anchore/syft/syft/source" 22 ) 23 24 var cycloneDXValidHash = map[string]cyclonedx.HashAlgorithm{ 25 "sha1": cyclonedx.HashAlgoSHA1, 26 "md5": cyclonedx.HashAlgoMD5, 27 "sha256": cyclonedx.HashAlgoSHA256, 28 "sha384": cyclonedx.HashAlgoSHA384, 29 "sha512": cyclonedx.HashAlgoSHA512, 30 "blake2b256": cyclonedx.HashAlgoBlake2b_256, 31 "blake2b384": cyclonedx.HashAlgoBlake2b_384, 32 "blake2b512": cyclonedx.HashAlgoBlake2b_512, 33 "blake3": cyclonedx.HashAlgoBlake3, 34 } 35 36 func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { 37 cdxBOM := cyclonedx.NewBOM() 38 39 // NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid): 40 // https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36 41 // "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" 42 cdxBOM.SerialNumber = uuid.New().URN() 43 cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source) 44 45 coordinates, locationSorter := getCoordinates(s) 46 47 // Packages 48 packages := s.Artifacts.Packages.Sorted() 49 components := make([]cyclonedx.Component, len(packages)) 50 for i, p := range packages { 51 components[i] = helpers.EncodeComponent(p, s.Source.Supplier, locationSorter) 52 } 53 components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) 54 55 artifacts := s.Artifacts 56 57 for _, coordinate := range coordinates { 58 var metadata *file.Metadata 59 // File Info 60 fileMetadata, exists := artifacts.FileMetadata[coordinate] 61 // no file metadata then don't include in SBOM 62 // the syft config allows for sometimes only capturing files owned by packages 63 // so there can be a map miss here where we have less metadata than all coordinates 64 if !exists { 65 continue 66 } 67 if fileMetadata.Type == stfile.TypeDirectory || 68 fileMetadata.Type == stfile.TypeSocket || 69 fileMetadata.Type == stfile.TypeSymLink { 70 // skip dir, symlinks and sockets for the final bom 71 continue 72 } 73 metadata = &fileMetadata 74 75 // Digests 76 var digests []file.Digest 77 if digestsForLocation, exists := artifacts.FileDigests[coordinate]; exists { 78 digests = digestsForLocation 79 } 80 81 cdxHashes := digestsToHashes(digests) 82 components = append(components, cyclonedx.Component{ 83 BOMRef: string(coordinate.ID()), 84 Type: cyclonedx.ComponentTypeFile, 85 Name: metadata.Path, 86 Hashes: &cdxHashes, 87 }) 88 } 89 cdxBOM.Components = &components 90 91 dependencies := toDependencies(s.Relationships) 92 if len(dependencies) > 0 { 93 cdxBOM.Dependencies = &dependencies 94 } 95 96 return cdxBOM 97 } 98 99 func getCoordinates(s sbom.SBOM) ([]file.Coordinates, func(a, b file.Location) int) { 100 var layers []string 101 if m, ok := s.Source.Metadata.(source.ImageMetadata); ok { 102 for _, l := range m.Layers { 103 layers = append(layers, l.Digest) 104 } 105 } 106 107 coordSorter := file.CoordinatesSorter(layers) 108 coordinates := s.AllCoordinates() 109 110 slices.SortFunc(coordinates, coordSorter) 111 return coordinates, file.LocationSorter(layers) 112 } 113 114 func digestsToHashes(digests []file.Digest) []cyclonedx.Hash { 115 var hashes []cyclonedx.Hash 116 for _, digest := range digests { 117 lookup := strings.ToLower(digest.Algorithm) 118 cdxAlgo, exists := cycloneDXValidHash[lookup] 119 if !exists { 120 continue 121 } 122 hashes = append(hashes, cyclonedx.Hash{ 123 Algorithm: cdxAlgo, 124 Value: digest.Value, 125 }) 126 } 127 return hashes 128 } 129 130 func toOSComponent(distro *linux.Release) []cyclonedx.Component { 131 if distro == nil { 132 return []cyclonedx.Component{} 133 } 134 eRefs := &[]cyclonedx.ExternalReference{} 135 if distro.BugReportURL != "" { 136 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 137 URL: distro.BugReportURL, 138 Type: cyclonedx.ERTypeIssueTracker, 139 }) 140 } 141 if distro.HomeURL != "" { 142 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 143 URL: distro.HomeURL, 144 Type: cyclonedx.ERTypeWebsite, 145 }) 146 } 147 if distro.SupportURL != "" { 148 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 149 URL: distro.SupportURL, 150 Type: cyclonedx.ERTypeOther, 151 Comment: "support", 152 }) 153 } 154 if distro.PrivacyPolicyURL != "" { 155 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 156 URL: distro.PrivacyPolicyURL, 157 Type: cyclonedx.ERTypeOther, 158 Comment: "privacyPolicy", 159 }) 160 } 161 if len(*eRefs) == 0 { 162 eRefs = nil 163 } 164 props := helpers.EncodeProperties(distro, "syft:distro") 165 var properties *[]cyclonedx.Property 166 if len(props) > 0 { 167 properties = &props 168 } 169 return []cyclonedx.Component{ 170 { 171 BOMRef: toOSBomRef(distro.ID, distro.VersionID), 172 Type: cyclonedx.ComponentTypeOS, 173 // is it idiomatic to be using SWID here for specific name and version information? 174 SWID: &cyclonedx.SWID{ 175 TagID: distro.ID, 176 Name: distro.ID, 177 Version: distro.VersionID, 178 }, 179 Description: distro.PrettyName, 180 Name: distro.ID, 181 Version: distro.VersionID, 182 // should we add a PURL? 183 CPE: formatCPE(distro.CPEName), 184 ExternalReferences: eRefs, 185 Properties: properties, 186 }, 187 } 188 } 189 190 func toOSBomRef(name string, version string) string { 191 if name == "" { 192 return "os:unknown" 193 } 194 if version == "" { 195 return fmt.Sprintf("os:%s", name) 196 } 197 return fmt.Sprintf("os:%s@%s", name, version) 198 } 199 200 func formatCPE(cpeString string) string { 201 c, err := cpe.NewAttributes(cpeString) 202 if err != nil { 203 log.Debugf("skipping invalid CPE: %s", cpeString) 204 return "" 205 } 206 return c.String() 207 } 208 209 // NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details. 210 func toBomDescriptor(name, version string, srcMetadata source.Description) *cyclonedx.Metadata { 211 return &cyclonedx.Metadata{ 212 Timestamp: time.Now().Format(time.RFC3339), 213 Tools: &cyclonedx.ToolsChoice{ 214 Components: &[]cyclonedx.Component{ 215 { 216 Type: cyclonedx.ComponentTypeApplication, 217 Author: "anchore", 218 Name: name, 219 Version: version, 220 }, 221 }, 222 }, 223 Supplier: toBomSupplier(srcMetadata), 224 Properties: toBomProperties(srcMetadata), 225 Component: toBomDescriptorComponent(srcMetadata), 226 } 227 } 228 229 func toBomSupplier(srcMetadata source.Description) *cyclonedx.OrganizationalEntity { 230 if srcMetadata.Supplier != "" { 231 return &cyclonedx.OrganizationalEntity{ 232 Name: srcMetadata.Supplier, 233 } 234 } 235 236 return nil 237 } 238 239 // used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency. 240 // NOTE: CycloneDX provides the ability to describe components and their dependency on other components. 241 // The dependency graph is capable of representing both direct and transitive relationships. 242 // If a relationship is either direct or transitive it can be included in this function. 243 // An example of a relationship to not include would be: OwnershipByFileOverlapRelationship. 244 func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { 245 switch ty { 246 case artifact.DependencyOfRelationship: 247 return true 248 default: 249 return false 250 } 251 } 252 253 func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependency { 254 dependencies := map[string]*cyclonedx.Dependency{} 255 for _, r := range relationships { 256 exists := isExpressiblePackageRelationship(r.Type) 257 if !exists { 258 log.Debugf("unable to convert relationship type to CycloneDX JSON, dropping: %#v", r) 259 continue 260 } 261 262 // we only capture package-to-package relationships for now 263 fromPkg, ok := r.From.(pkg.Package) 264 if !ok { 265 log.Tracef("unable to convert relationship fromPkg to CycloneDX JSON, dropping: %#v", r) 266 continue 267 } 268 269 toPkg, ok := r.To.(pkg.Package) 270 if !ok { 271 log.Tracef("unable to convert relationship toPkg to CycloneDX JSON, dropping: %#v", r) 272 continue 273 } 274 275 toRef := helpers.DeriveBomRef(toPkg) 276 dep := dependencies[toRef] 277 if dep == nil { 278 dep = &cyclonedx.Dependency{ 279 Ref: toRef, 280 Dependencies: &[]string{}, 281 } 282 dependencies[toRef] = dep 283 } 284 285 fromRef := helpers.DeriveBomRef(fromPkg) 286 if !slices.Contains(*dep.Dependencies, fromRef) { 287 *dep.Dependencies = append(*dep.Dependencies, fromRef) 288 } 289 } 290 291 result := make([]cyclonedx.Dependency, 0, len(dependencies)) 292 for _, dep := range dependencies { 293 slices.Sort(*dep.Dependencies) 294 result = append(result, *dep) 295 } 296 297 slices.SortFunc(result, func(a, b cyclonedx.Dependency) int { 298 return strings.Compare(a.Ref, b.Ref) 299 }) 300 301 return result 302 } 303 304 func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property { 305 metadata, ok := srcMetadata.Metadata.(source.ImageMetadata) 306 if ok { 307 props := helpers.EncodeProperties(metadata.Labels, "syft:image:labels") 308 // return nil if props is nil to avoid creating a pointer to a nil slice, 309 // which results in a null JSON value that does not comply with the CycloneDX schema. 310 // https://github.com/anchore/grype/issues/1759 311 if props == nil { 312 return nil 313 } 314 return &props 315 } 316 return nil 317 } 318 319 func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Component { 320 name := srcMetadata.Name 321 version := srcMetadata.Version 322 switch metadata := srcMetadata.Metadata.(type) { 323 case source.ImageMetadata: 324 if name == "" { 325 name = metadata.UserInput 326 } 327 if version == "" { 328 version = metadata.ManifestDigest 329 } 330 bomRef, err := artifact.IDByHash(metadata.ID) 331 if err != nil { 332 log.Debugf("unable to get fingerprint of source image metadata=%s: %+v", metadata.ID, err) 333 } 334 return &cyclonedx.Component{ 335 BOMRef: string(bomRef), 336 Type: cyclonedx.ComponentTypeContainer, 337 Name: name, 338 Version: version, 339 Supplier: toBomSupplier(srcMetadata), 340 } 341 case source.DirectoryMetadata: 342 if name == "" { 343 name = metadata.Path 344 } 345 bomRef, err := artifact.IDByHash(metadata.Path) 346 if err != nil { 347 log.Debugf("unable to get fingerprint of source directory metadata path=%s: %+v", metadata.Path, err) 348 } 349 return &cyclonedx.Component{ 350 BOMRef: string(bomRef), 351 // TODO: this is lossy... we can't know if this is a file or a directory 352 Type: cyclonedx.ComponentTypeFile, 353 Name: name, 354 Version: version, 355 Supplier: toBomSupplier(srcMetadata), 356 } 357 case source.FileMetadata: 358 if name == "" { 359 name = metadata.Path 360 } 361 bomRef, err := artifact.IDByHash(metadata.Path) 362 if err != nil { 363 log.Debugf("unable to get fingerprint of source file metadata path=%s: %+v", metadata.Path, err) 364 } 365 return &cyclonedx.Component{ 366 BOMRef: string(bomRef), 367 // TODO: this is lossy... we can't know if this is a file or a directory 368 Type: cyclonedx.ComponentTypeFile, 369 Name: name, 370 Version: version, 371 Supplier: toBomSupplier(srcMetadata), 372 } 373 } 374 375 return nil 376 }