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>
This commit is contained in:
gab 2026-07-01 08:11:33 -05:00 committed by GitHub
parent 148fe572bc
commit 656a4d46d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 6042 additions and 11 deletions

View File

@ -20,6 +20,7 @@ import (
"github.com/anchore/syft/syft/file/cataloger/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
@ -44,6 +45,7 @@ type Catalog struct {
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
// ecosystem-specific cataloger configuration
Cpp cppConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
@ -80,6 +82,7 @@ func DefaultCatalog() Catalog {
JavaScript: defaultJavaScriptConfig(),
Python: defaultPythonConfig(),
Nix: defaultNixConfig(),
Cpp: defaultCppConfig(),
Dotnet: defaultDotnetConfig(),
Golang: defaultGolangConfig(),
Java: defaultJavaConfig(),
@ -172,6 +175,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
}
return pkgcataloging.Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
Cpp: cpp.DefaultCatalogerConfig().
WithVcpkgAllowGitClone(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Cpp, task.Vcpkg), cfg.Cpp.VcpkgAllowGitClone)),
Dotnet: dotnet.DefaultCatalogerConfig().
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
@ -301,6 +306,7 @@ var publicisedEnrichmentOptions = []string{
task.Java,
task.JavaScript,
task.Python,
task.Vcpkg,
}
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {

View File

@ -0,0 +1,30 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
)
type cppConfig struct {
VcpkgAllowGitClone *bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"`
}
func defaultCppConfig() cppConfig {
// reference the cataloger default so capability generation can associate this config with the cpp
// ecosystem (it discovers ecosystem configs by their cataloger import). the value itself stays nil:
// nil defaults to false (no network), and leaving it unset lets --enrich opt in. cloning requires a
// network connection, which must be opt-in.
_ = cpp.DefaultCatalogerConfig()
return cppConfig{
VcpkgAllowGitClone: nil,
}
}
var _ interface {
clio.FieldDescriber
} = (*cppConfig)(nil)
func (o *cppConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.VcpkgAllowGitClone, `enables Syft to use clone remote repositories for vcpkg custom git registries.
(also useful if the builtin vcpkg registry is not cloned locally)`)
}

View File

