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