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/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent" "github.com/anchore/syft/syft/file/cataloger/filecontent"
"github.com/anchore/syft/syft/pkg/cataloger/binary" "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/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/java"
@ -44,6 +45,7 @@ type Catalog struct {
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"` Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
// ecosystem-specific cataloger configuration // ecosystem-specific cataloger configuration
Cpp cppConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"` Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"` Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"` Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
@ -80,6 +82,7 @@ func DefaultCatalog() Catalog {
JavaScript: defaultJavaScriptConfig(), JavaScript: defaultJavaScriptConfig(),
Python: defaultPythonConfig(), Python: defaultPythonConfig(),
Nix: defaultNixConfig(), Nix: defaultNixConfig(),
Cpp: defaultCppConfig(),
Dotnet: defaultDotnetConfig(), Dotnet: defaultDotnetConfig(),
Golang: defaultGolangConfig(), Golang: defaultGolangConfig(),
Java: defaultJavaConfig(), Java: defaultJavaConfig(),
@ -172,6 +175,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
} }
return pkgcataloging.Config{ return pkgcataloging.Config{
Binary: binary.DefaultClassifierCatalogerConfig(), Binary: binary.DefaultClassifierCatalogerConfig(),
Cpp: cpp.DefaultCatalogerConfig().
WithVcpkgAllowGitClone(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Cpp, task.Vcpkg), cfg.Cpp.VcpkgAllowGitClone)),
Dotnet: dotnet.DefaultCatalogerConfig(). Dotnet: dotnet.DefaultCatalogerConfig().
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL). WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL). WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
@ -301,6 +306,7 @@ var publicisedEnrichmentOptions = []string{
task.Java, task.Java,
task.JavaScript, task.JavaScript,
task.Python, task.Python,
task.Vcpkg,
} }
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool { 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.PhpPeclPkg)) // we have coverage for pear instead
definedPkgs.Remove(string(pkg.CondaPkg)) definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.ModelPkg)) definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg)) definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
var cases []testCase var cases []testCase
@ -165,6 +166,7 @@ func TestPkgCoverageDirectory(t *testing.T) {
definedPkgs.Remove(string(pkg.CondaPkg)) definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
definedPkgs.Remove(string(pkg.ModelPkg)) definedPkgs.Remove(string(pkg.ModelPkg))
definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg)) definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
// for directory scans we should not expect to see any of the following package types // 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. # This file is partially auto-generated. Run 'go generate ./internal/capabilities' to regenerate.
application: # AUTO-GENERATED - application-level config keys 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 - 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. 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 - key: dotnet.dep-packages-must-have-dll

View File

@ -3,7 +3,7 @@ package internal
const ( const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // 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. // 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 // Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field). // 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.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata // 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger // 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.SwiftPackageManagerResolvedEntry{},
pkg.SwiplPackEntry{}, pkg.SwiplPackEntry{},
pkg.TerraformLockProviderEntry{}, pkg.TerraformLockProviderEntry{},
pkg.VcpkgManifest{},
pkg.WordpressPluginEntry{}, pkg.WordpressPluginEntry{},
pkg.YarnLockEntry{}, pkg.YarnLockEntry{},
} }

View File

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

View File