@ -89,6 +89,7 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
var cases []testCase
@ -165,6 +166,7 @@ func TestPkgCoverageDirectory(t *testing.T) {
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
// for directory scans we should not expect to see any of the following package types

View File

@ -2,6 +2,8 @@
# This file is partially auto-generated. Run 'go generate ./internal/capabilities' to regenerate.
application: # AUTO-GENERATED - application-level config keys
- key: cpp.vcpkg-allow-git-clone
description: enables Syft to use clone remote repositories for vcpkg custom git registries. (also useful if the builtin vcpkg registry is not cloned locally)
- key: dotnet.dep-packages-must-claim-dll
description: only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL.
- key: dotnet.dep-packages-must-have-dll

View File

@ -3,7 +3,7 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.1.7"
JSONSchemaVersion = "16.1.8"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@ -14,5 +14,5 @@ const (
// 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
)

View File

@ -72,6 +72,7 @@ func AllTypes() []any {
pkg.SwiftPackageManagerResolvedEntry{},
pkg.SwiplPackEntry{},
pkg.TerraformLockProviderEntry{},
pkg.VcpkgManifest{},
pkg.WordpressPluginEntry{},
pkg.YarnLockEntry{},
}

View File

@ -130,6 +130,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"),
jsonNames(pkg.CondaMetaPackage{}, "conda-metadata-entry", "CondaPackageMetadata"),
jsonNames(pkg.GGUFFileHeader{}, "gguf-file-header"),
jsonNames(pkg.VcpkgManifest{}, "vcpkg-manifest"),
)
func expandLegacyNameVariants(names ...string) []string {

View File

@ -57,6 +57,10 @@ const (
// Python ecosystem labels
Python = "python"
// C/C++ ecosystem labels
Cpp = "cpp"
Vcpkg = "vcpkg"
)
//nolint:funlen
@ -83,6 +87,11 @@ func DefaultPackageTaskFactories() Factories {
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
return cpp.NewVcpkgManifestCataloger(cfg.PackagesConfig.Cpp)
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Cpp, Vcpkg),
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dart.NewPubspecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.1.7/document",
"$id": "anchore.io/schema/syft/json/16.1.8/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -2927,6 +2927,9 @@
{
"$ref": "#/$defs/TerraformLockProviderEntry"
},
{
"$ref": "#/$defs/VcpkgManifest"
},
{
"$ref": "#/$defs/WordpressPluginEntry"
}
@ -4385,6 +4388,86 @@
],
"description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)."
},
"VcpkgManifest": {
"properties": {
"description": {
"items": {
"type": "string"
},
"type": "array"
},
"documentation": {
"type": "string"
},
"full-version": {
"type": "string"
},
"version": {
"type": "string"
},
"port-version": {
"type": "integer"
},
"maintainers": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"supports": {
"type": "string"
},
"registry": {
"$ref": "#/$defs/VcpkgRegistryEntry",
"description": "to show where it came from"
},
"triplet": {
"type": "string",
"description": "found by looking at build folder to find target. ex. \"x64-linux\""
}
},
"type": "object",
"required": [
"full-version",
"version",
"port-version",
"name"
],
"description": "used for metadata."
},
"VcpkgRegistryEntry": {
"properties": {
"baseline": {
"type": "string"
},
"kind": {
"type": "string"
},
"packages": {
"items": {
"type": "string"
},
"type": "array"
},
"path": {
"type": "string"
},
"reference": {
"type": "string"
},
"repository": {
"type": "string"
}
},
"type": "object",
"required": [
"kind"
],
"description": "Matches definition of Vcpkg \"Registry\"."
},
"WordpressPluginEntry": {
"properties": {
"pluginInstallDirectory": {

View File

@ -2,6 +2,7 @@ package pkgcataloging
import (
"github.com/anchore/syft/syft/pkg/cataloger/binary"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
@ -13,6 +14,7 @@ import (
type Config struct {
Binary binary.ClassifierCatalogerConfig `yaml:"binary" json:"binary" mapstructure:"binary"`
Cpp cpp.CatalogerConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnet.CatalogerConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golang.CatalogerConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
JavaArchive java.ArchiveCatalogerConfig `yaml:"java-archive" json:"java-archive" mapstructure:"java-archive"`
@ -25,6 +27,7 @@ type Config struct {
func DefaultConfig() Config {
return Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
Cpp: cpp.DefaultCatalogerConfig(),
Dotnet: dotnet.DefaultCatalogerConfig(),
Golang: golang.DefaultCatalogerConfig(),
JavaArchive: java.DefaultArchiveCatalogerConfig(),
@ -44,6 +47,11 @@ func (c Config) WithBinaryConfig(cfg binary.ClassifierCatalogerConfig) Config {
return c
}
func (c Config) WithCppConfig(cfg cpp.CatalogerConfig) Config {
c.Cpp = cfg
return c
}
func (c Config) WithDotnetConfig(cfg dotnet.CatalogerConfig) Config {
c.Dotnet = cfg
return c

View File

@ -138,6 +138,11 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
case pkg.SwiplPackEntry:
author = formatPersonOrOrg(metadata.Author, metadata.AuthorEmail)
case pkg.VcpkgManifest:
if len(metadata.Maintainers) > 0 {
author = metadata.Maintainers[0]
}
}
if typ == "" && author != "" {

View File

@ -60,6 +60,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.GGUFFileHeader{},
pkg.DenoLockEntry{},
pkg.DenoRemoteLockEntry{},
pkg.VcpkgManifest{},
)
tests := []struct {
name string

View File

@ -86,6 +86,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from Terraform dependency lock file"
case pkg.ModelPkg:
answer = "acquired package info from AI artifact (e.g. GGUF File)"
case pkg.VcpkgPkg:
answer = "acquired package info from vcpkg manifest file"
default:
answer = "acquired package info from the following paths"
}

View File

@ -199,6 +199,14 @@ func Test_SourceInfo(t *testing.T) {
"from conda metadata",
},
},
{
input: pkg.Package{
Type: pkg.VcpkgPkg,
},
expected: []string{
"from vcpkg manifest",
},
},
{
input: pkg.Package{
Type: pkg.PortagePkg,

View File

@ -1,5 +1,11 @@
# Cataloger capabilities. See ../README.md for documentation.
configs: # AUTO-GENERATED - config structs and their fields
cpp.CatalogerConfig:
fields:
- key: VcpkgAllowGitClone
description: VcpkgAllowGitClone enables cloning remote git registries to resolve vcpkg manifest dependencies (requires network access).
app_key: cpp.vcpkg-allow-git-clone
catalogers:
- ecosystem: c++ # MANUAL
name: conan-cataloger # AUTO-GENERATED
@ -126,3 +132,49 @@ catalogers:
default: false
- name: package_manager.package_integrity_hash
default: false
- ecosystem: c++ # MANUAL
name: vcpkg-manifest-cataloger # AUTO-GENERATED
type: generic # AUTO-GENERATED
source: # AUTO-GENERATED
file: syft/pkg/cataloger/cpp/cataloger.go
function: NewVcpkgManifestCataloger
config: cpp.CatalogerConfig # AUTO-GENERATED
selectors: # AUTO-GENERATED
- cpp
- declared
- directory
- language
- package
- vcpkg
parsers: # AUTO-GENERATED structure
- function: parseVcpkgManifest
detector: # AUTO-GENERATED
method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED
- '**/vcpkg.json'
metadata_types: # AUTO-GENERATED
- pkg.VcpkgManifest
package_types: # AUTO-GENERATED
- vcpkg
purl_types: # AUTO-GENERATED
- vcpkg
json_schema_types: # AUTO-GENERATED
- VcpkgManifest
capabilities: # MANUAL - preserved across regeneration
- name: license
default: true
- name: dependency.depth
default:
- direct
- indirect
- name: dependency.edges
default: complete
- name: dependency.kinds
default:
- runtime
- name: package_manager.files.listing
default: false
- name: package_manager.files.digests
default: false
- name: package_manager.package_integrity_hash
default: false

View File

@ -20,3 +20,35 @@ func NewConanInfoCataloger() pkg.Cataloger {
return generic.NewCataloger("conan-info-cataloger").
WithParserByGlobs(parseConaninfo, "**/conaninfo.txt")
}
// vcpkg (the Microsoft C/C++ package manager) has two modes
// (https://learn.microsoft.com/en-us/vcpkg/concepts/classic-mode):
// - classic mode: `vcpkg install <pkg>` populates a central tree at $VCPKG_ROOT/installed/.
// - manifest mode: a vcpkg.json declares dependencies; after a build the resolved tree appears
// under vcpkg_installed/ (a vcpkg/status DB + per-triplet dirs).
//
// what is on disk depends on where in the lifecycle the scan happens:
// - source checkout (the dir-scan case): vcpkg.json (+ vcpkg-configuration.json, overlay ports)
// are present. vcpkg_installed/ is a build artifact and is gitignored, so it is NOT present;
// exact transitive versions live in the registry, not the manifest.
// - built artifact: vcpkg_installed/ holds the actually-installed truth.
//
// NewVcpkgManifestCataloger covers ONLY the manifest (dir/source) case: it reads vcpkg.json and its
// declared dependencies and resolves each dependency's manifest from the vcpkg registry. resolving the
// registry needs a local registry clone or a network clone, which is opt-in via
// CatalogerConfig.VcpkgAllowGitClone (wired to --enrich).
//
// it deliberately does NOT cover installed state: vcpkg_installed/ (vcpkg/status, per-package
// vcpkg.spdx.json, copyright, ABI info), the build triplet (a build-time choice recorded only under
// vcpkg_installed/), and classic-mode central installs.
//
// why there is no installed-state (vcpkg/status) cataloger yet: vcpkg_installed/ tends to be a gitignored
// build artifact, so whether it survives into a scannable target is pattern-dependent. it is dropped by slim
// multi-stage runtime images (the documented best practice copies only the app binary / binary cache
// between stages) and by dev/base images (which bootstrap the tool, not packages); no canonical public
// image ships a populated vcpkg/status by default. it is retained mainly in single-stage builder / CI /
// "fat" app images. so a status-based cataloger has real but not universal payoff and is deferred until
// it is worth the maintenance.
func NewVcpkgManifestCataloger(opts CatalogerConfig) pkg.Cataloger {
return generic.NewCataloger("vcpkg-manifest-cataloger").WithParserByGlobs(newVcpkgCataloger(opts.VcpkgAllowGitClone).parseVcpkgManifest, "**/vcpkg.json")
}

View File

@ -6,7 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestCataloger_Globs(t *testing.T) {
func TestCatalogerConan_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
@ -32,7 +32,7 @@ func TestCataloger_Globs(t *testing.T) {
}
}
func TestCatalogerInfo_Globs(t *testing.T) {
func TestCatalogerConanInfo_Globs(t *testing.T) {
tests := []struct {
name string
fixture string

View File

@ -0,0 +1,19 @@
package cpp
type CatalogerConfig struct {
// VcpkgAllowGitClone enables cloning remote git registries to resolve vcpkg manifest dependencies (requires network access).
// app-config: cpp.vcpkg-allow-git-clone
VcpkgAllowGitClone bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"`
}
func DefaultCatalogerConfig() CatalogerConfig {
return CatalogerConfig{
// syft defaults to not sending requests over the network. You must opt in
VcpkgAllowGitClone: false,
}
}
func (c CatalogerConfig) WithVcpkgAllowGitClone(input bool) CatalogerConfig {
c.VcpkgAllowGitClone = input
return c
}

View File

@ -0,0 +1,697 @@
package vcpkg
import (
"context"
"encoding/json"
"fmt"
"os"
"slices"
"strconv"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/storage"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/anchore/syft/internal/cache"
"github.com/anchore/syft/syft/pkg"
)
// this is the default registry for vcpkg. it is the default "builtin" registry if a builtin one isn't specified
var defaultRegistry = pkg.VcpkgRegistryEntry{
Baseline: "master",
Kind: pkg.Git,
Repository: "https://github.com/microsoft/vcpkg",
}
// represents contents of "vcpkg.json" file. (a.k.a the manifest file)
type Vcpkg struct {
BuiltinBaseline string `json:"builtin-baseline,omitempty"`
DefaultFeatures []any `json:"default-features,omitempty"`
// string or []VcpkgDependency
Dependencies []any `json:"dependencies,omitempty"`
// string or []string
Description any `json:"description,omitempty"`
Documentation string `json:"documentation,omitempty"`
Features map[string]vcpkgFeatureEntry `json:"features,omitempty"`
Homepage string `json:"homepage,omitempty"`
// In SPDX license expression format. see https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json
License string `json:"license,omitempty"`
Maintainers []string `json:"maintainers,omitempty"`
Name string `json:"name,omitempty"`
// Only overrides defined by the top-level project are used
Overrides []vcpkgOverrideEntry `json:"overrides,omitempty"`
PortVersion float64 `json:"port-version,omitempty"`
Supports string `json:"supports,omitempty"`
// at most one of these version fields will be present and represent different versioning strategies
// see https://learn.microsoft.com/en-us/vcpkg/users/versioning#version-schemes for more details
Version string `json:"version,omitempty"`
VersionSemver string `json:"version-semver,omitempty"`
VersionDate string `json:"version-date,omitempty"`
VersionString string `json:"version-string,omitempty"`
}
// Confusingly not the same as Feature Object
// see https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json#feature vs https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json#feature-object
type vcpkgFeatureEntry struct {
Description string `json:"description"`
// string or []VcpkgDependency
Dependencies []any `json:"dependencies,omitempty"`
// "Platform expression"
Supports string `json:"supports,omitempty"`
// SPDX license expression
License string `json:"license,omitempty"`
}
type vcpkgOverrideEntry struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
VersionSemver string `json:"version-semver,omitempty"`
VersionDate string `json:"version-date,omitempty"`
VersionString string `json:"version-string,omitempty"`
PortVersion float64 `json:"port-version,omitempty"`
}
// represents contents of "vcpkg-configuration.json" file
type Config struct {
DefaultRegistry *pkg.VcpkgRegistryEntry `json:"default-registry"`
OverlayPorts []string `json:"overlay-ports,omitempty"`
OverlayTriplets []string `json:"overlay-triplets,omitempty"`
Registries []pkg.VcpkgRegistryEntry `json:"registries,omitempty"`
}
// used to get specific dependency from git history
type vcpkgGitVersionObjectEntry struct {
// Sha1 value used to retrieve specific git tree object from Github. https://docs.github.com/en/rest/git/trees?apiVersion=2022-11-28
GitTree string `json:"git-tree"`
Version string `json:"version,omitempty"`
VersionSemver string `json:"version-semver,omitempty"`
VersionDate string `json:"version-date,omitempty"`
VersionString string `json:"version-string,omitempty"`
PortVersion float64 `json:"port-version"`
}
// represents versions file "<name-of-dependency>.json" found in versions folder
type vcpkgGitVersions struct {
Versions []vcpkgGitVersionObjectEntry `json:"versions"`
}
// Filesystem VersionObject
type vcpkgFsVersionObjectEntry struct {
Path string `json:"path"`
Version string `json:"version,omitempty"`
VersionSemver string `json:"version-semver,omitempty"`
VersionDate string `json:"version-date,omitempty"`
VersionString string `json:"version-string,omitempty"`
PortVersion float64 `json:"port-version"`
}
// represents filesystem versions file "<name-of-dependency>.json" found in versions folder
type vcpkgFsVersions struct {
Versions []vcpkgFsVersionObjectEntry `json:"versions"`
}
// helpful to define relationships between Vcpkgs
// ResolvedManifest is a source manifest (vcpkg.json) together with the registry it was resolved from.
// it carries the raw Vcpkg so the cataloger can build the SBOM metadata and set package-level fields
// (e.g. license) on pkg.Package rather than on the metadata.
type ResolvedManifest struct {
Vcpkg *Vcpkg
Registry *pkg.VcpkgRegistryEntry
}
type ManifestNode struct {
Parent *ResolvedManifest
Child *ResolvedManifest
}
type vcpkgBaselineVersionObjectEntry struct {
Baseline string `json:"baseline"`
PortVersion float64 `json:"port-version"`
}
func (v *Vcpkg) GetFullVersion() string {
popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
if v.PortVersion != 0 {
return popVer + "#" + strconv.Itoa(int(v.PortVersion))
}
return popVer
}
func (v *vcpkgGitVersionObjectEntry) GetFullVersion() string {
popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
if v.PortVersion != 0 {
return popVer + "#" + strconv.Itoa(int(v.PortVersion))
}
return popVer
}
func (v *vcpkgFsVersionObjectEntry) GetFullVersion() string {
popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
if v.PortVersion != 0 {
return popVer + "#" + strconv.Itoa(int(v.PortVersion))
}
return popVer
}
func (v *vcpkgOverrideEntry) GetFullVersion() string {
popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
if v.PortVersion != 0 {
return popVer + "#" + strconv.Itoa(int(v.PortVersion))
}
return popVer
}
func (v *vcpkgGitVersionObjectEntry) GetPopulatedVersion() string {
return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
}
func (v *vcpkgFsVersionObjectEntry) GetPopulatedVersion() string {
return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
}
func (v *Vcpkg) GetPopulatedVersion() string {
return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
}
func getPopulatedVersion(version, versionSemver, versionDate, versionString string) string {
switch {
case version != "":
return version
case versionSemver != "":
return versionSemver
case versionDate != "":
return versionDate
case versionString != "":
return versionString
default:
return ""
}
}
const vcpkgRepo string = "https://github.com/microsoft/vcpkg"
type ID struct {
location string
head string
name string
version string
}
// Resolver is a short-lived utility to resolve vcpkg manifests from multiple sources, including:
// the filesystem, local maven cache directories, remote maven repositories, and the syft cache
type Resolver struct {
ctx context.Context
gitRepos map[string]storage.Storer
allowGitClone bool
cfg *Config
resolved map[ID]*ResolvedManifest
}
// NewResolver constructs a new Resolver with the given vcpkg configuration.
func NewResolver(ctx context.Context, cfg *Config, allowGitClone bool) *Resolver {
return &Resolver{
ctx: ctx,
gitRepos: map[string]storage.Storer{},
allowGitClone: allowGitClone,
cfg: cfg,
resolved: map[ID]*ResolvedManifest{},
}
}
// Get all of the manifest/vcpkg.json files for a vcpkg dependency
func (r *Resolver) FindManifests(dependency any, df bool, builtinBaseline string, overrides []vcpkgOverrideEntry, parent *ResolvedManifest) ([]ManifestNode, error) {
var name string
var fullVersion string
defaultFeatures := df
var features []any
// can be either string or VcpkgDependency
switch d := dependency.(type) {
case string:
name = d
case map[string]any:
if n, ok := d["name"].(string); ok {
name = n
}
if v, ok := d["version>="].(string); ok {
fullVersion = v
}
if v, ok := d["default-features"].(bool); ok {
defaultFeatures = defaultFeatures && v
}
if v, ok := d["features"].([]any); ok {
features = v
}
}
// for when top-level manifest has this dependency version overridden
if over, ok := depVerOverriden(name, overrides); ok {
fullVersion = over.Version
}
reg := r.depRegistry(name, builtinBaseline)
vcpkg, err := r.findManifestFromReg(reg, name, fullVersion)
if err != nil {
return nil, err
}
child := &ResolvedManifest{Vcpkg: vcpkg, Registry: reg}
// need to add dependency to map, even if it doesn't find its manifest, to avoid infinite loops caused by circular depenencies
switch reg.Kind {
case pkg.Git:
r.resolved[ID{reg.Repository, reg.Baseline, name, fullVersion}] = child
case pkg.Builtin:
vcpkgRoot := os.Getenv("VCPKG_ROOT")
r.resolved[ID{vcpkgRoot, reg.Baseline, name, fullVersion}] = child
case pkg.FileSystem:
r.resolved[ID{reg.Path, reg.Baseline, name, fullVersion}] = child
}
vcpkg.appendFtrDepsToDeps(defaultFeatures, features)
manNodes := []ManifestNode{}
manNodes = append(manNodes, ManifestNode{
Parent: parent,
Child: child,
})
if len(vcpkg.Dependencies) != 0 {
for _, dep := range vcpkg.Dependencies {
resolvedManifest, ok := r.depResolved(dep, builtinBaseline)
if ok {
// this is to catch duplicates
manNodes = append(manNodes, ManifestNode{
// child is parent in this case
Parent: child,
Child: resolvedManifest,
})
} else {
childManNodes, err := r.FindManifests(dep, df, builtinBaseline, overrides, child)
if err != nil {
return nil, fmt.Errorf("could not find vcpkg.json file for dependency. %w", err)
}
manNodes = append(manNodes, childManNodes...)
}
}
}
return manNodes, nil
}
func (v *Vcpkg) appendFtrDepsToDeps(df bool, features []any) {
// collect explicitly requested feature names (a feature is either a string or a feature object)
requested := map[string]bool{}
for _, feature := range features {
switch fo := feature.(type) {
case string:
requested[fo] = true
case map[string]any:
// json decodes a feature object into map[string]any, not a struct
if name, ok := fo["name"].(string); ok {
requested[name] = true
}
}
}
// add each feature's deps at most once: when requested, or when it's a default feature and defaults are enabled.
// this must be independent of the requested loop so default features apply even when none are requested.
for name, f := range v.Features {
if requested[name] || (df && isDefaultFeature(name, v.DefaultFeatures)) {
v.Dependencies = append(v.Dependencies, f.Dependencies...)
}
}
}
func depVerOverriden(name string, overrides []vcpkgOverrideEntry) (*vcpkgOverrideEntry, bool) {
for _, over := range overrides {
if over.Name == name {
return &over, true
}
}
return nil, false
}
func (r *Resolver) resolveRepo(repoStr string) (*git.Repository, error) {
if r.gitRepos[repoStr] != nil {
return git.Open(r.gitRepos[repoStr], nil)
} else if r.allowGitClone {
repo, err := git.CloneContext(r.ctx, memory.NewStorage(), nil, &git.CloneOptions{
URL: repoStr,
})
if err != nil {
return nil, err
}
r.gitRepos[repoStr] = repo.Storer
return repo, nil
}
return nil, fmt.Errorf("could not resolve %s. enable vcpkg-allow-git-clone flag to allow cloning of remote git repos", repoStr)
}
func (r *Resolver) getRepo(repoStr string, path *string) (*git.Repository, error) {
vcpkgCachePath := getVcpkgGitCachePath()
// best to use vcpkg cache. Only able to locate if it's in the same directory as syft cache
if r.gitRepos[repoStr] == nil && vcpkgCachePath != "" {
repo, err := git.PlainOpen(vcpkgCachePath)
if err == nil {
r.gitRepos[repoStr] = repo.Storer
return repo, err
}
}
if r.gitRepos[repoStr] == nil && path != nil {
repo, err := git.PlainOpen(*path)
if err == nil {
r.gitRepos[repoStr] = repo.Storer
return repo, err
}
}
return r.resolveRepo(repoStr)
}
// get path of vcpkg cache
func getVcpkgGitCachePath() string {
roots := cache.GetManager().RootDirs()
if len(roots) == 0 {
return ""
}
// vcpkg keeps its git registry cache as a sibling of the syft cache dir
return roots[0] + "/../vcpkg/registries/git"
}
// determines which registry to use by the name of the dependency
func (r *Resolver) depRegistry(name string, builtinBaseline string) *pkg.VcpkgRegistryEntry {
var reg *pkg.VcpkgRegistryEntry
if r.cfg != nil {
for _, res := range r.cfg.Registries {
if slices.Contains(res.Packages, name) {
reg = &res
}
}
if r.cfg.DefaultRegistry != nil && reg == nil {
reg = r.cfg.DefaultRegistry
}
}
if reg == nil {
// copy the package-global so concurrent catalogers don't clobber its baseline
def := defaultRegistry
def.Baseline = builtinBaseline
reg = &def
} else if reg.Kind == pkg.Builtin {
reg.Baseline = builtinBaseline
}
return reg
}
// checks if dependency has been retrieved already this run. Without this check, there were infinite loops from circular dependencies
func (r *Resolver) depResolved(dep any, builtinBaseline string) (*ResolvedManifest, bool) {
var name string
var version string
switch d := dep.(type) {
case string:
name = d
case map[string]any:
if n, ok := d["name"].(string); ok {
name = n
}
if v, ok := d["version>="].(string); ok {
version = v
}
}
reg := r.depRegistry(name, builtinBaseline)
var location string
switch {
case reg.Repository != "":
location = reg.Repository
case reg.Path != "":
location = reg.Path
default:
location = vcpkgRepo
}
resolved, ok := r.resolved[ID{location, reg.Baseline, name, version}]
return resolved, ok
}
func isDefaultFeature(name string, defaultFeatures []any) bool {
for _, df := range defaultFeatures {
switch d := df.(type) {
case string:
if name == d {
return true
}
case map[string]any:
if n, ok := d["name"].(string); ok && name == n {
return true
}
}
}
return false
}
func (r *Resolver) findManifestFromReg(reg *pkg.VcpkgRegistryEntry, name, fullVersion string) (*Vcpkg, error) {
if reg == nil {
return nil, fmt.Errorf("no vcpkg registry found which is required")
}
switch reg.Kind {
case pkg.Git:
var vcpkg *Vcpkg
var err error
if reg.Repository == "" {
return nil, fmt.Errorf("no repo found for vcpkg git registry")
}
if strings.TrimSuffix(reg.Repository, ".git") == vcpkgRepo {
path := os.Getenv("VCPKG_ROOT")
gitRepo, err := r.getRepo(vcpkgRepo, &path)
if err != nil {
return nil, err
}
vcpkg, err = r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion)
if err != nil {
return nil, err
}
} else {
gitRepo, err := r.getRepo(reg.Repository, nil)
if err != nil {
return nil, err
}
vcpkg, err = r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion)
if err != nil {
return nil, err
}
}
return vcpkg, err
case pkg.Builtin:
path := os.Getenv("VCPKG_ROOT")
gitRepo, err := r.getRepo(vcpkgRepo, &path)
if err != nil {
return nil, err
}
vcpkg, err := r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion)
if err != nil {
return nil, err
}
return vcpkg, err
case pkg.FileSystem:
if reg.Path == "" {
return nil, fmt.Errorf("no path found for vcpkg filesystem registry")
}
baseline := reg.Baseline
if baseline == "" {
baseline = "default"
}
vcpkg, err := r.getManifestFromFilesystem(reg.Path, baseline, name, fullVersion)
if err != nil {
return nil, err
}
return vcpkg, err
default:
return nil, fmt.Errorf("vcpkg registry has no kind which is required")
}
}
// locates and gets the manifest file via the go-git
func (r *Resolver) getManifestFromGitRepo(repo *git.Repository, head, name, fullVersion string) (*Vcpkg, error) {
headObj, err := repo.CommitObject(plumbing.NewHash(head))
if err != nil {
return nil, err
}
tree, err := headObj.Tree()
if err != nil {
return nil, err
}
var resultMan Vcpkg
if fullVersion != "" {
verPath := "versions/" + name[0:1] + "-/" + name + ".json"
verFile, err := findFileInTree(verPath, tree)
if err != nil {
return nil, fmt.Errorf("failed to get versions file from vcpkg git tree. %w", err)
}
content, err := verFile.Contents()
if err != nil {
return nil, fmt.Errorf("failed to get contents of versions file from vcpkg git tree. %w", err)
}
var versions vcpkgGitVersions
err = json.Unmarshal([]byte(content), &versions)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal versions file from vcpkg git tree. %w", err)
}
// get tree object sha for the port version
var gitTreeHash string
for _, v := range versions.Versions {
if fullVersion == v.GetFullVersion() {
gitTreeHash = v.GitTree
break
}
}
if gitTreeHash == "" {
return nil, fmt.Errorf("version %q not found in versions file for port %q", fullVersion, name)
}
verTree, err := repo.TreeObject(plumbing.NewHash(gitTreeHash))
if err != nil {
return nil, fmt.Errorf("failed to get tree from hash %v. %w", gitTreeHash, err)
}
manFile, err := verTree.File("vcpkg.json")
if err != nil {
return nil, fmt.Errorf("failed to get vcpkg.json file from git tree. %w", err)
}
manContents, err := manFile.Contents()
if err != nil {
return nil, fmt.Errorf("failed to get contents of vcpkg.json file from git tree. %w", err)
}
err = json.Unmarshal([]byte(manContents), &resultMan)
if err != nil {
return nil, err
}
} else {
portPath := "ports/" + name + "/vcpkg.json"
manFile, err := findFileInTree(portPath, tree)
if err != nil {
return nil, fmt.Errorf("failed to get vcpkg.json file from ports directory in git tree. %w", err)
}
manContents, err := manFile.Contents()
if err != nil {
return nil, fmt.Errorf("failed to get contents of vcpkg.json file from ports directory in git tree. %w", err)
}
err = json.Unmarshal([]byte(manContents), &resultMan)
if err != nil {
return nil, err
}
}
return &resultMan, err
}
func findFileInTree(path string, tree *object.Tree) (*object.File, error) {
verFile, err := tree.File(path)
return verFile, err
}
// locates and gets the manifest file via the filesystem path
func (r *Resolver) getManifestFromFilesystem(path, baseline, name, fullVersion string) (*Vcpkg, error) {
if path == "" {
return &Vcpkg{}, fmt.Errorf("no/empty path specified for vcpkg filesystem registry")
}
var finalVer string
if fullVersion == "" {
baselineBytes, err := os.ReadFile(path + "/versions/baseline.json")
if err != nil {
return &Vcpkg{}, err
}
var baselineGen map[string]any
err = json.Unmarshal(baselineBytes, &baselineGen)
if err != nil {
return &Vcpkg{}, err
}
var baselineMatch map[string]any
for k, v := range baselineGen {
if k == baseline {
baselineMatch, _ = v.(map[string]any)
break
}
}
var baselineVer vcpkgBaselineVersionObjectEntry
for k, v := range baselineMatch {
if k == name {
foundBaseline, ok := v.(map[string]any)
if !ok {
continue
}
bl, _ := foundBaseline["baseline"].(string)
// default for go json package when unmarshalling number is float64
pv, _ := foundBaseline["port-version"].(float64)
baselineVer = vcpkgBaselineVersionObjectEntry{
Baseline: bl,
PortVersion: pv,
}
}
}
if baselineVer.Baseline == "" {
return nil, fmt.Errorf("could not find a baseline version for dependency with name %v", name)
}
if baselineVer.PortVersion > 0 {
finalVer = baselineVer.Baseline + "#" + strconv.Itoa(int(baselineVer.PortVersion))
} else {
finalVer = baselineVer.Baseline
}
} else {
finalVer = fullVersion
}
return getFsManifest(path, name, finalVer)
}
// retrieves manifest file from the filesystem
func getFsManifest(path, name, ver string) (*Vcpkg, error) {
versionBytes, err := os.ReadFile(path + "/versions/" + name[0:1] + "-/" + name + ".json")
if err != nil {
return nil, err
}
var verFileCont vcpkgFsVersions
err = json.Unmarshal(versionBytes, &verFileCont)
if err != nil {
return nil, err
}
for _, v := range verFileCont.Versions {
if v.GetFullVersion() == ver {
// the $ character can be used to reference the root of the registry
manifestPath := strings.ReplaceAll(v.Path, "$", path)
manBytes, err := os.ReadFile(manifestPath + "/vcpkg.json")
if err != nil {
return nil, err
}
var vcpkgManRes Vcpkg
err = json.Unmarshal(manBytes, &vcpkgManRes)
if err != nil {
return nil, err
}
return &vcpkgManRes, nil
}
}
return nil, fmt.Errorf("failed to find vcpkg.json file for dependency name: %v", name)
}
func (v *Vcpkg) BuildManifest(reg *pkg.VcpkgRegistryEntry, triplet string) *pkg.VcpkgManifest {
var desc []string
switch d := v.Description.(type) {
case string:
desc = append(desc, d)
case []any:
// json decodes a description array into []any, not []string
for _, item := range d {
if s, ok := item.(string); ok {
desc = append(desc, s)
}
}
}
// give each manifest its own registry copy: the resolver hands out a shared *VcpkgRegistryEntry
// (e.g. cfg.DefaultRegistry) to every package, and a shared pointer means one package's metadata
// can alias another's
var regCopy *pkg.VcpkgRegistryEntry
if reg != nil {
r := *reg
regCopy = &r
}
return &pkg.VcpkgManifest{
Description: desc,
Documentation: v.Documentation,
FullVersion: v.GetFullVersion(),
Version: v.GetPopulatedVersion(),
PortVersion: int(v.PortVersion),
Maintainers: v.Maintainers,
Name: v.Name,
Supports: v.Supports,
Registry: regCopy,
Triplet: triplet,
}
}

View File

@ -1,11 +1,15 @@
package cpp
import (
"context"
"net/url"
"strconv"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/cpp/internal/vcpkg"
)
type conanRef struct {
@ -92,7 +96,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p
Name: ref.Name,
Version: ref.Version,
Locations: file.NewLocationSet(locations...),
PURL: packageURL(ref),
PURL: packageURLFromConanRef(ref),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: metadata,
@ -103,7 +107,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p
return &p
}
func packageURL(ref *conanRef) string {
func packageURLFromConanRef(ref *conanRef) string {
qualifiers := packageurl.Qualifiers{}
if ref.Channel != "" {
qualifiers = append(qualifiers, packageurl.Qualifier{
@ -120,3 +124,63 @@ func packageURL(ref *conanRef) string {
"",
).ToString()
}
func newVcpkgPackage(ctx context.Context, rm *vcpkg.ResolvedManifest, l file.Location) pkg.Package {
// build the SBOM metadata from the source manifest; license is a package-level field (pkg.Package),
// not metadata, so it is read off the source manifest here rather than carried in pkg.VcpkgManifest
man := rm.Vcpkg.BuildManifest(rm.Registry, "")
p := pkg.Package{
Name: man.Name,
Version: man.FullVersion,
Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, rm.Vcpkg.License, l)),
Locations: file.NewLocationSet(l),
PURL: packageURLFromVcpkgManifest(man),
Language: pkg.CPP,
Type: pkg.VcpkgPkg,
Metadata: man,
}
p.SetID()
return p
}
func packageURLFromVcpkgManifest(v *pkg.VcpkgManifest) string {
qualifiers := packageurl.Qualifiers{}
//
if v.Triplet != "" {
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "triplet",
Value: v.Triplet,
})
}
if v.PortVersion != 0 {
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "port_revision",
Value: strconv.Itoa(v.PortVersion),
})
}
if v.Registry != nil && v.Registry.Repository != "" {
unescRepoURL, err := url.PathUnescape(v.Registry.Repository)
if err == nil {
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "repository_url",
Value: unescRepoURL,
})
}
}
if v.Registry != nil && v.Registry.Baseline != "" {
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "repository_revision",
Value: v.Registry.Baseline,
})
}
return packageurl.NewPackageURL(
// https://github.com/package-url/purl-spec/pull/245
"vcpkg",
"",
v.Name,
v.Version,
qualifiers,
"",
).ToString()
}

View File

@ -0,0 +1,182 @@
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
}

