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