@ -57,6 +57,10 @@ const (
// Python ecosystem labels // Python ecosystem labels
Python = "python" Python = "python"
// C/C++ ecosystem labels
Cpp = "cpp"
Vcpkg = "vcpkg"
) )
//nolint:funlen //nolint:funlen
@ -83,6 +87,11 @@ func DefaultPackageTaskFactories() Factories {
// language-specific package declared catalogers /////////////////////////////////////////////////////////////////////////// // language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"), 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.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dart.NewPubspecCataloger, 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"), 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", "$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", "$ref": "#/$defs/Document",
"$defs": { "$defs": {
"AlpmDbEntry": { "AlpmDbEntry": {
@ -2927,6 +2927,9 @@
{ {
"$ref": "#/$defs/TerraformLockProviderEntry" "$ref": "#/$defs/TerraformLockProviderEntry"
}, },
{
"$ref": "#/$defs/VcpkgManifest"
},
{ {
"$ref": "#/$defs/WordpressPluginEntry" "$ref": "#/$defs/WordpressPluginEntry"
} }
@ -4385,6 +4388,86 @@
], ],
"description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." "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": { "WordpressPluginEntry": {
"properties": { "properties": {
"pluginInstallDirectory": { "pluginInstallDirectory": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,11 @@
# Cataloger capabilities. See ../README.md for documentation. # 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: catalogers:
- ecosystem: c++ # MANUAL - ecosystem: c++ # MANUAL
name: conan-cataloger # AUTO-GENERATED name: conan-cataloger # AUTO-GENERATED
@ -126,3 +132,49 @@ catalogers:
default: false default: false
- name: package_manager.package_integrity_hash - name: package_manager.package_integrity_hash
default: false 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"). return generic.NewCataloger("conan-info-cataloger").
WithParserByGlobs(parseConaninfo, "**/conaninfo.txt") 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" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
) )
func TestCataloger_Globs(t *testing.T) { func TestCatalogerConan_Globs(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fixture 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 { tests := []struct {
name string name string
fixture 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 package cpp
import ( import (
"context"
"net/url"
"strconv"
"strings" "strings"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/cpp/internal/vcpkg"
) )
type conanRef struct { type conanRef struct {
@ -92,7 +96,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p
Name: ref.Name, Name: ref.Name,
Version: ref.Version, Version: ref.Version,
Locations: file.NewLocationSet(locations...), Locations: file.NewLocationSet(locations...),
PURL: packageURL(ref), PURL: packageURLFromConanRef(ref),
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
Metadata: metadata, Metadata: metadata,
@ -103,7 +107,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p
return &p return &p
} }
func packageURL(ref *conanRef) string { func packageURLFromConanRef(ref *conanRef) string {
qualifiers := packageurl.Qualifiers{} qualifiers := packageurl.Qualifiers{}
if ref.Channel != "" { if ref.Channel != "" {
qualifiers = append(qualifiers, packageurl.Qualifier{ qualifiers = append(qualifiers, packageurl.Qualifier{
@ -120,3 +124,63 @@ func packageURL(ref *conanRef) string {
"", "",
).ToString() ).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. The conan lock file is created in the following way.
@ -14,4 +16,9 @@ This is necessary to verify that the dependency tree is properly parsed.
`sed -i 's|mfast/1.2.2#c6f6387c9b99780f0ee05e25f99d0f39|mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39|g' conan.lock` `sed -i 's|mfast/1.2.2#c6f6387c9b99780f0ee05e25f99d0f39|mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39|g' conan.lock`
3. Manually delete the package id and prev from tinyxml2 entry to test conan lock parsing if they are missing: 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|\"package_id\": \"6557f18ca99c0b6a233f43db00e30efaa525e27e\",||g' conan.lock`
`sed -i 's|\"prev\": \"548bb273d2980991baa519453d68e5cd\",||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" SwiftPkg Type = "swift"
SwiplPackPkg Type = "swiplpack" SwiplPackPkg Type = "swiplpack"
TerraformPkg Type = "terraform" TerraformPkg Type = "terraform"
VcpkgPkg Type = "vcpkg"
WordpressPluginPkg Type = "wordpress-plugin" WordpressPluginPkg Type = "wordpress-plugin"
HomebrewPkg Type = "homebrew" HomebrewPkg Type = "homebrew"
AppleAppBundlePkg Type = "apple-app-bundle" AppleAppBundlePkg Type = "apple-app-bundle"
@ -98,6 +99,7 @@ var AllPkgs = []Type{
SwiftPkg, SwiftPkg,
SwiplPackPkg, SwiplPackPkg,
TerraformPkg, TerraformPkg,
VcpkgPkg,
WordpressPluginPkg, WordpressPluginPkg,
HomebrewPkg, HomebrewPkg,
AppleAppBundlePkg, AppleAppBundlePkg,
@ -174,6 +176,8 @@ func (t Type) PackageURLType() string {
return "swiplpack" return "swiplpack"
case TerraformPkg: case TerraformPkg:
return "terraform" return "terraform"
case VcpkgPkg:
return "vcpkg"
case WordpressPluginPkg: case WordpressPluginPkg:
return "wordpress-plugin" return "wordpress-plugin"
case HomebrewPkg: case HomebrewPkg:
@ -262,6 +266,8 @@ func TypeByName(name string) Type {
return SwiplPackPkg return SwiplPackPkg
case "terraform": case "terraform":
return TerraformPkg return TerraformPkg
case "vcpkg":
return VcpkgPkg
case "wordpress-plugin": case "wordpress-plugin":
return WordpressPluginPkg return WordpressPluginPkg
case "homebrew": 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(ModelPkg)) // no valid purl for ai artifacts currently
expectedTypes.Remove(string(AppleAppBundlePkg)) // no standard purl type for apple app bundles 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(PhpPeclPkg)) // we should always consider this a pear package
expectedTypes.Remove(string(VcpkgPkg))
for _, test := range tests { for _, test := range tests {
t.Run(string(test.expected), func(t *testing.T) { 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"`
}