View File

@ -0,0 +1,244 @@
package cpp
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/go-git/go-git/v5"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/internal/cache"
"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/internal/pkgtest"
)
func TestParseVcpkgManifest(t *testing.T) {
fixture := "testdata/vcpkg/helloworld"
// the resolver reads the vcpkg registry from a local git clone rather than cloning over the network
// at test time. the clone is materialized just-in-time on first run (network required once) into a
// gitignored cache dir, then reused offline on later runs.
useLocalVcpkgRegistryCache(t)
fileLocs := []file.Location{file.NewLocation("vcpkg.json")}
fixtureLocationSet := file.NewLocationSet(fileLocs...)
ctx := pkgtest.Context(t)
fmtPkg := pkg.Package{
Name: "fmt",
Version: "11.0.2#1",
PURL: "pkg:vcpkg/fmt@11.0.2?port_revision=1&repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
FoundBy: "vcpkg-manifest-cataloger",
Locations: fixtureLocationSet,
Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
Language: pkg.CPP,
Type: pkg.VcpkgPkg,
Metadata: &pkg.VcpkgManifest{
Description: []string{"{fmt} is an open-source formatting library providing a fast and safe alternative to C stdio and C++ iostreams."},
FullVersion: "11.0.2#1",
Version: "11.0.2",
PortVersion: 1,
Name: "fmt",
Registry: &pkg.VcpkgRegistryEntry{
Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
Kind: pkg.Git,
Repository: "https://github.com/anchore/vcpkg-test-fixture",
},
},
}
helloPkg := pkg.Package{
Name: "hello",
Version: "0.1.0",
PURL: "pkg:vcpkg/hello@0.1.0",
FoundBy: "vcpkg-manifest-cataloger",
Locations: fixtureLocationSet,
Language: pkg.CPP,
Type: pkg.VcpkgPkg,
Metadata: &pkg.VcpkgManifest{
FullVersion: "0.1.0",
Version: "0.1.0",
Name: "hello",
Registry: nil,
},
}
vcpkgCmakeConfigPkg := pkg.Package{
Name: "vcpkg-cmake-config",
Version: "2024-05-23",
PURL: "pkg:vcpkg/vcpkg-cmake-config@2024-05-23?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
FoundBy: "vcpkg-manifest-cataloger",
Locations: fixtureLocationSet,
Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
Language: pkg.CPP,
Type: pkg.VcpkgPkg,
Metadata: &pkg.VcpkgManifest{
Documentation: "https://learn.microsoft.com/vcpkg/maintainers/functions/vcpkg_cmake_config_fixup",
FullVersion: "2024-05-23",
Version: "2024-05-23",
Name: "vcpkg-cmake-config",
Registry: &pkg.VcpkgRegistryEntry{
Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
Kind: pkg.Git,
Repository: "https://github.com/anchore/vcpkg-test-fixture",
},
},
}
vcpkgCmakePkg := pkg.Package{
Name: "vcpkg-cmake",
Version: "2024-04-23",
PURL: "pkg:vcpkg/vcpkg-cmake@2024-04-23?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
FoundBy: "vcpkg-manifest-cataloger",
Locations: fixtureLocationSet,
Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
Language: pkg.CPP,
Type: pkg.VcpkgPkg,
Metadata: &pkg.VcpkgManifest{
Documentation: "https://learn.microsoft.com/vcpkg/maintainers/functions/vcpkg_cmake_configure",
FullVersion: "2024-04-23",
Version: "2024-04-23",
Name: "vcpkg-cmake",
Registry: &pkg.VcpkgRegistryEntry{
Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
Kind: pkg.Git,
Repository: "https://github.com/anchore/vcpkg-test-fixture",
},
},
}
sampleLibPkg := pkg.Package{
Name: "vcpkg-sample-library",
Version: "1.0.2",
PURL: "pkg:vcpkg/vcpkg-sample-library@1.0.2?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
FoundBy: "vcpkg-manifest-cataloger",
Locations: fixtureLocationSet,
Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
Language: pkg.CPP,
Type: pkg.VcpkgPkg,
Metadata: &pkg.VcpkgManifest{
Description: []string{"A sample C++ library designed to serve as a foundational example for a tutorial on packaging libraries with vcpkg."},
FullVersion: "1.0.2",
Version: "1.0.2",
Name: "vcpkg-sample-library",
Registry: &pkg.VcpkgRegistryEntry{
Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
Kind: pkg.Git,
Repository: "https://github.com/anchore/vcpkg-test-fixture",
},
},
}
expectedPkgs := []pkg.Package{
fmtPkg,
fmtPkg,
helloPkg,
vcpkgCmakeConfigPkg,
vcpkgCmakeConfigPkg,
vcpkgCmakePkg,
vcpkgCmakePkg,
sampleLibPkg,
}
// relationships require IDs to be set to be sorted similarly
for i := range expectedPkgs {
expectedPkgs[i].SetID()
}
pkg.Sort(expectedPkgs)
expectedRelationships := []artifact.Relationship{
{
From: fmtPkg,
To: helloPkg,
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: vcpkgCmakePkg,
To: fmtPkg,
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: vcpkgCmakeConfigPkg,
To: fmtPkg,
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: sampleLibPkg,
To: helloPkg,
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: fmtPkg,
To: sampleLibPkg,
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: vcpkgCmakePkg,
To: sampleLibPkg,
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: vcpkgCmakeConfigPkg,
To: sampleLibPkg,
Type: artifact.DependencyOfRelationship,
Data: nil,
},
}
catalogerCfg := CatalogerConfig{
VcpkgAllowGitClone: false,
}
pkgtest.TestCataloger(t, fixture, NewVcpkgManifestCataloger(catalogerCfg), expectedPkgs, expectedRelationships)
}
// the registry the helloworld fixture pins; see testdata/vcpkg/helloworld/vcpkg-configuration.json
const vcpkgTestRegistryURL = "https://github.com/anchore/vcpkg-test-fixture"
// useLocalVcpkgRegistryCache points syft's cache manager at a gitignored testdata cache and ensures a local
// clone of the vcpkg test registry exists there, so the resolver resolves it offline via git.PlainOpen
// (getVcpkgGitCachePath resolves to "<syft cache root>/../vcpkg/registries/git").
func useLocalVcpkgRegistryCache(t *testing.T) {
t.Helper()
cacheRoot := filepath.Join("testdata", "cache")
syftCacheRoot := filepath.Join(cacheRoot, "syft")
registryGitDir := filepath.Join(cacheRoot, "vcpkg", "registries", "git")
prepareVcpkgRegistryCache(t, registryGitDir)
mgr, err := cache.NewFromDir(syftCacheRoot, 24*time.Hour)
require.NoError(t, err)
prev := cache.GetManager()
cache.SetManager(mgr)
t.Cleanup(func() { cache.SetManager(prev) })
}
// prepareVcpkgRegistryCache clones the vcpkg test registry into gitDir if a valid clone is not already present.
// the clone happens once (first run, requires network); later runs reuse it offline.
func prepareVcpkgRegistryCache(t *testing.T, gitDir string) {
t.Helper()
if _, err := git.PlainOpen(gitDir); err == nil {
return // valid cache already present
}
// clear any partial/stale state left by an interrupted run
require.NoError(t, os.RemoveAll(gitDir))
require.NoError(t, os.MkdirAll(filepath.Dir(gitDir), 0o755))
// clone into a temp sibling then atomically move into place so an interrupted or concurrent run can't
// observe a half-written cache
tmp, err := os.MkdirTemp(filepath.Dir(gitDir), "clone-")
require.NoError(t, err)
defer os.RemoveAll(tmp)
cloneTarget := filepath.Join(tmp, "git")
if _, err := git.PlainClone(cloneTarget, false, &git.CloneOptions{URL: vcpkgTestRegistryURL}); err != nil {
t.Skipf("vcpkg registry cache is not present and could not be prepared (first run requires network): %v", err)
}
require.NoError(t, os.Rename(cloneTarget, gitDir))
}

