github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/common/cyclonedxhelpers/format.go (about) 1 package cyclonedxhelpers 2 3 import ( 4 "slices" 5 "strings" 6 "time" 7 8 "github.com/CycloneDX/cyclonedx-go" 9 "github.com/google/uuid" 10 11 "github.com/anchore/syft/syft/artifact" 12 "github.com/anchore/syft/syft/cpe" 13 "github.com/anchore/syft/syft/linux" 14 "github.com/anchore/syft/syft/pkg" 15 "github.com/anchore/syft/syft/sbom" 16 "github.com/anchore/syft/syft/source" 17 "github.com/lineaje-labs/syft/internal/log" 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(s.Relationships) 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 Properties: toBomProperties(srcMetadata), 125 Component: toBomDescriptorComponent(srcMetadata), 126 } 127 } 128 129 // used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency. 130 // NOTE: CycloneDX provides the ability to describe components and their dependency on other components. 131 // The dependency graph is capable of representing both direct and transitive relationships. 132 // If a relationship is either direct or transitive it can be included in this function. 133 // An example of a relationship to not include would be: OwnershipByFileOverlapRelationship. 134 func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { 135 switch ty { 136 case artifact.DependencyOfRelationship: 137 return true 138 default: 139 return false 140 } 141 } 142 143 func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependency { 144 dependencies := map[string]*cyclonedx.Dependency{} 145 for _, r := range relationships { 146 exists := isExpressiblePackageRelationship(r.Type) 147 if !exists { 148 log.Debugf("unable to convert relationship type to CycloneDX JSON, dropping: %#v", r) 149 continue 150 } 151 152 // we only capture package-to-package relationships for now 153 fromPkg, ok := r.From.(pkg.Package) 154 if !ok { 155 log.Tracef("unable to convert relationship fromPkg to CycloneDX JSON, dropping: %#v", r) 156 continue 157 } 158 159 toPkg, ok := r.To.(pkg.Package) 160 if !ok { 161 log.Tracef("unable to convert relationship toPkg to CycloneDX JSON, dropping: %#v", r) 162 continue 163 } 164 165 toRef := deriveBomRef(toPkg) 166 dep := dependencies[toRef] 167 if dep == nil { 168 dep = &cyclonedx.Dependency{ 169 Ref: toRef, 170 Dependencies: &[]string{}, 171 } 172 dependencies[toRef] = dep 173 } 174 175 fromRef := deriveBomRef(fromPkg) 176 if !slices.Contains(*dep.Dependencies, fromRef) { 177 *dep.Dependencies = append(*dep.Dependencies, fromRef) 178 } 179 } 180 181 result := make([]cyclonedx.Dependency, 0, len(dependencies)) 182 for _, dep := range dependencies { 183 slices.Sort(*dep.Dependencies) 184 result = append(result, *dep) 185 } 186 187 slices.SortFunc(result, func(a, b cyclonedx.Dependency) int { 188 return strings.Compare(a.Ref, b.Ref) 189 }) 190 191 return result 192 } 193 194 func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property { 195 metadata, ok := srcMetadata.Metadata.(source.StereoscopeImageSourceMetadata) 196 if ok { 197 props := encodeProperties(metadata.Labels, "syft:image:labels") 198 return &props 199 } 200 return nil 201 } 202 203 func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Component { 204 name := srcMetadata.Name 205 version := srcMetadata.Version 206 switch metadata := srcMetadata.Metadata.(type) { 207 case source.StereoscopeImageSourceMetadata: 208 if name == "" { 209 name = metadata.UserInput 210 } 211 if version == "" { 212 version = metadata.ManifestDigest 213 } 214 bomRef, err := artifact.IDByHash(metadata.ID) 215 if err != nil { 216 log.Warnf("unable to get fingerprint of source image metadata=%s: %+v", metadata.ID, err) 217 } 218 return &cyclonedx.Component{ 219 BOMRef: string(bomRef), 220 Type: cyclonedx.ComponentTypeContainer, 221 Name: name, 222 Version: version, 223 } 224 case source.DirectorySourceMetadata: 225 if name == "" { 226 name = metadata.Path 227 } 228 bomRef, err := artifact.IDByHash(metadata.Path) 229 if err != nil { 230 log.Warnf("unable to get fingerprint of source directory metadata path=%s: %+v", metadata.Path, err) 231 } 232 return &cyclonedx.Component{ 233 BOMRef: string(bomRef), 234 // TODO: this is lossy... we can't know if this is a file or a directory 235 Type: cyclonedx.ComponentTypeFile, 236 Name: name, 237 Version: version, 238 } 239 case source.FileSourceMetadata: 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 file 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 } 255 256 return nil 257 }