mirror of
https://github.com/anchore/syft.git
synced 2026-07-04 18:18:26 +02:00
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:
parent
148fe572bc
commit
656a4d46d7
@ -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 {
|
||||
|
||||
30
cmd/syft/internal/options/cpp.go
Normal file
30
cmd/syft/internal/options/cpp.go
Normal 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)`)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -72,6 +72,7 @@ func AllTypes() []any {
|
||||
pkg.SwiftPackageManagerResolvedEntry{},
|
||||
pkg.SwiplPackEntry{},
|
||||
pkg.TerraformLockProviderEntry{},
|
||||
pkg.VcpkgManifest{},
|
||||
pkg.WordpressPluginEntry{},
|
||||
pkg.YarnLockEntry{},
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"),
|
||||
|
||||
4505
schema/json/schema-16.1.8.json
Normal file
4505
schema/json/schema-16.1.8.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 != "" {
|
||||
|
||||
@ -60,6 +60,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
pkg.GGUFFileHeader{},
|
||||
pkg.DenoLockEntry{},
|
||||
pkg.DenoRemoteLockEntry{},
|
||||
pkg.VcpkgManifest{},
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
19
syft/pkg/cataloger/cpp/config.go
Normal file
19
syft/pkg/cataloger/cpp/config.go
Normal 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
|
||||
}
|
||||
697
syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go
Normal file
697
syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
182
syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go
Normal file
182
syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go
Normal 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
|
||||
}
|
||||
244
syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go
Normal file
244
syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go
Normal 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))
|
||||
}
|
||||
2
syft/pkg/cataloger/cpp/testdata/.gitignore
vendored
Normal file
2
syft/pkg/cataloger/cpp/testdata/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/cache/
|
||||
test-observations.json
|
||||
15
syft/pkg/cataloger/cpp/testdata/README.md
vendored
15
syft/pkg/cataloger/cpp/testdata/README.md
vendored
@ -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.
|
||||
|
||||
@ -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`
|
||||
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`
|
||||
`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
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{}
|
||||
1
syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json
vendored
Normal file
1
syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json
vendored
Normal file
1
syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
7
syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp
vendored
Normal file
7
syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
#include <fmt/core.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
fmt::print("Hello World!\n");
|
||||
return 0;
|
||||
}
|
||||
7
syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json
vendored
Normal file
7
syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"default-registry": {
|
||||
"kind": "git",
|
||||
"baseline": "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
|
||||
"repository": "https://github.com/anchore/vcpkg-test-fixture"
|
||||
}
|
||||
}
|
||||
10
syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json
vendored
Normal file
10
syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "hello",
|
||||
"version": "0.1.0",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "fmt"
|
||||
},
|
||||
"vcpkg-sample-library"
|
||||
]
|
||||
}
|
||||
@ -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":
|
||||
|
||||
@ -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
35
syft/pkg/vcpkg.go
Normal 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"`
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user