View File

@ -0,0 +1,2 @@
/cache/
test-observations.json

View File

@ -1,8 +1,10 @@
# Conan test data
# C++ test data
This folder contains the test data for the Conan package manager.
This folder contains the test data for the Conan and Vcpkg package managers.
## conan.lock
## Conan test data
### conan.lock
The conan lock file is created in the following way.
@ -15,3 +17,8 @@ This is necessary to verify that the dependency tree is properly parsed.
3. Manually delete the package id and prev from tinyxml2 entry to test conan lock parsing if they are missing:
`sed -i 's|\"package_id\": \"6557f18ca99c0b6a233f43db00e30efaa525e27e\",||g' conan.lock`
`sed -i 's|\"prev\": \"548bb273d2980991baa519453d68e5cd\",||g' conan.lock`
## Vcpkg test data
vcpkg/helloworld contains a vcpkg project that contains only the files necessary to retrieve all of the manifests for dependencies listed
in the vcpkg.json file. These manifests exist in a remote vcpkg git registry located at https://github.com/anchore/vcpkg-test-fixture

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,7 @@
#include <fmt/core.h>
int main()
{
fmt::print("Hello World!\n");
return 0;
}

View File

@ -0,0 +1,7 @@
{
"default-registry": {
"kind": "git",
"baseline": "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
"repository": "https://github.com/anchore/vcpkg-test-fixture"
}
}

