syft/syft/pkg/cataloger/cpp/parse_conanlock.go
Musang Kim 1866e25f9a
chore: fix conan parser typos (#3802)
Signed-off-by: Musang <s.musang.kim@gmail.com>
2025-04-15 10:51:02 -04:00

199 lines
5.4 KiB
Go

package cpp
import (
"context"
"encoding/json"
"strings"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
var _ generic.Parser = parseConanLock
type conanLock struct {
GraphLock struct {
Nodes map[string]struct {
Ref string `json:"ref"`
PackageID string `json:"package_id"`
Context string `json:"context"`
Prev string `json:"prev"`
Requires []string `json:"requires"`
PythonRequires string `json:"py_requires"`
Options string `json:"options"`
Path string `json:"path"`
} `json:"nodes"`
} `json:"graph_lock"`
Version string `json:"version"`
ProfileHost string `json:"profile_host"`
ProfileBuild string `json:"profile_build,omitempty"`
// conan v0.5+ lockfiles use "requires", "build_requires" and "python_requires"
Requires []string `json:"requires,omitempty"`
BuildRequires []string `json:"build_requires,omitempty"`
PythonRequires []string `json:"python_requires,omitempty"`
}
// parseConanLock is a parser function for conan.lock (v1 and V2) contents, returning all packages discovered.
func parseConanLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var cl conanLock
if err := json.NewDecoder(reader).Decode(&cl); err != nil {
return nil, nil, err
}
// requires is a list of package indices. We first need to fill it, and then we can resolve the package
// in a second iteration
var indexToPkgMap = map[string]pkg.Package{}
v2Pkgs := handleConanLockV2(cl, reader, indexToPkgMap)
// we do not want to store the index list requires in the conan metadata, because it is not useful to have it in
// the SBOM. Instead, we will store it in a map and then use it to build the relationships
// maps pkg.ID to a list of indices
var parsedPkgRequires = map[artifact.ID][]string{}
v1Pkgs := handleConanLockV1(cl, reader, parsedPkgRequires, indexToPkgMap)
var relationships []artifact.Relationship
var pkgs []pkg.Package
pkgs = append(pkgs, v1Pkgs...)
pkgs = append(pkgs, v2Pkgs...)
for _, p := range pkgs {
requires := parsedPkgRequires[p.ID()]
for _, r := range requires {
// this is a pkg that package "p" depends on... make a relationship
relationships = append(relationships, artifact.Relationship{
From: indexToPkgMap[r],
To: p,
Type: artifact.DependencyOfRelationship,
})
}
}
return pkgs, relationships, unknown.IfEmptyf(pkgs, "unable to determine packages")
}
// handleConanLockV1 handles the parsing of conan lock v1 files (aka v0.4)
func handleConanLockV1(cl conanLock, reader file.LocationReadCloser, parsedPkgRequires map[artifact.ID][]string, indexToPkgMap map[string]pkg.Package) []pkg.Package {
var pkgs []pkg.Package
for idx, node := range cl.GraphLock.Nodes {
metadata := pkg.ConanV1LockEntry{
Ref: node.Ref,
Options: parseOptions(node.Options),
Path: node.Path,
Context: node.Context,
PackageID: node.PackageID,
Prev: node.Prev,
}
p := newConanlockPackage(
metadata,
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if p != nil {
pk := *p
pkgs = append(pkgs, pk)
parsedPkgRequires[pk.ID()] = node.Requires
indexToPkgMap[idx] = pk
}
}
return pkgs
}
// handleConanLockV2 handles the parsing of conan lock v2 files (aka v0.5)
func handleConanLockV2(cl conanLock, reader file.LocationReadCloser, indexToPkgMap map[string]pkg.Package) []pkg.Package {
var pkgs []pkg.Package
for _, ref := range cl.Requires {
reference, name := parseConanV2Reference(ref)
if name == "" {
continue
}
p := newConanReferencePackage(
reference,
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if p != nil {
pk := *p
pkgs = append(pkgs, pk)
indexToPkgMap[name] = pk
}
}
return pkgs
}
func parseOptions(options string) []pkg.KeyValue {
o := make([]pkg.KeyValue, 0)
if len(options) == 0 {
return nil
}
kvps := strings.Split(options, "\n")
for _, kvp := range kvps {
kv := strings.Split(kvp, "=")
if len(kv) == 2 {
o = append(o, pkg.KeyValue{
Key: kv[0],
Value: kv[1],
})
}
}
return o
}
func parseConanV2Reference(ref string) (pkg.ConanV2LockEntry, string) {
// very flexible format name/version[@username[/channel]][#rrev][:pkgid[#prev]][%timestamp]
reference := pkg.ConanV2LockEntry{Ref: ref}
parts := strings.SplitN(ref, "%", 2)
if len(parts) == 2 {
ref = parts[0]
reference.TimeStamp = parts[1]
}
parts = strings.SplitN(ref, ":", 2)
if len(parts) == 2 {
ref = parts[0]
parts = strings.SplitN(parts[1], "#", 2)
reference.PackageID = parts[0]
if len(parts) == 2 {
reference.PackageRevision = parts[1]
}
}
parts = strings.SplitN(ref, "#", 2)
if len(parts) == 2 {
ref = parts[0]
reference.RecipeRevision = parts[1]
}
parts = strings.SplitN(ref, "@", 2)
if len(parts) == 2 {
ref = parts[0]
UsernameChannel := parts[1]
parts = strings.SplitN(UsernameChannel, "/", 2)
reference.Username = parts[0]
if len(parts) == 2 {
reference.Channel = parts[1]
}
}
parts = strings.SplitN(ref, "/", 2)
var name string
if len(parts) == 2 {
name = parts[0]
} else {
// consumer conanfile.txt or conanfile.py might not have a name
name = ""
}
return reference, name
}