github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/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 "github.com/CycloneDX/cyclonedx-go" 10 "github.com/google/uuid" 11 12 "github.com/anchore/syft/internal/log" 13 "github.com/anchore/syft/syft/artifact" 14 "github.com/anchore/syft/syft/cpe" 15 "github.com/anchore/syft/syft/format/internal/cyclonedxutil/helpers" 16 "github.com/anchore/syft/syft/linux" 17 "github.com/anchore/syft/syft/pkg" 18 "github.com/anchore/syft/syft/sbom" 19 "github.com/anchore/syft/syft/source" 20 ) 21 22 func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { 23 cdxBOM := cyclonedx.NewBOM() 24 25 // NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid): 26 // https://github.com/CycloneDX/specification/blob/master/schema/bom-1.3-strict.schema.json#L36 27 // "pattern": "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" 28 cdxBOM.SerialNumber = uuid.New().URN() 29 cdxBOM.Metadata = toBomDescriptor(s.Descriptor.Name, s.Descriptor.Version, s.Source) 30 31 packages := s.Artifacts.Packages.Sorted() 32 components := make([]cyclonedx.Component, len(packages)) 33 for i, p := range packages { 34 components[i] = helpers.EncodeComponent(p) 35 } 36 components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) 37 cdxBOM.Components = &components 38 39 dependencies := toDependencies(s.Relationships) 40 if len(dependencies) > 0 { 41 cdxBOM.Dependencies = &dependencies 42 } 43 44 return cdxBOM 45 } 46 47 func toOSComponent(distro *linux.Release) []cyclonedx.Component { 48 if distro == nil { 49 return []cyclonedx.Component{} 50 } 51 eRefs := &[]cyclonedx.ExternalReference{} 52 if distro.BugReportURL != "" { 53 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 54 URL: distro.BugReportURL, 55 Type: cyclonedx.ERTypeIssueTracker, 56 }) 57 } 58 if distro.HomeURL != "" { 59 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 60 URL: distro.HomeURL, 61 Type: cyclonedx.ERTypeWebsite, 62 }) 63 } 64 if distro.SupportURL != "" { 65 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 66 URL: distro.SupportURL, 67 Type: cyclonedx.ERTypeOther, 68 Comment: "support", 69 }) 70 } 71 if distro.PrivacyPolicyURL != "" { 72 *eRefs = append(*eRefs, cyclonedx.ExternalReference{ 73 URL: distro.PrivacyPolicyURL, 74 Type: cyclonedx.ERTypeOther, 75 Comment: "privacyPolicy", 76 }) 77 } 78 if len(*eRefs) == 0 { 79 eRefs = nil 80 } 81 props := helpers.EncodeProperties(distro, "syft:distro") 82 var properties *[]cyclonedx.Property 83 if len(props) > 0 { 84 properties = &props 85 } 86 return []cyclonedx.Component{ 87 { 88 BOMRef: toOSBomRef(distro.ID, distro.VersionID), 89 Type: cyclonedx.ComponentTypeOS, 90 // is it idiomatic to be using SWID here for specific name and version information? 91 SWID: &cyclonedx.SWID{ 92 TagID: distro.ID, 93 Name: distro.ID, 94 Version: distro.VersionID, 95 }, 96 Description: distro.PrettyName, 97 Name: distro.ID, 98 Version: distro.VersionID, 99 // should we add a PURL? 100 CPE: formatCPE(distro.CPEName), 101 ExternalReferences: eRefs, 102 Properties: properties, 103 }, 104 } 105 } 106 107 func toOSBomRef(name string, version string) string { 108 if name == "" { 109 return "os:unknown" 110 } 111 if version == "" { 112 return fmt.Sprintf("os:%s", name) 113 } 114 return fmt.Sprintf("os:%s@%s", name, version) 115 } 116 117 func formatCPE(cpeString string) string { 118 c, err := cpe.NewAttributes(cpeString) 119 if err != nil { 120 log.Debugf("skipping invalid CPE: %s", cpeString) 121 return "" 122 } 123 return c.String() 124 } 125 126 // NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details. 127 func toBomDescriptor(name, version string, srcMetadata source.Description) *cyclonedx.Metadata { 128 return &cyclonedx.Metadata{ 129 Timestamp: time.Now().Format(time.RFC3339), 130 Tools: &cyclonedx.ToolsChoice{ 131 Components: &[]cyclonedx.Component{ 132 { 133 Type: cyclonedx.ComponentTypeApplication, 134 Author: "anchore", 135 Name: name, 136 Version: version, 137 }, 138 }, 139 }, 140 Properties: toBomProperties(srcMetadata), 141 Component: toBomDescriptorComponent(srcMetadata), 142 } 143 } 144 145 // used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency. 146 // NOTE: CycloneDX provides the ability to describe components and their dependency on other components. 147 // The dependency graph is capable of representing both direct and transitive relationships. 148 // If a relationship is either direct or transitive it can be included in this function. 149 // An example of a relationship to not include would be: OwnershipByFileOverlapRelationship. 150 func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { 151 switch ty { 152 case artifact.DependencyOfRelationship: 153 return true 154 default: 155 return false 156 } 157 } 158 159 func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependency { 160 dependencies := map[string]*cyclonedx.Dependency{} 161 for _, r := range relationships { 162 exists := isExpressiblePackageRelationship(r.Type) 163 if !exists { 164 log.Debugf("unable to convert relationship type to CycloneDX JSON, dropping: %#v", r) 165 continue 166 } 167 168 // we only capture package-to-package relationships for now 169 fromPkg, ok := r.From.(pkg.Package) 170 if !ok { 171 log.Tracef("unable to convert relationship fromPkg to CycloneDX JSON, dropping: %#v", r) 172 continue 173 } 174 175 toPkg, ok := r.To.(pkg.Package) 176 if !ok { 177 log.Tracef("unable to convert relationship toPkg to CycloneDX JSON, dropping: %#v", r) 178 continue 179 } 180 181 toRef := helpers.DeriveBomRef(toPkg) 182 dep := dependencies[toRef] 183 if dep == nil { 184 dep = &cyclonedx.Dependency{ 185 Ref: toRef, 186 Dependencies: &[]string{}, 187 } 188 dependencies[toRef] = dep 189 } 190 191 fromRef := helpers.DeriveBomRef(fromPkg) 192 if !slices.Contains(*dep.Dependencies, fromRef) { 193 *dep.Dependencies = append(*dep.Dependencies, fromRef) 194 } 195 } 196 197 result := make([]cyclonedx.Dependency, 0, len(dependencies)) 198 for _, dep := range dependencies { 199 slices.Sort(*dep.Dependencies) 200 result = append(result, *dep) 201 } 202 203 slices.SortFunc(result, func(a, b cyclonedx.Dependency) int { 204 return strings.Compare(a.Ref, b.Ref) 205 }) 206 207 return result 208 } 209 210 func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property { 211 metadata, ok := srcMetadata.Metadata.(source.ImageMetadata) 212 if ok { 213 props := helpers.EncodeProperties(metadata.Labels, "syft:image:labels") 214 return &props 215 } 216 return nil 217 } 218 219 func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Component { 220 name := srcMetadata.Name 221 version := srcMetadata.Version 222 switch metadata := srcMetadata.Metadata.(type) { 223 case source.ImageMetadata: 224 if name == "" { 225 name = metadata.UserInput 226 } 227 if version == "" { 228 version = metadata.ManifestDigest 229 } 230 bomRef, err := artifact.IDByHash(metadata.ID) 231 if err != nil { 232 log.Warnf("unable to get fingerprint of source image metadata=%s: %+v", metadata.ID, err) 233 } 234 return &cyclonedx.Component{ 235 BOMRef: string(bomRef), 236 Type: cyclonedx.ComponentTypeContainer, 237 Name: name, 238 Version: version, 239 } 240 case source.DirectoryMetadata: 241 if name == "" { 242 name = metadata.Path 243 } 244 bomRef, err := artifact.IDByHash(metadata.Path) 245 if err != nil { 246 log.Warnf("unable to get fingerprint of source directory metadata path=%s: %+v", metadata.Path, err) 247 } 248 return &cyclonedx.Component{ 249 BOMRef: string(bomRef), 250 // TODO: this is lossy... we can't know if this is a file or a directory 251 Type: cyclonedx.ComponentTypeFile, 252 Name: name, 253 Version: version, 254 } 255 case source.FileMetadata: 256 if name == "" { 257 name = metadata.Path 258 } 259 bomRef, err := artifact.IDByHash(metadata.Path) 260 if err != nil { 261 log.Warnf("unable to get fingerprint of source file metadata path=%s: %+v", metadata.Path, err) 262 } 263 return &cyclonedx.Component{ 264 BOMRef: string(bomRef), 265 // TODO: this is lossy... we can't know if this is a file or a directory 266 Type: cyclonedx.ComponentTypeFile, 267 Name: name, 268 Version: version, 269 } 270 } 271 272 return nil 273 }