View File

@ -0,0 +1,10 @@
{
"name": "hello",
"version": "0.1.0",
"dependencies": [
{
"name": "fmt"
},
"vcpkg-sample-library"
]
}

View File

@ -52,6 +52,7 @@ const (
SwiftPkg Type = "swift"
SwiplPackPkg Type = "swiplpack"
TerraformPkg Type = "terraform"
VcpkgPkg Type = "vcpkg"
WordpressPluginPkg Type = "wordpress-plugin"
HomebrewPkg Type = "homebrew"
AppleAppBundlePkg Type = "apple-app-bundle"
@ -98,6 +99,7 @@ var AllPkgs = []Type{
SwiftPkg,
SwiplPackPkg,
TerraformPkg,
VcpkgPkg,
WordpressPluginPkg,
HomebrewPkg,
AppleAppBundlePkg,
@ -174,6 +176,8 @@ func (t Type) PackageURLType() string {
return "swiplpack"
case TerraformPkg:
return "terraform"
case VcpkgPkg:
return "vcpkg"
case WordpressPluginPkg:
return "wordpress-plugin"
case HomebrewPkg:
@ -262,6 +266,8 @@ func TypeByName(name string) Type {
return SwiplPackPkg
case "terraform":
return TerraformPkg
case "vcpkg":
return VcpkgPkg
case "wordpress-plugin":
return WordpressPluginPkg
case "homebrew":

View File

@ -163,6 +163,7 @@ func TestTypeFromPURL(t *testing.T) {
expectedTypes.Remove(string(ModelPkg)) // no valid purl for ai artifacts currently
expectedTypes.Remove(string(AppleAppBundlePkg)) // no standard purl type for apple app bundles
expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package
expectedTypes.Remove(string(VcpkgPkg))
for _, test := range tests {
t.Run(string(test.expected), func(t *testing.T) {

35
syft/pkg/vcpkg.go Normal file
View File

@ -0,0 +1,35 @@
package pkg
type VcpkgRegistryKind string
const (
FileSystem VcpkgRegistryKind = "filesystem"
Git VcpkgRegistryKind = "git"
Builtin VcpkgRegistryKind = "builtin"
)
// used for metadata. Includes summary of data found in manifest (vcpkg.json file) relevant to just that vcpkg
type VcpkgManifest struct {
Description []string `json:"description,omitempty"`
Documentation string `json:"documentation,omitempty"`
FullVersion string `json:"full-version"`
Version string `json:"version"`
PortVersion int `json:"port-version"`
Maintainers []string `json:"maintainers,omitempty"`
Name string `json:"name"`
Supports string `json:"supports,omitempty"`
// to show where it came from
Registry *VcpkgRegistryEntry `json:"registry,omitempty"`
// found by looking at build folder to find target. ex. "x64-linux"
Triplet string `json:"triplet,omitempty"`
}
// Matches definition of Vcpkg "Registry". https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-configuration-json#registry
type VcpkgRegistryEntry struct {
Baseline string `json:"baseline,omitempty"`
Kind VcpkgRegistryKind `json:"kind"`
Packages []string `json:"packages,omitempty"`
Path string `json:"path,omitempty"`
Reference string `json:"reference,omitempty"`
Repository string `json:"repository,omitempty"`
}