mirror of
https://github.com/anchore/syft.git
synced 2026-10-24 12:02:22 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2293641e3b | ||
|
|
9dd9ce0bdd | ||
|
|
949ac70369 | ||
|
|
07fb23487f | ||
|
|
f45586b457 | ||
|
|
0fe98ed1f2 | ||
|
|
5f67883fe8 | ||
|
|
d288dd67ae | ||
|
|
da745b13e8 | ||
|
|
68da404bd7 | ||
|
|
35bf33bcf0 | ||
|
|
7f73d6a603 | ||
|
|
ef40eec38d | ||
|
|
80212d87c2 | ||
|
|
b14123ecc1 | ||
|
|
e00aeb16e4 | ||
|
|
fa6f6d70f0 | ||
|
|
0975d80dc0 | ||
|
|
4d823eaaa7 | ||
|
|
138e42b428 | ||
|
|
ecd8fb7740 | ||
|
|
dd639c09b2 | ||
|
|
31a352d030 | ||
|
|
08e913a2ce | ||
|
|
5ef5b1badc | ||
|
|
b236f8c1f7 | ||
|
|
476ce60768 | ||
|
|
16223e6dd7 | ||
|
|
1286689419 | ||
|
|
295454945c | ||
|
|
86baeeb481 | ||
|
|
2dcf5163b8 | ||
|
|
12b8ba47fb | ||
|
|
138d9ce2a0 | ||
|
|
8a229b16c4 | ||
|
|
9af0098a68 | ||
|
|
2840ea6653 | ||
|
|
b38e40e3e6 | ||
|
|
a6eedecd31 | ||
|
|
691b1357b2 | ||
|
|
c890e7f17f |
+1
-1
@@ -16,7 +16,7 @@ tools:
|
||||
# used to upload test fixture cache
|
||||
- name: oras
|
||||
version:
|
||||
want: v1.3.2
|
||||
want: v1.3.3
|
||||
method: github-release
|
||||
with:
|
||||
repo: oras-project/oras
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
if: ${{ github.event.inputs.phase == 'all' }}
|
||||
permissions:
|
||||
contents: read # required for fetching tags
|
||||
uses: anchore/workflows/.github/workflows/check-version-available.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2
|
||||
uses: anchore/workflows/.github/workflows/check-version-available.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
|
||||
with:
|
||||
version: ${{ github.event.inputs.version }}
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
packages: write # required for publishing release artifacts to GitHub packages
|
||||
id-token: write # required for keyless signing (cosign/sigstore OIDC)
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
@@ -89,13 +89,13 @@ jobs:
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
|
||||
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -20,12 +20,12 @@ jobs:
|
||||
contents: read
|
||||
security-events: write # for uploading SARIF results
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run zizmor"
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
||||
with:
|
||||
# there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default)
|
||||
advanced-security: true
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -213,7 +213,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+52
-9
@@ -3,8 +3,10 @@ package main
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
. "github.com/anchore/go-make"
|
||||
"github.com/anchore/go-make/config"
|
||||
"github.com/anchore/go-make/file"
|
||||
"github.com/anchore/go-make/lang"
|
||||
"github.com/anchore/go-make/run"
|
||||
@@ -27,22 +29,43 @@ func main() {
|
||||
gotest.Name("unit"),
|
||||
gotest.ExcludeGlob("**/test/**"),
|
||||
gotest.CoverageThreshold(62),
|
||||
race(),
|
||||
),
|
||||
|
||||
// integration tests: native go-make Task. The race-detector smoke against a
|
||||
// real image stays bundled here (RunsOn integration) so `make integration`
|
||||
// behaves like the Taskfile version did.
|
||||
gotest.Tasks(
|
||||
gotest.Name("integration"),
|
||||
gotest.IncludeGlob("./cmd/syft/internal/test/integration/..."),
|
||||
gotest.Verbose(),
|
||||
gotest.NoCoverage(),
|
||||
),
|
||||
// integration tests: run `go test` directly instead of via gotest.Tasks(), which
|
||||
// has no way to set a timeout. The suite is a single package of ~36 sequential
|
||||
// tests over 18 docker fixture images, so with a cold fixture cache (every image
|
||||
// built + saved inline, under -race) it runs well past `go test`'s default 10m
|
||||
// timeout -- which is exactly the case when regenerating the fixture cache from
|
||||
// scratch. -count=1 keeps the go test cache from short-circuiting a run whose
|
||||
// side effect (the built fixtures) is the thing we're after.
|
||||
//
|
||||
// The race-detector smoke against a real image stays bundled here (RunsOn
|
||||
// integration) so `make integration` behaves like the Taskfile version did.
|
||||
Task{
|
||||
Name: "integration",
|
||||
Description: "run integration tests",
|
||||
RunsOn: lang.List("test"),
|
||||
Run: func() {
|
||||
raceFlag := ""
|
||||
if raceEnabled() {
|
||||
raceFlag = " -race"
|
||||
}
|
||||
Run(
|
||||
"go test -count=1 -timeout=30m -v"+raceFlag+" ./cmd/syft/internal/test/integration/...",
|
||||
run.Env("GODEBUG", "dontfreezetheworld=1"),
|
||||
)
|
||||
},
|
||||
},
|
||||
Task{
|
||||
Name: "integration:race-smoke",
|
||||
Description: "exercise the CLI with the race detector",
|
||||
RunsOn: lang.List("integration"),
|
||||
Run: func() {
|
||||
if !raceEnabled() {
|
||||
Log("race detector disabled (RACE=false); skipping race smoke")
|
||||
return
|
||||
}
|
||||
Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2")
|
||||
},
|
||||
},
|
||||
@@ -132,6 +155,26 @@ func main() {
|
||||
)
|
||||
}
|
||||
|
||||
// raceEnabled is the single switch for the race detector across every test suite.
|
||||
// Unset it and we keep go-make's behavior (on in CI, off locally and on windows);
|
||||
// set RACE=false to turn it off everywhere -- worth doing when rebuilding the test
|
||||
// fixture cache from scratch, where every suite is dominated by building docker
|
||||
// fixtures and the race detector only adds wall clock. RACE=true forces it on.
|
||||
func raceEnabled() bool {
|
||||
if enabled, err := strconv.ParseBool(config.Env("RACE", "")); err == nil {
|
||||
return enabled
|
||||
}
|
||||
return config.CI && !config.Windows
|
||||
}
|
||||
|
||||
// race applies raceEnabled() to a gotest suite. gotest exposes no functional option
|
||||
// for the race detector, but gotest.Config.Race is exported.
|
||||
func race() gotest.Option {
|
||||
return func(c *gotest.Config) {
|
||||
c.Race = raceEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
// snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile:
|
||||
// <repoRoot>/snapshot/<os>-build_<os>_<arch>/syft, where arch maps amd64->amd64_v1
|
||||
// and arm64->arm64_v8.0 to match goreleaser's per-target output directory naming.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/go-make/config"
|
||||
"github.com/anchore/go-make/tasks/gotest"
|
||||
)
|
||||
|
||||
func TestRaceEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env string // "" means RACE is unset
|
||||
ci bool
|
||||
want bool
|
||||
}{
|
||||
{name: "default in CI", ci: true, want: true},
|
||||
{name: "default locally", ci: false, want: false},
|
||||
{name: "explicitly off in CI", env: "false", ci: true, want: false},
|
||||
{name: "explicitly on locally", env: "true", ci: false, want: true},
|
||||
{name: "unparseable falls back to default", env: "yes-please", ci: true, want: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// always go through t.Setenv (it registers the restore) so an ambient
|
||||
// RACE from the caller's environment can't leak into the unset cases
|
||||
t.Setenv("RACE", tt.env)
|
||||
if tt.env == "" {
|
||||
os.Unsetenv("RACE")
|
||||
}
|
||||
orig := config.CI
|
||||
config.CI = tt.ci
|
||||
t.Cleanup(func() { config.CI = orig })
|
||||
|
||||
if got := raceEnabled(); got != tt.want {
|
||||
t.Errorf("raceEnabled() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
// the gotest suites get the same answer through the functional option
|
||||
var cfg gotest.Config
|
||||
race()(&cfg)
|
||||
if cfg.Race != tt.want {
|
||||
t.Errorf("race() set Race = %v, want %v", cfg.Race, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/anchore/syft/actions/workflows/validations.yaml" target="_blank"><img alt="Validations" src="https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/anchore/syft" target="_blank"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/anchore/syft"></a>
|
||||
<a href="https://github.com/anchore/syft/releases/latest" target="_blank"><img alt="GitHub release" src="https://img.shields.io/github/release/anchore/syft.svg"></a>
|
||||
<a href="https://github.com/anchore/syft" target="_blank"><img alt="GitHub go.mod Go version" src="https://img.shields.io/github/go-mod/go-version/anchore/syft.svg"></a>
|
||||
<a href="https://github.com/anchore/syft/blob/main/LICENSE" target="_blank"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"></a>
|
||||
|
||||
@@ -125,6 +125,7 @@ tasks:
|
||||
desc: Run tests for pipeline utils
|
||||
cmds:
|
||||
- cmd: .github/scripts/labeler_test.py
|
||||
- cmd: go test -C .make ./...
|
||||
|
||||
snapshot-smoke-test:
|
||||
desc: Run a smoke test on the snapshot builds + docker images
|
||||
|
||||
@@ -199,7 +199,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
||||
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
|
||||
).
|
||||
WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)).
|
||||
WithCaptureSymbols(cfg.Golang.CaptureSymbols),
|
||||
WithCaptureSymbols(cfg.Golang.CaptureSymbols).
|
||||
WithCaptureSymbolsModules(cfg.Golang.CaptureSymbolsModules),
|
||||
JavaScript: javascript.DefaultCatalogerConfig().
|
||||
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
|
||||
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
)
|
||||
@@ -19,6 +20,7 @@ type golangConfig struct {
|
||||
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
|
||||
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
|
||||
CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"`
|
||||
CaptureSymbolsModules []string `json:"capture-symbols-modules" yaml:"capture-symbols-modules" mapstructure:"capture-symbols-modules"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -42,8 +44,16 @@ if unset this defaults to $GONOPROXY`)
|
||||
always show (devel) as the version. Use these options to control heuristics to guess
|
||||
a more accurate version from the binary.`)
|
||||
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
|
||||
// note: descriptions must be static string literals; the app config discovery that generates the
|
||||
// capability docs reads them straight out of the AST
|
||||
descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are:
|
||||
"none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)`)
|
||||
"none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every
|
||||
module under golang.org/x/), and "all" (all module packages plus stdlib)`)
|
||||
descriptions.Add(&o.CaptureSymbolsModules, `glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols
|
||||
captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not.
|
||||
a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2;
|
||||
spelling a suffix out in the pattern selects only that major version.
|
||||
this can only widen the selection, never narrow it, and is inert when capture-symbols is none`)
|
||||
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
|
||||
descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
|
||||
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
|
||||
@@ -51,7 +61,24 @@ a more accurate version from the binary.`)
|
||||
}
|
||||
|
||||
func (o *golangConfig) PostLoad() error {
|
||||
raw := strings.TrimSpace(string(o.CaptureSymbols))
|
||||
o.CaptureSymbols = o.CaptureSymbols.Parse()
|
||||
|
||||
// an unrecognized value still resolves to "none", but say so rather than silently capturing nothing.
|
||||
// stay quiet for unset and an explicit "none", which is the default and would otherwise warn on nearly
|
||||
// every scan.
|
||||
if o.CaptureSymbols == cataloging.SymbolScopeNone && raw != "" && !strings.EqualFold(raw, string(cataloging.SymbolScopeNone)) {
|
||||
log.Warnf("unknown golang.capture-symbols value %q, defaulting to %q (valid values: none, stdlib, extended-stdlib, all)", raw, cataloging.SymbolScopeNone)
|
||||
}
|
||||
|
||||
// trim only, deliberately not Flatten: viper already splits a comma-separated scalar (env var or a bare
|
||||
// yaml string) into a slice before this point, and there is no CLI flag feeding this key. The one thing
|
||||
// Flatten would add is splitting commas *inside* a list entry, which silently breaks doublestar brace
|
||||
// alternation like github.com/{foo,bar}/**. Viper's split does not trim, so that part is still needed.
|
||||
for i, pattern := range o.CaptureSymbolsModules {
|
||||
o.CaptureSymbolsModules[i] = strings.TrimSpace(pattern)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,7 +103,8 @@ func defaultGolangConfig() golangConfig {
|
||||
FromContents: def.MainModuleVersion.FromContents,
|
||||
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
|
||||
},
|
||||
UsePackagesLib: nil, // this defaults to true, which is the API default
|
||||
CaptureSymbols: def.CaptureSymbols,
|
||||
UsePackagesLib: nil, // this defaults to true, which is the API default
|
||||
CaptureSymbols: def.CaptureSymbols,
|
||||
CaptureSymbolsModules: def.CaptureSymbolsModules,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
|
||||
func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg golangConfig
|
||||
expected cataloging.SymbolScope
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
name string
|
||||
cfg golangConfig
|
||||
expected cataloging.SymbolScope
|
||||
expectedModules []string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "normalize all",
|
||||
@@ -25,6 +26,32 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
cfg: golangConfig{CaptureSymbols: "stdlib"},
|
||||
expected: cataloging.SymbolScopeStdlib,
|
||||
},
|
||||
{
|
||||
name: "normalize extended-stdlib",
|
||||
cfg: golangConfig{CaptureSymbols: " Extended-Stdlib "},
|
||||
expected: cataloging.SymbolScopeExtendedStdlib,
|
||||
},
|
||||
{
|
||||
name: "module patterns keep embedded commas",
|
||||
cfg: golangConfig{
|
||||
CaptureSymbols: "stdlib",
|
||||
// brace alternation contains a comma; splitting on it would corrupt the pattern
|
||||
CaptureSymbolsModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
|
||||
},
|
||||
expected: cataloging.SymbolScopeStdlib,
|
||||
expectedModules: []string{"github.com/{foo,bar}/**", "golang.org/x/**"},
|
||||
},
|
||||
{
|
||||
// viper splits a comma-separated scalar (env var or bare yaml string) but does not trim,
|
||||
// so a leading space would otherwise survive into a pattern that silently matches nothing
|
||||
name: "module patterns are trimmed",
|
||||
cfg: golangConfig{
|
||||
CaptureSymbols: "stdlib",
|
||||
CaptureSymbolsModules: []string{"golang.org/x/**", " github.com/foo/** "},
|
||||
},
|
||||
expected: cataloging.SymbolScopeStdlib,
|
||||
expectedModules: []string{"golang.org/x/**", "github.com/foo/**"},
|
||||
},
|
||||
{
|
||||
name: "empty defaults to none",
|
||||
cfg: golangConfig{CaptureSymbols: ""},
|
||||
@@ -32,7 +59,7 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid value defaults to none",
|
||||
cfg: golangConfig{CaptureSymbols: "bogus"},
|
||||
cfg: golangConfig{CaptureSymbols: "stdlbi"},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
{
|
||||
@@ -40,6 +67,16 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
cfg: golangConfig{CaptureSymbols: "true"},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
{
|
||||
name: "explicit none resolves to none",
|
||||
cfg: golangConfig{CaptureSymbols: "none"},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
{
|
||||
name: "explicit none is not case sensitive",
|
||||
cfg: golangConfig{CaptureSymbols: " NONE "},
|
||||
expected: cataloging.SymbolScopeNone,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -52,6 +89,7 @@ func Test_golangConfig_PostLoad(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols)
|
||||
assert.Equal(t, tt.expectedModules, tt.cfg.CaptureSymbolsModules)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,15 +34,15 @@ require (
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/dave/jennifer v1.7.1
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
github.com/diskfs/go-diskfs v1.9.3
|
||||
github.com/diskfs/go-diskfs v1.9.4
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.7.0
|
||||
github.com/gkampitakis/go-snaps v0.5.23
|
||||
github.com/go-git/go-billy/v5 v5.9.0
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
github.com/go-git/go-billy/v5 v5.9.1
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
@@ -52,18 +52,18 @@ require (
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gpustack/gguf-parser-go v0.24.1
|
||||
github.com/gpustack/gguf-parser-go v0.25.0
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.8.6
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/invopop/jsonschema v0.14.0
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.1
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
|
||||
github.com/klauspost/compress v1.19.0
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/klauspost/compress v1.19.1
|
||||
github.com/magiconair/properties v1.18.11
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
|
||||
@@ -83,7 +83,7 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/ulikunitz/xz v0.5.16
|
||||
github.com/vbatts/go-mtree v0.7.0
|
||||
github.com/vifraa/gopom v1.0.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
|
||||
@@ -91,14 +91,14 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
go.yaml.in/yaml/v3 v3.0.5
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/mod v0.38.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.48.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.53.0
|
||||
modernc.org/sqlite v1.55.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -118,7 +118,7 @@ require (
|
||||
cloud.google.com/go/storage v1.61.3 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/DataDog/zstd v1.5.5 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
@@ -168,7 +168,7 @@ require (
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/containerd/cgroups/v3 v3.1.3 // indirect
|
||||
github.com/containerd/containerd/api v1.11.1 // indirect
|
||||
github.com/containerd/containerd/v2 v2.3.3 // indirect
|
||||
@@ -189,8 +189,8 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
@@ -299,7 +299,7 @@ require (
|
||||
github.com/zclconf/go-cty v1.16.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
@@ -319,12 +319,12 @@ require (
|
||||
gonum.org/v1/gonum v0.17.0 // indirect
|
||||
google.golang.org/api v0.271.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.73.4 // indirect
|
||||
modernc.org/libc v1.74.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -82,8 +82,8 @@ github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByX
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
|
||||
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
@@ -290,8 +290,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ=
|
||||
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
|
||||
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
|
||||
@@ -333,8 +333,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M=
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||
github.com/diskfs/go-diskfs v1.9.3 h1:cLciNCeZ4QAXVxyPJDr1ZJ9N9CCG3rQlQ/z/Cs/cNDM=
|
||||
github.com/diskfs/go-diskfs v1.9.3/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc=
|
||||
github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc=
|
||||
github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
@@ -371,14 +371,14 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
|
||||
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I=
|
||||
@@ -418,12 +418,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
|
||||
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -558,8 +558,8 @@ github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj
|
||||
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gpustack/gguf-parser-go v0.24.1 h1:nTYtL8HFK6ZhB90RKBu4oX2b3ZHpJLrMmKRfL9w9Cyc=
|
||||
github.com/gpustack/gguf-parser-go v0.24.1/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
|
||||
github.com/gpustack/gguf-parser-go v0.25.0 h1:1AMBhMKtI24nTtn588Bq53FqNiOvEw1x9Nb4HbRrThs=
|
||||
github.com/gpustack/gguf-parser-go v0.25.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc=
|
||||
@@ -624,8 +624,8 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i
|
||||
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ=
|
||||
github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
@@ -645,8 +645,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -670,8 +670,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/magiconair/properties v1.18.11 h1:j5ozYZl0zCjG7ahMDH0GWIobOvvUzT0BdAguG0ViKy0=
|
||||
github.com/magiconair/properties v1.18.11/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg=
|
||||
github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
@@ -947,8 +947,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
|
||||
github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||
github.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E=
|
||||
github.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY=
|
||||
github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
|
||||
@@ -998,8 +998,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
@@ -1022,8 +1022,9 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
@@ -1431,10 +1432,10 @@ google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ6
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1462,8 +1463,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@@ -1513,20 +1514,20 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
|
||||
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -1535,8 +1536,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
|
||||
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -15,7 +15,9 @@ application: # AUTO-GENERATED - application-level config keys
|
||||
- key: dotnet.relax-dll-claims-when-bundling-detected
|
||||
description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)
|
||||
- key: golang.capture-symbols
|
||||
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)'
|
||||
description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib)'
|
||||
- key: golang.capture-symbols-modules
|
||||
description: glob patterns matched against go module paths (e.g. github.com/klauspost/**) that should have symbols captured in addition to whatever capture-symbols selects. ** crosses path separators, * does not. a trailing major version suffix is ignored when matching, so github.com/foo/* covers github.com/foo/bar/v2; spelling a suffix out in the pattern selects only that major version. this can only widen the selection, never narrow it, and is inert when capture-symbols is none
|
||||
- key: golang.local-mod-cache-dir
|
||||
description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
|
||||
- key: golang.local-vendor-dir
|
||||
|
||||
@@ -35,3 +35,24 @@ Create the new schema by running `make generate-json-schema` from the root of th
|
||||
- If there is an existing schema for the given version and the new schema **does not** match the existing schema, an error is shown indicating to increment the version appropriately (see the "Versioning" section)
|
||||
|
||||
***Note: never delete a JSON schema and never change an existing JSON schema once it has been published in a release!*** Only add new schemas with a newly incremented version. All previous schema files must be stored in the `schema/json/` directory.
|
||||
|
||||
### Exception: `description`-only corrections
|
||||
|
||||
A published schema may be amended in place for one narrow case: the change touches only `description` text and leaves the data shape identical. Descriptions are documentation carried alongside the schema rather than constraints a validator evaluates, so correcting one cannot invalidate a document that already validated against that version. Minting a new version instead would leave the old one permanently describing the tool incorrectly, and spend a version number on no semantic change.
|
||||
|
||||
This applies when **every** one of the following holds:
|
||||
|
||||
- the only differences are `description` values
|
||||
- no field, type, enum, `required` entry, or `$ref` is added, removed, or altered
|
||||
- the `$id` version is unchanged
|
||||
|
||||
Anything else, including adding a field that happens to be optional, is a schema change and needs a version bump per the "Versioning" section above.
|
||||
|
||||
The generator blocks an in-place edit by design, since it refuses to overwrite a file that differs from what it would produce. To amend one, delete the schema file and regenerate so it is rewritten from the current Go doc comments:
|
||||
|
||||
```bash
|
||||
rm schema/json/schema-$VERSION.json
|
||||
make generate-json-schema
|
||||
```
|
||||
|
||||
The result is byte-identical to what the generator produces, so do not hand-edit the JSON. Re-running `make generate-json-schema` afterwards should report `No change to the existing schema!`, and `make check-json-schema-drift` should pass. Confirm with `git diff` that the only changes are the intended description lines.
|
||||
|
||||
@@ -1662,7 +1662,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -1662,7 +1662,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"type": "object",
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
|
||||
"description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, the\n\"extended-stdlib\" scope populates the stdlib package plus every module under golang.org/x/, and the\n\"stdlib\" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate\nany additional modules they match."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -12,6 +12,10 @@ const (
|
||||
// SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols.
|
||||
SymbolScopeStdlib SymbolScope = "stdlib"
|
||||
|
||||
// SymbolScopeExtendedStdlib captures symbols for the synthetic "stdlib" package as well as every module
|
||||
// under golang.org/x/ (the extended standard library).
|
||||
SymbolScopeExtendedStdlib SymbolScope = "extended-stdlib"
|
||||
|
||||
// SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package.
|
||||
SymbolScopeAll SymbolScope = "all"
|
||||
)
|
||||
@@ -21,6 +25,8 @@ func (s SymbolScope) Parse() SymbolScope {
|
||||
switch strings.ToLower(strings.TrimSpace(string(s))) {
|
||||
case string(SymbolScopeAll):
|
||||
return SymbolScopeAll
|
||||
case string(SymbolScopeExtendedStdlib):
|
||||
return SymbolScopeExtendedStdlib
|
||||
case string(SymbolScopeStdlib):
|
||||
return SymbolScopeStdlib
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ func Test_SymbolScope_Parse(t *testing.T) {
|
||||
{" all ", SymbolScopeAll},
|
||||
{"stdlib", SymbolScopeStdlib},
|
||||
{"Stdlib", SymbolScopeStdlib},
|
||||
{"extended-stdlib", SymbolScopeExtendedStdlib},
|
||||
{"Extended-Stdlib", SymbolScopeExtendedStdlib},
|
||||
{"EXTENDED-STDLIB", SymbolScopeExtendedStdlib},
|
||||
{" extended-stdlib ", SymbolScopeExtendedStdlib},
|
||||
{"extended_stdlib", SymbolScopeNone},
|
||||
{"none", SymbolScopeNone},
|
||||
{"", SymbolScopeNone},
|
||||
{"true", SymbolScopeNone},
|
||||
|
||||
@@ -152,6 +152,31 @@ func NewLocationFromImage(accessPath string, ref file.Reference, img *image.Imag
|
||||
}
|
||||
}
|
||||
|
||||
// NewVirtualLocationFromImage creates a new Location surfaced at realPath but whose contents and metadata are
|
||||
// described by the given reference. This is used to present a hardlink as the underlying type it points to (at the
|
||||
// hardlink's own path) so that image results are in parity with directory results, which cannot distinguish a
|
||||
// hardlink from a regular file. Note that RealPath (the hardlink's own path) intentionally differs from
|
||||
// ref.RealPath (the target's path); callers must not assume the two agree for these locations.
|
||||
func NewVirtualLocationFromImage(realPath, accessPath string, ref file.Reference, img *image.Image) Location {
|
||||
// the FileSystemID comes from the target ref's layer. a hardlink and its target are always materialized in the
|
||||
// same layer tar (a tar hardlink entry can only reference a file within the same archive), so the target's layer
|
||||
// digest is also the hardlink path's layer.
|
||||
layer := img.FileCatalog.Layer(ref)
|
||||
return Location{
|
||||
LocationData: LocationData{
|
||||
Coordinates: Coordinates{
|
||||
RealPath: realPath,
|
||||
FileSystemID: layer.Metadata.Digest,
|
||||
},
|
||||
AccessPath: accessPath,
|
||||
ref: ref,
|
||||
},
|
||||
LocationMetadata: LocationMetadata{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocationFromDirectory creates a new Location representing the given path (extracted from the Reference) relative to the given directory.
|
||||
func NewLocationFromDirectory(responsePath string, fd string, ref file.Reference) Location {
|
||||
return Location{
|
||||
|
||||
@@ -53,8 +53,20 @@ func (r *ContainerImageAllLayers) HasPath(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqueFileIDs stereoscopeFile.ReferenceSet, layerIdx int) ([]stereoscopeFile.Reference, error) {
|
||||
uniqueFiles := make([]stereoscopeFile.Reference, 0)
|
||||
func (r *ContainerImageAllLayers) locationsByRef(ref stereoscopeFile.Reference, accessPath string, uniqueFileIDs stereoscopeFile.ReferenceSet, layerPos int) ([]file.Location, error) {
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
// if the access path is itself a hardlink, surface it as the underlying type it points to, at its own path
|
||||
// (bound to the target's content), so that image results are in parity with directory results (which cannot
|
||||
// tell a hardlink from a regular file). the path-based lookup is required because the search that produced ref
|
||||
// already followed the basename link, collapsing the hardlink onto its target.
|
||||
if ownRef, targetRef, ok := r.hardLinkAtPath(accessPath, r.layers[layerPos]); ok {
|
||||
if !uniqueFileIDs.Contains(ownRef) {
|
||||
uniqueFileIDs.Add(ownRef)
|
||||
uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), accessPath, targetRef, r.img))
|
||||
}
|
||||
return uniqueLocations, nil
|
||||
}
|
||||
|
||||
// since there is potentially considerable work for each symlink/hardlink that needs to be resolved, let's check to see if this is a symlink/hardlink first
|
||||
entry, err := r.img.FileCatalog.Get(ref)
|
||||
@@ -65,22 +77,41 @@ func (r *ContainerImageAllLayers) fileByRef(ref stereoscopeFile.Reference, uniqu
|
||||
if entry.Type == stereoscopeFile.TypeHardLink || entry.Type == stereoscopeFile.TypeSymLink {
|
||||
// a link may resolve in this layer or higher, assuming a squashed tree is used to search
|
||||
// we should search all possible resolutions within the valid source
|
||||
for _, subLayerIdx := range r.layers[layerIdx:] {
|
||||
for _, subLayerIdx := range r.layers[layerPos:] {
|
||||
resolvedRef, err := r.img.ResolveLinkByLayerSquash(ref, subLayerIdx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve link from layer (layer=%d ref=%+v): %w", subLayerIdx, ref, err)
|
||||
}
|
||||
if resolvedRef.HasReference() && !uniqueFileIDs.Contains(*resolvedRef.Reference) {
|
||||
uniqueFileIDs.Add(*resolvedRef.Reference)
|
||||
uniqueFiles = append(uniqueFiles, *resolvedRef.Reference)
|
||||
uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, *resolvedRef.Reference, r.img))
|
||||
}
|
||||
}
|
||||
} else if !uniqueFileIDs.Contains(ref) {
|
||||
uniqueFileIDs.Add(ref)
|
||||
uniqueFiles = append(uniqueFiles, ref)
|
||||
uniqueLocations = append(uniqueLocations, file.NewLocationFromImage(accessPath, ref, r.img))
|
||||
}
|
||||
|
||||
return uniqueFiles, nil
|
||||
return uniqueLocations, nil
|
||||
}
|
||||
|
||||
// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a
|
||||
// hardlink within the given layer. ok is false when path does not exist there or is not a hardlink. The lookup does
|
||||
// not follow the basename link so that the hardlink's own path is preserved. This adds a tree walk per
|
||||
// matched ref (path x layer); if it shows up in profiles, fold the hardlink check into the existing search resolution.
|
||||
func (r *ContainerImageAllLayers) hardLinkAtPath(path string, layerIdx int) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) {
|
||||
var own stereoscopeFile.Reference
|
||||
// use the squashed-to-layer view (not just this layer's additions) so a hardlink added in a lower layer is still
|
||||
// detected when the path is searched at a higher layer; otherwise it would collapse onto its target there.
|
||||
exists, resolution, err := r.img.Layers[layerIdx].SquashedTree.File(stereoscopeFile.Path(path))
|
||||
if err != nil || !exists || !resolution.HasReference() {
|
||||
return own, own, false
|
||||
}
|
||||
target, ok := r.resolveHardLinkTarget(*resolution.Reference, layerIdx)
|
||||
if !ok {
|
||||
return own, own, false
|
||||
}
|
||||
return *resolution.Reference, target, true
|
||||
}
|
||||
|
||||
// FilesByPath returns all file.References that match the given paths from any layer in the image.
|
||||
@@ -112,14 +143,13 @@ func (r *ContainerImageAllLayers) FilesByPath(paths ...string) ([]file.Location,
|
||||
}
|
||||
}
|
||||
|
||||
results, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx)
|
||||
locations, err := r.locationsByRef(*ref.Reference, path, uniqueFileIDs, idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, result := range results {
|
||||
l := file.NewLocationFromImage(path, result, r.img)
|
||||
r.annotateLocation(&l)
|
||||
uniqueLocations = append(uniqueLocations, l)
|
||||
for i := range locations {
|
||||
r.annotateLocation(&locations[i])
|
||||
uniqueLocations = append(uniqueLocations, locations[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,14 +188,13 @@ func (r *ContainerImageAllLayers) FilesByGlob(patterns ...string) ([]file.Locati
|
||||
}
|
||||
}
|
||||
|
||||
refResults, err := r.fileByRef(*result.Reference, uniqueFileIDs, idx)
|
||||
locations, err := r.locationsByRef(*result.Reference, string(result.RequestPath), uniqueFileIDs, idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, refResult := range refResults {
|
||||
l := file.NewLocationFromImage(string(result.RequestPath), refResult, r.img)
|
||||
r.annotateLocation(&l)
|
||||
uniqueLocations = append(uniqueLocations, l)
|
||||
for i := range locations {
|
||||
r.annotateLocation(&locations[i])
|
||||
uniqueLocations = append(uniqueLocations, locations[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,14 +262,13 @@ func (r *ContainerImageAllLayers) FilesByMIMEType(types ...string) ([]file.Locat
|
||||
continue
|
||||
}
|
||||
|
||||
refResults, err := r.fileByRef(*ref.Reference, uniqueFileIDs, idx)
|
||||
locations, err := r.locationsByRef(*ref.Reference, string(ref.RequestPath), uniqueFileIDs, idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, refResult := range refResults {
|
||||
l := file.NewLocationFromImage(string(ref.RequestPath), refResult, r.img)
|
||||
r.annotateLocation(&l)
|
||||
uniqueLocations = append(uniqueLocations, l)
|
||||
for i := range locations {
|
||||
r.annotateLocation(&locations[i])
|
||||
uniqueLocations = append(uniqueLocations, locations[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,6 +284,11 @@ func (r *ContainerImageAllLayers) AllLocations(ctx context.Context) <-chan file.
|
||||
tree := r.img.Layers[layerIdx].Tree
|
||||
for _, ref := range tree.AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
l := file.NewLocationFromImage(string(ref.RealPath), ref, r.img)
|
||||
// surface a hardlink as the underlying type it points to (at its own path) so image results match
|
||||
// directory results, which cannot distinguish a hardlink from a regular file.
|
||||
if targetRef, ok := r.resolveHardLinkTarget(ref, layerIdx); ok {
|
||||
l = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img)
|
||||
}
|
||||
r.annotateLocation(&l)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -273,6 +306,21 @@ func (r *ContainerImageAllLayers) FileMetadataByLocation(location file.Location)
|
||||
return fileMetadataByLocation(r.img, location)
|
||||
}
|
||||
|
||||
// resolveHardLinkTarget returns the reference of a hardlink's underlying target (resolved relative to the given layer)
|
||||
// when ref is a hardlink; ok is false otherwise. No resolution is performed for non-hardlinks (symlinks keep their
|
||||
// existing resolution semantics).
|
||||
func (r *ContainerImageAllLayers) resolveHardLinkTarget(ref stereoscopeFile.Reference, layerIdx int) (stereoscopeFile.Reference, bool) {
|
||||
metadata, err := r.img.FileCatalog.Get(ref)
|
||||
if err != nil || metadata.Type != stereoscopeFile.TypeHardLink {
|
||||
return ref, false
|
||||
}
|
||||
resolved, err := r.img.ResolveLinkByLayerSquash(ref, layerIdx)
|
||||
if err != nil || !resolved.HasReference() {
|
||||
return ref, false
|
||||
}
|
||||
return *resolved.Reference, true
|
||||
}
|
||||
|
||||
func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) {
|
||||
if !r.markVisibility || l == nil {
|
||||
return
|
||||
@@ -282,26 +330,24 @@ func (r *ContainerImageAllLayers) annotateLocation(l *file.Location) {
|
||||
annotation := file.VisibleAnnotation
|
||||
|
||||
// if we find a location for a path that matches the query (e.g. **/node_modules) but is not present in the squashed tree, skip it
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(l.RealPath, filetree.DoNotFollowDeadBasenameLinks)
|
||||
if err != nil || !ref.HasReference() {
|
||||
annotation = file.HiddenAnnotation
|
||||
} else if ref.ID() != givenRef.ID() {
|
||||
// we may have the path in the squashed tree, but this must not be in the same layer
|
||||
if !r.pathResolvesToRef(l.RealPath, givenRef) {
|
||||
annotation = file.HiddenAnnotation
|
||||
}
|
||||
|
||||
// not only should the real path to the file exist, but the way we took to get there should also exist
|
||||
// (e.g. if we are looking for /etc/passwd, but the real path is /etc/passwd -> /etc/passwd-1, then we should
|
||||
// make certain that /etc/passwd-1 exists)
|
||||
if annotation == file.VisibleAnnotation && l.AccessPath != "" {
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(l.AccessPath, filetree.DoNotFollowDeadBasenameLinks)
|
||||
if err != nil || !ref.HasReference() {
|
||||
annotation = file.HiddenAnnotation
|
||||
} else if ref.ID() != givenRef.ID() {
|
||||
// we may have the path in the squashed tree, but this must not be in the same layer
|
||||
annotation = file.HiddenAnnotation
|
||||
}
|
||||
if annotation == file.VisibleAnnotation && l.AccessPath != "" && !r.pathResolvesToRef(l.AccessPath, givenRef) {
|
||||
annotation = file.HiddenAnnotation
|
||||
}
|
||||
|
||||
l.Annotations[file.VisibleAnnotationKey] = annotation
|
||||
}
|
||||
|
||||
// pathResolvesToRef reports whether the given path in the squashed tree resolves to the given reference.
|
||||
// SearchByPath always follows basename links, so a hardlink surfaced at its own path (whose reference is its
|
||||
// target) still resolves to the target here and is correctly considered visible.
|
||||
func (r *ContainerImageAllLayers) pathResolvesToRef(path string, target stereoscopeFile.Reference) bool {
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.DoNotFollowDeadBasenameLinks)
|
||||
return err == nil && ref.HasReference() && ref.ID() == target.ID()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,17 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
for _, path := range paths {
|
||||
// if the requested path is itself a hardlink, surface it at its own path (bound to its target's content)
|
||||
// rather than collapsing it onto the target's path, so that image results are in parity with directory
|
||||
// results (which cannot tell a hardlink from a regular file).
|
||||
if ownRef, targetRef, ok := r.hardLinkAtPath(path); ok {
|
||||
if !uniqueFileIDs.Contains(ownRef) {
|
||||
uniqueFileIDs.Add(ownRef)
|
||||
uniqueLocations = append(uniqueLocations, file.NewVirtualLocationFromImage(string(ownRef.RealPath), path, targetRef, r.img))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ref, err := r.img.SquashedSearchContext.SearchByPath(path, filetree.FollowBasenameLinks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,7 +93,7 @@ func (r *ContainerImageSquash) FilesByPath(paths ...string) ([]file.Location, er
|
||||
//
|
||||
//nolint:gocognit
|
||||
func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
|
||||
uniqueCoordinates := file.NewCoordinateSet()
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
for _, pattern := range patterns {
|
||||
@@ -116,10 +127,14 @@ func (r *ContainerImageSquash) FilesByGlob(patterns ...string) ([]file.Location,
|
||||
return nil, fmt.Errorf("failed to find files by path (result=%+v): %w", result, err)
|
||||
}
|
||||
for _, resolvedLocation := range resolvedLocations {
|
||||
if uniqueFileIDs.Contains(resolvedLocation.Reference()) {
|
||||
// dedup on the surfaced coordinate rather than the underlying reference: distinct hardlinks share a
|
||||
// single target reference but each has its own real path, so a reference-based dedup would collapse
|
||||
// them back onto one entry (the exact behavior this parity fix removes). symlink resolutions keep
|
||||
// their target's real path, so they still collapse as before.
|
||||
if uniqueCoordinates.Contains(resolvedLocation.Coordinates) {
|
||||
continue
|
||||
}
|
||||
uniqueFileIDs.Add(resolvedLocation.Reference())
|
||||
uniqueCoordinates.Add(resolvedLocation.Coordinates)
|
||||
uniqueLocations = append(uniqueLocations, resolvedLocation)
|
||||
}
|
||||
}
|
||||
@@ -179,10 +194,16 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc
|
||||
go func() {
|
||||
defer close(results)
|
||||
for _, ref := range r.img.SquashedTree().AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
loc := file.NewLocationFromImage(string(ref.RealPath), ref, r.img)
|
||||
// surface a hardlink as the underlying type it points to (at its own path) so image results match
|
||||
// directory results, which cannot distinguish a hardlink from a regular file.
|
||||
if targetRef, ok := r.resolveHardLinkTarget(ref); ok {
|
||||
loc = file.NewVirtualLocationFromImage(string(ref.RealPath), string(ref.RealPath), targetRef, r.img)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case results <- file.NewLocationFromImage(string(ref.RealPath), ref, r.img):
|
||||
case results <- loc:
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -190,6 +211,37 @@ func (r *ContainerImageSquash) AllLocations(ctx context.Context) <-chan file.Loc
|
||||
return results
|
||||
}
|
||||
|
||||
// hardLinkAtPath returns the hardlink's own reference and its resolved target reference when the basename of path is a
|
||||
// hardlink. ok is false when path does not exist or is not a hardlink. The lookup does not follow the basename link so
|
||||
// that the hardlink's own path is preserved. This adds a tree walk per FilesByPath path; if it shows up in
|
||||
// profiles, fold the hardlink check into the existing SearchByPath resolution.
|
||||
func (r *ContainerImageSquash) hardLinkAtPath(path string) (stereoscopeFile.Reference, stereoscopeFile.Reference, bool) {
|
||||
var own stereoscopeFile.Reference
|
||||
exists, resolution, err := r.img.SquashedTree().File(stereoscopeFile.Path(path))
|
||||
if err != nil || !exists || !resolution.HasReference() {
|
||||
return own, own, false
|
||||
}
|
||||
target, ok := r.resolveHardLinkTarget(*resolution.Reference)
|
||||
if !ok {
|
||||
return own, own, false
|
||||
}
|
||||
return *resolution.Reference, target, true
|
||||
}
|
||||
|
||||
// resolveHardLinkTarget returns the reference of a hardlink's underlying target when ref is a hardlink; ok is false
|
||||
// otherwise. No resolution is performed for non-hardlinks (symlinks keep their existing resolution semantics).
|
||||
func (r *ContainerImageSquash) resolveHardLinkTarget(ref stereoscopeFile.Reference) (stereoscopeFile.Reference, bool) {
|
||||
metadata, err := r.img.FileCatalog.Get(ref)
|
||||
if err != nil || metadata.Type != stereoscopeFile.TypeHardLink {
|
||||
return ref, false
|
||||
}
|
||||
resolved, err := r.img.ResolveLinkByImageSquash(ref)
|
||||
if err != nil || !resolved.HasReference() {
|
||||
return ref, false
|
||||
}
|
||||
return *resolved.Reference, true
|
||||
}
|
||||
|
||||
func (r *ContainerImageSquash) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
refs, err := r.img.SquashedSearchContext.SearchByMIMEType(types...)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
@@ -574,3 +575,87 @@ func TestSquashResolver_AllLocations(t *testing.T) {
|
||||
|
||||
assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List()))
|
||||
}
|
||||
|
||||
// a hardlink should be surfaced at its own path as the underlying type it points to (a regular file with the target's
|
||||
// content and metadata), so that image results are in parity with directory results (which cannot distinguish a
|
||||
// hardlink from a regular file). this must hold for both the squashed and all-layers resolvers.
|
||||
func TestImageResolvers_Hardlinks(t *testing.T) {
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-hardlinks")
|
||||
|
||||
resolvers := map[string]file.Resolver{}
|
||||
squash, err := NewFromContainerImageSquash(img)
|
||||
require.NoError(t, err)
|
||||
resolvers["squashed"] = squash
|
||||
|
||||
allLayers, err := NewFromContainerImageAllLayers(img)
|
||||
require.NoError(t, err)
|
||||
resolvers["all-layers"] = allLayers
|
||||
|
||||
const wantContents = "hardlinked contents\n"
|
||||
|
||||
// asserts a location surfaces the hardlink at its own path as a regular file with the target's content, and is
|
||||
// not marked hidden (all-layers annotates visibility; squash leaves the annotation unset, which must not read as
|
||||
// hidden either).
|
||||
assertHardlink := func(t *testing.T, resolver file.Resolver, wantPath string, loc file.Location) {
|
||||
t.Helper()
|
||||
assert.Equal(t, wantPath, loc.RealPath, "expected the hardlink's own path, not the target's")
|
||||
assert.NotEqual(t, file.HiddenAnnotation, loc.Annotations[file.VisibleAnnotationKey], "path=%s should not be hidden", wantPath)
|
||||
|
||||
meta, err := resolver.FileMetadataByLocation(loc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, stereoscopeFile.TypeRegular, meta.Type, "path=%s", wantPath)
|
||||
|
||||
reader, err := resolver.FileContentsByLocation(loc)
|
||||
require.NoError(t, err)
|
||||
actual, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wantContents, string(actual), "path=%s", wantPath)
|
||||
}
|
||||
|
||||
for name, resolver := range resolvers {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("FilesByPath surfaces a hardlink at its own path with the target's content", func(t *testing.T) {
|
||||
for _, path := range []string{"/hardlink-a", "/hardlink-b", "/file.txt"} {
|
||||
locs, err := resolver.FilesByPath(path)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, locs, 1, "path=%s", path)
|
||||
assertHardlink(t, resolver, path, locs[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilesByGlob surfaces every hardlink in a matched set, not just one", func(t *testing.T) {
|
||||
// the whole point of #5019: a glob that matches multiple hardlinks to the same target must return
|
||||
// all of them (each at its own path), not collapse them onto a single entry.
|
||||
locs, err := resolver.FilesByGlob("**/hardlink-*")
|
||||
require.NoError(t, err)
|
||||
|
||||
byPath := map[string]file.Location{}
|
||||
for _, loc := range locs {
|
||||
_, dup := byPath[loc.RealPath]
|
||||
assert.Falsef(t, dup, "path %s emitted more than once", loc.RealPath)
|
||||
byPath[loc.RealPath] = loc
|
||||
}
|
||||
for _, path := range []string{"/hardlink-a", "/hardlink-b"} {
|
||||
loc, ok := byPath[path]
|
||||
require.Truef(t, ok, "expected glob to surface %s", path)
|
||||
assertHardlink(t, resolver, path, loc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AllLocations includes every hardlink path as a regular file exactly once", func(t *testing.T) {
|
||||
counts := map[string]int{}
|
||||
locsByPath := map[string]file.Location{}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for loc := range resolver.AllLocations(ctx) {
|
||||
counts[loc.RealPath]++
|
||||
locsByPath[loc.RealPath] = loc
|
||||
}
|
||||
for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} {
|
||||
require.Equalf(t, 1, counts[path], "expected %s to be reported exactly once by AllLocations", path)
|
||||
assertHardlink(t, resolver, path, locsByPath[path])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# LAYER 0:
|
||||
FROM busybox:1.34.0@sha256:e8e5cca392e3cf056fcdb3093e7ac2bf83fcf28b3bcf5818fe8ae71cf360c231
|
||||
|
||||
# LAYER 1: a regular file with two hardlinks to it, all created in the same layer so the layer tar contains one
|
||||
# regular file entry and two hardlink (tar TypeLink) entries pointing at it.
|
||||
RUN echo "hardlinked contents" > /file.txt && ln /file.txt /hardlink-a && ln /file.txt /hardlink-b
|
||||
|
||||
# squash representation (of the files added here; busybox base also contributes hardlinked applets under /bin)
|
||||
# .
|
||||
# ├── file.txt
|
||||
# ├── hardlink-a (hardlink -> file.txt)
|
||||
# └── hardlink-b (hardlink -> file.txt)
|
||||
@@ -32,12 +32,15 @@ type parsedData struct {
|
||||
pkg.ApkDBEntry
|
||||
}
|
||||
|
||||
const maxApkDBFieldSize = 10 * 1024 * 1024
|
||||
|
||||
// parseApkDB parses packages from a given APK "installed" flat-file DB. For more
|
||||
// information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec.
|
||||
//
|
||||
//nolint:funlen
|
||||
func parseApkDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Buffer(nil, maxApkDBFieldSize)
|
||||
|
||||
var errs error
|
||||
var apks []parsedData
|
||||
|
||||
@@ -719,6 +719,25 @@ func Test_processChecksum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseApkDBAllowsLargeFieldValues(t *testing.T) {
|
||||
contents := strings.Join([]string{
|
||||
"P:large-description",
|
||||
"V:1.0-r0",
|
||||
"A:x86_64",
|
||||
"S:1",
|
||||
"I:1",
|
||||
"T:" + strings.Repeat("a", 70*1024),
|
||||
"",
|
||||
}, "\n")
|
||||
reader := file.NewLocationReadCloser(file.NewLocation("large-installed-db"), io.NopCloser(strings.NewReader(contents)))
|
||||
|
||||
pkgs, _, err := parseApkDB(context.Background(), nil, new(generic.Environment), reader)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pkgs, 1)
|
||||
assert.Equal(t, "large-description", pkgs[0].Name)
|
||||
}
|
||||
|
||||
func Test_parseApkDB_expectedPkgNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
|
||||
@@ -433,6 +433,16 @@ catalogers:
|
||||
cpes:
|
||||
- cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*
|
||||
type: BinaryPkg
|
||||
- method: glob
|
||||
criteria:
|
||||
- '**/bun'
|
||||
packages:
|
||||
- class: bun-binary
|
||||
name: bun
|
||||
purl: pkg:generic/bun
|
||||
cpes:
|
||||
- cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*
|
||||
type: BinaryPkg
|
||||
- method: glob
|
||||
criteria:
|
||||
- '**/ghc*'
|
||||
|
||||
@@ -1499,6 +1499,28 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/1.11.3/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "deno",
|
||||
Version: "1.11.3",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/deno@1.11.3",
|
||||
Locations: locations("deno"),
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/1.11.4/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "deno",
|
||||
Version: "1.11.4",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/deno@1.11.4",
|
||||
Locations: locations("deno"),
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/1.16.4/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
@@ -1565,6 +1587,61 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "deno/2.9.2/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "deno",
|
||||
Version: "2.9.2",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/deno@2.9.2",
|
||||
Locations: locations("deno"),
|
||||
Metadata: metadata("deno-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/0.5.9/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "0.5.9",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@0.5.9",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/1.0.0/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "1.0.0",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@1.0.0",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/1.1.0/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "1.1.0",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@1.1.0",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "bun/1.3.5/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
Name: "bun",
|
||||
Version: "1.3.5",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/bun@1.3.5",
|
||||
Locations: locations("bun"),
|
||||
Metadata: metadata("bun-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "haskell-ghc/7.10.3/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
@@ -2992,8 +3069,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||
Name: "busybox",
|
||||
Version: "1.35.0",
|
||||
PURL: "pkg:generic/busybox@1.35.0",
|
||||
Locations: locations("/bin/["),
|
||||
Metadata: metadata("busybox-binary", "/bin/[", "/bin/busybox"),
|
||||
Locations: locations("/bin/busybox"),
|
||||
Metadata: metadata("busybox-binary", "/bin/busybox"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -588,11 +588,20 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
Class: "deno-binary",
|
||||
FileGlob: "**/deno",
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
m.FileContentsVersionMatcher(
|
||||
// Deno/2.6.3Deno/
|
||||
// Deno/1.41.0cli/
|
||||
`Deno/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)(Deno/|cli/)`,
|
||||
),
|
||||
m.FileContentsVersionMatcher(
|
||||
// Deno/2.6.3
|
||||
// Deno/1.41.0
|
||||
`Deno/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
|
||||
),
|
||||
m.FileContentsVersionMatcher(
|
||||
// cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlwindows
|
||||
`deno-canary/[0-9a-z]{40}(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
|
||||
),
|
||||
m.FileContentsVersionMatcher(
|
||||
// deno::tools::standalonedeno-65db94feba9d4d51a09b74629f566dbc90484fbarelease/v1.29.4windows
|
||||
// cli/tools/standalone.rsdeno-74064c9d8c222b33b2a552ea0af1054f57002a96release/v1.28.3windows
|
||||
@@ -611,6 +620,20 @@ func DefaultClassifiers() []binutils.Classifier {
|
||||
PURL: mustPURL("pkg:generic/deno@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
|
||||
},
|
||||
{
|
||||
Class: "bun-binary",
|
||||
FileGlob: "**/bun",
|
||||
EvidenceMatcher: binutils.MatchAny(
|
||||
// bun 1.1.0
|
||||
// Bun v1.0.0
|
||||
m.FileContentsVersionMatcher(`(?m)[Bb]un v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
// bun-1.1.0
|
||||
m.FileContentsVersionMatcher(`(?m)bun-(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
),
|
||||
Package: "bun",
|
||||
PURL: mustPURL("pkg:generic/bun@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "haskell-ghc-binary",
|
||||
FileGlob: "**/ghc*",
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000001
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000001
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v0.5.9
|
||||
bun-0.5.9
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000002
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000002
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v1.0.0
|
||||
bun-1.0.0
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000003
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000003
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v1.1.0
|
||||
bun-1.1.0
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: bun
|
||||
offset: 1000000
|
||||
length: 100
|
||||
snippetSha256: 0000000000000000000000000000000000000000000000000000000000000004
|
||||
fileSha256: 0000000000000000000000000000000000000000000000000000000000000004
|
||||
|
||||
### byte snippet to follow ###
|
||||
]
|
||||
|
||||
Bun v1.3.5
|
||||
bun-1.3.5
|
||||
JavaScript runtime for developing, testing, and bundling
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
name: deno
|
||||
offset: 4221667
|
||||
length: 120
|
||||
snippetSha256: 89509d15b73fd2d901b20ebfe82c67fd27f70e7b01dcbf376e7e80e6c80f97d9
|
||||
fileSha256: 530a95c93ddfa41bd4d72d665e677d5af9a4275f1520e638305e60af5083a6d6
|
||||
|
||||
### byte snippet to follow ###
|
||||
g ctrl+d or close()
|
||||
Error: Uncaught cli/tools/standalone.rsdeno-canary/f4bed1081456089559c82441a13c4fb700840cac1.11.3dlw
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
name: deno
|
||||
offset: 4225763
|
||||
length: 120
|
||||
snippetSha256: b47e8cf36bdf93e4bc63a2d7bd0a5d759975f12751a66908661777eba7e90025
|
||||
fileSha256: 1a55c23bb0b666d1b26d9d16f3a9d27210b6b13421031f7f18ff18abfcb8664f
|
||||
|
||||
### byte snippet to follow ###
|
||||
g ctrl+d or close()
|
||||
Error: Uncaught cli/tools/standalone.rsdeno-canary/ed16f4332263e81bf00a39ebc2200291b1fcbb8e1.11.4dlw
|
||||
BIN
Binary file not shown.
+24
@@ -140,6 +140,22 @@ from-images:
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 1.11.3
|
||||
images:
|
||||
- ref: denoland/deno:1.11.3@sha256:340d071dc71c8403c75713694db04f59b8cb7f6c7244798cd7447d55f2750596
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 1.11.4
|
||||
images:
|
||||
- ref: denoland/deno:1.11.4@sha256:8e9a449a50b71c779ad1c10ec79055b20a331e78a3852818fac47bc3e85b49f4
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 1.16.4
|
||||
images:
|
||||
@@ -188,6 +204,14 @@ from-images:
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
- name: deno
|
||||
version: 2.9.2
|
||||
images:
|
||||
- ref: denoland/deno:ubuntu-2.9.2@sha256:0d27941f871700ebd95953c36fb1a817135c20f5afe3a645935672dfab72b2e9
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/deno
|
||||
|
||||
# haskell:7.10.3 detail information is missing.
|
||||
- name: haskell-ghc
|
||||
version: 7.10.3
|
||||
|
||||
@@ -156,7 +156,7 @@ func parseErlangString(data []byte, i *int) (erlangNode, error) {
|
||||
}
|
||||
if c == '\\' {
|
||||
*i++
|
||||
if len(data) >= *i {
|
||||
if *i >= len(data) {
|
||||
return node(nil), fmt.Errorf("invalid escape without closed string at %d", *i)
|
||||
}
|
||||
c = data[*i]
|
||||
|
||||
@@ -96,6 +96,11 @@ func Test_parseErlang(t *testing.T) {
|
||||
{ foo, bar }
|
||||
]}`,
|
||||
},
|
||||
{
|
||||
name: "string with an escaped quote",
|
||||
content: `
|
||||
{escaped, ["a\"b"]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -112,3 +117,14 @@ func Test_parseErlang(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseErlangString_escapedByte(t *testing.T) {
|
||||
// a backslash escape mid-string used to always error out, since the bounds
|
||||
// check at the escape site was inverted (len(data) >= *i is true for
|
||||
// almost every position, not just an out-of-range one).
|
||||
data := []byte(`"a\"b"`)
|
||||
i := 0
|
||||
got, err := parseErlangString(data, &i)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `a"b`, got.String())
|
||||
}
|
||||
|
||||
@@ -25,8 +25,11 @@ configs: # AUTO-GENERATED - config structs and their fields
|
||||
description: NoProxy is a list of glob patterns that match go module names that should not be fetched from the go proxy. When not set, syft will use the GOPRIVATE and GONOPROXY env vars.
|
||||
app_key: golang.no-proxy
|
||||
- key: CaptureSymbols
|
||||
description: CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib).
|
||||
description: CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module under golang.org/x/), and "all" (all module packages plus stdlib).
|
||||
app_key: golang.capture-symbols
|
||||
- key: CaptureSymbolsModules
|
||||
description: 'CaptureSymbolsModules is a list of glob patterns (doublestar syntax, where ** crosses path separators and * does not) matched against go module paths. Matching modules get symbols in addition to whatever CaptureSymbols selects, so this can only widen the selection and never narrow it. It has no effect under the "none" scope. A trailing major version suffix is not part of a module''s identity, so a pattern matches with or without it: both "github.com/foo/bar" and "github.com/foo/*" select "github.com/foo/bar/v2". A pattern that spells out a suffix selects only that major version.'
|
||||
app_key: golang.capture-symbols-modules
|
||||
catalogers:
|
||||
- ecosystem: go # MANUAL
|
||||
name: go-module-binary-cataloger # AUTO-GENERATED
|
||||
|
||||
@@ -51,10 +51,20 @@ type CatalogerConfig struct {
|
||||
MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`
|
||||
|
||||
// CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are
|
||||
// "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib).
|
||||
// "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module
|
||||
// under golang.org/x/), and "all" (all module packages plus stdlib).
|
||||
// app-config: golang.capture-symbols
|
||||
CaptureSymbols cataloging.SymbolScope `yaml:"capture-symbols" json:"capture-symbols" mapstructure:"capture-symbols"`
|
||||
|
||||
// CaptureSymbolsModules is a list of glob patterns (doublestar syntax, where ** crosses path separators
|
||||
// and * does not) matched against go module paths. Matching modules get symbols in addition to whatever
|
||||
// CaptureSymbols selects, so this can only widen the selection and never narrow it. It has no effect
|
||||
// under the "none" scope. A trailing major version suffix is not part of a module's identity, so a
|
||||
// pattern matches with or without it: both "github.com/foo/bar" and "github.com/foo/*" select
|
||||
// "github.com/foo/bar/v2". A pattern that spells out a suffix selects only that major version.
|
||||
// app-config: golang.capture-symbols-modules
|
||||
CaptureSymbolsModules []string `yaml:"capture-symbols-modules,omitempty" json:"capture-symbols-modules,omitempty" mapstructure:"capture-symbols-modules"`
|
||||
|
||||
// Whether to use the golang.org/x/tools/go/packages, which executes golang tooling found on the path in addition to potential network access
|
||||
UsePackagesLib bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
|
||||
}
|
||||
@@ -196,6 +206,11 @@ func (g CatalogerConfig) WithCaptureSymbols(input cataloging.SymbolScope) Catalo
|
||||
return g
|
||||
}
|
||||
|
||||
func (g CatalogerConfig) WithCaptureSymbolsModules(input []string) CatalogerConfig {
|
||||
g.CaptureSymbolsModules = input
|
||||
return g
|
||||
}
|
||||
|
||||
func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig {
|
||||
g.UsePackagesLib = useLib
|
||||
return g
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -51,7 +50,7 @@ const devel = "(devel)"
|
||||
type goBinaryCataloger struct {
|
||||
licenseResolver goLicenseResolver
|
||||
mainModuleVersion MainModuleVersionConfig
|
||||
symbolScope cataloging.SymbolScope
|
||||
symbolSelector symbolSelector
|
||||
|
||||
// stdlibSymbols holds the standard-library function symbols discovered per binary (keyed by the
|
||||
// binary's location), grouped by import path, populated during parsing and consumed by stdlibProcessor
|
||||
@@ -65,7 +64,7 @@ func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger {
|
||||
return &goBinaryCataloger{
|
||||
licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts),
|
||||
mainModuleVersion: opts.MainModuleVersion,
|
||||
symbolScope: opts.CaptureSymbols,
|
||||
symbolSelector: newSymbolSelector(opts.CaptureSymbols, opts.CaptureSymbolsModules),
|
||||
stdlibSymbols: make(map[file.Coordinates]map[string][]string),
|
||||
}
|
||||
}
|
||||
@@ -121,7 +120,7 @@ func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Res
|
||||
}
|
||||
defer internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
|
||||
|
||||
mods, errs := scanFile(reader.Location, unionReader, c.symbolScope != cataloging.SymbolScopeNone)
|
||||
mods, errs := scanFile(reader.Location, unionReader, c.symbolSelector.enabled())
|
||||
|
||||
var rels []artifact.Relationship
|
||||
for _, mod := range mods {
|
||||
@@ -184,11 +183,10 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
|
||||
symbolsByModule, stdlibSymbols := moduleSymbols(mod.symbols, &mod.Main, mod.Deps)
|
||||
c.recordStdlibSymbols(location.Coordinates, stdlibSymbols)
|
||||
|
||||
if c.symbolScope != cataloging.SymbolScopeAll {
|
||||
// only the "all" scope attaches per-module symbols; for the "stdlib" scope we keep just the
|
||||
// recorded stdlib symbols. nil map lookups below then yield nil symbol lists for each module.
|
||||
symbolsByModule = nil
|
||||
}
|
||||
// keep only the modules the selector covers; the main module goes through the same map, so this
|
||||
// incidentally decides the main module too (which is intended: it is treated like any dependency).
|
||||
// unselected modules fall out entirely, so the lookups below yield nil rather than an empty map.
|
||||
symbolsByModule = c.symbolSelector.filter(symbolsByModule)
|
||||
|
||||
var pkgs []pkg.Package
|
||||
for _, dep := range mod.Deps {
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -1441,9 +1443,12 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scope cataloging.SymbolScope
|
||||
modules []string
|
||||
extraDeps []*debug.Module
|
||||
symbols []binarySymbol
|
||||
wantMainSyms map[string][]string
|
||||
wantDepSyms map[string][]string
|
||||
wantExtraSyms []map[string][]string
|
||||
wantStdlibSyms map[string][]string
|
||||
}{
|
||||
{
|
||||
@@ -1468,6 +1473,84 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
|
||||
},
|
||||
wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
|
||||
},
|
||||
{
|
||||
// golang.org/x/net is reached through a vendored import path here, which must still be
|
||||
// attributed to (and selected by) the module that owns it
|
||||
name: "extended-stdlib captures golang.org/x modules and stdlib only",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
extraDeps: extendedDeps,
|
||||
symbols: slices.Concat(populatedSymbols, extendedSymbols),
|
||||
wantExtraSyms: []map[string][]string{
|
||||
{"golang.org/x/net/http2": {"NewClientConn"}},
|
||||
nil,
|
||||
},
|
||||
wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
|
||||
},
|
||||
{
|
||||
name: "module patterns widen extended-stdlib",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modules: []string{"github.com/klauspost/**"},
|
||||
extraDeps: extendedDeps,
|
||||
symbols: slices.Concat(populatedSymbols, extendedSymbols),
|
||||
wantExtraSyms: []map[string][]string{
|
||||
{"golang.org/x/net/http2": {"NewClientConn"}},
|
||||
{"github.com/klauspost/compress/zstd": {"NewReader"}},
|
||||
},
|
||||
wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
|
||||
},
|
||||
{
|
||||
name: "module patterns can select the main module",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/anchore/**"},
|
||||
extraDeps: extendedDeps,
|
||||
symbols: slices.Concat(populatedSymbols, extendedSymbols),
|
||||
// the main package is keyed by the "main" import path the linker assigns, not its real path
|
||||
wantMainSyms: map[string][]string{"main": {"main"}},
|
||||
wantExtraSyms: []map[string][]string{nil, nil},
|
||||
wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
|
||||
},
|
||||
{
|
||||
name: "module patterns are inert under the none scope",
|
||||
scope: cataloging.SymbolScopeNone,
|
||||
modules: []string{"github.com/**", "golang.org/x/**"},
|
||||
extraDeps: extendedDeps,
|
||||
// scanFile never runs under "none", so the build info carries no symbols to begin with
|
||||
symbols: nil,
|
||||
wantExtraSyms: []map[string][]string{nil, nil},
|
||||
},
|
||||
{
|
||||
// all short-circuits ahead of the pattern walk, so a narrow module pattern list cannot subtract
|
||||
// from it. this is also the guard on that short-circuit still existing.
|
||||
name: "module patterns cannot narrow the all scope",
|
||||
scope: cataloging.SymbolScopeAll,
|
||||
modules: []string{"golang.org/x/**"},
|
||||
extraDeps: extendedDeps,
|
||||
symbols: slices.Concat(populatedSymbols, extendedSymbols),
|
||||
wantMainSyms: map[string][]string{"main": {"main"}},
|
||||
wantDepSyms: map[string][]string{
|
||||
"github.com/foo/bar": {"Parse"},
|
||||
"github.com/foo/bar/baz": {"Helper"},
|
||||
},
|
||||
wantExtraSyms: []map[string][]string{
|
||||
{"golang.org/x/net/http2": {"NewClientConn"}},
|
||||
{"github.com/klauspost/compress/zstd": {"NewReader"}},
|
||||
},
|
||||
wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
|
||||
},
|
||||
{
|
||||
// a module pattern that restates what the preset already covers must not duplicate or drop anything:
|
||||
// selection is a per-module boolean, so overlap is idempotent
|
||||
name: "a module pattern overlapping the preset changes nothing",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modules: []string{"golang.org/x/**"},
|
||||
extraDeps: extendedDeps,
|
||||
symbols: slices.Concat(populatedSymbols, extendedSymbols),
|
||||
wantExtraSyms: []map[string][]string{
|
||||
{"golang.org/x/net/http2": {"NewClientConn"}},
|
||||
nil,
|
||||
},
|
||||
wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -1476,27 +1559,52 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
|
||||
BuildInfo: &debug.BuildInfo{
|
||||
GoVersion: "go1.22.0",
|
||||
Main: debug.Module{Path: "github.com/anchore/syft", Version: "v1.0.0"},
|
||||
Deps: []*debug.Module{{Path: "github.com/foo/bar", Version: "v1.2.3"}},
|
||||
Deps: append([]*debug.Module{{Path: "github.com/foo/bar", Version: "v1.2.3"}}, tt.extraDeps...),
|
||||
},
|
||||
arch: "amd64",
|
||||
symbols: tt.symbols,
|
||||
}
|
||||
|
||||
c := newGoBinaryCataloger(CatalogerConfig{CaptureSymbols: tt.scope})
|
||||
c := newGoBinaryCataloger(CatalogerConfig{CaptureSymbols: tt.scope, CaptureSymbolsModules: tt.modules})
|
||||
reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader("")))
|
||||
require.NoError(t, err)
|
||||
|
||||
mainPkg, pkgs := c.buildGoPkgInfo(context.Background(), fileresolver.Empty{}, location, mod, mod.arch, reader)
|
||||
require.NotNil(t, mainPkg)
|
||||
require.Len(t, pkgs, 1)
|
||||
require.Len(t, pkgs, 1+len(tt.extraDeps))
|
||||
|
||||
assert.Equal(t, tt.wantMainSyms, mainPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "main module symbols")
|
||||
assert.Equal(t, tt.wantDepSyms, pkgs[0].Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "dependency symbols")
|
||||
for i, want := range tt.wantExtraSyms {
|
||||
assert.Equal(t, want, pkgs[1+i].Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "symbols for %s", pkgs[1+i].Name)
|
||||
}
|
||||
assert.Equal(t, tt.wantStdlibSyms, c.stdlibSymbolsFor(location.Coordinates), "recorded stdlib symbols")
|
||||
|
||||
// a module selected by nothing must carry no symbols key at all: assert on the serialized form,
|
||||
// since an empty (non-nil) map would still emit "symbols":{} despite the omitempty tag
|
||||
for _, p := range append([]pkg.Package{*mainPkg}, pkgs...) {
|
||||
encoded, err := json.Marshal(p.Metadata)
|
||||
require.NoError(t, err)
|
||||
if p.Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols == nil {
|
||||
assert.NotContains(t, string(encoded), `"symbols"`, "expected no symbols key for %s", p.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
extendedDeps = []*debug.Module{
|
||||
{Path: "golang.org/x/net", Version: "v0.30.0"},
|
||||
{Path: "github.com/klauspost/compress", Version: "v1.17.0"},
|
||||
}
|
||||
|
||||
extendedSymbols = []binarySymbol{
|
||||
{packagePath: "vendor/golang.org/x/net/http2", name: "vendor/golang.org/x/net/http2.NewClientConn"},
|
||||
{packagePath: "github.com/klauspost/compress/zstd", name: "github.com/klauspost/compress/zstd.NewReader"},
|
||||
}
|
||||
)
|
||||
|
||||
// Test_recordStdlibSymbols_merge covers the merge path where the same binary location records stdlib
|
||||
// symbols more than once. This happens in production for universal/fat Mach-O binaries: scanFile yields
|
||||
// one build info per architecture and each is recorded under the same location coordinates.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/kastenhq/goversion/version"
|
||||
|
||||
@@ -51,6 +52,7 @@ func scanFile(location file.Location, reader unionreader.UnionReader, captureSym
|
||||
// we can still catalog packages, even if we can't get the crypto information
|
||||
errs = unknown.Appendf(errs, location, "unable to read golang version info: %w", err)
|
||||
}
|
||||
v = append(v, getNativeFIPSSettings(bi.Settings)...)
|
||||
arch := getGOARCH(bi.Settings)
|
||||
if arch == "" {
|
||||
arch, err = getGOARCHFromBin(r)
|
||||
@@ -101,6 +103,25 @@ func getCryptoSettingsFromVersion(v version.Version) []string {
|
||||
return cryptoSettings
|
||||
}
|
||||
|
||||
func getNativeFIPSSettings(settings []debug.BuildSetting) []string {
|
||||
var cryptoSettings []string
|
||||
for _, s := range settings {
|
||||
switch s.Key {
|
||||
case "GOFIPS140":
|
||||
if s.Value != "" {
|
||||
cryptoSettings = append(cryptoSettings, "GOFIPS140="+s.Value)
|
||||
}
|
||||
case "DefaultGODEBUG":
|
||||
for _, kv := range strings.Split(s.Value, ",") {
|
||||
if setting, val, ok := strings.Cut(kv, "="); ok && setting == "fips140" {
|
||||
cryptoSettings = append(cryptoSettings, "GODEBUG=fips140="+val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cryptoSettings
|
||||
}
|
||||
|
||||
func getBuildInfo(r io.ReaderAt, location file.Location) (bi *debug.BuildInfo, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
||||
@@ -110,3 +110,50 @@ func Test_getCryptoSettingsFromVersion(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getNativeFIPSSettings(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
settings []debug.BuildSetting
|
||||
result []string
|
||||
}{
|
||||
{
|
||||
name: "not set",
|
||||
settings: []debug.BuildSetting{{Key: "GOARCH", Value: "arm64"}},
|
||||
result: nil,
|
||||
},
|
||||
{
|
||||
name: "GOFIPS140=off is reported verbatim",
|
||||
settings: []debug.BuildSetting{{Key: "GOFIPS140", Value: "off"}},
|
||||
result: []string{"GOFIPS140=off"},
|
||||
},
|
||||
{
|
||||
name: "pinned module version with mode on",
|
||||
settings: []debug.BuildSetting{
|
||||
{Key: "GOFIPS140", Value: "v1.0.0"},
|
||||
{Key: "DefaultGODEBUG", Value: "fips140=on"},
|
||||
},
|
||||
result: []string{"GOFIPS140=v1.0.0", "GODEBUG=fips140=on"},
|
||||
},
|
||||
{ // GOFIPS140=latest enables FIPS mode without pinning a module version
|
||||
name: "latest with mode on",
|
||||
settings: []debug.BuildSetting{
|
||||
{Key: "GOFIPS140", Value: "latest"},
|
||||
{Key: "DefaultGODEBUG", Value: "fips140=on"},
|
||||
},
|
||||
result: []string{"GOFIPS140=latest", "GODEBUG=fips140=on"},
|
||||
},
|
||||
{ // fips140 is one entry among many in DefaultGODEBUG
|
||||
name: "fips140 among other godebug defaults",
|
||||
settings: []debug.BuildSetting{
|
||||
{Key: "DefaultGODEBUG", Value: "asynctimerchan=1,fips140=on,tlssha1=1"},
|
||||
},
|
||||
result: []string{"GODEBUG=fips140=on"},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res := getNativeFIPSSettings(tt.settings)
|
||||
assert.ElementsMatch(t, res, tt.result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"golang.org/x/mod/module"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
)
|
||||
|
||||
// scopePatterns maps a capture-symbols scope onto the go module path globs it selects. The "none" and
|
||||
// "all" scopes are intentionally absent: both are answered without matching. The "stdlib" scope selects
|
||||
// no modules at all (the synthetic stdlib package is not a module and is handled separately).
|
||||
var scopePatterns = map[cataloging.SymbolScope][]string{
|
||||
cataloging.SymbolScopeExtendedStdlib: {"golang.org/x/**"},
|
||||
}
|
||||
|
||||
// symbolSelector decides which go module paths get function symbols attached to their metadata. The scope
|
||||
// preset and any user-supplied module patterns are compiled into a single glob list so there is exactly
|
||||
// one matcher answering "does this module path get symbols" (two matchers over the same subject drift).
|
||||
type symbolSelector struct {
|
||||
scope cataloging.SymbolScope
|
||||
patterns []string
|
||||
}
|
||||
|
||||
func newSymbolSelector(scope cataloging.SymbolScope, modules []string) symbolSelector {
|
||||
// normalize here rather than trusting the caller: the CLI runs Parse in PostLoad, but a library
|
||||
// consumer setting CaptureSymbols directly (or via WithCaptureSymbols) does not, and an unnormalized
|
||||
// value falls through both switches below into stdlib-only capture rather than the intended scope.
|
||||
scope = scope.Parse()
|
||||
|
||||
var patterns []string
|
||||
for _, pattern := range slices.Concat(scopePatterns[scope], modules) {
|
||||
if !doublestar.ValidatePattern(pattern) {
|
||||
// a typo in a filter that decides what gets vulnerability-scanned must not pass quietly:
|
||||
// someone would believe they captured symbols they did not. Drop just this pattern and keep
|
||||
// the rest of the selection in force.
|
||||
log.WithFields("pattern", pattern).Warn("ignoring malformed golang capture-symbols-modules pattern")
|
||||
continue
|
||||
}
|
||||
patterns = append(patterns, pattern)
|
||||
}
|
||||
return symbolSelector{scope: scope, patterns: patterns}
|
||||
}
|
||||
|
||||
// enabled reports whether symbols should be extracted from the binary at all.
|
||||
func (s symbolSelector) enabled() bool {
|
||||
return s.scope != cataloging.SymbolScopeNone
|
||||
}
|
||||
|
||||
// selects reports whether the given go module path gets symbols. The binary's own main module is treated
|
||||
// exactly like a dependency.
|
||||
func (s symbolSelector) selects(modulePath string) bool {
|
||||
switch s.scope {
|
||||
case cataloging.SymbolScopeNone:
|
||||
return false
|
||||
case cataloging.SymbolScopeAll:
|
||||
return true
|
||||
}
|
||||
|
||||
// a major version suffix is part of a module's path but not part of its identity, so patterns are matched
|
||||
// against the path both with and without it: `github.com/foo/bar` and `github.com/foo/*` each select
|
||||
// `github.com/foo/bar/v2`, which is what whoever wrote either one meant, and a config does not quietly
|
||||
// stop covering a module the day it bumps a major version. Spelling a suffix out in the pattern still
|
||||
// selects that major version alone, since the unsuffixed path cannot match a pattern carrying one.
|
||||
// Only a trailing suffix is a version: in `github.com/foo/v2/bar` the `v2` is an ordinary path element,
|
||||
// and SplitPathVersion leaves it there.
|
||||
unversioned, major, ok := module.SplitPathVersion(modulePath)
|
||||
versioned := ok && major != ""
|
||||
|
||||
for _, pattern := range s.patterns {
|
||||
if globMatches(pattern, modulePath) {
|
||||
return true
|
||||
}
|
||||
if versioned && globMatches(pattern, unversioned) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// globMatches treats a pattern that cannot compile as no match, which newSymbolSelector has already warned about.
|
||||
func globMatches(pattern, modulePath string) bool {
|
||||
matched, err := doublestar.Match(pattern, modulePath)
|
||||
if err != nil {
|
||||
// unreachable: newSymbolSelector rejects (and warns about) patterns that cannot compile
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
// filter drops the entries of a module-path-keyed symbol map that the selector does not select. It returns
|
||||
// nil when nothing is selected so callers attach no symbols map at all (rather than an empty one, which
|
||||
// would defeat the omitempty JSON tag).
|
||||
func (s symbolSelector) filter(byModule map[string]map[string][]string) map[string]map[string][]string {
|
||||
switch s.scope {
|
||||
case cataloging.SymbolScopeNone:
|
||||
return nil
|
||||
case cataloging.SymbolScopeAll:
|
||||
return byModule
|
||||
}
|
||||
for modulePath := range byModule {
|
||||
if !s.selects(modulePath) {
|
||||
delete(byModule, modulePath)
|
||||
}
|
||||
}
|
||||
if len(byModule) == 0 {
|
||||
return nil
|
||||
}
|
||||
return byModule
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
)
|
||||
|
||||
func Test_symbolSelector_selects(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scope cataloging.SymbolScope
|
||||
modules []string
|
||||
modulePath string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "none selects nothing",
|
||||
scope: cataloging.SymbolScopeNone,
|
||||
modulePath: "github.com/foo/bar",
|
||||
},
|
||||
{
|
||||
name: "none is inert even with module patterns",
|
||||
scope: cataloging.SymbolScopeNone,
|
||||
modules: []string{"github.com/foo/**"},
|
||||
modulePath: "github.com/foo/bar",
|
||||
},
|
||||
{
|
||||
name: "all selects everything",
|
||||
scope: cataloging.SymbolScopeAll,
|
||||
modulePath: "github.com/foo/bar",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "stdlib selects no modules",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modulePath: "golang.org/x/crypto",
|
||||
},
|
||||
{
|
||||
name: "extended-stdlib matches a golang.org/x module",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modulePath: "golang.org/x/crypto",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// ** crosses path separators, which is what lets one pattern cover the whole subtree
|
||||
name: "extended-stdlib matches a nested golang.org/x module",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modulePath: "golang.org/x/tools/gopls",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// the pattern must not match on a bare prefix, only at a path boundary
|
||||
name: "extended-stdlib does not match golang.org/xtra",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modulePath: "golang.org/xtra",
|
||||
},
|
||||
{
|
||||
name: "extended-stdlib does not match an unrelated module",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modulePath: "github.com/klauspost/compress",
|
||||
},
|
||||
{
|
||||
name: "single star matches one path segment",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/*"},
|
||||
modulePath: "github.com/klauspost/compress",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// a single star does not cross "/", but a major version suffix is not a path segment for this
|
||||
// purpose: the module is matched with the suffix stripped as well, so a config written before a
|
||||
// major bump keeps covering the module after it
|
||||
name: "single star reaches across a major version suffix",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/*"},
|
||||
modulePath: "github.com/klauspost/compress/v2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "single star does not cross a separator that is not a version suffix",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/*"},
|
||||
modulePath: "github.com/klauspost/compress/internal/thing",
|
||||
},
|
||||
{
|
||||
// only a trailing suffix is a version. here "v2" is an ordinary path element naming the major
|
||||
// subdirectory a nested module lives in, so it is matched literally and ** is the way to reach it
|
||||
name: "a mid-path version-like element is an ordinary segment",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/anchore/*/thing"},
|
||||
modulePath: "github.com/anchore/syft/v2/thing",
|
||||
},
|
||||
{
|
||||
name: "doublestar reaches a mid-path version-like element",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/anchore/**/thing"},
|
||||
modulePath: "github.com/anchore/syft/v2/thing",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "doublestar crosses a separator",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/**"},
|
||||
modulePath: "github.com/klauspost/compress/v2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "an exact path covers every major version of that module",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/compress"},
|
||||
modulePath: "github.com/klauspost/compress/v2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// spelling the suffix out is how a single major version is targeted: v1's path carries no suffix,
|
||||
// so there is nothing for a suffixed pattern to match
|
||||
name: "a pattern naming a major version selects only that one",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/compress/v2"},
|
||||
modulePath: "github.com/klauspost/compress",
|
||||
},
|
||||
{
|
||||
name: "a pattern naming a major version selects it",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/compress/v2"},
|
||||
modulePath: "github.com/klauspost/compress/v2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// gopkg.in spells the major version as a .vN suffix on the last element, which SplitPathVersion
|
||||
// understands, so it strips the same way
|
||||
name: "gopkg.in style suffixes strip too",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"gopkg.in/yaml"},
|
||||
modulePath: "gopkg.in/yaml.v2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// /v1 and /v0 are not valid major version suffixes, so this is not a versioned path at all and
|
||||
// nothing is stripped from it
|
||||
name: "a v1 element is not a version suffix",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"github.com/klauspost/compress"},
|
||||
modulePath: "github.com/klauspost/compress/v1",
|
||||
},
|
||||
{
|
||||
name: "module patterns widen a preset",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modules: []string{"github.com/klauspost/**"},
|
||||
modulePath: "github.com/klauspost/compress",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "module patterns cannot narrow a preset",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modules: []string{"github.com/klauspost/**"},
|
||||
modulePath: "golang.org/x/net",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exact module path",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{"google.golang.org/grpc"},
|
||||
modulePath: "google.golang.org/grpc",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "an empty module pattern list is a no-op",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
modules: []string{},
|
||||
modulePath: "github.com/foo/bar",
|
||||
},
|
||||
{
|
||||
// all short-circuits before any matching, so a module pattern list cannot subtract from it
|
||||
name: "module patterns cannot narrow all",
|
||||
scope: cataloging.SymbolScopeAll,
|
||||
modules: []string{"github.com/klauspost/**"},
|
||||
modulePath: "github.com/foo/bar",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// selection is a per-module boolean, so a pattern overlapping the preset is idempotent
|
||||
name: "a module pattern overlapping the preset is idempotent",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modules: []string{"golang.org/x/**"},
|
||||
modulePath: "golang.org/x/net",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// library consumers set CaptureSymbols directly without going through PostLoad, so the
|
||||
// selector normalizes rather than falling through to stdlib-only capture
|
||||
name: "unnormalized scope is parsed",
|
||||
scope: "All",
|
||||
modulePath: "github.com/foo/bar",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unrecognized scope resolves to none",
|
||||
scope: "bogus",
|
||||
modules: []string{"github.com/foo/**"},
|
||||
modulePath: "github.com/foo/bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, newSymbolSelector(tt.scope, tt.modules).selects(tt.modulePath))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// a malformed pattern must be dropped at construction, leaving the remaining patterns in force rather
|
||||
// than aborting the selection or silently matching nothing
|
||||
func Test_symbolSelector_malformedPattern(t *testing.T) {
|
||||
s := newSymbolSelector(cataloging.SymbolScopeExtendedStdlib, []string{"[", "github.com/klauspost/**"})
|
||||
|
||||
require.Equal(t, []string{"golang.org/x/**", "github.com/klauspost/**"}, s.patterns)
|
||||
assert.True(t, s.selects("golang.org/x/net"), "preset still applies")
|
||||
assert.True(t, s.selects("github.com/klauspost/compress"), "valid sibling pattern still applies")
|
||||
assert.False(t, s.selects("github.com/foo"), "malformed pattern selects nothing")
|
||||
}
|
||||
|
||||
func Test_symbolSelector_filter(t *testing.T) {
|
||||
symbols := func() map[string]map[string][]string {
|
||||
return map[string]map[string][]string{
|
||||
"golang.org/x/net": {"golang.org/x/net/http2": {"NewClientConn"}},
|
||||
"github.com/klauspost/compress": {"github.com/klauspost/compress/zstd": {"NewReader"}},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scope cataloging.SymbolScope
|
||||
modules []string
|
||||
want []string // remaining module paths
|
||||
}{
|
||||
{
|
||||
name: "none drops everything",
|
||||
scope: cataloging.SymbolScopeNone,
|
||||
},
|
||||
{
|
||||
name: "stdlib drops every module",
|
||||
scope: cataloging.SymbolScopeStdlib,
|
||||
},
|
||||
{
|
||||
name: "extended-stdlib keeps only golang.org/x",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
want: []string{"golang.org/x/net"},
|
||||
},
|
||||
{
|
||||
name: "module patterns widen the selection",
|
||||
scope: cataloging.SymbolScopeExtendedStdlib,
|
||||
modules: []string{"github.com/klauspost/**"},
|
||||
want: []string{"golang.org/x/net", "github.com/klauspost/compress"},
|
||||
},
|
||||
{
|
||||
name: "all keeps everything",
|
||||
scope: cataloging.SymbolScopeAll,
|
||||
want: []string{"golang.org/x/net", "github.com/klauspost/compress"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := newSymbolSelector(tt.scope, tt.modules).filter(symbols())
|
||||
if len(tt.want) == 0 {
|
||||
// nil, not an empty map, so the omitempty JSON tag keeps the field out of output
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
assert.ElementsMatch(t, tt.want, slices.Collect(maps.Keys(got)))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
// mainPackage is the import path the linker assigns to the binary's main package.
|
||||
const mainPackage = "main"
|
||||
|
||||
// vendorPrefix is the import-path prefix carried by vendored packages (e.g. from `go mod vendor`, or the
|
||||
// standard library's own vendored dependencies such as "vendor/golang.org/x/net/http2").
|
||||
const vendorPrefix = "vendor/"
|
||||
|
||||
// binarySymbol represents a single function symbol extracted from a go binary's pclntab.
|
||||
type binarySymbol struct {
|
||||
// packagePath is the import path of the package that owns the symbol (e.g. "github.com/foo/bar/internal/baz")
|
||||
@@ -52,10 +56,7 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
|
||||
continue
|
||||
}
|
||||
seen[fn.Name] = struct{}{}
|
||||
syms = append(syms, binarySymbol{
|
||||
packagePath: fn.PackageName(),
|
||||
name: fn.Name,
|
||||
})
|
||||
syms = append(syms, makeBinarySymbol(fn.Name, fn.PackageName()))
|
||||
}
|
||||
|
||||
// debug/gosym only exposes top-level functions; functions that the compiler inlined into their
|
||||
@@ -71,7 +72,7 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
syms = append(syms, binarySymbol{packagePath: pkgPath, name: name})
|
||||
syms = append(syms, makeBinarySymbol(name, pkgPath))
|
||||
}
|
||||
|
||||
return syms, nil
|
||||
@@ -94,6 +95,55 @@ func packagePathFromSymbolName(name string) string {
|
||||
return name[:slash+1+dot]
|
||||
}
|
||||
|
||||
// makeBinarySymbol builds a binarySymbol, unescaping the %xx sequences the go linker introduces in the
|
||||
// import-path portion of a symbol name (see unescapePackagePath). The local-symbol suffix (method and
|
||||
// function names) is left untouched, so only the path prefix shared by packagePath and name is rewritten.
|
||||
func makeBinarySymbol(name, pkgPath string) binarySymbol {
|
||||
unescaped := unescapePackagePath(pkgPath)
|
||||
if unescaped != pkgPath && strings.HasPrefix(name, pkgPath) {
|
||||
name = unescaped + name[len(pkgPath):]
|
||||
}
|
||||
return binarySymbol{packagePath: unescaped, name: name}
|
||||
}
|
||||
|
||||
// unescapePackagePath reverses the escaping cmd/internal/objabi.PathToPrefix applies to import paths in
|
||||
// symbol names: bytes like '.' (at or after the final '/'), '%', '"', control bytes, and high bytes are
|
||||
// written as lowercase "%xx". For example "gopkg.in/yaml.v2" is stored as "gopkg.in/yaml%2ev2", so this
|
||||
// restores it before matching against the (unescaped) module paths from build info. A lone or malformed
|
||||
// '%' sequence is left as-is.
|
||||
func unescapePackagePath(path string) string {
|
||||
if !strings.Contains(path, "%") {
|
||||
return path
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(path))
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] == '%' && i+2 < len(path) {
|
||||
if hi, ok1 := unhex(path[i+1]); ok1 {
|
||||
if lo, ok2 := unhex(path[i+2]); ok2 {
|
||||
b.WriteByte(hi<<4 | lo)
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WriteByte(path[i])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func unhex(c byte) (byte, bool) {
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
return c - '0', true
|
||||
case c >= 'a' && c <= 'f':
|
||||
return c - 'a' + 10, true
|
||||
case c >= 'A' && c <= 'F':
|
||||
return c - 'A' + 10, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// nameWithoutTypeArgs strips the type-argument portion from an instantiated generic symbol name, e.g.
|
||||
// "foo/bar.Do[net/url.Values]" -> "foo/bar.Do". The slashes and dots inside the brackets would otherwise
|
||||
// corrupt package-path derivation (yielding "foo/bar.Do[net" for the example above). Mirrors
|
||||
@@ -273,8 +323,13 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) {
|
||||
// "(*T).M"). Symbols from the "main" package are attributed to the main module and keyed by the "main"
|
||||
// import path the linker assigns. Standard-library symbols (which belong to no module) are collected
|
||||
// separately and returned as the second value, grouped by import path, so they can be attached to the
|
||||
// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a
|
||||
// recognizable stdlib import path are dropped.
|
||||
// synthetic "stdlib" package. Vendored packages carry a "vendor/" import-path prefix: such symbols match
|
||||
// both modules whose own path carries the prefix and modules without it (matched with the prefix trimmed),
|
||||
// and the prefix is retained in the group key only when the owning module itself is named "vendor/...".
|
||||
// Module-less vendored packages (the stdlib's own vendored dependencies, e.g.
|
||||
// "vendor/golang.org/x/net/http2") are dropped: stdlib vulnerabilities seem to be reported against the public
|
||||
// packages (e.g. "crypto/x509"), not the vendored internal copies. Compiler/runtime-internal symbols that
|
||||
// are neither module-owned nor a recognizable stdlib import path are likewise dropped.
|
||||
func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string]map[string][]string, stdlib map[string][]string) {
|
||||
if len(symbols) == 0 {
|
||||
return nil, nil
|
||||
@@ -302,16 +357,17 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
|
||||
attrPath = main.Path
|
||||
}
|
||||
|
||||
var best string
|
||||
for _, modPath := range modulePaths {
|
||||
if len(modPath) > len(best) && (attrPath == modPath || strings.HasPrefix(attrPath, modPath+"/")) {
|
||||
best = modPath
|
||||
}
|
||||
best := findBestMatch(modulePaths, attrPath)
|
||||
|
||||
// the vendor/ prefix is only retained when the owning module itself is named "vendor/...";
|
||||
// in all other cases (non-vendored modules and vendored stdlib) the recorded import path is trimmed
|
||||
if !strings.HasPrefix(best, vendorPrefix) {
|
||||
importPath = strings.TrimPrefix(importPath, vendorPrefix)
|
||||
}
|
||||
|
||||
local := localSymbolName(sym.name, importPath)
|
||||
if best == "" {
|
||||
if importPath != mainPackage && isStandardImportPath(importPath) {
|
||||
if importPath != mainPackage && isStandardImportPath(importPath) { // drop stdlib vendored packages
|
||||
stdlib[importPath] = append(stdlib[importPath], local)
|
||||
}
|
||||
continue
|
||||
@@ -333,11 +389,36 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
|
||||
return results, stdlib
|
||||
}
|
||||
|
||||
// findBestMatch returns the module path that owns the given package path taking into account vendor/ prefixes: a vendor/ import path
|
||||
// will take precedence and continue to match, non-vendored imports will match against their vendored equivalent
|
||||
func findBestMatch(modulePaths []string, importPath string) string {
|
||||
trimmedPath, trimmed := strings.CutPrefix(importPath, vendorPrefix)
|
||||
candidatePaths := []string{importPath, trimmedPath}
|
||||
if !trimmed {
|
||||
candidatePaths = candidatePaths[:1]
|
||||
}
|
||||
|
||||
var best string
|
||||
for _, candidate := range candidatePaths {
|
||||
for _, modPath := range modulePaths {
|
||||
// the prefix must end at a path-segment boundary in candidate, so that e.g. the module
|
||||
// "github.com/foo/bar" matches the package "github.com/foo/bar/baz" but not "github.com/foo/barbaz"
|
||||
if len(modPath) > len(best) && strings.HasPrefix(candidate, modPath) && (candidate == modPath || candidate[len(modPath)] == '/') {
|
||||
best = modPath
|
||||
}
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// localSymbolName strips the owning package's import path prefix from a fully qualified symbol name, e.g.
|
||||
// "github.com/foo/bar.(*T).M" with import path "github.com/foo/bar" becomes "(*T).M". The name is returned
|
||||
// unchanged when it does not carry the expected prefix.
|
||||
func localSymbolName(name, importPath string) string {
|
||||
if importPath != "" && strings.HasPrefix(name, importPath+".") {
|
||||
if !strings.HasPrefix(importPath, vendorPrefix) {
|
||||
name = strings.TrimPrefix(name, vendorPrefix)
|
||||
}
|
||||
if len(importPath) < len(name) && strings.HasPrefix(name, importPath) && name[len(importPath)] == '.' {
|
||||
return name[len(importPath)+1:]
|
||||
}
|
||||
return name
|
||||
|
||||
@@ -16,6 +16,8 @@ func Test_moduleSymbols(t *testing.T) {
|
||||
deps := []*debug.Module{
|
||||
{Path: "github.com/foo/bar"},
|
||||
{Path: "github.com/foo/bar/v2"},
|
||||
{Path: "vendor/github.com/vendored/mod"},
|
||||
{Path: "github.com/vendored/mod"},
|
||||
nil,
|
||||
}
|
||||
|
||||
@@ -74,6 +76,63 @@ func Test_moduleSymbols(t *testing.T) {
|
||||
"runtime": {"main"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vendored packages match non-vendored modules and are recorded under the canonical import path",
|
||||
symbols: []binarySymbol{
|
||||
{packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"},
|
||||
{packagePath: "vendor/github.com/foo/bar/internal/util", name: "vendor/github.com/foo/bar/internal/util.(*Helper).Do"},
|
||||
},
|
||||
expected: map[string]map[string][]string{
|
||||
"github.com/foo/bar": {
|
||||
"github.com/foo/bar": {"Parse"},
|
||||
"github.com/foo/bar/internal/util": {"(*Helper).Do"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stdlib-vendored packages are dropped",
|
||||
symbols: []binarySymbol{
|
||||
{packagePath: "vendor/golang.org/x/net/http2", name: "vendor/golang.org/x/net/http2.(*Framer).ReadFrame"},
|
||||
},
|
||||
expected: map[string]map[string][]string{},
|
||||
},
|
||||
{
|
||||
name: "modules whose own path carries the vendor/ prefix match exactly and win over the trimmed match",
|
||||
symbols: []binarySymbol{
|
||||
{packagePath: "vendor/github.com/vendored/mod", name: "vendor/github.com/vendored/mod.Run"},
|
||||
{packagePath: "github.com/vendored/mod", name: "github.com/vendored/mod.Run"},
|
||||
},
|
||||
expected: map[string]map[string][]string{
|
||||
"vendor/github.com/vendored/mod": {
|
||||
"vendor/github.com/vendored/mod": {"Run"},
|
||||
},
|
||||
"github.com/vendored/mod": {
|
||||
"github.com/vendored/mod": {"Run"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vendored and unvendored symbols for the same package are merged and deduplicated under the canonical import path",
|
||||
symbols: []binarySymbol{
|
||||
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
|
||||
{packagePath: "vendor/github.com/foo/bar", name: "vendor/github.com/foo/bar.Parse"},
|
||||
},
|
||||
expected: map[string]map[string][]string{
|
||||
"github.com/foo/bar": {
|
||||
"github.com/foo/bar": {"Parse"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// "github.com/foo/barbaz" shares a string prefix with the "github.com/foo/bar" module but not a
|
||||
// path-segment boundary, so it must not be attributed to it (and, having a dotted first element,
|
||||
// it is not a stdlib path either — it is dropped)
|
||||
name: "module paths only match the package path at a path-segment boundary",
|
||||
symbols: []binarySymbol{
|
||||
{packagePath: "github.com/foo/barbaz", name: "github.com/foo/barbaz.Parse"},
|
||||
},
|
||||
expected: map[string]map[string][]string{},
|
||||
},
|
||||
{
|
||||
name: "duplicate symbols are deduplicated",
|
||||
symbols: []binarySymbol{
|
||||
@@ -97,6 +156,58 @@ func Test_moduleSymbols(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_escapedImportPathAttribution(t *testing.T) {
|
||||
// the go linker escapes the '.' in the last path element, so "gopkg.in/yaml.v2" appears in the symbol
|
||||
// table as "gopkg.in/yaml%2ev2". attribution must still land these under the unescaped module path.
|
||||
mainModule := &debug.Module{Path: "github.com/someorg/somecli"}
|
||||
deps := []*debug.Module{{Path: "gopkg.in/yaml.v2"}}
|
||||
|
||||
symbols := []binarySymbol{
|
||||
makeBinarySymbol("gopkg.in/yaml%2ev2.(*decoder).alias", "gopkg.in/yaml%2ev2"),
|
||||
makeBinarySymbol("gopkg.in/yaml%2ev2.(*TypeError).Error", "gopkg.in/yaml%2ev2"),
|
||||
}
|
||||
|
||||
gotByModule, gotStdlib := moduleSymbols(symbols, mainModule, deps)
|
||||
assert.Nil(t, gotStdlib)
|
||||
assert.Equal(t, map[string]map[string][]string{
|
||||
"gopkg.in/yaml.v2": {
|
||||
"gopkg.in/yaml.v2": {"(*TypeError).Error", "(*decoder).alias"},
|
||||
},
|
||||
}, gotByModule)
|
||||
}
|
||||
|
||||
func Test_unescapePackagePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"gopkg.in/yaml%2ev2", "gopkg.in/yaml.v2"},
|
||||
{"foo%25bar", "foo%bar"},
|
||||
{"foo%22bar", "foo\"bar"},
|
||||
// no escapes: unchanged
|
||||
{"github.com/foo/bar", "github.com/foo/bar"},
|
||||
{"", ""},
|
||||
// malformed sequences are left as-is
|
||||
{"foo%2", "foo%2"},
|
||||
{"foo%zz", "foo%zz"},
|
||||
{"foo%", "foo%"},
|
||||
// uppercase hex is accepted too
|
||||
{"foo%2Ebar", "foo.bar"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, unescapePackagePath(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_makeBinarySymbol(t *testing.T) {
|
||||
// only the import-path prefix is unescaped; the local-symbol suffix is preserved verbatim
|
||||
sym := makeBinarySymbol("gopkg.in/yaml%2ev2.(*decoder).alias", "gopkg.in/yaml%2ev2")
|
||||
assert.Equal(t, "gopkg.in/yaml.v2", sym.packagePath)
|
||||
assert.Equal(t, "gopkg.in/yaml.v2.(*decoder).alias", sym.name)
|
||||
}
|
||||
|
||||
func Test_localSymbolName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -239,6 +350,9 @@ func Test_packagePathFromSymbolName(t *testing.T) {
|
||||
// module paths that begin with "go." are not compiler-generated
|
||||
{"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"},
|
||||
{"go.opentelemetry.io/otel.Tracer", "go.opentelemetry.io/otel"},
|
||||
// vendored packages retain their "vendor/" import-path prefix in symbol names
|
||||
{"vendor/golang.org/x/net/http2.(*Framer).ReadFrame", "vendor/golang.org/x/net/http2"},
|
||||
{"vendor/github.com/foo/bar.Parse", "vendor/github.com/foo/bar"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
@@ -32,6 +32,11 @@ func candidateVendorsForPE(p pkg.Package) fieldCandidateSet {
|
||||
candidates.addValue("artifex")
|
||||
}
|
||||
|
||||
if product == "git" || fileDesc == "git setup" || company == "the git development community" {
|
||||
candidates.addValue("git_for_windows_project")
|
||||
candidates.addValue("gitforwindows")
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
@@ -59,5 +64,10 @@ func candidateProductsForPE(p pkg.Package) fieldCandidateSet {
|
||||
candidates.addValue("ghostscript")
|
||||
}
|
||||
|
||||
if product == "git" || fileDesc == "git setup" {
|
||||
candidates.addValue("git_for_windows")
|
||||
candidates.addValue("git")
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
@@ -112,6 +112,9 @@
|
||||
"github.com/gookit/goutil": [
|
||||
"cpe:2.3:a:go_util_project:go_util:*:*:*:*:*:go:*:*"
|
||||
],
|
||||
"github.com/gopacket/gopacket": [
|
||||
"cpe:2.3:a:gopacket:gopacket:*:*:*:*:*:go:*:*"
|
||||
],
|
||||
"github.com/hamba/avro/v2": [
|
||||
"cpe:2.3:a:avro_project:avro:*:*:*:*:*:go:*:*"
|
||||
],
|
||||
@@ -2333,6 +2336,9 @@
|
||||
"@ais-ltd/strategyen": [
|
||||
"cpe:2.3:a:ais-ltd:strategyen:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@amoy/common": [
|
||||
"cpe:2.3:a:amoyjs:common:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@angular/core": [
|
||||
"cpe:2.3:a:angular:angular:*:*:*:*:*:node.js:*:*",
|
||||
"cpe:2.3:a:angularjs:angularjs:*:*:*:*:*:*:*:*"
|
||||
@@ -2412,6 +2418,9 @@
|
||||
"@better-auth/oauth-provider": [
|
||||
"cpe:2.3:a:better-auth:better-auth\\/oauth-provider:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@better-auth/sso": [
|
||||
"cpe:2.3:a:better-auth:better-auth\\/sso:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@braintree/sanitize-url": [
|
||||
"cpe:2.3:a:paypal:braintree\\/sanitize-url:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -2496,13 +2505,20 @@
|
||||
"@fastify/express": [
|
||||
"cpe:2.3:a:fastify:fastify\\/express:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@fastify/forwarded": [
|
||||
"cpe:2.3:a:fastify:fastify\\/forwarded:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@fastify/http-proxy": [
|
||||
"cpe:2.3:a:fastify:fastify\\/http-proxy:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@fastify/middie": [
|
||||
"cpe:2.3:a:fastify:fastify\\/middie:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@fastify/rate-limit": [
|
||||
"cpe:2.3:a:fastify:fastify\\/rate-limit:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@fastify/reply-from": [
|
||||
"cpe:2.3:a:fastify:fastify\\/reply-from:*:*:*:*:*:node.js:*:*",
|
||||
"cpe:2.3:a:fastify:reply-from:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@fastly/js-compute": [
|
||||
@@ -2643,6 +2659,9 @@
|
||||
"@openzeppelin/contracts-upgradeable": [
|
||||
"cpe:2.3:a:openzeppelin:contracts_upgradeable:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@paperclipai/server": [
|
||||
"cpe:2.3:a:paperclip:paperclipai\\/server:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"@parse/push-adapter": [
|
||||
"cpe:2.3:a:parseplatform:parse_server_push_adapter:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -3095,6 +3114,9 @@
|
||||
"clawdbot": [
|
||||
"cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"clawvet": [
|
||||
"cpe:2.3:a:mohibshaikh:clawvet:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"cli": [
|
||||
"cpe:2.3:a:cli_project:cli:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -4249,6 +4271,9 @@
|
||||
"json8-merge-patch": [
|
||||
"cpe:2.3:a:json8-merge-patch_project:json8-merge-patch:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"jsonpath": [
|
||||
"cpe:2.3:a:dchester:jsonpath:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"jsonpointer": [
|
||||
"cpe:2.3:a:janl:jsonpointer:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -5115,6 +5140,9 @@
|
||||
"pandora-doomsday": [
|
||||
"cpe:2.3:a:pandora-doomsday_project:pandora-doomsday:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"paperclipai": [
|
||||
"cpe:2.3:a:paperclip:paperclipai:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"parse-dashboard": [
|
||||
"cpe:2.3:a:parseplatform:parse_dashboard:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -5664,6 +5692,9 @@
|
||||
"shout": [
|
||||
"cpe:2.3:a:shout_project:shout:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"sigstore": [
|
||||
"cpe:2.3:a:sigstore:sigstore:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"sillytavern": [
|
||||
"cpe:2.3:a:sillytavern:sillytavern:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -6103,6 +6134,9 @@
|
||||
"vite": [
|
||||
"cpe:2.3:a:vitejs:vite:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"vitest": [
|
||||
"cpe:2.3:a:vitest.dev:vitest:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
"vm2": [
|
||||
"cpe:2.3:a:vm2_project:vm2:*:*:*:*:*:node.js:*:*"
|
||||
],
|
||||
@@ -6530,6 +6564,9 @@
|
||||
"dash-uploader": [
|
||||
"cpe:2.3:a:fohrloop:dash-uploader:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"datamodel-code-generator": [
|
||||
"cpe:2.3:a:koxudaxi:datamodel-code-generator:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"datapizza-ai": [
|
||||
"cpe:2.3:a:datapizza:datapizza_ai:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
@@ -6729,6 +6766,9 @@
|
||||
"mpxj": [
|
||||
"cpe:2.3:a:mpxj:mpxj:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"msgpack": [
|
||||
"cpe:2.3:a:msgpack:messagepack:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"nanobot-ai": [
|
||||
"cpe:2.3:a:nanobot:nanobot:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
@@ -6837,6 +6877,9 @@
|
||||
"pyanyapi": [
|
||||
"cpe:2.3:a:pyanyapi_project:pyanyapi:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
"pyasn1": [
|
||||
"cpe:2.3:a:pyasn1:pyasn1:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"pybluemonday": [
|
||||
"cpe:2.3:a:python:pybluemonday:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
@@ -6852,6 +6895,9 @@
|
||||
"pydantic-ai": [
|
||||
"cpe:2.3:a:pydantic:pydantic_ai:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
"pydantic-settings": [
|
||||
"cpe:2.3:a:pydantic:pydantic-settings:*:*:*:*:*:*:*:*"
|
||||
],
|
||||
"pydash": [
|
||||
"cpe:2.3:a:derrickgilland:pydash:*:*:*:*:*:python:*:*"
|
||||
],
|
||||
@@ -7202,6 +7248,9 @@
|
||||
"encoded_id-rails": [
|
||||
"cpe:2.3:a:diaconou:encodedid\\:\\:rails:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"excon": [
|
||||
"cpe:2.3:a:excon_project:excon:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"faye": [
|
||||
"cpe:2.3:a:faye_project:faye:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
@@ -7331,6 +7380,9 @@
|
||||
"mpxj": [
|
||||
"cpe:2.3:a:mpxj:mpxj:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"msgpack": [
|
||||
"cpe:2.3:a:msgpack:messagepack:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"net-imap": [
|
||||
"cpe:2.3:a:ruby-lang:net\\:\\:imap:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
@@ -7565,6 +7617,9 @@
|
||||
"webrick": [
|
||||
"cpe:2.3:a:ruby-lang:webrick:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"websocket-driver": [
|
||||
"cpe:2.3:a:faye:websocket-driver:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
"websocket-extensions": [
|
||||
"cpe:2.3:a:websocket-extensions_project:websocket-extensions:*:*:*:*:*:ruby:*:*"
|
||||
],
|
||||
@@ -7609,6 +7664,9 @@
|
||||
"ammonia": [
|
||||
"cpe:2.3:a:ammonia_project:ammonia:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"anchor-lang": [
|
||||
"cpe:2.3:a:anchor-lang:anchor:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"anymap": [
|
||||
"cpe:2.3:a:anymap_project:anymap:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
@@ -7978,6 +8036,12 @@
|
||||
"one2html": [
|
||||
"cpe:2.3:a:msiemens:one2html:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"openssl": [
|
||||
"cpe:2.3:a:sfackler:openssl:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"opentelemetry": [
|
||||
"cpe:2.3:a:opentelemetry:opentelemetry:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"outer_cgi": [
|
||||
"cpe:2.3:a:outer_cgi_project:outer_cgi:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
@@ -8008,6 +8072,9 @@
|
||||
"postscript": [
|
||||
"cpe:2.3:a:postscript_project:postscript:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"quiche": [
|
||||
"cpe:2.3:a:cloudflare:quiche:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
"quinn": [
|
||||
"cpe:2.3:a:quinn_project:quinn:*:*:*:*:*:rust:*:*"
|
||||
],
|
||||
@@ -8557,6 +8624,9 @@
|
||||
"add-subtitle": [
|
||||
"cpe:2.3:a:viitorcloud:add_subtitle:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"add-svg-support-for-media-uploader-inventivo": [
|
||||
"cpe:2.3:a:inventivo:inventivo:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"add-to-any": [
|
||||
"cpe:2.3:a:addtoany:addtoany_share_buttons:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -8747,6 +8817,9 @@
|
||||
"advanced-recent-posts": [
|
||||
"cpe:2.3:a:advanced_recent_posts_project:advanced_recent_posts:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"advanced-schedule-posts": [
|
||||
"cpe:2.3:a:hijiriworld:advanced_schedule_posts:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"advanced-sermons": [
|
||||
"cpe:2.3:a:wpcodeus:advanced_sermons:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -8811,6 +8884,7 @@
|
||||
"cpe:2.3:a:ai_contact_us_form_project:ai_contact_us_form:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ai-engine": [
|
||||
"cpe:2.3:a:jordymeow:ai_engine:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:meowapps:ai_engine:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ai-image-alt-text-generator-for-wp": [
|
||||
@@ -8870,6 +8944,9 @@
|
||||
"album-and-image-gallery-plus-lightbox": [
|
||||
"cpe:2.3:a:essentialplugin:album_and_image_gallery_plus_lightbox:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"alemha-watermark": [
|
||||
"cpe:2.3:a:mnbaa:alemha_watermark:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"alfred-click-collect": [
|
||||
"cpe:2.3:a:alfred24_click_\\\u0026_collect_project:alfred24_click_\\\u0026_collect:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -9340,6 +9417,9 @@
|
||||
"autoptimize": [
|
||||
"cpe:2.3:a:autoptimize:autoptimize:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"autoship-cloud": [
|
||||
"cpe:2.3:a:patternsinthecloud:autoship_cloud_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"autotitle-for-wordpress": [
|
||||
"cpe:2.3:a:unalignedcode:autotitle:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -9497,6 +9577,9 @@
|
||||
"barcode-scanner-lite-pos-to-manage-products-inventory-and-orders": [
|
||||
"cpe:2.3:a:ukrsolution:barcode_scanner_and_inventory_manager:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"base64-encoderdecoder": [
|
||||
"cpe:2.3:a:mranderson:base64_encoder\\/decoder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"basic-interactive-world-map": [
|
||||
"cpe:2.3:a:wpmapplugins:basic_interactive_world_map:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -9951,6 +10034,9 @@
|
||||
"bsuite": [
|
||||
"cpe:2.3:a:bsuite_project:bsuite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"bu-section-editing": [
|
||||
"cpe:2.3:a:bu:bu_section_editing:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"bubble-menu": [
|
||||
"cpe:2.3:a:wow-company:bubble_menu:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
@@ -11021,6 +11107,9 @@
|
||||
"cost-calculator-builder": [
|
||||
"cpe:2.3:a:stylemixthemes:cost_calculator_builder:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"coub": [
|
||||
"cpe:2.3:a:chartscss:coub:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"countdown-block": [
|
||||
"cpe:2.3:a:wpdeveloper:countdown_block:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -11672,6 +11761,9 @@
|
||||
"donation-thermometer": [
|
||||
"cpe:2.3:a:donation_thermometer_project:donation_thermometer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"donations-block": [
|
||||
"cpe:2.3:a:bharatkambariya:donation_block_for_paypal:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"donations-for-woocommerce": [
|
||||
"cpe:2.3:a:wpzone:potent_donations_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -11759,6 +11851,9 @@
|
||||
"dtabs": [
|
||||
"cpe:2.3:a:dtabs_project:dtabs:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"dts-simple-share": [
|
||||
"cpe:2.3:a:missionmike:simple_share:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"duitku-social-payment-gateway": [
|
||||
"cpe:2.3:a:duitku:duitku_payment_gateway:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -12150,9 +12245,12 @@
|
||||
"cpe:2.3:a:mandsconsulting:email_before_download:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"email-encoder-bundle": [
|
||||
"cpe:2.3:a:jannisthuemmig:email_encoder:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:onlineoptimisation:email_encoder:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wp-webhooks:email_encoder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"email-keep": [
|
||||
"cpe:2.3:a:intricateweb:email_keep:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"email-log": [
|
||||
"cpe:2.3:a:email_log_project:email_log:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -12744,6 +12842,9 @@
|
||||
"five-minute-webshop": [
|
||||
"cpe:2.3:a:five_minute_webshop_project:five_minute_webshop:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"fl3r-accessibility-suite": [
|
||||
"cpe:2.3:a:fl3r:fl3r_accessibility_suite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"fl3r-feelbox": [
|
||||
"cpe:2.3:a:armandofiore:fl3r_feelbox:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:fl3r-feelbox_project:fl3r-feelbox:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -12989,6 +13090,9 @@
|
||||
"fotobook": [
|
||||
"cpe:2.3:a:fotobook_project:fotobook:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"foxypress": [
|
||||
"cpe:2.3:a:webmovementllc:foxypress:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"foxyshop": [
|
||||
"cpe:2.3:a:foxy:foxyshop:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13132,6 +13236,9 @@
|
||||
"game-server-status": [
|
||||
"cpe:2.3:a:game-server-status_project:game-server-status:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"game-users-share-buttons": [
|
||||
"cpe:2.3:a:gameusers:game_users_share_button:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gamepress": [
|
||||
"cpe:2.3:a:gamepress_project:gamepress:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13141,6 +13248,9 @@
|
||||
"gamipress-link": [
|
||||
"cpe:2.3:a:gamipress:gamipress_-_link:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gamipress-reset-user": [
|
||||
"cpe:2.3:a:gamipress:gamipress_-_reset_user:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"garden-gnome-package": [
|
||||
"cpe:2.3:a:ggnome:garden_gnome_package:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13225,6 +13335,9 @@
|
||||
"gestpay-for-woocommerce": [
|
||||
"cpe:2.3:a:fabrick:gestpay_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"get-bookings-wp": [
|
||||
"cpe:2.3:a:istmoplugins:get_bookings_wp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"get-custom-field-values": [
|
||||
"cpe:2.3:a:get_custom_field_values_project:get_custom_field_values:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13252,9 +13365,21 @@
|
||||
"gf-block-ips": [
|
||||
"cpe:2.3:a:brightplugins:block_ips_for_gravity_forms:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-constant-contact": [
|
||||
"cpe:2.3:a:rocketgenius:constant_contact:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-custom-style": [
|
||||
"cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-hubspot": [
|
||||
"cpe:2.3:a:crmperks:wp_gravity_forms_hubspot:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-insightly": [
|
||||
"cpe:2.3:a:crmperks:wp_gravity_forms_insightly:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-salesforce-crmperks": [
|
||||
"cpe:2.3:a:crmperks:wp_gravity_forms_salesforce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"gf-zoho": [
|
||||
"cpe:2.3:a:crmperks:wp_gravity_forms_zoho_crm_and_bigin:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13687,6 +13812,9 @@
|
||||
"heureka": [
|
||||
"cpe:2.3:a:heureka:heureka:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"hide-links": [
|
||||
"cpe:2.3:a:avovkdesign:hide_links:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"hide-my-wp": [
|
||||
"cpe:2.3:a:wpplugins:hide_my_wp_ghost:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -13711,6 +13839,9 @@
|
||||
"hkdev-maintenance-mode": [
|
||||
"cpe:2.3:a:helderk:maintenance_mode:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"hmapsprem": [
|
||||
"cpe:2.3:a:heroplugins:hero_maps_premium:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"hms-testimonials": [
|
||||
"cpe:2.3:a:hitmyserver:hms_testimonials:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -14085,6 +14216,9 @@
|
||||
"cpe:2.3:a:infusionsoft_project:infusionsoft:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:katz:infusionsoft_gravity_forms:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"infusionsoft-official-opt-in-forms": [
|
||||
"cpe:2.3:a:keap:keap_official_opt_in_forms:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"inline-google-spreadsheet-viewer": [
|
||||
"cpe:2.3:a:inline_google_spreadsheet_viewer_project:inline_google_spreadsheet_viewer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -14185,6 +14319,9 @@
|
||||
"intelly-related-posts": [
|
||||
"cpe:2.3:a:data443:inline_related_posts:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"interact-quiz-embed": [
|
||||
"cpe:2.3:a:tryinteract:embed_a_quiz_on_your_site:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"interactive-3d-flipbook-powered-physics-engine": [
|
||||
"cpe:2.3:a:3dflipbook:3d_flipbook:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -14421,6 +14558,9 @@
|
||||
"jvm-rich-text-icons": [
|
||||
"cpe:2.3:a:jorisvm:jvm_gutenberg_rich_text_icons:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"jwp-a11y": [
|
||||
"cpe:2.3:a:jidaikobo:jwp-a11y:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"kadence-blocks": [
|
||||
"cpe:2.3:a:kadencewp:gutenberg_blocks_with_ai:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -14910,6 +15050,9 @@
|
||||
"logo-slider-wp": [
|
||||
"cpe:2.3:a:logichunt:logo_slider:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"logs-de-connexion": [
|
||||
"cpe:2.3:a:floriansimunek:connexion_logs:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"logwpmail": [
|
||||
"cpe:2.3:a:premierethemes:log_wp_mail:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -14982,6 +15125,9 @@
|
||||
"cpe:2.3:a:custom_popup_builder_project:custom_popup_builder:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:popup_\\|_custom_popup_builder_project:popup_\\|_custom_popup_builder:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"magayo-lottery-results": [
|
||||
"cpe:2.3:a:magayo:magayo_lottery_results:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"magazine-blocks": [
|
||||
"cpe:2.3:a:themegrill:magazine_blocks:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15178,7 +15324,8 @@
|
||||
"cpe:2.3:a:master-addons:master_addons:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"master-slider": [
|
||||
"cpe:2.3:a:averta:master_slider:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:averta:master_slider:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:averta:master_slider:*:*:*:*:free:wordpress:*:*"
|
||||
],
|
||||
"masterstudy-lms-learning-management-system": [
|
||||
"cpe:2.3:a:stylemixthemes:masterstudy_lms:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -15198,6 +15345,9 @@
|
||||
"materialis-companion": [
|
||||
"cpe:2.3:a:extendthemes:materialis_companion:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"matomo": [
|
||||
"cpe:2.3:a:matomo:matomo:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"matrix-pre-loader": [
|
||||
"cpe:2.3:a:nkb-bd:preloader_matrix:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15216,6 +15366,9 @@
|
||||
"mdc-youtube-downloader": [
|
||||
"cpe:2.3:a:mdc_youtube_downloader_project:mdc_youtube_downloader:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"media-alt-renamer": [
|
||||
"cpe:2.3:a:jordymeow:media_alt_renamer:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"media-downloader": [
|
||||
"cpe:2.3:a:media_downloader_project:media_downloader:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15404,6 +15557,9 @@
|
||||
"mihdan-index-now": [
|
||||
"cpe:2.3:a:kobzarev:index_now:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mini-course-generator": [
|
||||
"cpe:2.3:a:minicoursegenerator:mini_course_generator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mini-mail-dashboard-widget": [
|
||||
"cpe:2.3:a:mini_mail_dashboard_widget_project:mini_mail_dashboard_widget:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15437,6 +15593,9 @@
|
||||
"ml-slider": [
|
||||
"cpe:2.3:a:metaslider:slider\\,_gallery\\,_and_carousel:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mm-breaking-news": [
|
||||
"cpe:2.3:a:mm-breaking_news_project:mm-breaking_news:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mmm-file-list": [
|
||||
"cpe:2.3:a:mediamanifesto:mmm_simple_file_list:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15523,6 +15682,9 @@
|
||||
"mortgage-calculators-wp": [
|
||||
"cpe:2.3:a:lenderd:mortgage_calculators_wp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"mortgage-loan-calculator": [
|
||||
"cpe:2.3:a:mlcalc:mortgage_loan_calculator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"most-and-least-read-posts-widget": [
|
||||
"cpe:2.3:a:whiletrue:most_and_least_read_posts_widget:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15547,6 +15709,9 @@
|
||||
"move-addons": [
|
||||
"cpe:2.3:a:moveaddons:move_addons_for_elementor:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"movie-database": [
|
||||
"cpe:2.3:a:heimkino-praxis:movie_database:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"movies": [
|
||||
"cpe:2.3:a:movies_project:movies:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -15681,6 +15846,9 @@
|
||||
"my-geo-posts-free": [
|
||||
"cpe:2.3:a:mindstien:my_geo_posts_free:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"my-quota": [
|
||||
"cpe:2.3:a:dropstr:my_quota:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"my-tickets": [
|
||||
"cpe:2.3:a:my_tickets_project:my_tickets:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16013,6 +16181,9 @@
|
||||
"official-sendle-shipping-method": [
|
||||
"cpe:2.3:a:joovii:sendle_shipping:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"offload-videos-bunny-netaws-s3": [
|
||||
"cpe:2.3:a:webgarh:offload_videos:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"og-tags": [
|
||||
"cpe:2.3:a:og_tags_project:og_tags:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16239,6 +16410,7 @@
|
||||
"cpe:2.3:a:benhuson:page_parts:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"page-scroll-to-id": [
|
||||
"cpe:2.3:a:malihu:page_scroll_to_id:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:page_scroll_to_id_project:page_scroll_to_id:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"page-views-count": [
|
||||
@@ -16672,6 +16844,9 @@
|
||||
"popup-zyrex": [
|
||||
"cpe:2.3:a:zyrex:popup:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"popup4phone": [
|
||||
"cpe:2.3:a:ivanweb:popup4phone:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"popupally": [
|
||||
"cpe:2.3:a:accessally:popupally:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -16685,6 +16860,9 @@
|
||||
"portfolio": [
|
||||
"cpe:2.3:a:bestwebsoft:portfolio:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"portfolio-and-projects": [
|
||||
"cpe:2.3:a:essentialplugin:portfolio_and_projects:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"portfolio-by-lisa-westlund": [
|
||||
"cpe:2.3:a:portfolio_project:portfolio:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -17172,7 +17350,10 @@
|
||||
"cpe:2.3:a:wepupil:quiz_expert_-_easy_quiz_maker\\,_exam_and_test_manager:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"quiz-maker": [
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:agency:wordpress:*:*",
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:business:wordpress:*:*",
|
||||
"cpe:2.3:a:ays-pro:quiz_maker:*:*:*:*:developer:wordpress:*:*"
|
||||
],
|
||||
"quiz-master-next": [
|
||||
"cpe:2.3:a:expresstech:quiz_and_survey_master:*:*:*:*:*:wordpress:*:*",
|
||||
@@ -17472,6 +17653,9 @@
|
||||
"responsive-css-editor": [
|
||||
"cpe:2.3:a:wpwox:responsive_css_editor:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"responsive-filterable-portfolio": [
|
||||
"cpe:2.3:a:i13websolution:responsive_filterable_portfolio:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"responsive-food-and-drink-menu": [
|
||||
"cpe:2.3:a:corporatezen:responsive_food_and_drink_menu:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -17501,6 +17685,12 @@
|
||||
"responsive-vector-maps": [
|
||||
"cpe:2.3:a:thinkupthemes:responsive_vector_maps:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"responsive-video-embed": [
|
||||
"cpe:2.3:a:lepileppanen:responsive_video_embed:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"rest-api-to-miniprogram": [
|
||||
"cpe:2.3:a:jianbo:rest_api_to_miniprogram:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"restaurant-cafe-addon-for-elementor": [
|
||||
"cpe:2.3:a:nicheaddons:restaurant_\\\u0026_cafe_addon_for_elementor:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -17686,6 +17876,9 @@
|
||||
"safe-svg": [
|
||||
"cpe:2.3:a:10up:safe_svg:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"safetymails-forms": [
|
||||
"cpe:2.3:a:henriquerodrigues:safetyforms:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"sagepay-server-gateway-for-woocommerce": [
|
||||
"cpe:2.3:a:patsatech:sagepay_server_gateway_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -17738,6 +17931,9 @@
|
||||
"schema-app-structured-data-for-schemaorg": [
|
||||
"cpe:2.3:a:schemaapp:schema_app_structured_data:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"school-management": [
|
||||
"cpe:2.3:a:dasinfomedia:school_management_system:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"school-management-system": [
|
||||
"cpe:2.3:a:weblizar:school_management_-_education_\\\u0026_learning_management:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18122,6 +18318,9 @@
|
||||
"simple-author-box": [
|
||||
"cpe:2.3:a:webfactoryltd:simple_author_box:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simple-backup": [
|
||||
"cpe:2.3:a:mywebsiteadvisor:simple_backup:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"simple-banner": [
|
||||
"cpe:2.3:a:simple_banner_project:simple_banner:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18576,6 +18775,9 @@
|
||||
"slideshow-se": [
|
||||
"cpe:2.3:a:slideshow_se_project:slideshow_se:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"slingblocks": [
|
||||
"cpe:2.3:a:funnelkit:slingblocks:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"small-package-quotes-fedex-edition": [
|
||||
"cpe:2.3:a:eniture:small_package_quotes:*:*:*:*:fedex:wordpress:*:*"
|
||||
],
|
||||
@@ -18868,6 +19070,9 @@
|
||||
"sportspress": [
|
||||
"cpe:2.3:a:themeboy:sportspress:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"spotbot": [
|
||||
"cpe:2.3:a:jakehelbig:spotbot:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"spotify-play-button-for-wordpress": [
|
||||
"cpe:2.3:a:followmedarling:spotify-play-button-for-wordpress:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -18898,6 +19103,9 @@
|
||||
"srbtranslatin": [
|
||||
"cpe:2.3:a:srbtranslatin_project:srbtranslatin:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ssv-events": [
|
||||
"cpe:2.3:a:moridrin:ssv_events:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"st-daily-tip": [
|
||||
"cpe:2.3:a:sanskruti:st-daily-tip:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19012,6 +19220,9 @@
|
||||
"strategery-migrations": [
|
||||
"cpe:2.3:a:strategery_migrations_project:strategery_migrations:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"stratum": [
|
||||
"cpe:2.3:a:motopress:stratum:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"stray-quotes": [
|
||||
"cpe:2.3:a:unalignedcode:stray_random_quotes:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19042,6 +19253,9 @@
|
||||
"subaccounts-for-woocommerce": [
|
||||
"cpe:2.3:a:mediaticus:subaccounts_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"sublanguage": [
|
||||
"cpe:2.3:a:maximeschoeni:sublanguage:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"subscribe-sidebar": [
|
||||
"cpe:2.3:a:blubrry:subscribe_sidebar:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19478,12 +19692,18 @@
|
||||
"timetics": [
|
||||
"cpe:2.3:a:arraytics:wp_timetics:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"timezonecalculator": [
|
||||
"cpe:2.3:a:bernhard-riedl:timezonecalculator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"timthumb-vulnerability-scanner": [
|
||||
"cpe:2.3:a:peterbutler:timthumb_vulnerability_scanner:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tinychat-roomspy": [
|
||||
"cpe:2.3:a:tinychat:room_spy:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tinymce-and-tinymce-advanced-professsional-formats-and-styles": [
|
||||
"cpe:2.3:a:blackbam:tinymce_and_tinymce_advanced_professsional_formats_and_styles:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tinymce-custom-styles": [
|
||||
"cpe:2.3:a:tinymce_custom_styles_project:tinymce_custom_styles:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19552,6 +19772,9 @@
|
||||
"tournamatch": [
|
||||
"cpe:2.3:a:tournamatch:tournamatch:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tournament-bracket-generator": [
|
||||
"cpe:2.3:a:blakelong:tournament_bracket_generator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"tp-education": [
|
||||
"cpe:2.3:a:themepalace:tp_education:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19970,6 +20193,9 @@
|
||||
"user-login-log": [
|
||||
"cpe:2.3:a:intechnosoftware:user_login_log:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"user-messages": [
|
||||
"cpe:2.3:a:marvinlabs:user_messages:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"user-meta": [
|
||||
"cpe:2.3:a:user-meta:user_meta_user_profile_builder_and_user_management:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -19993,6 +20219,9 @@
|
||||
"user-role": [
|
||||
"cpe:2.3:a:bestwebsoft:user_role:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"user-shortcodes-plus": [
|
||||
"cpe:2.3:a:kylebjohnson:user_shortcodes_plus:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"user-spam-remover": [
|
||||
"cpe:2.3:a:joelhardi:user_spam_remover:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20313,6 +20542,9 @@
|
||||
"weaverx-theme-support": [
|
||||
"cpe:2.3:a:weavertheme:weaver_xtreme_theme_support:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"web-application-firewall": [
|
||||
"cpe:2.3:a:miniorange:web_application_firewall:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"web-directory-free": [
|
||||
"cpe:2.3:a:salephpscripts:web_directory_free:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20605,6 +20837,9 @@
|
||||
"woo-gutenberg-products-block": [
|
||||
"cpe:2.3:a:automattic:woocommerce_blocks:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"woo-mailerlite": [
|
||||
"cpe:2.3:a:mailerlite:mailerlite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"woo-moneybird": [
|
||||
"cpe:2.3:a:techastha:integration_of_moneybird_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20659,6 +20894,9 @@
|
||||
"woo-related-products-refresh-on-reload": [
|
||||
"cpe:2.3:a:peachpay:related_products_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"woo-rfq-for-woocommerce": [
|
||||
"cpe:2.3:a:neahplugins:np_quote_request_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"woo-save-abandoned-carts": [
|
||||
"cpe:2.3:a:majas-lapu-izstrade:cartbounty:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20839,6 +21077,9 @@
|
||||
"wooemailreport": [
|
||||
"cpe:2.3:a:eggemplo:woocommerce_email_report:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wooexim": [
|
||||
"cpe:2.3:a:bulktheme:wooexim:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wooframework-tweaks": [
|
||||
"cpe:2.3:a:woocommerce:wooframework_tweaks:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -20954,6 +21195,9 @@
|
||||
"wordpress-users": [
|
||||
"cpe:2.3:a:jonathonkemp:wordpress_users:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"work-the-flow-file-upload": [
|
||||
"cpe:2.3:a:lyntonreed:work_the_flow_file_upload:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"workreap": [
|
||||
"cpe:2.3:a:amentotech:workreap:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21185,6 +21429,9 @@
|
||||
"wp-consultant": [
|
||||
"cpe:2.3:a:wp_consultant_project:wp_consultant:*:*:-:*:-:wordpress:*:*"
|
||||
],
|
||||
"wp-contact-form": [
|
||||
"cpe:2.3:a:ftwr:wp_contact_form:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-contact-slider": [
|
||||
"cpe:2.3:a:wpexperts:wp_contact_slider:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21483,6 +21730,9 @@
|
||||
"wp-fountain": [
|
||||
"cpe:2.3:a:wp_fountain_project:wp_fountain:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-front-end-login-and-register": [
|
||||
"cpe:2.3:a:hellomohsinkhan:wp_front-end_login_and_register:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-front-end-profile": [
|
||||
"cpe:2.3:a:wp_front_end_profile_project:wp_front_end_profile:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21643,6 +21893,9 @@
|
||||
"wp-jobs": [
|
||||
"cpe:2.3:a:intensewp:wp_jobs:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-jobs2careers": [
|
||||
"cpe:2.3:a:skipthedrive:wp_talroo:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-jquery-lightbox": [
|
||||
"cpe:2.3:a:lightpress:lightbox:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -21935,6 +22188,9 @@
|
||||
"wp-product-gallery-lite": [
|
||||
"cpe:2.3:a:accesspressthemes:wp_product_gallery_lite:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-programmmanager": [
|
||||
"cpe:2.3:a:mantus667:wp-pmanager:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-propagator": [
|
||||
"cpe:2.3:a:widgilabs:plugin_propagator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -22616,7 +22872,8 @@
|
||||
"cpe:2.3:a:maennchen1:wpshopgermany_-_protected_shops:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wpshopify": [
|
||||
"cpe:2.3:a:shopwp_project:shopwp:*:*:*:*:*:wordpress:*:*"
|
||||
"cpe:2.3:a:shopwp_project:shopwp:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wpshop:shopwp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wpsimpletools-log-viewer": [
|
||||
"cpe:2.3:a:wpsimpletools:basic_log_viewer:*:*:*:*:*:wordpress:*:*"
|
||||
@@ -22762,6 +23019,7 @@
|
||||
"cpe:2.3:a:ydesignservices:yds_support_ticket_system:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"yellow-pencil-visual-theme-customizer": [
|
||||
"cpe:2.3:a:waspthemes:yellowpencil:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:yellowpencil:visual_css_style_editor:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"yellow-yard": [
|
||||
@@ -23143,6 +23401,9 @@
|
||||
"empowerwp": [
|
||||
"cpe:2.3:a:extendthemes:empowerwp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"enfold": [
|
||||
"cpe:2.3:a:kriesi:enfold:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"enlighten": [
|
||||
"cpe:2.3:a:accesspressthemes:enlighten:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -23193,6 +23454,9 @@
|
||||
"hello-agency": [
|
||||
"cpe:2.3:a:cozythemes:hello_agency:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"himalayas": [
|
||||
"cpe:2.3:a:themegrill:himalayas:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"homey": [
|
||||
"cpe:2.3:a:favethemes:homey:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
@@ -23387,6 +23651,9 @@
|
||||
"tijaji": [
|
||||
"cpe:2.3:a:tijaji:tijaji:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"total": [
|
||||
"cpe:2.3:a:hashthemes:total:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"transcend": [
|
||||
"cpe:2.3:a:cpothemes:transcend:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
|
||||
@@ -1921,4 +1921,16 @@ var DefaultArtifactIDToGroupID = map[string]string{
|
||||
"kafka_2.8.2": "org.apache.kafka",
|
||||
"kafka_2.9.1": "org.apache.kafka",
|
||||
"kafka_2.9.2": "org.apache.kafka",
|
||||
|
||||
// legacy "Jackson 1.x" (aka "jackson-asl") artifacts predate the convention of embedding
|
||||
// META-INF/maven/.../pom.properties in the jar, so groupIDFromKnownPackageList is the only
|
||||
// way to recover the correct group ID for jars built without that metadata (e.g. Ant-built
|
||||
// jars from before ~2014). Without this, the group ID falls back to the artifact name itself,
|
||||
// producing purls that do not match the vulnerability database's namespace.
|
||||
// See https://github.com/anchore/syft/issues/4598
|
||||
"jackson-core-asl": "org.codehaus.jackson",
|
||||
"jackson-mapper-asl": "org.codehaus.jackson",
|
||||
"jackson-jaxrs": "org.codehaus.jackson",
|
||||
"jackson-xc": "org.codehaus.jackson",
|
||||
"jackson-smile": "org.codehaus.jackson",
|
||||
}
|
||||
|
||||
@@ -1,26 +1,49 @@
|
||||
package cpegenerate
|
||||
|
||||
import "github.com/anchore/syft/syft/pkg"
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func candidateVendorsForRPM(p pkg.Package) fieldCandidateSet {
|
||||
vendors := newFieldCandidateSet()
|
||||
var vendor string
|
||||
|
||||
switch m := p.Metadata.(type) {
|
||||
case pkg.RpmDBEntry:
|
||||
if m.Vendor != "" {
|
||||
vendors.add(fieldCandidate{
|
||||
value: normalizeName(m.Vendor),
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
}
|
||||
vendor = m.Vendor
|
||||
case pkg.RpmArchive:
|
||||
if m.Vendor != "" {
|
||||
vendors.add(fieldCandidate{
|
||||
value: normalizeName(m.Vendor),
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
}
|
||||
vendor = m.Vendor
|
||||
}
|
||||
|
||||
vendor = stripTrailingURL(vendor)
|
||||
if vendor != "" {
|
||||
vendors.add(fieldCandidate{
|
||||
value: normalizeName(vendor),
|
||||
disallowSubSelections: true,
|
||||
})
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
func stripTrailingURL(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if !strings.HasSuffix(trimmed, ">") {
|
||||
return value
|
||||
}
|
||||
|
||||
open := strings.LastIndex(trimmed, "<")
|
||||
if open == -1 {
|
||||
return value
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(trimmed[open+1 : len(trimmed)-1])
|
||||
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
return value
|
||||
}
|
||||
|
||||
return strings.TrimSpace(trimmed[:open])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cpegenerate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func TestCandidateVendorsForRPM(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
metadata any
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "database vendor with publisher URL",
|
||||
metadata: pkg.RpmDBEntry{
|
||||
Vendor: "SUSE LLC <https://www.suse.com/>",
|
||||
},
|
||||
expected: []string{"susellc"},
|
||||
},
|
||||
{
|
||||
name: "archive vendor with publisher URL",
|
||||
metadata: pkg.RpmArchive{
|
||||
Vendor: "SUSE LLC <https://www.suse.com/>",
|
||||
},
|
||||
expected: []string{"susellc"},
|
||||
},
|
||||
{
|
||||
name: "plain vendor",
|
||||
metadata: pkg.RpmDBEntry{
|
||||
Vendor: "Red Hat, Inc.",
|
||||
},
|
||||
expected: []string{"redhat"},
|
||||
},
|
||||
{
|
||||
name: "non-URL angle-bracket suffix",
|
||||
metadata: pkg.RpmDBEntry{
|
||||
Vendor: "Example <support@example.com>",
|
||||
},
|
||||
expected: []string{"example<support@example.com>"},
|
||||
},
|
||||
{
|
||||
name: "non-HTTP URL",
|
||||
metadata: pkg.RpmDBEntry{
|
||||
Vendor: "Example <ftp://example.com>",
|
||||
},
|
||||
expected: []string{"example<ftp://example.com>"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
p := pkg.Package{Metadata: test.metadata}
|
||||
assert.ElementsMatch(t, test.expected, candidateVendorsForRPM(p).uniqueValues())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,23 @@ func Test_groupIDFromJavaMetadata(t *testing.T) {
|
||||
metadata: pkg.JavaArchive{},
|
||||
expect: "org.springframework.ldap",
|
||||
},
|
||||
{
|
||||
// regression for github.com/anchore/syft/issues/4598: legacy Jackson 1.x ("-asl")
|
||||
// jars built before ~2014 have no embedded pom.properties, so without the known
|
||||
// package list the group ID falls back to the artifact name itself, producing a
|
||||
// purl that doesn't match the vulnerability database's namespace (e.g. the correct
|
||||
// group for jackson-mapper-asl is org.codehaus.jackson, not jackson-mapper-asl).
|
||||
name: "known package list jackson-mapper-asl",
|
||||
pkgName: "jackson-mapper-asl",
|
||||
metadata: pkg.JavaArchive{},
|
||||
expect: "org.codehaus.jackson",
|
||||
},
|
||||
{
|
||||
name: "known package list jackson-core-asl",
|
||||
pkgName: "jackson-core-asl",
|
||||
metadata: pkg.JavaArchive{},
|
||||
expect: "org.codehaus.jackson",
|
||||
},
|
||||
{
|
||||
name: "java manifest",
|
||||
metadata: pkg.JavaArchive{
|
||||
|
||||
@@ -27,10 +27,11 @@ type packageLock struct {
|
||||
|
||||
// lockDependency represents a single package dependency listed in the package.lock json file
|
||||
type lockDependency struct {
|
||||
Version string `json:"version"`
|
||||
Resolved string `json:"resolved"`
|
||||
Integrity string `json:"integrity"`
|
||||
Dev bool `json:"dev"`
|
||||
Version string `json:"version"`
|
||||
Resolved string `json:"resolved"`
|
||||
Integrity string `json:"integrity"`
|
||||
Dev bool `json:"dev"`
|
||||
Dependencies map[string]lockDependency `json:"dependencies"`
|
||||
}
|
||||
|
||||
type lockPackage struct {
|
||||
@@ -77,14 +78,7 @@ func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolve
|
||||
}
|
||||
|
||||
if lock.LockfileVersion == 1 {
|
||||
for name, pkgMeta := range lock.Dependencies {
|
||||
// skip packages that are only present as a dev dependency
|
||||
if !a.cfg.IncludeDevDependencies && pkgMeta.Dev {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgs = append(pkgs, newPackageLockV1Package(ctx, a.cfg, resolver, reader.Location, name, pkgMeta))
|
||||
}
|
||||
pkgs = append(pkgs, a.packageLockV1Packages(ctx, resolver, reader.Location, lock.Dependencies)...)
|
||||
}
|
||||
|
||||
if lock.LockfileVersion == 2 || lock.LockfileVersion == 3 {
|
||||
@@ -116,6 +110,32 @@ func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolve
|
||||
return pkgs, dependency.Resolve(packageLockDependencySpecifier, pkgs), unknown.IfEmptyf(pkgs, "unable to determine packages")
|
||||
}
|
||||
|
||||
func (a genericPackageLockAdapter) packageLockV1Packages(ctx context.Context, resolver file.Resolver, location file.Location, dependencies map[string]lockDependency) []pkg.Package {
|
||||
var pkgs []pkg.Package
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
var walk func(map[string]lockDependency)
|
||||
walk = func(dependencies map[string]lockDependency) {
|
||||
for name, pkgMeta := range dependencies {
|
||||
// Skipping a dev-only dependency also skips its dev-only subtree.
|
||||
if !a.cfg.IncludeDevDependencies && pkgMeta.Dev {
|
||||
continue
|
||||
}
|
||||
|
||||
p := newPackageLockV1Package(ctx, a.cfg, resolver, location, name, pkgMeta)
|
||||
if _, exists := seen[p.PURL]; !exists {
|
||||
pkgs = append(pkgs, p)
|
||||
seen[p.PURL] = struct{}{}
|
||||
}
|
||||
|
||||
walk(pkgMeta.Dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
walk(dependencies)
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) {
|
||||
// The license field could be either a string or an array.
|
||||
|
||||
|
||||
@@ -111,6 +111,66 @@ func TestParsePackageLock(t *testing.T) {
|
||||
pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParsePackageLockV1NestedDependencies(t *testing.T) {
|
||||
fixture := "testdata/pkg-lock/nested-package-lock-1.json"
|
||||
expectedPkgs := []pkg.Package{
|
||||
{
|
||||
Name: "duplicate",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:npm/duplicate@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz", Integrity: "sha512-duplicate"},
|
||||
},
|
||||
{
|
||||
Name: "middle",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:npm/middle@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/middle/-/middle-1.0.0.tgz", Integrity: "sha512-middle"},
|
||||
},
|
||||
{
|
||||
Name: "parent",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:npm/parent@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/parent/-/parent-1.0.0.tgz", Integrity: "sha512-parent"},
|
||||
},
|
||||
{
|
||||
Name: "shared",
|
||||
Version: "0.5.0",
|
||||
PURL: "pkg:npm/shared@0.5.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-0.5.0.tgz", Integrity: "sha512-shared-0.5"},
|
||||
},
|
||||
{
|
||||
Name: "shared",
|
||||
Version: "1.0.0",
|
||||
PURL: "pkg:npm/shared@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-1.0.0.tgz", Integrity: "sha512-shared-1"},
|
||||
},
|
||||
{
|
||||
Name: "shared",
|
||||
Version: "2.0.0",
|
||||
PURL: "pkg:npm/shared@2.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-2.0.0.tgz", Integrity: "sha512-shared-2"},
|
||||
},
|
||||
}
|
||||
for i := range expectedPkgs {
|
||||
expectedPkgs[i].Locations.Add(file.NewLocation(fixture))
|
||||
}
|
||||
|
||||
adapter := newGenericPackageLockAdapter(CatalogerConfig{})
|
||||
pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, nil)
|
||||
}
|
||||
|
||||
func TestParsePackageLockV2(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
fixture := "testdata/pkg-lock/package-lock-2.json"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"parent": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parent/-/parent-1.0.0.tgz",
|
||||
"integrity": "sha512-parent",
|
||||
"dependencies": {
|
||||
"shared": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shared/-/shared-1.0.0.tgz",
|
||||
"integrity": "sha512-shared-1"
|
||||
},
|
||||
"middle": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/middle/-/middle-1.0.0.tgz",
|
||||
"integrity": "sha512-middle",
|
||||
"dependencies": {
|
||||
"shared": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/shared/-/shared-0.5.0.tgz",
|
||||
"integrity": "sha512-shared-0.5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz",
|
||||
"integrity": "sha512-duplicate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shared/-/shared-2.0.0.tgz",
|
||||
"integrity": "sha512-shared-2"
|
||||
},
|
||||
"duplicate": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz",
|
||||
"integrity": "sha512-duplicate"
|
||||
},
|
||||
"dev-parent": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dev-nested": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc
|
||||
Name: m.Name,
|
||||
Version: m.Version,
|
||||
Locations: file.NewLocationSet(locations...),
|
||||
PURL: packageURL(m.Name, m.Version),
|
||||
PURL: cargoLockPackageURL(m),
|
||||
Language: pkg.Rust,
|
||||
Type: pkg.RustPkg,
|
||||
Metadata: m,
|
||||
@@ -25,6 +25,18 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc
|
||||
return p
|
||||
}
|
||||
|
||||
func cargoLockPackageURL(m pkg.RustCargoLockEntry) string {
|
||||
// Cargo omits source for packages resolved from the local workspace or a
|
||||
// path dependency. A cargo PURL without a repository qualifier identifies
|
||||
// a package on crates.io, so emitting one for a local package can cause
|
||||
// downstream scanners to match unrelated registry vulnerabilities.
|
||||
if m.Source == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return packageURL(m.Name, m.Version)
|
||||
}
|
||||
|
||||
func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg.Package {
|
||||
p := pkg.Package{
|
||||
Name: dep.Name,
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_packageURL(t *testing.T) {
|
||||
@@ -31,3 +33,34 @@ func Test_packageURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPackageFromCargoMetadataPURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "crates.io package",
|
||||
source: "registry+https://github.com/rust-lang/crates.io-index",
|
||||
want: "pkg:cargo/telemetry@0.1.0",
|
||||
},
|
||||
{
|
||||
name: "local path package",
|
||||
source: "",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := newPackageFromCargoMetadata(pkg.RustCargoLockEntry{
|
||||
Name: "telemetry",
|
||||
Version: "0.1.0",
|
||||
Source: tt.source,
|
||||
})
|
||||
|
||||
assert.Equal(t, tt.want, got.PURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ func TestCargoLockWithGitDependencies(t *testing.T) {
|
||||
helloWorld := pkg.Package{
|
||||
Name: "hello_world",
|
||||
Version: "0.1.0",
|
||||
PURL: "pkg:cargo/hello_world@0.1.0",
|
||||
PURL: "",
|
||||
Locations: locations,
|
||||
Language: pkg.Rust,
|
||||
Type: pkg.RustPkg,
|
||||
@@ -673,6 +673,77 @@ func TestCargoLockWithGitDependencies(t *testing.T) {
|
||||
pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestCargoLockWithPathDependencies(t *testing.T) {
|
||||
fixture := "testdata/path-deps/Cargo.lock"
|
||||
locations := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
app := pkg.Package{
|
||||
Name: "app",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
Language: pkg.Rust,
|
||||
Type: pkg.RustPkg,
|
||||
Licenses: pkg.NewLicenseSet(),
|
||||
Metadata: pkg.RustCargoLockEntry{
|
||||
Name: "app",
|
||||
Version: "1.0.0",
|
||||
Dependencies: []string{
|
||||
"memchr",
|
||||
"telemetry",
|
||||
},
|
||||
},
|
||||
}
|
||||
memchr := pkg.Package{
|
||||
Name: "memchr",
|
||||
Version: "2.7.4",
|
||||
PURL: "pkg:cargo/memchr@2.7.4",
|
||||
Locations: locations,
|
||||
Language: pkg.Rust,
|
||||
Type: pkg.RustPkg,
|
||||
Licenses: pkg.NewLicenseSet(),
|
||||
Metadata: pkg.RustCargoLockEntry{
|
||||
Name: "memchr",
|
||||
Version: "2.7.4",
|
||||
Source: "registry+https://github.com/rust-lang/crates.io-index",
|
||||
Checksum: "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3",
|
||||
Dependencies: []string{},
|
||||
},
|
||||
}
|
||||
telemetry := pkg.Package{
|
||||
Name: "telemetry",
|
||||
Version: "0.1.0",
|
||||
Locations: locations,
|
||||
Language: pkg.Rust,
|
||||
Type: pkg.RustPkg,
|
||||
Licenses: pkg.NewLicenseSet(),
|
||||
Metadata: pkg.RustCargoLockEntry{
|
||||
Name: "telemetry",
|
||||
Version: "0.1.0",
|
||||
Dependencies: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
app,
|
||||
memchr,
|
||||
telemetry,
|
||||
}
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
{
|
||||
From: memchr,
|
||||
To: app,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: telemetry,
|
||||
To: app,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestCargoLockDependencySpecification(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "app"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"telemetry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "telemetry"
|
||||
version = "0.1.0"
|
||||
+4
-2
@@ -30,8 +30,10 @@ type GolangBinaryBuildinfoEntry struct {
|
||||
// name is the import path, a ".", and the local name. One exception: the binary's main package appears
|
||||
// under the key "main" (the name the linker assigns), not its original source import path, which is not
|
||||
// recoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers
|
||||
// this package: the "all" scope populates every module package plus the synthetic stdlib package, while
|
||||
// the "stdlib" scope populates only the stdlib package.
|
||||
// this package: the "all" scope populates every module package plus the synthetic stdlib package, the
|
||||
// "extended-stdlib" scope populates the stdlib package plus every module under golang.org/x/, and the
|
||||
// "stdlib" scope populates only the stdlib package. The capture-symbols-modules glob patterns populate
|
||||
// any additional modules they match.
|
||||
Symbols map[string][]string `json:"symbols,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package snapsource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -21,6 +22,11 @@ type snapManifest struct {
|
||||
|
||||
const manifestLocation = "/meta/snap.yaml"
|
||||
|
||||
// errNoManifest means the payload is a readable squashfs that simply does not carry a snap manifest.
|
||||
// This is distinct from a manifest that is present but unusable, and callers are expected to treat
|
||||
// the two differently.
|
||||
var errNoManifest = errors.New("no snap manifest file found")
|
||||
|
||||
func parseManifest(resolver file.Resolver) (*snapManifest, error) {
|
||||
locations, err := resolver.FilesByPath(manifestLocation)
|
||||
if err != nil {
|
||||
@@ -28,7 +34,7 @@ func parseManifest(resolver file.Resolver) (*snapManifest, error) {
|
||||
}
|
||||
|
||||
if len(locations) == 0 {
|
||||
return nil, fmt.Errorf("no snap manifest file found")
|
||||
return nil, errNoManifest
|
||||
}
|
||||
|
||||
if len(locations) > 1 {
|
||||
@@ -48,9 +54,8 @@ func parseManifest(resolver file.Resolver) (*snapManifest, error) {
|
||||
return nil, fmt.Errorf("unable to decode snap manifest file: %w", err)
|
||||
}
|
||||
|
||||
if manifest.Name == "" || manifest.Version == "" {
|
||||
return nil, fmt.Errorf("invalid snap manifest file: missing name or version")
|
||||
}
|
||||
|
||||
// note: name and version are deliberately not validated here. a manifest that decoded but is
|
||||
// missing them still carries usable fields (summary, base, grade, ...) and throwing it away would
|
||||
// describe less than we know. the caller surfaces the missing identity.
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
@@ -77,22 +77,33 @@ func getRemoteSnapFile(ctx context.Context, fs afero.Fs, getter intFile.Getter,
|
||||
return newSnapFileFromRemote(ctx, fs, cfg, getter, info)
|
||||
}
|
||||
|
||||
func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (*snapFile, error) {
|
||||
func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (_ *snapFile, err error) {
|
||||
t, err := afero.TempDir(fs, "", "syft-snap-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
closer := func() error {
|
||||
return fs.RemoveAll(t)
|
||||
}
|
||||
|
||||
// any failure past this point must not leave the temp directory (or a partially downloaded snap)
|
||||
// behind. On success the caller owns the directory by way of snapFile.Cleanup.
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if closeErr := closer(); closeErr != nil {
|
||||
log.WithFields("error", closeErr, "directory", t).Warn("unable to remove temp directory for snap")
|
||||
}
|
||||
}()
|
||||
|
||||
snapFilePath := path.Join(t, path.Base(info.URL))
|
||||
err = downloadSnap(getter, info, snapFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download snap file: %w", err)
|
||||
}
|
||||
|
||||
closer := func() error {
|
||||
return fs.RemoveAll(t)
|
||||
}
|
||||
|
||||
mimeType, digests, err := getSnapFileInfo(ctx, fs, snapFilePath, cfg.DigestAlgorithms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,6 +3,7 @@ package snapsource
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -119,7 +120,21 @@ func newFromPath(cfg Config, f *snapFile) (source.Source, error) {
|
||||
closer: f.Cleanup,
|
||||
}
|
||||
|
||||
return s, s.extractManifest()
|
||||
if err := s.extractManifest(); err != nil {
|
||||
// It is tried on any path ending in .snap or .squashfs, so declining one is routine and
|
||||
// a later provider usually succeeds. When the user asked for --from snap there is no
|
||||
// fallback and this error reaches them intact.
|
||||
log.WithFields("error", err, "path", f.Path).Debug("unable to read snap")
|
||||
|
||||
// never hand back a source alongside an error: callers that discard the source on error would
|
||||
// leave behind the open squashfs file and the temp directory holding a downloaded snap
|
||||
if closeErr := s.Close(); closeErr != nil {
|
||||
log.WithFields("error", closeErr, "path", f.Path).Warn("unable to clean up snap source")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *snapSource) extractManifest() error {
|
||||
@@ -128,13 +143,26 @@ func (s *snapSource) extractManifest() error {
|
||||
return fmt.Errorf("unable to create snap file resolver: %w", err)
|
||||
}
|
||||
|
||||
// a manifest problem must never fail the source. this source is the only provider that can descend
|
||||
// into squashfs (filesource cannot), so returning an error here would fall through to a provider
|
||||
// that reports zero packages for a payload that has plenty. describe what we can instead.
|
||||
manifest, err := parseManifest(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse snap manifest file: %w", err)
|
||||
switch {
|
||||
case errors.Is(err, errNoManifest):
|
||||
// not every squashfs payload is a snap, and one without a manifest is still worth cataloging
|
||||
log.WithFields("path", s.squashfsPath).Debug("no snap manifest file found")
|
||||
return nil
|
||||
case err != nil:
|
||||
// the manifest is there but unusable, which is worth surfacing: the SBOM will have no source
|
||||
// name or version to show for it
|
||||
log.WithFields("error", err, "path", s.squashfsPath).Warn("unable to parse snap manifest file")
|
||||
return nil
|
||||
}
|
||||
|
||||
if manifest != nil {
|
||||
s.manifest = *manifest
|
||||
s.manifest = *manifest
|
||||
|
||||
if s.manifest.Name == "" || s.manifest.Version == "" {
|
||||
log.WithFields("path", s.squashfsPath).Warn("snap manifest is missing name or version")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -176,25 +204,34 @@ func (s snapSource) Describe() source.Description {
|
||||
}
|
||||
}
|
||||
|
||||
// Close releases everything the source holds. Every step runs even if an earlier one fails, since
|
||||
// bailing early would skip the temp directory removal and leave a downloaded snap on disk. Safe to
|
||||
// call more than once.
|
||||
func (s *snapSource) Close() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
var errs error
|
||||
if s.squashFileCloser != nil {
|
||||
if err := s.squashFileCloser(); err != nil {
|
||||
return fmt.Errorf("unable to close snap resolver: %w", err)
|
||||
errs = errors.Join(errs, fmt.Errorf("unable to close snap resolver: %w", err))
|
||||
}
|
||||
s.squashFileCloser = nil
|
||||
}
|
||||
s.resolver = nil
|
||||
if s.fs != nil {
|
||||
if err := s.fs.Close(); err != nil {
|
||||
return fmt.Errorf("unable to close snap squashfs: %w", err)
|
||||
errs = errors.Join(errs, fmt.Errorf("unable to close snap squashfs: %w", err))
|
||||
}
|
||||
s.fs = nil
|
||||
}
|
||||
if s.closer != nil {
|
||||
if err := s.closer(); err != nil {
|
||||
return fmt.Errorf("unable to close snap source: %w", err)
|
||||
errs = errors.Join(errs, fmt.Errorf("unable to close snap source: %w", err))
|
||||
}
|
||||
s.closer = nil
|
||||
}
|
||||
return nil
|
||||
return errs
|
||||
}
|
||||
|
||||
func (s *snapSource) FileResolver(_ source.Scope) (file.Resolver, error) {
|
||||
|
||||
@@ -2,15 +2,23 @@ package snapsource
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
diskFile "github.com/diskfs/go-diskfs/backend/file"
|
||||
"github.com/diskfs/go-diskfs/filesystem/squashfs"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/go-homedir"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func TestNewFromLocal(t *testing.T) {
|
||||
@@ -84,3 +92,205 @@ func TestNewFromLocal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromPathOnResolverError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
// remote snaps carry a cleanup closure for the temp directory holding the download; local snaps
|
||||
// have none, since the file belongs to the user
|
||||
remote bool
|
||||
wantCleanupRuns int
|
||||
wantFileRemoved bool
|
||||
}{
|
||||
{
|
||||
name: "downloaded snap is cleaned up",
|
||||
remote: true,
|
||||
wantCleanupRuns: 1,
|
||||
wantFileRemoved: true,
|
||||
},
|
||||
{
|
||||
name: "local snap file is left in place",
|
||||
remote: false,
|
||||
wantCleanupRuns: 0,
|
||||
wantFileRemoved: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// the resolver reads the squashfs from the real filesystem, so the payload must live on disk
|
||||
tempDir := t.TempDir()
|
||||
snapPath := filepath.Join(tempDir, "not-really-a-snap.snap")
|
||||
require.NoError(t, os.WriteFile(snapPath, []byte("not squashfs"), 0600))
|
||||
|
||||
var cleanupRuns int
|
||||
f := &snapFile{
|
||||
Path: snapPath,
|
||||
MimeType: "text/plain",
|
||||
}
|
||||
if tt.remote {
|
||||
f.Cleanup = func() error {
|
||||
cleanupRuns++
|
||||
return os.RemoveAll(tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := newFromPath(Config{Request: snapPath}, f)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unable to create snap file resolver")
|
||||
|
||||
// a source returned alongside an error is discarded by callers without being closed, which
|
||||
// would leave the downloaded snap behind
|
||||
assert.Nil(t, got, "expected nil source on error")
|
||||
assert.Equal(t, tt.wantCleanupRuns, cleanupRuns)
|
||||
|
||||
_, statErr := os.Stat(snapPath)
|
||||
if tt.wantFileRemoved {
|
||||
assert.True(t, os.IsNotExist(statErr), "snap file was not cleaned up")
|
||||
} else {
|
||||
assert.NoError(t, statErr, "the user's snap file must never be removed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// a manifest problem must never fail the source: snapsource is the only provider that can descend
|
||||
// into squashfs, so an error here falls through to filesource and every package inside the payload
|
||||
// silently disappears from the SBOM.
|
||||
func TestNewFromPathToleratesManifestProblems(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest string // written to /meta/snap.yaml; empty means no manifest at all
|
||||
wantName string
|
||||
wantVersion string
|
||||
wantSummary string
|
||||
}{
|
||||
{
|
||||
name: "no manifest at all",
|
||||
manifest: "",
|
||||
},
|
||||
{
|
||||
name: "manifest is not valid yaml",
|
||||
manifest: "name: etcd\n\tversion: 3.4\n",
|
||||
},
|
||||
{
|
||||
name: "manifest is missing name and version",
|
||||
manifest: "summary: a thing\nbase: core22\n",
|
||||
wantSummary: "a thing",
|
||||
},
|
||||
{
|
||||
name: "manifest is complete",
|
||||
manifest: "name: etcd\nversion: 3.4.0\nsummary: a thing\n",
|
||||
wantName: "etcd",
|
||||
wantVersion: "3.4.0",
|
||||
wantSummary: "a thing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
snapPath := writeSquashfs(t, tt.manifest)
|
||||
|
||||
src, err := newFromPath(Config{Request: snapPath}, &snapFile{Path: snapPath})
|
||||
require.NoError(t, err, "a manifest problem must not fail the source")
|
||||
require.NotNil(t, src)
|
||||
t.Cleanup(func() { _ = src.Close() })
|
||||
|
||||
desc := src.Describe()
|
||||
assert.Equal(t, tt.wantName, desc.Name)
|
||||
assert.Equal(t, tt.wantVersion, desc.Version)
|
||||
|
||||
meta, ok := desc.Metadata.(source.SnapMetadata)
|
||||
require.True(t, ok, "expected snap metadata")
|
||||
assert.Equal(t, tt.wantSummary, meta.Summary, "fields that did decode must survive")
|
||||
|
||||
// the whole point: the payload is still walkable, so cataloging sees what is inside
|
||||
r, err := src.FileResolver(source.SquashedScope)
|
||||
require.NoError(t, err)
|
||||
locations, err := r.FilesByPath("/payload.txt")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, locations, 1, "contents must remain catalogable regardless of the manifest")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapSourceClose(t *testing.T) {
|
||||
newSource := func(closeErr, cleanupErr error) (*snapSource, *int) {
|
||||
var cleanupRuns int
|
||||
return &snapSource{
|
||||
mutex: &sync.Mutex{},
|
||||
squashFileCloser: func() error {
|
||||
return closeErr
|
||||
},
|
||||
closer: func() error {
|
||||
cleanupRuns++
|
||||
return cleanupErr
|
||||
},
|
||||
}, &cleanupRuns
|
||||
}
|
||||
|
||||
t.Run("temp directory is removed even when closing the file handle fails", func(t *testing.T) {
|
||||
s, cleanupRuns := newSource(errors.New("handle is busy"), nil)
|
||||
|
||||
err := s.Close()
|
||||
|
||||
// the whole point: a failure here used to return early and skip the cleanup below, leaving a
|
||||
// downloaded snap on disk
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "handle is busy")
|
||||
assert.Equal(t, 1, *cleanupRuns, "temp directory removal was skipped")
|
||||
})
|
||||
|
||||
t.Run("every failure is reported, not just the first", func(t *testing.T) {
|
||||
s, _ := newSource(errors.New("handle is busy"), errors.New("directory is busy"))
|
||||
|
||||
err := s.Close()
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "handle is busy")
|
||||
assert.Contains(t, err.Error(), "directory is busy")
|
||||
})
|
||||
|
||||
t.Run("is safe to call twice", func(t *testing.T) {
|
||||
s, cleanupRuns := newSource(nil, nil)
|
||||
|
||||
require.NoError(t, s.Close())
|
||||
require.NoError(t, s.Close())
|
||||
assert.Equal(t, 1, *cleanupRuns, "cleanup must not run again on a second close")
|
||||
})
|
||||
}
|
||||
|
||||
// writeSquashfs builds a real squashfs image containing /payload.txt and, when manifest is non-empty,
|
||||
// /meta/snap.yaml. Returns the path to the image.
|
||||
func writeSquashfs(t *testing.T, manifest string) string {
|
||||
t.Helper()
|
||||
|
||||
snapPath := filepath.Join(t.TempDir(), "test.snap")
|
||||
f, err := os.Create(snapPath)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, f.Close()) }()
|
||||
|
||||
fs, err := squashfs.Create(diskFile.New(f, false), 0, 0, 4096)
|
||||
require.NoError(t, err)
|
||||
|
||||
write := func(path, contents string) {
|
||||
t.Helper()
|
||||
w, err := fs.OpenFile(path, os.O_CREATE|os.O_RDWR)
|
||||
require.NoError(t, err)
|
||||
_, err = w.Write([]byte(contents))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// note: go-diskfs wants paths relative to the image root, without a leading slash
|
||||
require.NoError(t, fs.Mkdir("."))
|
||||
write("payload.txt", "hello")
|
||||
|
||||
if manifest != "" {
|
||||
require.NoError(t, fs.Mkdir("meta"))
|
||||
write(strings.TrimPrefix(manifestLocation, "/"), manifest)
|
||||
}
|
||||
|
||||
require.NoError(t, fs.Finalize(squashfs.FinalizeOptions{}))
|
||||
return snapPath
|
||||
}
|
||||
|
||||
@@ -264,6 +264,10 @@ func TestNewSnapFileFromRemote(t *testing.T) {
|
||||
|
||||
_, err = fs.Stat(result.Path)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
|
||||
// cleanup owns the whole temp directory, not just the snap file within it
|
||||
_, err = fs.Stat(filepath.Dir(result.Path))
|
||||
assert.True(t, os.IsNotExist(err), "temp directory outlived the cleanup closure")
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -315,6 +319,75 @@ func TestNewSnapFileFromRemote(t *testing.T) {
|
||||
expectError: true,
|
||||
errorMsg: "failed to download snap file",
|
||||
},
|
||||
{
|
||||
name: "download writes partial data then fails",
|
||||
cfg: Config{
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
info: &remoteSnap{
|
||||
snapIdentity: snapIdentity{
|
||||
Name: "partial-snap",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
URL: "https://api.snapcraft.io/download/partial_snap.snap",
|
||||
},
|
||||
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
|
||||
mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/partial_snap.snap", mock.Anything).Run(func(args mock.Arguments) {
|
||||
// simulate a transfer that writes some bytes before the connection drops
|
||||
require.NoError(t, createMockSquashfsFile(fs, args.String(0)))
|
||||
}).Return(fmt.Errorf("connection reset by peer"))
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "failed to download snap file",
|
||||
},
|
||||
{
|
||||
// isSquashFSFile trusts the .snap extension, so non-squashfs content under that name is accepted
|
||||
name: "downloaded file has a snap extension but non-snap content",
|
||||
cfg: Config{
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
info: &remoteSnap{
|
||||
snapIdentity: snapIdentity{
|
||||
Name: "not-a-snap",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
URL: "https://api.snapcraft.io/download/not_a_snap.snap",
|
||||
},
|
||||
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
|
||||
mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/not_a_snap.snap", mock.Anything).Run(func(args mock.Arguments) {
|
||||
require.NoError(t, afero.WriteFile(fs, args.String(0), []byte("not squashfs"), 0644))
|
||||
}).Return(nil)
|
||||
},
|
||||
expectError: false,
|
||||
validate: func(t *testing.T, result *snapFile, fs afero.Fs) {
|
||||
assert.NotNil(t, result)
|
||||
assert.Contains(t, result.Path, "not_a_snap.snap")
|
||||
assert.NotNil(t, result.Cleanup)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "downloaded file is not a snap",
|
||||
cfg: Config{
|
||||
DigestAlgorithms: []crypto.Hash{crypto.SHA256},
|
||||
},
|
||||
info: &remoteSnap{
|
||||
snapIdentity: snapIdentity{
|
||||
Name: "not-a-snap",
|
||||
Channel: "stable",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
URL: "https://api.snapcraft.io/download/not_a_snap.tar.gz",
|
||||
},
|
||||
setupMock: func(mockGetter *mockFileGetter, fs afero.Fs) {
|
||||
mockGetter.On("GetFile", mock.AnythingOfType("string"), "https://api.snapcraft.io/download/not_a_snap.tar.gz", mock.Anything).Run(func(args mock.Arguments) {
|
||||
require.NoError(t, afero.WriteFile(fs, args.String(0), []byte("not squashfs"), 0644))
|
||||
}).Return(nil)
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "not a valid squashfs/snap file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -334,8 +407,18 @@ func TestNewSnapFileFromRemote(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||
}
|
||||
assert.Nil(t, result)
|
||||
|
||||
// the temp directory and any partially downloaded payload must not be left behind. The
|
||||
// download destination handed to the getter lives directly under the temp directory.
|
||||
require.Len(t, mockGetter.Calls, 1)
|
||||
tempDir := filepath.Dir(mockGetter.Calls[0].Arguments.String(0))
|
||||
_, statErr := fs.Stat(tempDir)
|
||||
assert.True(t, os.IsNotExist(statErr), "temp directory %q was not cleaned up after failure", tempDir)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
if result.Cleanup != nil {
|
||||
t.Cleanup(func() { _ = result.Cleanup() })
|
||||
}
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, result, fs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user