syft/syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go
gab 656a4d46d7
Vcpkg Cataloger (#4081)
* Vcpkg cataloger for vcpkg "Manifest Mode"

Find and parse vcpkg-lock.json to get HEAD commit hash

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* just use local vcpkg git repo if it exists, clone it if it doesn't

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* Config opt for git remote clones for vcpkg and README update

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>

* Look in vcpkg cache git repo for custom git repos

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add triplet to metadata and support overlay-ports from config file

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* Add PURL to packages (not sure if this is correct)

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* flatten structs in pkg module and move vcpkg structs to resolver

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* account for overriden versions in toplevel manifest

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* generate json schema for vcpkg metadata

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>

* test for basic vcpkg project

dependencies for vcpkg registry to be pulled in

add tree hashes and use correct git hash in builtin-baseline for helloworld test

vcpkg-registry for testing that uses object hashes from syft repo

fix broken tests

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>

* formatting

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>

* fix static-analysis violations

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix integration test failure

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>

* remove uneeded files from vcpkg test fixture and use custom registry

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>

* change vcpkg registry to anchore one

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* purl spec based on open PR

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* generate-json-schema

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rebased and generate json schema 16.0.40

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>

* address low hanging fruit

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* handle additional comments

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* migrate to testdata

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* improve docs and testing

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix static analysis

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove license from pkg metadata

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix capabilities claim

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Gabriel Rau <gabetrau@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-07-01 09:11:33 -04:00

183 lines
5.8 KiB
Go

package cpp
import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"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/cpp/internal/vcpkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// vcpkg manifests are small JSON files; cap reads to guard against pathological inputs
const maxVcpkgManifestSize = 5 * 1024 * 1024
type vcpkgCataloger struct {
allowGitClone bool
}
func newVcpkgCataloger(allowGitClone bool) *vcpkgCataloger {
return &vcpkgCataloger{
allowGitClone: allowGitClone,
}
}
// parser is for vcpkg in "Manifest" mode. This is opposed to "Classic" mode which or is more akin to a system package manager. (https://learn.microsoft.com/en-us/vcpkg/concepts/classic-mode)
func (v *vcpkgCataloger) parseVcpkgManifest(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
conf, err := findVcpkgConfig(resolver)
if err != nil {
return nil, nil, fmt.Errorf("something went wrong parsing vcpkg-configuration.json file: %w", err)
}
var toplevelVcpkg vcpkg.Vcpkg
dec := json.NewDecoder(reader)
err = dec.Decode(&toplevelVcpkg)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse vcpkg.json file: %w", err)
}
var vcpkgs []vcpkg.Vcpkg
vcpkgs = append(vcpkgs, toplevelVcpkg)
overlayVcpkgs, err := findOverlayManifests(resolver, conf.OverlayPorts)
if err != nil {
return nil, nil, fmt.Errorf("could not get overlay port manifests: %w", err)
}
vcpkgs = append(vcpkgs, overlayVcpkgs...)
var pkgs []pkg.Package
var relationships []artifact.Relationship
for _, parentVcpkg := range vcpkgs {
pv := parentVcpkg
// the top-level project manifest has no registry (it is the thing being scanned, not a resolved dep)
parentMan := &vcpkg.ResolvedManifest{Vcpkg: &pv, Registry: nil}
pPkg := newVcpkgPackage(ctx, parentMan, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
pkgs = append(
pkgs,
pPkg)
r := vcpkg.NewResolver(
ctx,
conf,
v.allowGitClone,
)
for _, dep := range parentVcpkg.Dependencies {
cMans, fetchErr := r.FindManifests(dep, true, toplevelVcpkg.BuiltinBaseline, toplevelVcpkg.Overrides, parentMan)
if fetchErr != nil {
// best-effort: a single unresolvable dependency shouldn't discard packages already found
log.Debugf("vcpkg: unable to resolve dependency in %q: %v", parentVcpkg.Name, fetchErr)
continue
}
pkgs, relationships = appendPkgsAndRelationships(ctx, cMans, overlayVcpkgs, reader, relationships, pkgs)
}
}
pkg.Sort(pkgs)
return pkgs, relationships, nil
}
func appendPkgsAndRelationships(ctx context.Context, cMans []vcpkg.ManifestNode, overlayVcpkgs []vcpkg.Vcpkg, reader file.LocationReadCloser, relationships []artifact.Relationship, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) {
p := pkgs
r := relationships
for _, c := range cMans {
if c.Child != nil && !hasBeenOverlayed(c.Child.Vcpkg.Name, overlayVcpkgs) {
cPkg := newVcpkgPackage(ctx, c.Child, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
if c.Parent != nil {
pPkg := newVcpkgPackage(ctx, c.Parent, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
pPkg.FoundBy = "vcpkg-manifest-cataloger"
cPkg.FoundBy = "vcpkg-manifest-cataloger"
rship := artifact.Relationship{
// From is the dependency, To is the dependent (syft convention)
From: cPkg,
To: pPkg,
Type: artifact.DependencyOfRelationship,
}
r = append(
r,
rship)
}
p = append(
p,
cPkg)
}
}
return p, r
}
// These are to be used in place of dependencies with the same name
func findOverlayManifests(resolver file.Resolver, overlayPorts []string) ([]vcpkg.Vcpkg, error) {
var manifests []vcpkg.Vcpkg
for _, op := range overlayPorts {
// overlay port path is relative to location of vcpkg-configuration.json file
locs, err := resolver.FilesByGlob(op + "/**/vcpkg.json")
if err != nil {
return nil, err
}
for _, loc := range locs {
man, err := findManAtLoc(loc, resolver)
if err != nil {
return nil, err
}
manifests = append(manifests, *man)
}
}
return manifests, nil
}
func findManAtLoc(loc file.Location, resolver file.Resolver) (*vcpkg.Vcpkg, error) {
manCont, err := resolver.FileContentsByLocation(loc)
if err != nil {
return nil, err
}
defer internal.CloseAndLogError(manCont, loc.RealPath)
manBytes, err := io.ReadAll(io.LimitReader(manCont, maxVcpkgManifestSize))
if err != nil {
return nil, err
}
var man vcpkg.Vcpkg
err = json.Unmarshal(manBytes, &man)
if err != nil {
return nil, err
}
return &man, nil
}
// check to see if a package is not going to get pulled in because it's apart of the overlay-ports in vcpkg-configuration.json file
func hasBeenOverlayed(pkgName string, overlayMans []vcpkg.Vcpkg) bool {
for _, om := range overlayMans {
if om.Name == pkgName {
return true
}
}
return false
}
// needed to know what vcpkg registries to use for what packages when looking for manifest files
func findVcpkgConfig(resolver file.Resolver) (*vcpkg.Config, error) {
locs, err := resolver.FilesByGlob("**/vcpkg-configuration.json")
if err != nil {
return nil, err
}
if len(locs) != 0 {
cfgCont, err := resolver.FileContentsByLocation(locs[0])
if err != nil {
return nil, err
}
defer internal.CloseAndLogError(cfgCont, locs[0].RealPath)
cfgBytes, err := io.ReadAll(io.LimitReader(cfgCont, maxVcpkgManifestSize))
if err != nil {
return nil, err
}
var vcpkgConf vcpkg.Config
err = json.Unmarshal(cfgBytes, &vcpkgConf)
if err != nil {
return nil, err
}
return &vcpkgConf, err
}
return &vcpkg.Config{}, nil
}