Christopher Angelo Phillips 4a60c41f38
feat: 4184 gguf parser (ai artifact cataloger) part 1 (#4279)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-11-13 17:43:48 -05:00

284 lines
6.7 KiB
Go

package helpers
import (
"fmt"
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
if bom == nil {
return nil, fmt.Errorf("no content defined in CycloneDX BOM")
}
s := &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(),
LinuxDistribution: linuxReleaseFromComponents(*bom.Components),
},
Source: extractComponents(bom.Metadata),
Descriptor: extractDescriptor(bom.Metadata),
}
idMap := make(map[string]interface{})
if err := collectBomPackages(bom, s, idMap); err != nil {
return nil, err
}
collectRelationships(bom, s, idMap)
return s, nil
}
func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error {
componentsPresent := false
if bom.Components != nil {
for i := range *bom.Components {
collectPackages(&(*bom.Components)[i], s, idMap)
}
componentsPresent = true
}
if bom.Metadata != nil && bom.Metadata.Component != nil {
collectPackages(bom.Metadata.Component, s, idMap)
componentsPresent = true
}
if !componentsPresent {
return fmt.Errorf("no components are defined in the CycloneDX BOM")
}
return nil
}
func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) {
switch component.Type {
case cyclonedx.ComponentTypeOS:
case cyclonedx.ComponentTypeContainer:
case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary, cyclonedx.ComponentTypeMachineLearningModel:
p := decodeComponent(component)
idMap[component.BOMRef] = p
if component.BOMRef != "" {
// always prefer the IDs from the SBOM over derived IDs
p.OverrideID(artifact.ID(component.BOMRef))
} else {
p.SetID()
}
syftID := p.ID()
if syftID != "" {
idMap[string(syftID)] = p
}
s.Artifacts.Packages.Add(*p)
}
if component.Components != nil {
for i := range *component.Components {
collectPackages(&(*component.Components)[i], s, idMap)
}
}
}
func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release {
for i := range components {
component := &components[i]
if component.Type == cyclonedx.ComponentTypeOS {
return linuxReleaseFromOSComponent(component)
}
}
return nil
}
func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release {
if component == nil {
return nil
}
var name string
var version string
if component.SWID != nil {
name = component.SWID.Name
version = component.SWID.Version
}
if name == "" {
name = component.Name
}
if name == "" {
name = getPropertyValue(component, "id")
}
if version == "" {
version = component.Version
}
if version == "" {
version = getPropertyValue(component, "versionID")
}
rel := &linux.Release{
CPEName: component.CPE,
PrettyName: name,
Name: name,
ID: name,
IDLike: []string{name},
Version: version,
VersionID: version,
}
if component.ExternalReferences != nil {
for _, ref := range *component.ExternalReferences {
switch ref.Type {
case cyclonedx.ERTypeIssueTracker:
rel.BugReportURL = ref.URL
case cyclonedx.ERTypeWebsite:
rel.HomeURL = ref.URL
case cyclonedx.ERTypeOther:
switch ref.Comment {
case "support":
rel.SupportURL = ref.URL
case "privacyPolicy":
rel.PrivacyPolicyURL = ref.URL
}
}
}
}
if component.Properties != nil {
values := map[string]string{}
for _, p := range *component.Properties {
values[p.Name] = p.Value
}
DecodeInto(&rel, values, "syft:distro", CycloneDXFields)
}
return rel
}
func getPropertyValue(component *cyclonedx.Component, name string) string {
if component.Properties != nil {
for _, p := range *component.Properties {
if p.Name == name {
return p.Value
}
}
}
return ""
}
func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) {
if bom.Dependencies == nil {
return
}
for _, d := range *bom.Dependencies {
if d.Dependencies == nil {
continue
}
toPtr, toExists := idMap[d.Ref]
if !toExists {
continue
}
to, ok := PtrToStruct(toPtr).(artifact.Identifiable)
if !ok {
continue
}
for _, t := range *d.Dependencies {
fromPtr, fromExists := idMap[t]
if !fromExists {
continue
}
from, ok := PtrToStruct(fromPtr).(artifact.Identifiable)
if !ok {
continue
}
s.Relationships = append(s.Relationships, artifact.Relationship{
From: from,
To: to,
// match assumptions in encoding, that this is the only type of relationship captured:
Type: artifact.DependencyOfRelationship,
})
}
}
}
func extractComponents(meta *cyclonedx.Metadata) source.Description {
if meta == nil || meta.Component == nil {
return source.Description{}
}
c := meta.Component
supplier := ""
// First check component-level supplier
if c.Supplier != nil && c.Supplier.Name != "" {
supplier = c.Supplier.Name
}
// Fall back to metadata-level supplier if component supplier is not set
if supplier == "" && meta.Supplier != nil && meta.Supplier.Name != "" {
supplier = meta.Supplier.Name
}
switch c.Type {
case cyclonedx.ComponentTypeContainer:
var labels map[string]string
if meta.Properties != nil {
labels = decodeProperties(*meta.Properties, "syft:image:labels:")
}
return source.Description{
ID: "",
Supplier: supplier,
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
Metadata: source.ImageMetadata{
UserInput: c.Name,
ID: c.BOMRef,
ManifestDigest: c.Version,
Labels: labels,
},
}
case cyclonedx.ComponentTypeFile:
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
// TODO: this is lossy... we can't know if this is a file or a directory
return source.Description{
ID: "",
Supplier: supplier,
Metadata: source.FileMetadata{Path: c.Name},
}
}
return source.Description{}
}
// if there is more than one tool in meta.Tools' list the last item will be used
// as descriptor. If there is a way to know which tool to use here please fix it.
func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) {
if meta == nil || meta.Tools == nil {
return
}
// handle 1.5 component element
if meta.Tools.Components != nil {
for _, t := range *meta.Tools.Components {
desc.Name = t.Name
desc.Version = t.Version
return
}
}
// handle pre-1.5 tool element
if meta.Tools.Tools != nil {
for _, t := range *meta.Tools.Tools {
desc.Name = t.Name
desc.Version = t.Version
return
}
}
return
}