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