mirror of
https://github.com/anchore/syft.git
synced 2026-10-14 07:07:18 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d5731482 | ||
|
|
af3aaa0397 | ||
|
|
70098e20bb | ||
|
|
784b17f66c | ||
|
|
d5cd5f6091 | ||
|
|
749ccc59c1 | ||
|
|
273e31e806 | ||
|
|
9beaec2e97 | ||
|
|
ca0cc52d47 | ||
|
|
dd723bb3c5 | ||
|
|
fb68489590 | ||
|
|
5590d5c1ce | ||
|
|
0956753409 | ||
|
|
f966bcfd03 | ||
|
|
c43f4fb416 | ||
|
|
db0c33481e | ||
|
|
a658cc7402 | ||
|
|
254a562b4e | ||
|
|
bc20e66d08 | ||
|
|
6e16396d18 | ||
|
|
5cdc0845fb | ||
|
|
baca15f186 | ||
|
|
b608ab77e1 | ||
|
|
f4c941f08d | ||
|
|
2d5760145c | ||
|
|
f11bb95c1e | ||
|
|
d2fa5ed41c | ||
|
|
7508d75b2b | ||
|
|
3472b48177 | ||
|
|
4bdbd1aa2a | ||
|
|
557ad73ee6 | ||
|
|
cb09dd9e19 | ||
|
|
2f39322f81 | ||
|
|
fe0b78b7fe | ||
|
|
8a7f08e2c8 | ||
|
|
29d601d1f1 | ||
|
|
3ac95bfbd0 | ||
|
|
24262b9e31 | ||
|
|
b04bc0fbfe | ||
|
|
aafa161ff6 | ||
|
|
eeb4193d4a | ||
|
|
5cf8cc9bc3 | ||
|
|
f4a69e6d35 |
+4
-4
@@ -2,7 +2,7 @@ tools:
|
||||
# we want to use a pinned version of binny to manage the toolchain (so binny manages itself!)
|
||||
- name: binny
|
||||
version:
|
||||
want: v0.7.0
|
||||
want: v0.8.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: anchore/binny
|
||||
@@ -26,7 +26,7 @@ tools:
|
||||
# used for linting
|
||||
- name: golangci-lint
|
||||
version:
|
||||
want: v1.59.0
|
||||
want: v1.59.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: golangci/golangci-lint
|
||||
@@ -58,7 +58,7 @@ tools:
|
||||
# used to release all artifacts
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v1.26.2
|
||||
want: v2.0.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -111,7 +111,7 @@ tools:
|
||||
# used for triggering a release
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.49.2
|
||||
want: v2.51.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
|
||||
@@ -5,7 +5,7 @@ inputs:
|
||||
go-version:
|
||||
description: "Go version to install"
|
||||
required: true
|
||||
default: "1.21.x"
|
||||
default: "1.22.x"
|
||||
go-dependencies:
|
||||
description: "Download go dependencies"
|
||||
required: true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
# the job by event.
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 #v5.0.1
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 #v3.25.6
|
||||
uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 #v3.25.10
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 #v3.25.6
|
||||
uses: github/codeql-action/autobuild@23acc5c183826b7a8a97bce3cecc52db901f8251 #v3.25.10
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 #v3.25.6
|
||||
uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 #v3.25.10
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- run: python .github/scripts/labeler.py
|
||||
env:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Update version file
|
||||
run: make ci-release-version-file
|
||||
|
||||
@@ -15,7 +15,15 @@ jobs:
|
||||
environment: release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Check if running on main
|
||||
if: github.ref != 'refs/heads/main'
|
||||
# we are using the following flag when running `cosign blob-verify` for checksum signature verification:
|
||||
# --certificate-identity-regexp "https://github.com/anchore/.github/workflows/release.yaml@refs/heads/main"
|
||||
# if we are not on the main branch, the signature will not be verifiable since the suffix requires the main branch
|
||||
# at the time of when the OIDC token was issued on the Github Actions runner.
|
||||
run: echo "This can only be run on the main branch otherwise releases produced will not be verifiable with cosign" && exit 1
|
||||
|
||||
- name: Check if tag already exists
|
||||
# note: this will fail if the tag already exists
|
||||
@@ -97,7 +105,7 @@ jobs:
|
||||
# required for goreleaser signs section with cosign
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'anchore/syft' # only run for main repo
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'anchore/syft' # only run for main repo
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'anchore/syft' # only run for main repo
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 #v5.0.1
|
||||
with:
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
name: "Static analysis"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
name: "Build snapshot artifacts"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
@@ -187,7 +187,10 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
@@ -231,7 +234,7 @@ jobs:
|
||||
needs: [Build-Snapshot-Artifacts]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
draft: false
|
||||
@@ -80,7 +82,7 @@ nfpms:
|
||||
- deb
|
||||
|
||||
brews:
|
||||
- tap:
|
||||
- repository:
|
||||
owner: anchore
|
||||
name: homebrew-syft
|
||||
token: "{{.Env.GITHUB_BREW_TOKEN}}"
|
||||
|
||||
@@ -40,11 +40,10 @@ Syft binaries are provided for Linux, macOS and Windows.
|
||||
> curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
> ```
|
||||
|
||||
... or, you can specify a release version and destination directory for the installation:
|
||||
|
||||
> ```bash
|
||||
> curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <DESTINATION_DIR> <RELEASE_VERSION>
|
||||
> ```
|
||||
Install script options:
|
||||
- `-b`: Specify a custom installation directory (defaults to `./bin`)
|
||||
- `-d`: More verbose logging levels (`-d` for debug, `-dd` for trace)
|
||||
- `-v`: Verify the signature of the downloaded artifact before installation (requires [`cosign`](https://github.com/sigstore/cosign) to be installed)
|
||||
|
||||
### Homebrew
|
||||
```bash
|
||||
|
||||
@@ -43,6 +43,7 @@ type attestOptions struct {
|
||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||
Attest options.Attest `yaml:"attest" mapstructure:"attest"`
|
||||
Cache options.Cache `json:"-" yaml:"cache" mapstructure:"cache"`
|
||||
}
|
||||
|
||||
func Attest(app clio.Application) *cobra.Command {
|
||||
@@ -77,6 +78,7 @@ func defaultAttestOptions() attestOptions {
|
||||
Output: defaultAttestOutputOptions(),
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Catalog: options.DefaultCatalog(),
|
||||
Cache: options.DefaultCache(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ type scanOptions struct {
|
||||
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||
Cache options.Cache `json:"-" yaml:"cache" mapstructure:"cache"`
|
||||
}
|
||||
|
||||
func defaultScanOptions() *scanOptions {
|
||||
@@ -75,6 +76,7 @@ func defaultScanOptions() *scanOptions {
|
||||
Output: options.DefaultOutput(),
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Catalog: options.DefaultCatalog(),
|
||||
Cache: options.DefaultCache(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/cache"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// Cache provides configuration for the Syft caching behavior
|
||||
type Cache struct {
|
||||
Dir string `yaml:"dir" mapstructure:"dir"`
|
||||
TTL string `yaml:"ttl" mapstructure:"ttl"`
|
||||
}
|
||||
|
||||
func (c *Cache) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&c.Dir, "root directory to cache any downloaded content; empty string will use an in-memory cache")
|
||||
descriptions.Add(&c.TTL, "time to live for cached data; setting this to 0 will disable caching entirely")
|
||||
}
|
||||
|
||||
func (c *Cache) PostLoad() error {
|
||||
ttl, err := parseDuration(c.TTL)
|
||||
if err != nil {
|
||||
log.Warnf("unable to parse duration '%v', using default (%s) due to: %v", c.TTL, durationToString(defaultTTL()), err)
|
||||
ttl = defaultTTL()
|
||||
}
|
||||
// if TTL is <= 0, disable caching entirely
|
||||
if ttl <= 0 {
|
||||
cache.SetManager(nil)
|
||||
return nil
|
||||
}
|
||||
// if dir == "" but we have a TTL, use an in-memory cache
|
||||
if c.Dir == "" {
|
||||
cache.SetManager(cache.NewInMemory(ttl))
|
||||
return nil
|
||||
}
|
||||
dir, err := homedir.Expand(c.Dir)
|
||||
if err != nil {
|
||||
log.Warnf("unable to expand cache directory %s: %v", c.Dir, err)
|
||||
cache.SetManager(cache.NewInMemory(ttl))
|
||||
} else {
|
||||
m, err := cache.NewFromDir(dir, ttl)
|
||||
if err != nil {
|
||||
log.Warnf("unable to get filesystem cache at %s: %v", c.Dir, err)
|
||||
cache.SetManager(cache.NewInMemory(ttl))
|
||||
} else {
|
||||
cache.SetManager(m)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
clio.PostLoader
|
||||
clio.FieldDescriber
|
||||
} = (*Cache)(nil)
|
||||
|
||||
func DefaultCache() Cache {
|
||||
return Cache{
|
||||
Dir: defaultDir(),
|
||||
TTL: durationToString(defaultTTL()),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultTTL() time.Duration {
|
||||
return 7 * 24 * time.Hour
|
||||
}
|
||||
|
||||
func defaultDir() string {
|
||||
var err error
|
||||
cacheRoot := xdg.CacheHome
|
||||
if cacheRoot == "" {
|
||||
cacheRoot, err = homedir.Dir()
|
||||
if err != nil {
|
||||
cacheRoot = os.TempDir()
|
||||
log.Debugf("unable to get stable cache directory due to: %v, defaulting cache to temp dir: %s", err, cacheRoot)
|
||||
} else {
|
||||
cacheRoot = filepath.Join(cacheRoot, ".cache")
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Join(cacheRoot, "syft")
|
||||
}
|
||||
|
||||
func durationToString(duration time.Duration) string {
|
||||
days := int64(duration / (24 * time.Hour))
|
||||
remain := duration % (24 * time.Hour)
|
||||
out := ""
|
||||
if days > 0 {
|
||||
out = fmt.Sprintf("%vd", days)
|
||||
}
|
||||
if remain != 0 {
|
||||
out += remain.String()
|
||||
}
|
||||
if out == "" {
|
||||
return "0"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseDuration(duration string) (time.Duration, error) {
|
||||
whitespace := regexp.MustCompile(`\s+`)
|
||||
duration = strings.ToLower(whitespace.ReplaceAllString(duration, ""))
|
||||
parts := strings.SplitN(duration, "d", 2)
|
||||
var days time.Duration
|
||||
var remain time.Duration
|
||||
var err error
|
||||
if len(parts) > 1 {
|
||||
numDays, daysErr := strconv.Atoi(parts[0])
|
||||
if daysErr != nil {
|
||||
return 0, daysErr
|
||||
}
|
||||
days = time.Duration(numDays) * 24 * time.Hour
|
||||
if len(parts[1]) > 0 {
|
||||
remain, err = time.ParseDuration(parts[1])
|
||||
}
|
||||
} else {
|
||||
remain, err = time.ParseDuration(duration)
|
||||
}
|
||||
return days + remain, err
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/cache"
|
||||
)
|
||||
|
||||
func Test_defaultDir(t *testing.T) {
|
||||
tmpDir := filepath.Join(t.TempDir(), "cache-temp")
|
||||
xdgCacheDir := filepath.Join(tmpDir, "fake-xdg-cache")
|
||||
homeDir := filepath.Join(tmpDir, "fake-home")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no-xdg",
|
||||
env: map[string]string{
|
||||
"HOME": homeDir,
|
||||
},
|
||||
expected: homeDir,
|
||||
},
|
||||
{
|
||||
name: "xdg-cache",
|
||||
env: map[string]string{
|
||||
"XDG_CACHE_HOME": xdgCacheDir,
|
||||
},
|
||||
expected: xdgCacheDir,
|
||||
},
|
||||
}
|
||||
|
||||
// capture all the initial environment variables to reset them before we reset library caches
|
||||
env := map[string]string{
|
||||
"HOME": "",
|
||||
"XDG_DATA_HOME": "",
|
||||
"XDG_DATA_DIRS": "",
|
||||
"XDG_CONFIG_HOME": "",
|
||||
"XDG_CONFIG_DIRS": "",
|
||||
"XDG_STATE_HOME": "",
|
||||
"XDG_CACHE_HOME": "",
|
||||
"XDG_RUNTIME_DIR": "",
|
||||
}
|
||||
for k := range env {
|
||||
env[k] = os.Getenv(k)
|
||||
}
|
||||
|
||||
unsetEnv := func(t *testing.T) {
|
||||
for k := range env {
|
||||
t.Setenv(k, "")
|
||||
}
|
||||
}
|
||||
|
||||
resetEnv := func() {
|
||||
for k, v := range env {
|
||||
if v == "" {
|
||||
_ = os.Unsetenv(k)
|
||||
} else {
|
||||
_ = os.Setenv(k, v)
|
||||
}
|
||||
}
|
||||
homedir.Reset()
|
||||
xdg.Reload()
|
||||
}
|
||||
|
||||
t.Cleanup(resetEnv)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
defer resetEnv()
|
||||
|
||||
unsetEnv(t)
|
||||
for k, v := range test.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
homedir.Reset()
|
||||
xdg.Reload()
|
||||
|
||||
got := defaultDir()
|
||||
|
||||
require.True(t, strings.HasPrefix(got, test.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cacheOptions(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
tests := []struct {
|
||||
name string
|
||||
opts Cache
|
||||
test func(t *testing.T)
|
||||
}{
|
||||
{
|
||||
name: "disable-1",
|
||||
opts: Cache{
|
||||
Dir: "some-dir",
|
||||
TTL: "0",
|
||||
},
|
||||
test: func(t *testing.T) {
|
||||
c := cache.GetManager().GetCache("test-disable-1", "v-disable-1")
|
||||
err := c.Write("key-disable-1", strings.NewReader("some-value-disable-1"))
|
||||
require.NoError(t, err)
|
||||
rdr, err := c.Read("key-disable-1")
|
||||
require.Nil(t, rdr)
|
||||
require.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "disable-2",
|
||||
opts: Cache{
|
||||
Dir: "some-dir",
|
||||
TTL: "0s",
|
||||
},
|
||||
test: func(t *testing.T) {
|
||||
c := cache.GetManager().GetCache("test-disable-2", "v-disable-2")
|
||||
err := c.Write("key-disable-2", strings.NewReader("some-value-disable-2"))
|
||||
require.NoError(t, err)
|
||||
rdr, err := c.Read("key-disable-2")
|
||||
require.Nil(t, rdr)
|
||||
require.Error(t, err)
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "disable-3",
|
||||
opts: Cache{
|
||||
Dir: "some-dir",
|
||||
TTL: "0d",
|
||||
},
|
||||
test: func(t *testing.T) {
|
||||
c := cache.GetManager().GetCache("test-disable-3", "v-disable-3")
|
||||
err := c.Write("key-disable-3", strings.NewReader("some-value-disable-3"))
|
||||
require.NoError(t, err)
|
||||
rdr, err := c.Read("key-disable-3")
|
||||
require.Nil(t, rdr)
|
||||
require.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "in-memory",
|
||||
opts: Cache{
|
||||
Dir: "",
|
||||
TTL: "10m",
|
||||
},
|
||||
test: func(t *testing.T) {
|
||||
c := cache.GetManager().GetCache("test-mem", "v-mem")
|
||||
err := c.Write("key-mem", strings.NewReader("some-value-mem"))
|
||||
require.NoError(t, err)
|
||||
rdr, err := c.Read("key-mem")
|
||||
require.NotNil(t, rdr)
|
||||
defer internal.CloseAndLogError(rdr, "")
|
||||
require.NoError(t, err)
|
||||
data, err := io.ReadAll(rdr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "some-value-mem", string(data))
|
||||
require.NoDirExists(t, filepath.Join("test-mem", "v-mem"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "on disk",
|
||||
opts: Cache{
|
||||
Dir: tmp,
|
||||
TTL: "10m",
|
||||
},
|
||||
test: func(t *testing.T) {
|
||||
c := cache.GetManager().GetCache("test-disk", "v-disk")
|
||||
err := c.Write("key-disk", strings.NewReader("some-value-disk"))
|
||||
require.NoError(t, err)
|
||||
rdr, err := c.Read("key-disk")
|
||||
require.NotNil(t, rdr)
|
||||
defer internal.CloseAndLogError(rdr, "")
|
||||
require.NoError(t, err)
|
||||
data, err := io.ReadAll(rdr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "some-value-disk", string(data))
|
||||
require.DirExists(t, filepath.Join(tmp, "test-disk", "v-disk"))
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
original := cache.GetManager()
|
||||
defer cache.SetManager(original)
|
||||
|
||||
err := test.opts.PostLoad()
|
||||
require.NoError(t, err)
|
||||
|
||||
test.test(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
duration string
|
||||
expect time.Duration
|
||||
err require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
duration: "0d",
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
duration: "0m",
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
duration: "0s",
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
duration: "0",
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
duration: "1d",
|
||||
expect: 24 * time.Hour,
|
||||
},
|
||||
{
|
||||
duration: "7d",
|
||||
expect: 7 * 24 * time.Hour,
|
||||
},
|
||||
{
|
||||
duration: "365D",
|
||||
expect: 365 * 24 * time.Hour,
|
||||
},
|
||||
{
|
||||
duration: "7d1h1m1s",
|
||||
expect: 7*24*time.Hour + time.Hour + time.Minute + time.Second,
|
||||
},
|
||||
{
|
||||
duration: "7d 1h 1m 1s",
|
||||
expect: 7*24*time.Hour + time.Hour + time.Minute + time.Second,
|
||||
},
|
||||
{
|
||||
duration: "2h",
|
||||
expect: 2 * time.Hour,
|
||||
},
|
||||
{
|
||||
duration: "2h5m",
|
||||
expect: 2*time.Hour + 5*time.Minute,
|
||||
},
|
||||
{
|
||||
duration: "2h 5m",
|
||||
expect: 2*time.Hour + 5*time.Minute,
|
||||
},
|
||||
{
|
||||
duration: "d24h",
|
||||
err: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.duration, func(t *testing.T) {
|
||||
got, err := parseDuration(test.duration)
|
||||
if test.err != nil {
|
||||
test.err(t, err)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, test.expect, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_durationToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
duration time.Duration
|
||||
expect string
|
||||
err require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
expect: "1d",
|
||||
duration: 24 * time.Hour,
|
||||
},
|
||||
{
|
||||
expect: "7d",
|
||||
duration: 7 * 24 * time.Hour,
|
||||
},
|
||||
{
|
||||
expect: "7d1h1m1s",
|
||||
duration: 7*24*time.Hour + time.Hour + time.Minute + time.Second,
|
||||
},
|
||||
{
|
||||
expect: "2h0m0s",
|
||||
duration: 2 * time.Hour,
|
||||
},
|
||||
{
|
||||
expect: "2h5m0s",
|
||||
duration: 2*time.Hour + 5*time.Minute,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.expect, func(t *testing.T) {
|
||||
got := durationToString(test.duration)
|
||||
require.Equal(t, test.expect, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
module github.com/anchore/syft
|
||||
|
||||
go 1.21.0
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.8.0
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.0
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
@@ -24,12 +24,12 @@ require (
|
||||
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/charmbracelet/bubbletea v0.26.3
|
||||
github.com/charmbracelet/bubbletea v0.26.4
|
||||
github.com/charmbracelet/lipgloss v0.11.0
|
||||
github.com/dave/jennifer v1.7.0
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/docker v26.1.3+incompatible
|
||||
github.com/docker/docker v26.1.4+incompatible
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
@@ -71,22 +71,26 @@ require (
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/vbatts/go-mtree v0.5.3
|
||||
github.com/vbatts/go-mtree v0.5.4
|
||||
github.com/vifraa/gopom v1.0.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
|
||||
github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/mod v0.17.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/mod v0.18.0
|
||||
golang.org/x/net v0.26.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.29.10
|
||||
modernc.org/sqlite v1.30.1
|
||||
)
|
||||
|
||||
require google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
|
||||
require github.com/magiconair/properties v1.8.7
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/magiconair/properties v1.8.7
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
@@ -98,7 +102,6 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/adrg/xdg v0.4.0 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect
|
||||
@@ -106,7 +109,7 @@ require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.2 // indirect
|
||||
github.com/charmbracelet/x/input v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.0 // indirect
|
||||
@@ -226,13 +229,13 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.19.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/term v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
@@ -240,7 +243,7 @@ require (
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.49.3 // indirect
|
||||
modernc.org/libc v1.52.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
|
||||
@@ -57,9 +57,11 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M=
|
||||
github.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.0 h1:inaif7qD8bivyxp7XLgxUYtOXWtDez7+j72qKTMQTb8=
|
||||
github.com/CycloneDX/cyclonedx-go v0.9.0/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw=
|
||||
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=
|
||||
@@ -155,14 +157,14 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg=
|
||||
github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q=
|
||||
github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40=
|
||||
github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0=
|
||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
|
||||
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
|
||||
github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
|
||||
github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
|
||||
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ=
|
||||
github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
@@ -229,8 +231,8 @@ github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qe
|
||||
github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo=
|
||||
github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
|
||||
github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
|
||||
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
@@ -791,8 +793,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/vbatts/go-mtree v0.5.3 h1:S/jYlfG8rZ+a0bhZd+RANXejy7M4Js8fq9U+XoWTd5w=
|
||||
github.com/vbatts/go-mtree v0.5.3/go.mod h1:eXsdoPMdL2jcJx6HweWi9lYQxBsTp4lNhqqAjgkZUg8=
|
||||
github.com/vbatts/go-mtree v0.5.4 h1:OMAb8jaCyiFA7zXj0Zc/oARcxBDBoeu2LizjB8BVJl0=
|
||||
github.com/vbatts/go-mtree v0.5.4/go.mod h1:5GqJbVhm9BBiCc4K5uc/c42FPgXulHaQs4sFUEfIWMo=
|
||||
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
|
||||
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
|
||||
github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=
|
||||
@@ -875,8 +877,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -917,8 +919,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -966,8 +968,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1079,15 +1081,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1100,8 +1102,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1163,8 +1165,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1357,18 +1359,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
|
||||
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
|
||||
modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
|
||||
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
|
||||
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo=
|
||||
modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
|
||||
modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
|
||||
modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M=
|
||||
modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
@@ -1377,8 +1379,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg=
|
||||
modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
|
||||
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
|
||||
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
+220
-53
@@ -2,34 +2,26 @@
|
||||
# note: we require errors to propagate (don't set -e)
|
||||
set -u
|
||||
|
||||
PROJECT_NAME="syft"
|
||||
PROJECT_NAME=syft
|
||||
OWNER=anchore
|
||||
REPO="${PROJECT_NAME}"
|
||||
GITHUB_DOWNLOAD_PREFIX=https://github.com/${OWNER}/${REPO}/releases/download
|
||||
INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME}
|
||||
PROGRAM_ARGS=$@
|
||||
|
||||
# signature verification options
|
||||
|
||||
# the location to the cosign binary (allowed to be overridden by the user)
|
||||
COSIGN_BINARY=${COSIGN_BINARY:-cosign}
|
||||
VERIFY_SIGN=false
|
||||
# this is the earliest tag in the repo where cosign sign-blob was introduced in the release process (see the goreleaser config)
|
||||
VERIFY_SIGN_SUPPORTED_VERSION=v0.104.0
|
||||
# this is the earliest tag in the repo where the -v flag was introduced to this install.sh script
|
||||
VERIFY_SIGN_FLAG_VERSION=v1.6.0
|
||||
|
||||
# do not change the name of this parameter (this must always be backwards compatible)
|
||||
DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-true}
|
||||
|
||||
#
|
||||
# usage [script-name]
|
||||
#
|
||||
usage() (
|
||||
this=$1
|
||||
cat <<EOF
|
||||
$this: download go binaries for anchore/syft
|
||||
|
||||
Usage: $this [-b] dir [-d] [tag]
|
||||
-b the installation directory (dDefaults to ./bin)
|
||||
-d turns on debug logging
|
||||
-dd turns on trace logging
|
||||
[tag] the specific release to use (if missing, then the latest will be used)
|
||||
EOF
|
||||
exit 2
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# https://github.com/client9/shlib - portable posix shell functions
|
||||
# Public domain - http://unlicense.org
|
||||
@@ -270,21 +262,21 @@ hash_sha256() (
|
||||
)
|
||||
|
||||
hash_sha256_verify() (
|
||||
TARGET=$1
|
||||
target=$1
|
||||
checksums=$2
|
||||
if [ -z "$checksums" ]; then
|
||||
log_err "hash_sha256_verify checksum file not specified in arg2"
|
||||
log_err "hash_sha256_verify checksum file not specified as argument"
|
||||
return 1
|
||||
fi
|
||||
BASENAME=${TARGET##*/}
|
||||
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||
target_basename=${target##*/}
|
||||
want=$(grep "${target_basename}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||
if [ -z "$want" ]; then
|
||||
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
||||
log_err "hash_sha256_verify unable to find checksum for '${target}' in '${checksums}'"
|
||||
return 1
|
||||
fi
|
||||
got=$(hash_sha256 "$TARGET")
|
||||
got=$(hash_sha256 "$target")
|
||||
if [ "$want" != "$got" ]; then
|
||||
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
|
||||
log_err "hash_sha256_verify checksum for '$target' did not verify ${want} vs $got"
|
||||
return 1
|
||||
fi
|
||||
)
|
||||
@@ -358,28 +350,69 @@ github_release_tag() (
|
||||
echo "$tag"
|
||||
)
|
||||
|
||||
# github_release_asset_url [release-url-prefix] [name] [version] [output-dir] [filename]
|
||||
#
|
||||
# outputs the url to the release asset
|
||||
#
|
||||
github_release_asset_url() (
|
||||
download_url="$1"
|
||||
name="$2"
|
||||
version="$3"
|
||||
filename="$4"
|
||||
|
||||
complete_filename="${name}_${version}_${filename}"
|
||||
complete_url="${download_url}/${complete_filename}"
|
||||
|
||||
echo "${complete_url}"
|
||||
)
|
||||
|
||||
# download_github_release_checksums_files [release-url-prefix] [name] [version] [output-dir] [filename]
|
||||
#
|
||||
# outputs path to the downloaded checksums related file
|
||||
#
|
||||
download_github_release_checksums_files() (
|
||||
download_url="$1"
|
||||
name="$2"
|
||||
version="$3"
|
||||
output_dir="$4"
|
||||
filename="$5"
|
||||
|
||||
log_trace "download_github_release_checksums_files(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir}, filename=${filename})"
|
||||
|
||||
complete_filename="${name}_${version}_${filename}"
|
||||
complete_url=$(github_release_asset_url "${download_url}" "${name}" "${version}" "${filename}")
|
||||
output_path="${output_dir}/${complete_filename}"
|
||||
|
||||
http_download "${output_path}" "${complete_url}" ""
|
||||
asset_file_exists "${output_path}"
|
||||
|
||||
log_trace "download_github_release_checksums_files() returned '${output_path}' for file '${complete_filename}'"
|
||||
|
||||
echo "${output_path}"
|
||||
)
|
||||
|
||||
# download_github_release_checksums [release-url-prefix] [name] [version] [output-dir]
|
||||
#
|
||||
# outputs path to the downloaded checksums file
|
||||
#
|
||||
download_github_release_checksums() (
|
||||
download_url="$1"
|
||||
name="$2"
|
||||
version="$3"
|
||||
output_dir="$4"
|
||||
download_github_release_checksums_files "$@" "checksums.txt"
|
||||
)
|
||||
|
||||
log_trace "download_github_release_checksums(url=${download_url}, name=${name}, version=${version}, output_dir=${output_dir})"
|
||||
# github_release_checksums_sig_url [release-url-prefix] [name] [version]
|
||||
#
|
||||
# outputs the url to the release checksums signature file
|
||||
#
|
||||
github_release_checksums_sig_url() (
|
||||
github_release_asset_url "$@" "checksums.txt.sig"
|
||||
)
|
||||
|
||||
checksum_filename=${name}_${version}_checksums.txt
|
||||
checksum_url=${download_url}/${checksum_filename}
|
||||
output_path="${output_dir}/${checksum_filename}"
|
||||
|
||||
http_download "${output_path}" "${checksum_url}" ""
|
||||
asset_file_exists "${output_path}"
|
||||
|
||||
log_trace "download_github_release_checksums() returned '${output_path}'"
|
||||
|
||||
echo "${output_path}"
|
||||
# github_release_checksums_cert_url [release-url-prefix] [name] [version]
|
||||
#
|
||||
# outputs the url to the release checksums certificate file
|
||||
#
|
||||
github_release_checksums_cert_url() (
|
||||
github_release_asset_url "$@" "checksums.txt.pem"
|
||||
)
|
||||
|
||||
# search_for_asset [checksums-file-path] [name] [os] [arch] [format]
|
||||
@@ -535,7 +568,10 @@ download_and_install_asset() (
|
||||
format="$8"
|
||||
binary="$9"
|
||||
|
||||
asset_filepath=$(download_asset "${download_url}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}")
|
||||
if ! asset_filepath=$(download_asset "${download_url}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}"); then
|
||||
log_err "could not download asset for os='${os}' arch='${arch}' format='${format}'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# don't continue if we couldn't download an asset
|
||||
if [ -z "${asset_filepath}" ]; then
|
||||
@@ -546,6 +582,36 @@ download_and_install_asset() (
|
||||
install_asset "${asset_filepath}" "${install_path}" "${binary}"
|
||||
)
|
||||
|
||||
# verify_sign [checksums-file-path] [certificate-reference] [signature-reference] [version]
|
||||
#
|
||||
# attempts verify the signature of the checksums file from the release workflow in Github Actions run against the main branch.
|
||||
#
|
||||
verify_sign() {
|
||||
checksums_file=$1
|
||||
cert_reference=$2
|
||||
sig_reference=$3
|
||||
|
||||
log_trace "verifying artifact $1"
|
||||
|
||||
log_file=$(mktemp)
|
||||
|
||||
${COSIGN_BINARY} \
|
||||
verify-blob "$checksums_file" \
|
||||
--certificate "$cert_reference" \
|
||||
--signature "$sig_reference" \
|
||||
--certificate-identity "https://github.com/${OWNER}/${REPO}/.github/workflows/release.yaml@refs/heads/main" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" > "${log_file}" 2>&1
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
log_err "$(cat "${log_file}")"
|
||||
rm -f "${log_file}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "${log_file}"
|
||||
}
|
||||
|
||||
|
||||
# download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format] [binary]
|
||||
#
|
||||
# outputs the path to the downloaded asset asset_filepath
|
||||
@@ -572,6 +638,20 @@ download_asset() (
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$VERIFY_SIGN" = true ]; then
|
||||
checksum_sig_file_url=$(github_release_checksums_sig_url "${download_url}" "${name}" "${version}")
|
||||
log_trace "checksums signature url: ${checksum_sig_file_url}"
|
||||
|
||||
checksums_cert_file_url=$(github_release_checksums_cert_url "${download_url}" "${name}" "${version}")
|
||||
log_trace "checksums certificate url: ${checksums_cert_file_url}"
|
||||
|
||||
if ! verify_sign "${checksums_filepath}" "${checksums_cert_file_url}" "${checksum_sig_file_url}"; then
|
||||
log_err "signature verification failed"
|
||||
return 1
|
||||
fi
|
||||
log_info "signature verification succeeded"
|
||||
fi
|
||||
|
||||
asset_url="${download_url}/${asset_filename}"
|
||||
asset_filepath="${destination}/${asset_filename}"
|
||||
http_download "${asset_filepath}" "${asset_url}" ""
|
||||
@@ -609,6 +689,79 @@ install_asset() (
|
||||
install "${archive_dir}/${binary}" "${destination}/"
|
||||
)
|
||||
|
||||
# compare two semver strings. Returns 0 if version1 >= version2, 1 otherwise.
|
||||
# Note: pre-release (-) and metadata (+) are not supported.
|
||||
compare_semver() {
|
||||
# remove leading 'v' if present
|
||||
version1=${1#v}
|
||||
version2=${2#v}
|
||||
|
||||
IFS=. read -r major1 minor1 patch1 <<EOF
|
||||
$version1
|
||||
EOF
|
||||
IFS=. read -r major2 minor2 patch2 <<EOF
|
||||
$version2
|
||||
EOF
|
||||
|
||||
if [ "$major1" -gt "$major2" ]; then
|
||||
return 0
|
||||
elif [ "$major1" -lt "$major2" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$minor1" -gt "$minor2" ]; then
|
||||
return 0
|
||||
elif [ "$minor1" -lt "$minor2" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$patch1" -gt "$patch2" ]; then
|
||||
return 0
|
||||
elif [ "$patch1" -lt "$patch2" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# versions are equal
|
||||
return 0
|
||||
}
|
||||
|
||||
prep_signature_verification() {
|
||||
version="$1"
|
||||
|
||||
if [ "$VERIFY_SIGN" != true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# is there any cryptographic material produced at release that we can use for signature verification?
|
||||
if ! compare_semver "$version" "$VERIFY_SIGN_SUPPORTED_VERSION"; then
|
||||
log_err "${PROJECT_NAME} release '$version' does not support signature verification"
|
||||
log_err "you can still install ${PROJECT_NAME} by removing the -v flag or using a release that supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')"
|
||||
log_err "aborting installation"
|
||||
return 1
|
||||
else
|
||||
log_trace "${PROJECT_NAME} release '$version' supports signature verification (>= '$VERIFY_SIGN_SUPPORTED_VERSION')"
|
||||
fi
|
||||
|
||||
# will invoking an earlier version of this script work (considering the -v flag)?
|
||||
if ! compare_semver "$version" "$VERIFY_SIGN_FLAG_VERSION"; then
|
||||
# the -v argument did not always exist, so we cannot be guaranteed that invoking an earlier version of this script
|
||||
# will work (error with "illegal option -v"). However, the user requested signature verification, so we will
|
||||
# attempt to install the application with this version of the script (keeping signature verification).
|
||||
DOWNLOAD_TAG_INSTALL_SCRIPT=false
|
||||
log_debug "provided version install script does not support -v flag (>= '$VERIFY_SIGN_FLAG_VERSION'), using current script for installation"
|
||||
else
|
||||
log_trace "provided version install script supports -v flag (>= '$VERIFY_SIGN_FLAG_VERSION')"
|
||||
fi
|
||||
|
||||
# check to see if the cosign binary is installed
|
||||
if is_command "${COSIGN_BINARY}"; then
|
||||
log_trace "${COSIGN_BINARY} binary is installed"
|
||||
else
|
||||
log_err "signature verification is requested but ${COSIGN_BINARY} binary is not installed (see https://docs.sigstore.dev/system_config/installation/ to install it)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() (
|
||||
# parse arguments
|
||||
|
||||
@@ -616,7 +769,7 @@ main() (
|
||||
install_dir=${install_dir:-./bin}
|
||||
|
||||
# note: never change the program flags or arguments (this must always be backwards compatible)
|
||||
while getopts "b:dh?x" arg; do
|
||||
while getopts "b:dvh?x" arg; do
|
||||
case "$arg" in
|
||||
b) install_dir="$OPTARG" ;;
|
||||
d)
|
||||
@@ -628,25 +781,37 @@ main() (
|
||||
log_set_priority $log_trace_priority
|
||||
fi
|
||||
;;
|
||||
h | \?) usage "$0" ;;
|
||||
v) VERIFY_SIGN=true;;
|
||||
h | \?)
|
||||
cat <<EOF
|
||||
Download and install a released binary for ${OWNER}/${REPO} from the github releases page
|
||||
|
||||
Usage: $0 [-v] [-b DIR] [-d] [TAG]
|
||||
-b DIR the installation directory (defaults to ./bin)
|
||||
-d turns on debug logging
|
||||
-dd turns on trace logging
|
||||
-v verify checksum signature (requires cosign binary to be installed).
|
||||
TAG the specific release to use (if missing, then the latest will be used)
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
x) set -x ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
|
||||
set +u
|
||||
tag=$1
|
||||
|
||||
if [ -z "${tag}" ]; then
|
||||
log_debug "checking github for the current release tag"
|
||||
log_info "checking github for the current release tag"
|
||||
tag=""
|
||||
else
|
||||
log_debug "checking github for release tag='${tag}'"
|
||||
log_info "checking github for release tag='${tag}'"
|
||||
fi
|
||||
set -u
|
||||
|
||||
tag=$(get_release_tag "${OWNER}" "${REPO}" "${tag}")
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
if ! tag=$(get_release_tag "${OWNER}" "${REPO}" "${tag}"); then
|
||||
log_err "unable to find tag='${tag}'"
|
||||
log_err "do not specify a version or select a valid version from https://github.com/${OWNER}/${REPO}/releases"
|
||||
return 1
|
||||
@@ -661,6 +826,10 @@ main() (
|
||||
binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}")
|
||||
download_url="${GITHUB_DOWNLOAD_PREFIX}/${tag}"
|
||||
|
||||
if ! prep_signature_verification "$version"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# we always use the install.sh script that is associated with the tagged release. Why? the latest install.sh is not
|
||||
# guaranteed to be able to install every version of the application. We use the DOWNLOAD_TAG_INSTALL_SCRIPT env var
|
||||
# to indicate if we should continue processing with the existing script or to download the script from the given tag.
|
||||
@@ -678,10 +847,8 @@ main() (
|
||||
|
||||
log_debug "downloading files into ${download_dir}"
|
||||
|
||||
download_and_install_asset "${download_url}" "${download_dir}" "${install_dir}" "${PROJECT_NAME}" "${os}" "${arch}" "${version}" "${format}" "${binary}"
|
||||
|
||||
# don't continue if we couldn't install the asset
|
||||
if [ "$?" != "0" ]; then
|
||||
if ! download_and_install_asset "${download_url}" "${download_dir}" "${install_dir}" "${PROJECT_NAME}" "${os}" "${arch}" "${version}" "${format}" "${binary}"; then
|
||||
log_err "failed to install ${PROJECT_NAME}"
|
||||
return 1
|
||||
fi
|
||||
@@ -697,4 +864,4 @@ if [ -z "${TEST_INSTALL_SH}" ]; then
|
||||
main "$@"
|
||||
exit $?
|
||||
fi
|
||||
set -u
|
||||
set -u
|
||||
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
# Caching
|
||||
|
||||
All caches are created from a global `manager`. By defaut this is a `bypassedCache`, which performs no caching.
|
||||
One benefit of this is that tests don't need to worry about caching causing issues unless they explicitly need
|
||||
to test the cache and can opt-in using the `cache.TestCache(t)` helper.
|
||||
|
||||
Syft sets a `filesystemCache` when the [cache options](../../cmd/syft/internal/options/cache.go) are loaded.
|
||||
|
||||
When using the `filesystemCache` all items are stored on disk under a root directory, generally in the form of:
|
||||
```
|
||||
<rootDir>/<named-cache>/<data-version>/path/to/data
|
||||
```
|
||||
|
||||
# Using the cache
|
||||
|
||||
The easiest and preferred method to use the cache is a `cache.Resolver`, which automatically creates a `<data-version>`
|
||||
based on the _structure_ of the provided type.
|
||||
If the structure changes in any way it will end up with a new version key and all will re populate this new key,
|
||||
ignoring cached values from older, different versions.
|
||||
The resolver will store items using the `json` package to serialize/deserialize values, so to save space
|
||||
it is encouraged to use `omitempty`. For example:
|
||||
|
||||
```go
|
||||
type myCacheItem struct {
|
||||
Name string `json:"name",omitempty`
|
||||
}
|
||||
```
|
||||
|
||||
It is possible to use core types such as `pkg.Package` as long as they support the standard `json` serialization,
|
||||
but this is discouraged in order to decouple changes to them from affecting the information stored in the cache.
|
||||
|
||||
To get a cache for this type:
|
||||
```go
|
||||
resolver := cache.GetResolver[myCacheItem]("myCacheName", "v1")
|
||||
```
|
||||
|
||||
Using the `resolver` is a single call, which manages checking for items in the cache, expiry times,
|
||||
and if not found invoking the callback to populate the cache and return a value:
|
||||
```go
|
||||
data := resolver.Resolve("some/cache/key", func() (myCacheItem, error) {
|
||||
// do things to return a myCacheItem or error
|
||||
})
|
||||
```
|
||||
|
||||
If it is common that checking for an item will result in errors, and you do not want to re-run the resolve function
|
||||
when errors are encountered, instead of using `GetResolver`, you can use `GetResolverCachingErrors`, which is useful
|
||||
for things such as resolving artifacts over a network, where a number of them will not be resolved, and you do not want
|
||||
to continue to have the expense of running the network resolution. This should be used when it is acceptable a network
|
||||
outage and cached errors is an acceptable risk.
|
||||
|
||||
An example can be seen in the [golang cataloger](../../syft/pkg/cataloger/golang/licenses.go) fetching remote licenses.
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
package cache
|
||||
|
||||
import "io"
|
||||
|
||||
type bypassedCache struct{}
|
||||
|
||||
func (b *bypassedCache) Read(_ string) (ReaderAtCloser, error) {
|
||||
return nil, errNotFound
|
||||
}
|
||||
|
||||
func (b *bypassedCache) Write(_ string, contents io.Reader) error {
|
||||
if closer, ok := contents.(io.Closer); ok {
|
||||
_ = closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bypassedCache) GetCache(_, _ string) Cache {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *bypassedCache) RootDirs() []string {
|
||||
return nil
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_bypassedCache(t *testing.T) {
|
||||
m := bypassedCache{}
|
||||
cache := m.GetCache("name", "version")
|
||||
err := cache.Write("test", strings.NewReader("value"))
|
||||
require.NoError(t, err)
|
||||
rdr, err := cache.Read("test")
|
||||
require.Nil(t, rdr)
|
||||
require.ErrorIs(t, err, errNotFound)
|
||||
}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Manager is responsible for managing cache data and instantiating all caches
|
||||
type Manager interface {
|
||||
// GetCache returns a cache scoped to the given named, versioned data
|
||||
GetCache(name, version string) Cache
|
||||
|
||||
// RootDirs returns any root directories this cache manager uses
|
||||
RootDirs() []string
|
||||
}
|
||||
|
||||
// ReaderAtCloser is an amalgamation of: io.Reader, io.ReaderAt, and io.Closer
|
||||
type ReaderAtCloser interface {
|
||||
io.Reader
|
||||
io.ReaderAt
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Cache is what the application interacts with to get and set cached data
|
||||
type Cache interface {
|
||||
// Read returns a reader for the cache value, if found and not expired
|
||||
// or errors when unable to find / expired
|
||||
Read(key string) (ReaderAtCloser, error)
|
||||
|
||||
// Write writes the contents of the reader to the cache
|
||||
// and closes it, if the reader implements io.Closer
|
||||
Write(key string, contents io.Reader) error
|
||||
}
|
||||
|
||||
// GetManager returns the global cache manager, which is used to instantiate all caches
|
||||
func GetManager() Manager {
|
||||
return manager
|
||||
}
|
||||
|
||||
// SetManager sets the global cache manager, which is used to instantiate all caches.
|
||||
// Setting this to nil disables caching.
|
||||
func SetManager(m Manager) {
|
||||
if m == nil {
|
||||
manager = &bypassedCache{}
|
||||
} else {
|
||||
manager = m
|
||||
}
|
||||
}
|
||||
|
||||
var manager Manager = &bypassedCache{}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_SetManager(t *testing.T) {
|
||||
original := GetManager()
|
||||
defer SetManager(original)
|
||||
|
||||
SetManager(nil)
|
||||
|
||||
require.NotNil(t, GetManager())
|
||||
require.IsType(t, &bypassedCache{}, GetManager())
|
||||
|
||||
SetManager(NewInMemory(0))
|
||||
|
||||
require.NotNil(t, GetManager())
|
||||
require.IsType(t, &bypassedCache{}, GetManager())
|
||||
|
||||
SetManager(NewInMemory(1 * time.Hour))
|
||||
|
||||
require.NotNil(t, GetManager())
|
||||
require.IsType(t, &filesystemCache{}, GetManager())
|
||||
|
||||
SetManager(nil)
|
||||
require.NotNil(t, GetManager())
|
||||
require.IsType(t, &bypassedCache{}, GetManager())
|
||||
}
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
package cache
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GetResolverCachingErrors returns a Resolver that caches errors and will return them
|
||||
// instead of continuing to call the provided resolve functions
|
||||
func GetResolverCachingErrors[T any](name, version string) Resolver[T] {
|
||||
return &errorResolver[T]{
|
||||
resolver: GetResolver[errResponse[T]](name, version),
|
||||
}
|
||||
}
|
||||
|
||||
type errResponse[T any] struct {
|
||||
Error string `json:"err,omitempty"`
|
||||
Value T `json:"val,omitempty"`
|
||||
}
|
||||
|
||||
type errorResolver[T any] struct {
|
||||
resolver Resolver[errResponse[T]]
|
||||
}
|
||||
|
||||
func (r *errorResolver[T]) Resolve(key string, resolver resolverFunc[T]) (T, error) {
|
||||
v, err := r.resolver.Resolve(key, func() (errResponse[T], error) {
|
||||
v, err := resolver()
|
||||
out := errResponse[T]{
|
||||
Value: v,
|
||||
}
|
||||
if err != nil {
|
||||
out.Error = err.Error()
|
||||
}
|
||||
return out, nil
|
||||
})
|
||||
if err != nil {
|
||||
return v.Value, err
|
||||
}
|
||||
if v.Error != "" {
|
||||
return v.Value, fmt.Errorf(v.Error)
|
||||
}
|
||||
return v.Value, nil
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_errorResolver(t *testing.T) {
|
||||
original := GetManager()
|
||||
defer SetManager(original)
|
||||
SetManager(NewInMemory(time.Hour))
|
||||
|
||||
resolver := GetResolverCachingErrors[string]("theCache", "theVersion")
|
||||
|
||||
resolveCount := 0
|
||||
resolveFunc := func() (string, error) {
|
||||
resolveCount++
|
||||
return "theValue", nil
|
||||
}
|
||||
|
||||
val, err := resolver.Resolve("theKey", resolveFunc)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, resolveCount)
|
||||
require.Equal(t, "theValue", val)
|
||||
|
||||
val, err = resolver.Resolve("theKey", resolveFunc)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, resolveCount)
|
||||
require.Equal(t, "theValue", val)
|
||||
|
||||
errorCount := 0
|
||||
errorFunc := func() (string, error) {
|
||||
errorCount++
|
||||
return "", fmt.Errorf("an error")
|
||||
}
|
||||
|
||||
_, err = resolver.Resolve("errorValue", errorFunc)
|
||||
require.ErrorContains(t, err, "an error")
|
||||
require.Equal(t, 1, errorCount)
|
||||
|
||||
_, err = resolver.Resolve("errorValue", errorFunc)
|
||||
require.ErrorContains(t, err, "an error")
|
||||
require.Equal(t, 1, errorCount)
|
||||
}
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// NewFromDir creates a new cache manager which returns caches stored on disk, rooted at the given directory
|
||||
func NewFromDir(dir string, ttl time.Duration) (Manager, error) {
|
||||
dir = filepath.Clean(dir)
|
||||
fsys, err := subFs(afero.NewOsFs(), dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filesystemCache{
|
||||
dir: dir,
|
||||
fs: fsys,
|
||||
ttl: ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const filePermissions = 0700
|
||||
const dirPermissions = os.ModeDir | filePermissions
|
||||
|
||||
type filesystemCache struct {
|
||||
dir string
|
||||
fs afero.Fs
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func (d *filesystemCache) GetCache(name, version string) Cache {
|
||||
fsys, err := subFs(d.fs, name, version)
|
||||
if err != nil {
|
||||
log.Warnf("error getting cache for: %s/%s: %v", name, version, err)
|
||||
return &bypassedCache{}
|
||||
}
|
||||
return &filesystemCache{
|
||||
dir: filepath.Join(d.dir, name, version),
|
||||
fs: fsys,
|
||||
ttl: d.ttl,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *filesystemCache) RootDirs() []string {
|
||||
if d.dir == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{d.dir}
|
||||
}
|
||||
|
||||
func (d *filesystemCache) Read(key string) (ReaderAtCloser, error) {
|
||||
path := makeDiskKey(key)
|
||||
f, err := d.fs.Open(path)
|
||||
if err != nil {
|
||||
log.Tracef("no cache entry for %s %s: %v", d.dir, key, err)
|
||||
return nil, errNotFound
|
||||
} else if stat, err := f.Stat(); err != nil || stat == nil || time.Since(stat.ModTime()) > d.ttl {
|
||||
log.Tracef("cache entry is too old for %s %s", d.dir, key)
|
||||
return nil, errExpired
|
||||
}
|
||||
log.Tracef("using cache for %s %s", d.dir, key)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (d *filesystemCache) Write(key string, contents io.Reader) error {
|
||||
path := makeDiskKey(key)
|
||||
return afero.WriteReader(d.fs, path, contents)
|
||||
}
|
||||
|
||||
// subFs returns a writable directory with the given name under the root cache directory returned from findRoot,
|
||||
// the directory will be created if it does not exist
|
||||
func subFs(fsys afero.Fs, subDirs ...string) (afero.Fs, error) {
|
||||
dir := filepath.Join(subDirs...)
|
||||
dir = filepath.Clean(dir)
|
||||
stat, err := fsys.Stat(dir)
|
||||
if errors.Is(err, afero.ErrFileNotFound) {
|
||||
err = fsys.MkdirAll(dir, dirPermissions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create directory at '%s/%s': %v", dir, strings.Join(subDirs, "/"), err)
|
||||
}
|
||||
stat, err = fsys.Stat(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err != nil || stat == nil || !stat.IsDir() {
|
||||
return nil, fmt.Errorf("unable to verify directory '%s': %v", dir, err)
|
||||
}
|
||||
fsys = afero.NewBasePathFs(fsys, dir)
|
||||
return fsys, err
|
||||
}
|
||||
|
||||
var keyReplacer = regexp.MustCompile("[^-._/a-zA-Z0-9]")
|
||||
|
||||
// makeDiskKey makes a safe sub-path but not escape forward slashes, this allows for logical partitioning on disk
|
||||
func makeDiskKey(key string) string {
|
||||
// encode single dot directory
|
||||
if key == "." {
|
||||
return "%2E"
|
||||
}
|
||||
// replace any disallowed chars with encoded form
|
||||
key = keyReplacer.ReplaceAllStringFunc(key, url.QueryEscape)
|
||||
// allow . in names but not ..
|
||||
key = strings.ReplaceAll(key, "..", "%2E%2E")
|
||||
return key
|
||||
}
|
||||
|
||||
var errNotFound = fmt.Errorf("not found")
|
||||
var errExpired = fmt.Errorf("expired")
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
func Test_filesystemCache(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
man, err := NewFromDir(dir, 1*time.Minute)
|
||||
require.NoError(t, err)
|
||||
|
||||
cacheName := "test"
|
||||
cacheVersion := "v1"
|
||||
cache := man.GetCache(cacheName, cacheVersion)
|
||||
|
||||
cacheKey := "test-key"
|
||||
contentsValue := "some contents to cache"
|
||||
|
||||
err = cache.Write(cacheKey, strings.NewReader(contentsValue))
|
||||
require.NoError(t, err)
|
||||
|
||||
rdr, err := cache.Read(cacheKey)
|
||||
require.NoError(t, err)
|
||||
defer internal.CloseAndLogError(rdr, cacheKey)
|
||||
|
||||
contents, err := io.ReadAll(rdr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, contentsValue, string(contents))
|
||||
|
||||
// check the contents were actually written to disk as expected
|
||||
contents, err = os.ReadFile(filepath.Join(dir, cacheName, cacheVersion, cacheKey))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, contentsValue, string(contents))
|
||||
|
||||
_, err = cache.Read("otherKey")
|
||||
require.ErrorIs(t, err, errNotFound)
|
||||
}
|
||||
|
||||
func Test_makeDiskKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
in: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
in: ".",
|
||||
expected: "%2E",
|
||||
},
|
||||
{
|
||||
in: "..",
|
||||
expected: "%2E%2E",
|
||||
},
|
||||
{
|
||||
in: "github.com",
|
||||
expected: "github.com",
|
||||
},
|
||||
{
|
||||
in: "../github.com",
|
||||
expected: "%2E%2E/github.com",
|
||||
},
|
||||
{
|
||||
in: "github.com/../..",
|
||||
expected: "github.com/%2E%2E/%2E%2E",
|
||||
},
|
||||
{
|
||||
in: "github.com/%2E../..",
|
||||
expected: "github.com/%252E%2E%2E/%2E%2E",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.in, func(t *testing.T) {
|
||||
got := makeDiskKey(test.in)
|
||||
// validate appropriate escaping
|
||||
require.Equal(t, test.expected, got)
|
||||
// also validate that unescaped string matches original
|
||||
unescaped, err := url.QueryUnescape(got)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.in, unescaped)
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+71
@@ -0,0 +1,71 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
)
|
||||
|
||||
// hashType returns a stable hash based on the structure of the type
|
||||
func hashType[T any]() string {
|
||||
// get the base type and hash an empty instance
|
||||
var t T
|
||||
empty := emptyValue(reflect.TypeOf(t)).Interface()
|
||||
hash, err := hashstructure.Hash(empty, hashstructure.FormatV2, &hashstructure.HashOptions{
|
||||
ZeroNil: false,
|
||||
IgnoreZeroValue: false,
|
||||
SlicesAsSets: false,
|
||||
UseStringer: false,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to use type as cache key: %w", err))
|
||||
}
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
||||
func emptyValue(t reflect.Type) reflect.Value {
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer:
|
||||
e := t.Elem()
|
||||
v := emptyValue(e)
|
||||
if v.CanAddr() {
|
||||
return v.Addr()
|
||||
}
|
||||
ptrv := reflect.New(e)
|
||||
ptrv.Elem().Set(v)
|
||||
return ptrv
|
||||
case reflect.Slice:
|
||||
v := emptyValue(t.Elem())
|
||||
s := reflect.MakeSlice(t, 1, 1)
|
||||
s.Index(0).Set(v)
|
||||
return s
|
||||
case reflect.Struct:
|
||||
v := reflect.New(t).Elem()
|
||||
// get all empty field values, too
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
if isIgnored(f) {
|
||||
continue
|
||||
}
|
||||
fv := v.Field(i)
|
||||
if fv.CanSet() {
|
||||
fv.Set(emptyValue(f.Type))
|
||||
}
|
||||
}
|
||||
return v
|
||||
default:
|
||||
return reflect.New(t).Elem()
|
||||
}
|
||||
}
|
||||
|
||||
func isIgnored(f reflect.StructField) bool {
|
||||
if !f.IsExported() {
|
||||
return true
|
||||
}
|
||||
tag := f.Tag.Get("hash")
|
||||
if tag == "-" || tag == "ignore" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Vendored
+123
@@ -0,0 +1,123 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_hashType(t *testing.T) {
|
||||
type t1 struct {
|
||||
Name string
|
||||
}
|
||||
type t2 struct {
|
||||
Name string
|
||||
}
|
||||
type generic[T any] struct {
|
||||
Val T
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
hash func() string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "struct 1",
|
||||
hash: func() string { return hashType[t1]() },
|
||||
expected: "d106c3ffbf98a0b1",
|
||||
},
|
||||
{
|
||||
name: "slice of struct 1",
|
||||
hash: func() string { return hashType[[]t1]() },
|
||||
expected: "8122ace4ee1af0b4",
|
||||
},
|
||||
{
|
||||
name: "slice of struct 2",
|
||||
hash: func() string { return hashType[[]t2]() },
|
||||
expected: "8cc04b5808be5bf9",
|
||||
},
|
||||
{
|
||||
name: "ptr 1",
|
||||
hash: func() string { return hashType[*t1]() },
|
||||
expected: "d106c3ffbf98a0b1", // same hash as t1, which is ok since the structs are the same
|
||||
},
|
||||
{
|
||||
name: "slice of ptr 1",
|
||||
hash: func() string { return hashType[[]*t1]() },
|
||||
expected: "8122ace4ee1af0b4", // same hash as []t1, again underlying serialization is the same
|
||||
},
|
||||
{
|
||||
name: "slice of ptr 2",
|
||||
hash: func() string { return hashType[[]*t2]() },
|
||||
expected: "8cc04b5808be5bf9", // same hash as []t2, underlying serialization is the same
|
||||
},
|
||||
{
|
||||
name: "slice of ptr of slice of ptr",
|
||||
hash: func() string { return hashType[[]*[]*t1]() },
|
||||
expected: "500d9f5b3a5977ce",
|
||||
},
|
||||
{
|
||||
name: "generic 1",
|
||||
hash: func() string { return hashType[generic[t1]]() },
|
||||
expected: "b5fbb30e24400e81",
|
||||
},
|
||||
{
|
||||
name: "generic 2",
|
||||
hash: func() string { return hashType[generic[t2]]() },
|
||||
expected: "becdb767c6b22bfa",
|
||||
},
|
||||
{
|
||||
name: "generic with ptr 1",
|
||||
hash: func() string { return hashType[generic[*t1]]() },
|
||||
expected: "30c8855bf290fd83",
|
||||
},
|
||||
{
|
||||
name: "generic with ptr 2",
|
||||
hash: func() string { return hashType[generic[*t2]]() },
|
||||
expected: "b66366b6ce9e6361",
|
||||
},
|
||||
{
|
||||
name: "generic with slice 1",
|
||||
hash: func() string { return hashType[generic[[]t1]]() },
|
||||
expected: "d2ed158942fa6c29",
|
||||
},
|
||||
{
|
||||
name: "generic with slice 2",
|
||||
hash: func() string { return hashType[generic[[]t2]]() },
|
||||
expected: "7a7bec575871c179",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
require.Equal(t, test.expected, test.hash())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hashIgnores(t *testing.T) {
|
||||
hash := func(v any) string {
|
||||
v, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{})
|
||||
require.NoError(t, err)
|
||||
return fmt.Sprintf("%x", v)
|
||||
}
|
||||
type t1 struct {
|
||||
Name string
|
||||
notExported string
|
||||
}
|
||||
require.Equal(t, hash(t1{notExported: "a value"}), hashType[t1]())
|
||||
|
||||
type t2 struct {
|
||||
Name string
|
||||
Exported string `hash:"ignore"`
|
||||
}
|
||||
require.Equal(t, hash(t2{Exported: "another value"}), hashType[t2]())
|
||||
|
||||
type t3 struct {
|
||||
Name string
|
||||
Exported string `hash:"-"`
|
||||
}
|
||||
require.Equal(t, hash(t3{Exported: "still valued"}), hashType[t3]())
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// NewInMemory returns an in-memory only cache manager
|
||||
func NewInMemory(ttl time.Duration) Manager {
|
||||
if ttl <= 0 {
|
||||
return &bypassedCache{}
|
||||
}
|
||||
return &filesystemCache{
|
||||
dir: "",
|
||||
fs: afero.NewMemMapFs(),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
)
|
||||
|
||||
func Test_NewInMemory(t *testing.T) {
|
||||
man := NewInMemory(time.Hour)
|
||||
|
||||
cacheName := "test"
|
||||
cacheVersion := "v1"
|
||||
cache := man.GetCache(cacheName, cacheVersion)
|
||||
|
||||
cacheKey := "test-key"
|
||||
contentsValue := "some contents to cache"
|
||||
|
||||
err := cache.Write(cacheKey, strings.NewReader(contentsValue))
|
||||
require.NoError(t, err)
|
||||
|
||||
rdr, err := cache.Read(cacheKey)
|
||||
require.NoError(t, err)
|
||||
defer internal.CloseAndLogError(rdr, cacheKey)
|
||||
|
||||
contents, err := io.ReadAll(rdr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, contentsValue, string(contents))
|
||||
|
||||
_, err = cache.Read("otherKey")
|
||||
require.ErrorIs(t, err, errNotFound)
|
||||
}
|
||||
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// Resolver interface provides a single Resolve method, which will return from cache
|
||||
// or call the provided resolve function to get the value if not available in cache
|
||||
type Resolver[T any] interface {
|
||||
// Resolve attempts to resolve the given key from cache and convert it to the type of the cache,
|
||||
// or calls the resolver function if unable to resolve a cached value
|
||||
Resolve(key string, resolver resolverFunc[T]) (T, error)
|
||||
}
|
||||
|
||||
// GetResolver returns a cache resolver for persistent cached data across Syft runs, stored in a unique
|
||||
// location based on the provided name and versioned by the type
|
||||
func GetResolver[T any](name, version string) Resolver[T] {
|
||||
typeHash := hashType[T]()
|
||||
versionKey := path.Join(version, typeHash)
|
||||
return &cacheResolver[T]{
|
||||
name: fmt.Sprintf("%s/%s", name, versionKey),
|
||||
cache: manager.GetCache(name, versionKey),
|
||||
}
|
||||
}
|
||||
|
||||
const resolverKeySuffix = ".json"
|
||||
|
||||
type resolverFunc[T any] func() (T, error)
|
||||
|
||||
type cacheResolver[T any] struct {
|
||||
name string
|
||||
cache Cache
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
Resolver[int]
|
||||
} = (*cacheResolver[int])(nil)
|
||||
|
||||
func (r *cacheResolver[T]) Resolve(key string, resolver resolverFunc[T]) (T, error) {
|
||||
key += resolverKeySuffix
|
||||
|
||||
rdr, err := r.cache.Read(key)
|
||||
if rdr == nil || err != nil {
|
||||
return r.resolveAndCache(key, resolver)
|
||||
}
|
||||
defer internal.CloseAndLogError(rdr, key)
|
||||
|
||||
dec := json.NewDecoder(rdr)
|
||||
if dec == nil {
|
||||
log.Tracef("error getting cache json decoder for %s %v: %v", r.name, key, err)
|
||||
return r.resolveAndCache(key, resolver)
|
||||
}
|
||||
var t T
|
||||
err = dec.Decode(&t)
|
||||
if err != nil {
|
||||
log.Tracef("error decoding cached entry for %s %v: %v", r.name, key, err)
|
||||
return r.resolveAndCache(key, resolver)
|
||||
}
|
||||
// no error, able to resolve from cache
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *cacheResolver[T]) resolveAndCache(key string, resolver func() (T, error)) (T, error) {
|
||||
t, err := resolver()
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
var data bytes.Buffer
|
||||
enc := json.NewEncoder(&data)
|
||||
enc.SetEscapeHTML(false)
|
||||
err = enc.Encode(t)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
err = r.cache.Write(key, &data)
|
||||
return t, err
|
||||
}
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_Resolver(t *testing.T) {
|
||||
original := GetManager()
|
||||
defer SetManager(original)
|
||||
SetManager(NewInMemory(time.Hour))
|
||||
|
||||
type sub struct {
|
||||
Name string
|
||||
Value bool
|
||||
}
|
||||
|
||||
type thing struct {
|
||||
Value string
|
||||
Values []int
|
||||
Subs []*sub
|
||||
}
|
||||
|
||||
versionHash := hashType[thing]()
|
||||
cache := GetManager().GetCache("test", "v7/"+versionHash)
|
||||
|
||||
resolver := GetResolver[thing]("test", "v7")
|
||||
require.NotNil(t, resolver)
|
||||
|
||||
require.IsType(t, &cacheResolver[thing]{}, resolver)
|
||||
cr := resolver.(*cacheResolver[thing])
|
||||
|
||||
require.IsType(t, cache, cr.cache)
|
||||
|
||||
resolveErrCount := 0
|
||||
resolveThingErr := func() (thing, error) {
|
||||
resolveErrCount++
|
||||
return thing{}, fmt.Errorf("an error")
|
||||
}
|
||||
|
||||
_, err := resolver.Resolve("err", resolveThingErr)
|
||||
require.ErrorContains(t, err, "an error")
|
||||
require.Equal(t, 1, resolveErrCount)
|
||||
|
||||
_, err = resolver.Resolve("err", resolveThingErr)
|
||||
require.ErrorContains(t, err, "an error")
|
||||
require.Equal(t, 2, resolveErrCount)
|
||||
|
||||
aThing := thing{
|
||||
Value: "a value",
|
||||
Values: []int{7, 8, 9},
|
||||
Subs: []*sub{
|
||||
{
|
||||
Name: "sub1",
|
||||
Value: true,
|
||||
},
|
||||
{
|
||||
Name: "sub2",
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resolveThingCount := 0
|
||||
resolveThing := func() (thing, error) {
|
||||
resolveThingCount++
|
||||
return aThing, nil
|
||||
}
|
||||
|
||||
val, err := resolver.Resolve("thing", resolveThing)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, resolveThingCount)
|
||||
require.Equal(t, aThing, val)
|
||||
|
||||
val, err = resolver.Resolve("thing", resolveThing)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, resolveThingCount)
|
||||
require.Equal(t, aThing, val)
|
||||
|
||||
rdr, err := cache.Read("thing" + resolverKeySuffix)
|
||||
require.NoError(t, err)
|
||||
decoder := json.NewDecoder(rdr)
|
||||
|
||||
var val2 thing
|
||||
err = decoder.Decode(&val2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, aThing, val2)
|
||||
}
|
||||
@@ -3,5 +3,5 @@ package internal
|
||||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "16.0.12"
|
||||
JSONSchemaVersion = "16.0.14"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Set represents a generic set type.
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
||||
// NewSet creates a new empty Set.
|
||||
func NewSet[T comparable](start ...T) Set[T] {
|
||||
ret := make(Set[T])
|
||||
for _, v := range start {
|
||||
ret.Add(v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Add adds elements to the set.
|
||||
func (s Set[T]) Add(elements ...T) {
|
||||
for _, e := range elements {
|
||||
s[e] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes an element from the set.
|
||||
func (s Set[T]) Remove(element T) {
|
||||
delete(s, element)
|
||||
}
|
||||
|
||||
// Contains checks if an element is in the set.
|
||||
func (s Set[T]) Contains(element T) bool {
|
||||
_, ok := s[element]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ToSlice returns a sorted slice of elements in the set.
|
||||
func (s Set[T]) ToSlice() []T {
|
||||
ret := make([]T, len(s))
|
||||
idx := 0
|
||||
for v := range s {
|
||||
ret[idx] = v
|
||||
idx++
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool {
|
||||
return fmt.Sprintf("%v", ret[i]) < fmt.Sprintf("%v", ret[j])
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// Equals checks if two sets are equal.
|
||||
func (s Set[T]) Equals(o Set[T]) bool {
|
||||
if len(s) != len(o) {
|
||||
return false
|
||||
}
|
||||
for k := range s {
|
||||
if !o.Contains(k) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Empty checks if the set is empty.
|
||||
func (s Set[T]) Empty() bool {
|
||||
return len(s) == 0
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
start []int
|
||||
result Set[int]
|
||||
}{
|
||||
{"empty set", []int{}, NewSet[int]()},
|
||||
{"non-empty set", []int{1, 2, 3}, NewSet(1, 2, 3)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewSet(tt.start...)
|
||||
require.Equal(t, tt.result, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []int
|
||||
add []int
|
||||
result Set[int]
|
||||
}{
|
||||
{"add to empty set", []int{}, []int{1, 2, 3}, NewSet(1, 2, 3)},
|
||||
{"add to non-empty set", []int{1}, []int{2, 3}, NewSet(1, 2, 3)},
|
||||
{"add existing elements", []int{1, 2}, []int{2, 3}, NewSet(1, 2, 3)},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewSet(tt.input...)
|
||||
s.Add(tt.add...)
|
||||
assert.Equal(t, tt.result, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []int
|
||||
remove int
|
||||
result Set[int]
|
||||
}{
|
||||
{"remove from non-empty set", []int{1, 2, 3}, 2, NewSet(1, 3)},
|
||||
{"remove non-existent element", []int{1, 2}, 3, NewSet(1, 2)},
|
||||
{"remove from single-element set", []int{1}, 1, NewSet[int]()},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewSet(tt.input...)
|
||||
s.Remove(tt.remove)
|
||||
assert.Equal(t, tt.result, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []int
|
||||
contains int
|
||||
result bool
|
||||
}{
|
||||
{"element in set", []int{1, 2, 3}, 2, true},
|
||||
{"element not in set", []int{1, 2}, 3, false},
|
||||
{"empty set", []int{}, 1, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewSet(tt.input...)
|
||||
assert.Equal(t, tt.result, s.Contains(tt.contains))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []int
|
||||
result []int
|
||||
}{
|
||||
{"non-empty set", []int{3, 1, 2}, []int{1, 2, 3}},
|
||||
{"empty set", []int{}, []int{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewSet(tt.input...)
|
||||
assert.Equal(t, tt.result, s.ToSlice())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEquals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
set1 []int
|
||||
set2 []int
|
||||
result bool
|
||||
}{
|
||||
{"equal sets", []int{1, 2, 3}, []int{3, 2, 1}, true},
|
||||
{"different sets", []int{1, 2}, []int{2, 3}, false},
|
||||
{"empty sets", []int{}, []int{}, true},
|
||||
{"one empty set", []int{1, 2}, []int{}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s1 := NewSet(tt.set1...)
|
||||
s2 := NewSet(tt.set2...)
|
||||
assert.Equal(t, tt.result, s1.Equals(s2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []int
|
||||
result bool
|
||||
}{
|
||||
{"non-empty set", []int{1}, false},
|
||||
{"empty set", []int{}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := NewSet(tt.input...)
|
||||
assert.Equal(t, tt.result, s.Empty())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package internal
|
||||
|
||||
import "sort"
|
||||
|
||||
// StringSet represents a set of string types.
|
||||
type StringSet map[string]struct{}
|
||||
|
||||
// NewStringSet creates a new empty StringSet.
|
||||
func NewStringSet(start ...string) StringSet {
|
||||
ret := make(StringSet)
|
||||
for _, s := range start {
|
||||
ret.Add(s)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Add a string to the set.
|
||||
func (s StringSet) Add(i ...string) {
|
||||
for _, str := range i {
|
||||
s[str] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a string from the set.
|
||||
func (s StringSet) Remove(i string) {
|
||||
delete(s, i)
|
||||
}
|
||||
|
||||
// Contains indicates if the given string is contained within the set.
|
||||
func (s StringSet) Contains(i string) bool {
|
||||
_, ok := s[i]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ToSlice returns a sorted slice of strings that are contained within the set.
|
||||
func (s StringSet) ToSlice() []string {
|
||||
ret := make([]string, len(s))
|
||||
idx := 0
|
||||
for v := range s {
|
||||
ret[idx] = v
|
||||
idx++
|
||||
}
|
||||
sort.Strings(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s StringSet) Equals(o StringSet) bool {
|
||||
if len(s) != len(o) {
|
||||
return false
|
||||
}
|
||||
for k := range s {
|
||||
if !o.Contains(k) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s StringSet) Empty() bool {
|
||||
return len(s) < 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.12/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.14/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@@ -647,12 +647,24 @@
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"vendor": {
|
||||
"architecture": {
|
||||
"type": "string"
|
||||
},
|
||||
"osCPE": {
|
||||
"type": "string"
|
||||
},
|
||||
"os": {
|
||||
"type": "string"
|
||||
},
|
||||
"osVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"system": {
|
||||
"type": "string"
|
||||
},
|
||||
"vendor": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceRepo": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2051,15 +2063,74 @@
|
||||
"index"
|
||||
]
|
||||
},
|
||||
"PythonPoetryLockEntry": {
|
||||
"PythonPoetryLockDependencyEntry": {
|
||||
"properties": {
|
||||
"index": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"optional": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"markers": {
|
||||
"type": "string"
|
||||
},
|
||||
"extras": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"index"
|
||||
"name",
|
||||
"version",
|
||||
"optional"
|
||||
]
|
||||
},
|
||||
"PythonPoetryLockEntry": {
|
||||
"properties": {
|
||||
"index": {
|
||||
"type": "string"
|
||||
},
|
||||
"dependencies": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/PythonPoetryLockDependencyEntry"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"extras": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/PythonPoetryLockExtraEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"index",
|
||||
"dependencies"
|
||||
]
|
||||
},
|
||||
"PythonPoetryLockExtraEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"dependencies": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"dependencies"
|
||||
]
|
||||
},
|
||||
"RDescription": {
|
||||
|
||||
@@ -9,13 +9,11 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
@@ -43,7 +41,8 @@ func newDirectoryIndexer(path, base string, visitors ...PathIndexVisitor) *direc
|
||||
[]PathIndexVisitor{
|
||||
requireFileInfo,
|
||||
disallowByFileType,
|
||||
newUnixSystemMountFinder().disallowUnixSystemRuntimePath},
|
||||
skipPathsByMountTypeAndName(path),
|
||||
},
|
||||
visitors...,
|
||||
),
|
||||
errPaths: make(map[string]error),
|
||||
@@ -450,57 +449,6 @@ func (r *directoryIndexer) disallowRevisitingVisitor(_, path string, _ os.FileIn
|
||||
return nil
|
||||
}
|
||||
|
||||
type unixSystemMountFinder struct {
|
||||
disallowedMountPaths []string
|
||||
}
|
||||
|
||||
func newUnixSystemMountFinder() unixSystemMountFinder {
|
||||
infos, err := mountinfo.GetMounts(nil)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warnf("unable to get system mounts")
|
||||
return unixSystemMountFinder{}
|
||||
}
|
||||
|
||||
return unixSystemMountFinder{
|
||||
disallowedMountPaths: keepUnixSystemMountPaths(infos),
|
||||
}
|
||||
}
|
||||
|
||||
func keepUnixSystemMountPaths(infos []*mountinfo.Info) []string {
|
||||
var mountPaths []string
|
||||
for _, info := range infos {
|
||||
if info == nil {
|
||||
continue
|
||||
}
|
||||
// we're only interested in ignoring the logical filesystems typically found at these mount points:
|
||||
// - /proc
|
||||
// - procfs
|
||||
// - proc
|
||||
// - /sys
|
||||
// - sysfs
|
||||
// - /dev
|
||||
// - devfs - BSD/darwin flavored systems and old linux systems
|
||||
// - devtmpfs - driver core maintained /dev tmpfs
|
||||
// - udev - userspace implementation that replaced devfs
|
||||
// - tmpfs - used for /dev in special instances (within a container)
|
||||
|
||||
switch info.FSType {
|
||||
case "proc", "procfs", "sysfs", "devfs", "devtmpfs", "udev", "tmpfs":
|
||||
log.WithFields("mountpoint", info.Mountpoint).Debug("ignoring system mountpoint")
|
||||
|
||||
mountPaths = append(mountPaths, info.Mountpoint)
|
||||
}
|
||||
}
|
||||
return mountPaths
|
||||
}
|
||||
|
||||
func (f unixSystemMountFinder) disallowUnixSystemRuntimePath(_, path string, _ os.FileInfo, _ error) error {
|
||||
if internal.HasAnyOfPrefixes(path, f.disallowedMountPaths...) {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func disallowByFileType(_, _ string, info os.FileInfo, _ error) error {
|
||||
if info == nil {
|
||||
// we can't filter out by filetype for non-existent files
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/moby/sys/mountinfo"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -462,145 +461,3 @@ func relativePath(basePath, givenPath string) string {
|
||||
|
||||
return relPath
|
||||
}
|
||||
|
||||
func Test_disallowUnixSystemRuntimePath(t *testing.T) {
|
||||
unixSubject := unixSystemMountFinder{
|
||||
// mock out detecting the mount points
|
||||
disallowedMountPaths: []string{"/proc", "/sys", "/dev"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
base string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "relative path to proc is allowed",
|
||||
path: "proc/place",
|
||||
},
|
||||
{
|
||||
name: "relative path within proc is not allowed",
|
||||
path: "/proc/place",
|
||||
expected: fs.SkipDir,
|
||||
},
|
||||
{
|
||||
name: "path exactly to proc is not allowed",
|
||||
path: "/proc",
|
||||
expected: fs.SkipDir,
|
||||
},
|
||||
{
|
||||
name: "similar to proc",
|
||||
path: "/pro/c",
|
||||
},
|
||||
{
|
||||
name: "similar to proc",
|
||||
path: "/pro",
|
||||
},
|
||||
{
|
||||
name: "dev is not allowed",
|
||||
path: "/dev",
|
||||
expected: fs.SkipDir,
|
||||
},
|
||||
{
|
||||
name: "sys is not allowed",
|
||||
path: "/sys",
|
||||
expected: fs.SkipDir,
|
||||
},
|
||||
{
|
||||
name: "unrelated allowed path",
|
||||
path: "/something/sys",
|
||||
},
|
||||
{
|
||||
name: "do not consider base when matching paths (non-matching)",
|
||||
base: "/a/b/c",
|
||||
path: "/a/b/c/dev",
|
||||
},
|
||||
{
|
||||
name: "do not consider base when matching paths (matching)",
|
||||
base: "/a/b/c",
|
||||
path: "/dev",
|
||||
expected: fs.SkipDir,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.path, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, unixSubject.disallowUnixSystemRuntimePath(test.base, test.path, nil, nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_keepUnixSystemMountPaths(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
infos []*mountinfo.Info
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "all valid filesystems",
|
||||
infos: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/etc/hostname",
|
||||
FSType: "/dev/vda1",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/sys/fs/cgroup",
|
||||
FSType: "cgroup",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/",
|
||||
FSType: "overlay",
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no valid filesystems",
|
||||
infos: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/proc",
|
||||
FSType: "proc",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/proc-2",
|
||||
FSType: "procfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/sys",
|
||||
FSType: "sysfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/dev",
|
||||
FSType: "devfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/dev-u",
|
||||
FSType: "udev",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/dev-tmp",
|
||||
FSType: "devtmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/run",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"/proc",
|
||||
"/proc-2",
|
||||
"/sys",
|
||||
"/dev",
|
||||
"/dev-u",
|
||||
"/dev-tmp",
|
||||
"/run",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, keepUnixSystemMountPaths(tt.infos))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package fileresolver
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
type pathSkipper struct {
|
||||
// scanTarget is the root path that is being scanned (without any base-path logic applied).
|
||||
scanTarget string
|
||||
|
||||
// ignorableMountTypes is a set of mount types that should be ignored. Optionally a list of paths (the map values)
|
||||
// can be provided that this mount type should be ignored at. For example in some containers /dev is mounted
|
||||
// as a tmpfs and should be ignored, but /tmp should not be ignored. An empty list of paths means that paths
|
||||
// within the mount type should always be ignored.
|
||||
ignorableMountTypes map[string][]string
|
||||
|
||||
// current mount paths for the current system
|
||||
mounts []*mountinfo.Info
|
||||
mountsByType map[string][]*mountinfo.Info
|
||||
}
|
||||
|
||||
// skipPathsByMountTypeAndName accepts the root path and returns a PathIndexVisitor that will skip paths based
|
||||
// the filesystem type, the mountpoint, and configured blocklist paths for each filesystem type.
|
||||
// This will help syft dodge filesystem topologies that have the potential to make the search space much bigger in
|
||||
// areas known to not traditionally contain files of interest (installed software). It is meant to allow scanning
|
||||
// "/" on a unix host to succeed, while also not causing any files in a narrow directory scan to be skipped unnecessarily.
|
||||
func skipPathsByMountTypeAndName(root string) PathIndexVisitor {
|
||||
infos, err := mountinfo.GetMounts(nil)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warnf("unable to get system mounts")
|
||||
return func(_ string, _ string, _ os.FileInfo, _ error) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return newPathSkipperFromMounts(root, infos).pathIndexVisitor
|
||||
}
|
||||
|
||||
func newPathSkipperFromMounts(root string, infos []*mountinfo.Info) pathSkipper {
|
||||
// we're only interested in ignoring the logical filesystems typically found at these mount points:
|
||||
// - /proc
|
||||
// - procfs
|
||||
// - proc
|
||||
// - /sys
|
||||
// - sysfs
|
||||
// - /dev
|
||||
// - devfs - BSD/darwin flavored systems and old linux systems
|
||||
// - devtmpfs - driver core maintained /dev tmpfs
|
||||
// - udev - userspace implementation that replaced devfs
|
||||
// - tmpfs - used for /dev in special instances (within a container)
|
||||
ignorableMountTypes := map[string][]string{
|
||||
"proc": nil,
|
||||
"procfs": nil,
|
||||
"sysfs": nil,
|
||||
"devfs": nil,
|
||||
"devtmpfs": nil,
|
||||
"udev": nil,
|
||||
// note: there should be no order required (e.g. search /sys/thing before /sys) since that would imply that
|
||||
// we could not ignore a nested path within a path that would be ignored anyway.
|
||||
"tmpfs": {"/run", "/dev", "/var/run", "/var/lock", "/sys"},
|
||||
}
|
||||
|
||||
// The longest path is the most specific path, e.g.
|
||||
// if / is mounted as tmpfs, but /home/syft/permanent is mounted as ext4,
|
||||
// then the mount type for /home/syft/permanent/foo is ext4, and the mount info
|
||||
// stating that /home/syft/permanent is ext4 has the longer mount point.
|
||||
sort.Slice(infos, func(i, j int) bool {
|
||||
return len(infos[i].Mountpoint) > len(infos[j].Mountpoint)
|
||||
})
|
||||
|
||||
mountsByType := make(map[string][]*mountinfo.Info)
|
||||
|
||||
for _, mi := range infos {
|
||||
mountsByType[mi.FSType] = append(mountsByType[mi.FSType], mi)
|
||||
}
|
||||
|
||||
return pathSkipper{
|
||||
scanTarget: root,
|
||||
ignorableMountTypes: ignorableMountTypes,
|
||||
mounts: infos,
|
||||
mountsByType: mountsByType,
|
||||
}
|
||||
}
|
||||
|
||||
func (ps pathSkipper) pathIndexVisitor(_ string, givenPath string, _ os.FileInfo, _ error) error {
|
||||
for _, mi := range ps.mounts {
|
||||
conditionalPaths, ignorable := ps.ignorableMountTypes[mi.FSType]
|
||||
|
||||
if len(conditionalPaths) == 0 {
|
||||
// Rule 1: ignore any path within a mount point that is of the given filesystem type unconditionally
|
||||
if !containsPath(givenPath, mi.Mountpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ignorable {
|
||||
// we've matched on the most specific path at this point, which means we should stop searching
|
||||
// mount points for this path
|
||||
break
|
||||
}
|
||||
|
||||
log.WithFields(
|
||||
"path", givenPath,
|
||||
"mountpoint", mi.Mountpoint,
|
||||
"fs", mi.FSType,
|
||||
).Debug("ignoring path based on mountpoint filesystem type")
|
||||
|
||||
return fs.SkipDir
|
||||
}
|
||||
|
||||
// Rule 2: ignore any path within a mount point that is of the given filesystem type, only if
|
||||
// the path is on a known blocklist of paths for that filesystem type.
|
||||
// For example: /dev can be mounted as a tmpfs, which should always be skipped.
|
||||
for _, conditionalPath := range conditionalPaths {
|
||||
if !containsPath(givenPath, conditionalPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.WithFields(
|
||||
"path", givenPath,
|
||||
"mountpoint", mi.Mountpoint,
|
||||
"fs", mi.FSType,
|
||||
"condition", conditionalPath,
|
||||
).Debug("ignoring path based on mountpoint filesystem type")
|
||||
|
||||
return fs.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsPath(p1, p2 string) bool {
|
||||
p1Clean := simpleClean(p1)
|
||||
p2Clean := simpleClean(p2)
|
||||
if p1Clean == p2Clean {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(p1Clean, p2Clean+"/")
|
||||
}
|
||||
|
||||
func simpleClean(p string) string {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
return "."
|
||||
}
|
||||
if p == "/" {
|
||||
return "/"
|
||||
}
|
||||
return strings.TrimSuffix(p, "/")
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package fileresolver
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newPathSkipper(t *testing.T) {
|
||||
type expect struct {
|
||||
path string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}
|
||||
unixSubject := []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/proc",
|
||||
FSType: "procfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/sys",
|
||||
FSType: "sysfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/dev",
|
||||
FSType: "devfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/",
|
||||
FSType: "/dev/disk3s1s1",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/dev/shm",
|
||||
FSType: "shm",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/tmp",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
root string
|
||||
base string
|
||||
mounts []*mountinfo.Info
|
||||
want []expect
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
root: "/somewhere",
|
||||
mounts: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/home/somewhere/else",
|
||||
FSType: "/dev/disk3s6",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/somewhere",
|
||||
FSType: "/dev/disk3s7",
|
||||
},
|
||||
},
|
||||
want: []expect{
|
||||
{
|
||||
// within a known mountpoint with valid type (1)
|
||||
path: "/somewhere/dev",
|
||||
},
|
||||
{
|
||||
// is a known mountpoint with valid type
|
||||
path: "/somewhere",
|
||||
},
|
||||
{
|
||||
// within a known mountpoint with valid type (2)
|
||||
path: "/home/somewhere/else/too",
|
||||
},
|
||||
{
|
||||
// outside of any known mountpoint should not be an error
|
||||
path: "/bogus",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore paths within a scan target",
|
||||
root: "/somewhere",
|
||||
mounts: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/somewhere/doesnt/matter/proc",
|
||||
FSType: "procfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/somewhere",
|
||||
FSType: "/dev/disk3s7",
|
||||
},
|
||||
},
|
||||
want: []expect{
|
||||
{
|
||||
// within a known mountpoint with valid type (1)
|
||||
path: "/somewhere/dev",
|
||||
},
|
||||
{
|
||||
// is a known mountpoint with valid type
|
||||
path: "/somewhere",
|
||||
},
|
||||
{
|
||||
// mountpoint that should be ignored
|
||||
path: "/somewhere/doesnt/matter/proc",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
// within a mountpoint that should be ignored
|
||||
path: "/somewhere/doesnt/matter/proc",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested mountpoints behave correctly",
|
||||
root: "/somewhere",
|
||||
mounts: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/somewhere/dev",
|
||||
FSType: "devfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/somewhere/dev/includeme",
|
||||
FSType: "/dev/disk3s7",
|
||||
},
|
||||
},
|
||||
want: []expect{
|
||||
{
|
||||
// is a known mountpoint with valid type
|
||||
path: "/somewhere/dev",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
// is a known mountpoint with valid type
|
||||
path: "/somewhere/dev/includeme",
|
||||
},
|
||||
{
|
||||
// within a known mountpoint with valid type
|
||||
path: "/somewhere/dev/includeme/too!",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keep some tmpfs mounts conditionally",
|
||||
root: "/",
|
||||
mounts: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/run/somewhere",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/run/terrafirma",
|
||||
FSType: "/dev/disk3s8",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/tmp",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/else/othertmp",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/else/othertmp/includeme",
|
||||
FSType: "/dev/disk3s7",
|
||||
},
|
||||
},
|
||||
want: []expect{
|
||||
{
|
||||
// since /run is explicitly ignored, this should be skipped
|
||||
path: "/run/somewhere/else",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
path: "/run/terrafirma",
|
||||
},
|
||||
{
|
||||
path: "/run/terrafirma/nested",
|
||||
},
|
||||
{
|
||||
path: "/tmp",
|
||||
},
|
||||
{
|
||||
path: "/else/othertmp/includeme",
|
||||
},
|
||||
{
|
||||
path: "/else/othertmp/includeme/nested",
|
||||
},
|
||||
{
|
||||
// no mount path, so we should include it
|
||||
path: "/somewhere/dev/includeme",
|
||||
},
|
||||
{
|
||||
// keep additional tmpfs mounts that are not explicitly ignored
|
||||
path: "/else/othertmp",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore known trixy tmpfs paths",
|
||||
root: "/",
|
||||
mounts: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/",
|
||||
FSType: "/dev/disk3s7",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/dev",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/run",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/var/run",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/var/lock",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/sys",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
{
|
||||
Mountpoint: "/tmp",
|
||||
FSType: "tmpfs",
|
||||
},
|
||||
},
|
||||
want: []expect{
|
||||
{
|
||||
path: "/dev",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
path: "/run",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
path: "/var/run",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
path: "/var/lock",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
path: "/sys",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
// show that we honor ignoring nested paths
|
||||
{
|
||||
path: "/sys/nested",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
// show that paths outside of the known mountpoints are not skipped
|
||||
{
|
||||
path: "/stuff",
|
||||
},
|
||||
// show that we allow other tmpfs paths that are not on the blocklist
|
||||
{
|
||||
path: "/tmp/allowed",
|
||||
},
|
||||
// show sibling paths with same prefix (e.g. /sys vs /system) to that of not allowed paths are not skipped
|
||||
{
|
||||
path: "/system",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test unix paths",
|
||||
mounts: unixSubject,
|
||||
root: "/",
|
||||
want: []expect{
|
||||
{
|
||||
// relative path to proc is allowed
|
||||
path: "proc/place",
|
||||
},
|
||||
{
|
||||
// relative path within proc is not allowed
|
||||
path: "/proc/place",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
// path exactly to proc is not allowed
|
||||
path: "/proc",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
// similar to proc
|
||||
path: "/pro/c",
|
||||
},
|
||||
{
|
||||
// similar to proc
|
||||
path: "/pro",
|
||||
},
|
||||
{
|
||||
// dev is not allowed
|
||||
path: "/dev",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
{
|
||||
// sys is not allowed
|
||||
path: "/sys",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test unix paths with base",
|
||||
mounts: unixSubject,
|
||||
root: "/",
|
||||
base: "/a/b/c",
|
||||
want: []expect{
|
||||
{
|
||||
// do not consider base when matching paths (non-matching)
|
||||
path: "/a/b/c/dev",
|
||||
},
|
||||
{
|
||||
// do not consider base when matching paths (matching)
|
||||
path: "/dev",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mimic nixos setup",
|
||||
root: "/",
|
||||
mounts: []*mountinfo.Info{
|
||||
{
|
||||
Mountpoint: "/",
|
||||
FSType: "tmpfs", // this is an odd setup, but valid
|
||||
},
|
||||
{
|
||||
Mountpoint: "/home",
|
||||
FSType: "/dev/disk3s7",
|
||||
},
|
||||
},
|
||||
want: []expect{
|
||||
{
|
||||
path: "/home/somewhere",
|
||||
},
|
||||
{
|
||||
path: "/home",
|
||||
},
|
||||
{
|
||||
path: "/somewhere",
|
||||
},
|
||||
{
|
||||
// still not allowed...
|
||||
path: "/run",
|
||||
wantErr: assertSkipErr(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.base == "" {
|
||||
tt.base = tt.root
|
||||
}
|
||||
|
||||
require.NotEmpty(t, tt.want)
|
||||
ps := newPathSkipperFromMounts(tt.root, tt.mounts)
|
||||
|
||||
for _, exp := range tt.want {
|
||||
t.Run(exp.path, func(t *testing.T) {
|
||||
|
||||
got := ps.pathIndexVisitor(tt.base, exp.path, nil, nil)
|
||||
if exp.wantErr == nil {
|
||||
assert.NoError(t, got)
|
||||
return
|
||||
}
|
||||
exp.wantErr(t, got)
|
||||
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertSkipErr() assert.ErrorAssertionFunc {
|
||||
return assertErrorIs(fs.SkipDir)
|
||||
}
|
||||
|
||||
func assertErrorIs(want error) assert.ErrorAssertionFunc {
|
||||
return func(t assert.TestingT, got error, msgAndArgs ...interface{}) bool {
|
||||
return assert.ErrorIs(t, got, want, msgAndArgs...)
|
||||
}
|
||||
}
|
||||
+29
-4
@@ -15,9 +15,34 @@ type ClassifierMatch struct {
|
||||
|
||||
// ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of the binary
|
||||
type ELFBinaryPackageNoteJSONPayload struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
System string `json:"system,omitempty"`
|
||||
// these are well-known fields as defined by systemd ELF package metadata "spec" https://systemd.io/ELF_PACKAGE_METADATA/
|
||||
|
||||
// Type is the type of the package (e.g. "rpm", "deb", "apk", etc.)
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
// Architecture of the binary package (e.g. "amd64", "arm", etc.)
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
|
||||
// OS CPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)
|
||||
OSCPE string `json:"osCPE,omitempty"`
|
||||
|
||||
// OS is the OS name, typically corresponding to ID in os-release (e.g. "fedora")
|
||||
OS string `json:"os,omitempty"`
|
||||
|
||||
// osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. "33")
|
||||
OSVersion string `json:"osVersion,omitempty"`
|
||||
|
||||
// these are additional fields that are not part of the systemd spec
|
||||
|
||||
// System is a context-specific name for the system that the binary package is intended to run on or a part of
|
||||
System string `json:"system,omitempty"`
|
||||
|
||||
// Vendor is the individual or organization that produced the source code for the binary
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
|
||||
// SourceRepo is the URL to the source repository for which the binary was built from
|
||||
SourceRepo string `json:"sourceRepo,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
|
||||
// Commit is the commit hash of the source repository for which the binary was built from
|
||||
Commit string `json:"commit,omitempty"`
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ func dbEntryDependencySpecifier(p pkg.Package) dependency.Specification {
|
||||
provides = append(provides, stripVersionSpecifiers(meta.Provides)...)
|
||||
|
||||
return dependency.Specification{
|
||||
Provides: provides,
|
||||
Requires: stripVersionSpecifiers(meta.Dependencies),
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: provides,
|
||||
Requires: stripVersionSpecifiers(meta.Dependencies),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-c", "a-thing"},
|
||||
Requires: []string{"b-thing"},
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-c", "a-thing"},
|
||||
Requires: []string{"b-thing"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -39,8 +41,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a", "so:libc.musl-x86_64.so.1"},
|
||||
Requires: []string{"so:libc.musl-x86_64.so.2"},
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-a", "so:libc.musl-x86_64.so.1"},
|
||||
Requires: []string{"so:libc.musl-x86_64.so.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -53,8 +57,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a"},
|
||||
Requires: nil,
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-a"},
|
||||
Requires: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ func dbEntryDependencySpecifier(p pkg.Package) dependency.Specification {
|
||||
}
|
||||
|
||||
return dependency.Specification{
|
||||
Provides: provides,
|
||||
Requires: requires,
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: provides,
|
||||
Requires: requires,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-c", "a-thing", "a-thing"}, // note: gets deduplicated downstream
|
||||
Requires: []string{"b-thing"},
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-c", "a-thing", "a-thing"}, // note: gets deduplicated downstream
|
||||
Requires: []string{"b-thing"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -39,8 +41,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a", "libtree-sitter.so.me=1-64", "libtree-sitter.so.me"},
|
||||
Requires: []string{"libtree-sitter.so.thing=2-64"},
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-a", "libtree-sitter.so.me=1-64", "libtree-sitter.so.me"},
|
||||
Requires: []string{"libtree-sitter.so.thing=2-64"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -53,8 +57,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a"},
|
||||
Requires: nil,
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-a"},
|
||||
Requires: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -616,6 +616,16 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("busybox-binary", "[", "busybox"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: `util-linux/2.37.4/linux-amd64`,
|
||||
expected: pkg.Package{
|
||||
Name: "util-linux",
|
||||
Version: "2.37.4",
|
||||
PURL: "pkg:generic/util-linux@2.37.4",
|
||||
Locations: locations("getopt"),
|
||||
Metadata: metadata("util-linux-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalFixture: "java-jre-openjdk/1.8.0_352-b08/linux-amd64",
|
||||
expected: pkg.Package{
|
||||
|
||||
@@ -173,6 +173,15 @@ func DefaultClassifiers() []Classifier {
|
||||
PURL: mustPURL("pkg:generic/busybox@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:busybox:busybox:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "util-linux-binary",
|
||||
FileGlob: "**/getopt",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`\x00util-linux\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`),
|
||||
Package: "util-linux",
|
||||
PURL: mustPURL("pkg:generic/util-linux@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:kernel:util-linux:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "haproxy-binary",
|
||||
FileGlob: "**/haproxy",
|
||||
|
||||
@@ -2,15 +2,16 @@ package binary
|
||||
|
||||
import (
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet, licenses []pkg.License) pkg.Package {
|
||||
func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet) pkg.Package {
|
||||
p := pkg.Package{
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
Licenses: pkg.NewLicenseSet(licenses...),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicense(metadata.License)),
|
||||
PURL: packageURL(metadata),
|
||||
Type: pkg.BinaryPkg,
|
||||
Locations: locations,
|
||||
@@ -23,13 +24,59 @@ func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet, l
|
||||
}
|
||||
|
||||
func packageURL(metadata elfBinaryPackageNotes) string {
|
||||
// TODO: what if the System value is not set?
|
||||
var qualifiers []packageurl.Qualifier
|
||||
|
||||
os := metadata.OS
|
||||
osVersion := metadata.OSVersion
|
||||
|
||||
atts, err := cpe.NewAttributes(metadata.OSCPE)
|
||||
if err == nil {
|
||||
// only "upgrade" the OS information if there is something more specific to use in it's place
|
||||
if os == "" && osVersion == "" || os == "" && atts.Version != "" || atts.Product != "" && osVersion == "" {
|
||||
os = atts.Product
|
||||
osVersion = atts.Version
|
||||
}
|
||||
}
|
||||
|
||||
if os != "" {
|
||||
osQualifier := os
|
||||
if osVersion != "" {
|
||||
osQualifier += "-" + osVersion
|
||||
}
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
Key: "distro",
|
||||
Value: osQualifier,
|
||||
})
|
||||
}
|
||||
|
||||
ty := purlDistroType(metadata.Type)
|
||||
|
||||
namespace := os
|
||||
|
||||
if ty == packageurl.TypeGeneric || os == "" {
|
||||
namespace = metadata.System
|
||||
}
|
||||
|
||||
return packageurl.NewPackageURL(
|
||||
packageurl.TypeGeneric,
|
||||
metadata.System,
|
||||
ty,
|
||||
namespace,
|
||||
metadata.Name,
|
||||
metadata.Version,
|
||||
nil,
|
||||
qualifiers,
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func purlDistroType(ty string) string {
|
||||
switch ty {
|
||||
case "rpm":
|
||||
return packageurl.TypeRPM
|
||||
case "deb":
|
||||
return packageurl.TypeDebian
|
||||
case "apk":
|
||||
return packageurl.TypeAlpine
|
||||
case "alpm":
|
||||
return "alpm"
|
||||
}
|
||||
return packageurl.TypeGeneric
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
@@ -79,7 +81,7 @@ func (c *elfPackageCataloger) Catalog(_ context.Context, resolver file.Resolver)
|
||||
}
|
||||
|
||||
// create a package for each unique name/version pair (based on the first note found)
|
||||
pkgs = append(pkgs, newELFPackage(notes[0], noteLocations, nil))
|
||||
pkgs = append(pkgs, newELFPackage(notes[0], noteLocations))
|
||||
}
|
||||
|
||||
// why not return relationships? We have an executable cataloger that will note the dynamic libraries imported by
|
||||
@@ -154,11 +156,42 @@ func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var metadata elfBinaryPackageNotes
|
||||
if err := json.Unmarshal(notes, &metadata); err != nil {
|
||||
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to unmarshal ELF package notes as JSON")
|
||||
if len(notes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &metadata, err
|
||||
{
|
||||
var metadata elfBinaryPackageNotes
|
||||
if err := json.Unmarshal(notes, &metadata); err == nil {
|
||||
return &metadata, nil
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var header elf64SectionHeader
|
||||
headerSize := binary.Size(header) / 4
|
||||
if len(notes) > headerSize {
|
||||
var metadata elfBinaryPackageNotes
|
||||
newPayload := bytes.TrimRight(notes[headerSize:], "\x00")
|
||||
if err := json.Unmarshal(newPayload, &metadata); err == nil {
|
||||
return &metadata, nil
|
||||
}
|
||||
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to unmarshal ELF package notes as JSON")
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type elf64SectionHeader struct {
|
||||
ShName uint32
|
||||
ShType uint32
|
||||
ShFlags uint64
|
||||
ShAddr uint64
|
||||
ShOffset uint64
|
||||
ShSize uint64
|
||||
ShLink uint32
|
||||
ShInfo uint32
|
||||
ShAddralign uint64
|
||||
ShEntsize uint64
|
||||
}
|
||||
|
||||
@@ -9,53 +9,115 @@ import (
|
||||
)
|
||||
|
||||
func Test_ELF_Package_Cataloger(t *testing.T) {
|
||||
expectedPkgs := []pkg.Package{
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expected []pkg.Package
|
||||
}{
|
||||
{
|
||||
Name: "libhello_world.so",
|
||||
Version: "0.01",
|
||||
PURL: "pkg:generic/syftsys/libhello_world.so@0.01",
|
||||
FoundBy: "",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so"),
|
||||
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so"),
|
||||
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so"),
|
||||
),
|
||||
Language: "",
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "testfixture",
|
||||
Vendor: "syft",
|
||||
System: "syftsys",
|
||||
SourceRepo: "https://github.com/someone/somewhere.git",
|
||||
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
|
||||
name: "go case",
|
||||
fixture: "elf-test-fixtures",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "libhello_world.so",
|
||||
Version: "0.01",
|
||||
PURL: "pkg:generic/syftsys/libhello_world.so@0.01",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so"),
|
||||
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so"),
|
||||
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so"),
|
||||
),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
|
||||
),
|
||||
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "testfixture",
|
||||
Vendor: "syft",
|
||||
System: "syftsys",
|
||||
SourceRepo: "https://github.com/someone/somewhere.git",
|
||||
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "syfttestfixture",
|
||||
Version: "0.01",
|
||||
PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin2").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
|
||||
),
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "testfixture",
|
||||
Vendor: "syft",
|
||||
System: "syftsys",
|
||||
SourceRepo: "https://github.com/someone/somewhere.git",
|
||||
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "syfttestfixture",
|
||||
Version: "0.01",
|
||||
PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
|
||||
FoundBy: "",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin2").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
),
|
||||
Language: "",
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "testfixture",
|
||||
Vendor: "syft",
|
||||
System: "syftsys",
|
||||
SourceRepo: "https://github.com/someone/somewhere.git",
|
||||
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
|
||||
name: "fedora 64 bit binaries",
|
||||
fixture: "image-fedora-64bit",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "coreutils",
|
||||
Version: "9.5-1.fc41",
|
||||
PURL: "pkg:rpm/fedora/coreutils@9.5-1.fc41?distro=fedora-40",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/sha256sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
file.NewLocation("/sha1sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
),
|
||||
Licenses: pkg.NewLicenseSet(),
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "rpm",
|
||||
Architecture: "x86_64",
|
||||
OSCPE: "cpe:/o:fedoraproject:fedora:40",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fedora 32 bit binaries",
|
||||
fixture: "image-fedora-32bit",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "coreutils",
|
||||
Version: "9.0-5.fc36",
|
||||
PURL: "pkg:rpm/fedora/coreutils@9.0-5.fc36?distro=fedora-36",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/sha256sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
file.NewLocation("/sha1sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
),
|
||||
Licenses: pkg.NewLicenseSet(),
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "rpm",
|
||||
Architecture: "arm",
|
||||
OSCPE: "cpe:/o:fedoraproject:fedora:36",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.NewCatalogTester().
|
||||
WithImageResolver(t, "elf-test-fixtures").
|
||||
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
|
||||
Expects(expectedPkgs, nil).
|
||||
TestCataloger(t, NewELFPackageCataloger())
|
||||
for _, v := range cases {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
WithImageResolver(t, v.fixture).
|
||||
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
|
||||
Expects(v.expected, nil).
|
||||
TestCataloger(t, NewELFPackageCataloger())
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,36 +14,110 @@ import (
|
||||
func Test_packageURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
notes elfBinaryPackageNotes
|
||||
expected string
|
||||
metadata elfBinaryPackageNotes
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "elf-binary-package-cataloger",
|
||||
notes: elfBinaryPackageNotes{
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "github.com/anchore/syft",
|
||||
Version: "v0.1.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
System: "syftsys",
|
||||
},
|
||||
},
|
||||
expected: "pkg:generic/syftsys/github.com/anchore/syft@v0.1.0",
|
||||
want: "pkg:generic/syftsys/github.com/anchore/syft@v0.1.0",
|
||||
},
|
||||
{
|
||||
name: "elf binary package short name",
|
||||
notes: elfBinaryPackageNotes{
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "go.opencensus.io",
|
||||
Version: "v0.23.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
System: "syftsys",
|
||||
},
|
||||
},
|
||||
expected: "pkg:generic/syftsys/go.opencensus.io@v0.23.0",
|
||||
want: "pkg:generic/syftsys/go.opencensus.io@v0.23.0",
|
||||
},
|
||||
{
|
||||
name: "no info",
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "test",
|
||||
Version: "1.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "rpm",
|
||||
},
|
||||
},
|
||||
want: "pkg:rpm/test@1.0",
|
||||
},
|
||||
{
|
||||
name: "with system",
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "test",
|
||||
Version: "1.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "rpm",
|
||||
System: "system",
|
||||
},
|
||||
},
|
||||
want: "pkg:rpm/system/test@1.0",
|
||||
},
|
||||
{
|
||||
name: "with os info preferred",
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "test",
|
||||
Version: "1.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "rpm",
|
||||
OS: "fedora",
|
||||
OSVersion: "2.0",
|
||||
OSCPE: "cpe:/o:someone:redhat:3.0",
|
||||
},
|
||||
},
|
||||
want: "pkg:rpm/fedora/test@1.0?distro=fedora-2.0",
|
||||
},
|
||||
{
|
||||
name: "with os info fallback to CPE parsing (missing version)",
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "test",
|
||||
Version: "1.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "rpm",
|
||||
OS: "fedora",
|
||||
OSCPE: "cpe:/o:someone:redhat:3.0",
|
||||
},
|
||||
},
|
||||
want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0",
|
||||
},
|
||||
{
|
||||
name: "with os info preferred (missing OS)",
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "test",
|
||||
Version: "1.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
Type: "rpm",
|
||||
OSVersion: "2.0",
|
||||
OSCPE: "cpe:/o:someone:redhat:3.0",
|
||||
},
|
||||
},
|
||||
want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0",
|
||||
},
|
||||
{
|
||||
name: "missing type",
|
||||
metadata: elfBinaryPackageNotes{
|
||||
Name: "test",
|
||||
Version: "1.0",
|
||||
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
|
||||
System: "system",
|
||||
},
|
||||
},
|
||||
want: "pkg:generic/system/test@1.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, packageURL(test.notes))
|
||||
assert.Equal(t, test.want, packageURL(test.metadata))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -82,7 +156,7 @@ func Test_newELFPackage(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := newELFPackage(test.metadata, file.NewLocationSet(), nil)
|
||||
actual := newELFPackage(test.metadata, file.NewLocationSet())
|
||||
if diff := cmp.Diff(test.expected, actual, cmpopts.IgnoreFields(pkg.Package{}, "id"), cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{})); diff != "" {
|
||||
t.Errorf("newELFPackage() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
@@ -17,6 +17,14 @@ from-images:
|
||||
paths:
|
||||
- /bin/[
|
||||
|
||||
- name: util-linux
|
||||
version: 2.37.4
|
||||
images:
|
||||
- ref: cirros:0.6.2@sha256:7ba1f5bc57923f65c99677e53758d386320ed699b621d9630b1f2bd7ceb3a878
|
||||
platform: linux/amd64
|
||||
paths:
|
||||
- /usr/bin/getopt
|
||||
|
||||
- version: 5.1.16
|
||||
images:
|
||||
- ref: bash:5.1.16@sha256:c7a903a541d8f5fe693cbe7f5ece18a1b6a03734c76092d2b153d7e98a964927
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM --platform=linux/arm arm32v7/fedora:36 as build
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /bin/sha256sum /sha256sum
|
||||
COPY --from=build /bin/sha1sum /sha1sum
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM --platform=linux/amd64 fedora:41 as build
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /bin/sha256sum /sha256sum
|
||||
COPY --from=build /bin/sha1sum /sha1sum
|
||||
@@ -38,8 +38,10 @@ func dbEntryDependencySpecifier(p pkg.Package) dependency.Specification {
|
||||
}
|
||||
|
||||
return dependency.Specification{
|
||||
Provides: provides,
|
||||
Requires: requires,
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: provides,
|
||||
Requires: requires,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-c", "a-thing"},
|
||||
Requires: []string{"b-thing"},
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-c", "a-thing"},
|
||||
Requires: []string{"b-thing"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -39,8 +41,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a", "foo"},
|
||||
Requires: []string{"libgmp10", "default-mta", "mail-transport-agent"},
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-a", "foo"},
|
||||
Requires: []string{"libgmp10", "default-mta", "mail-transport-agent"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -53,8 +57,10 @@ func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a"},
|
||||
Requires: nil,
|
||||
ProvidesRequires: dependency.ProvidesRequires{
|
||||
Provides: []string{"package-a"},
|
||||
Requires: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,16 +4,9 @@ Package golang provides a concrete Cataloger implementation relating to packages
|
||||
package golang
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/mimetype"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
@@ -27,105 +20,16 @@ const (
|
||||
|
||||
// NewGoModuleFileCataloger returns a new cataloger object that searches within go.mod files.
|
||||
func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger {
|
||||
c := goModCataloger{
|
||||
licenses: newGoLicenses(modFileCatalogerName, opts),
|
||||
}
|
||||
return &progressingCataloger{
|
||||
cataloger: generic.NewCataloger(modFileCatalogerName).
|
||||
WithParserByGlobs(c.parseGoModFile, "**/go.mod"),
|
||||
}
|
||||
return generic.NewCataloger(modFileCatalogerName).
|
||||
WithParserByGlobs(newGoModCataloger(opts).parseGoModFile, "**/go.mod")
|
||||
}
|
||||
|
||||
// NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler.
|
||||
func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger {
|
||||
return &progressingCataloger{
|
||||
cataloger: generic.NewCataloger(binaryCatalogerName).
|
||||
WithParserByMimeTypes(
|
||||
newGoBinaryCataloger(opts).parseGoBinary,
|
||||
mimetype.ExecutableMIMETypeSet.List()...,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type progressingCataloger struct {
|
||||
cataloger *generic.Cataloger
|
||||
}
|
||||
|
||||
func (p *progressingCataloger) Name() string {
|
||||
return p.cataloger.Name()
|
||||
}
|
||||
|
||||
func (p *progressingCataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
pkgs, relationships, err := p.cataloger.Catalog(ctx, resolver)
|
||||
goCompilerPkgs := []pkg.Package{}
|
||||
totalLocations := file.NewLocationSet()
|
||||
for _, goPkg := range pkgs {
|
||||
mValue, ok := goPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// go binary packages should only contain a single location
|
||||
for _, location := range goPkg.Locations.ToSlice() {
|
||||
if !totalLocations.Contains(location) {
|
||||
stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations)
|
||||
if stdLibPkg != nil {
|
||||
goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg)
|
||||
totalLocations.Add(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pkgs = append(pkgs, goCompilerPkgs...)
|
||||
return pkgs, relationships, err
|
||||
}
|
||||
|
||||
func newGoStdLib(version string, location file.LocationSet) *pkg.Package {
|
||||
stdlibCpe, err := generateStdlibCpe(version)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
goCompilerPkg := &pkg.Package{
|
||||
Name: "stdlib",
|
||||
Version: version,
|
||||
PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")),
|
||||
CPEs: []cpe.CPE{stdlibCpe},
|
||||
Locations: location,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||
GoCompiledVersion: version,
|
||||
},
|
||||
}
|
||||
goCompilerPkg.SetID()
|
||||
|
||||
return goCompilerPkg
|
||||
}
|
||||
|
||||
func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) {
|
||||
// GoCompiledVersion when pulled from a binary is prefixed by go
|
||||
version = strings.TrimPrefix(version, "go")
|
||||
|
||||
// we also need to trim starting from the first +<metadata> to
|
||||
// correctly extract potential rc candidate information for cpe generation
|
||||
// ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned
|
||||
after, _, found := strings.Cut("+", version)
|
||||
if found {
|
||||
version = after
|
||||
}
|
||||
|
||||
// extracting <version> and <candidate>
|
||||
// https://regex101.com/r/985GsI/1
|
||||
captureGroups := internal.MatchNamedCaptureGroups(versionCandidateGroups, version)
|
||||
vr, ok := captureGroups["version"]
|
||||
if !ok || vr == "" {
|
||||
return stdlibCpe, fmt.Errorf("could not match candidate version for: %s", version)
|
||||
}
|
||||
|
||||
cpeString := fmt.Sprintf("cpe:2.3:a:golang:go:%s:-:*:*:*:*:*:*", captureGroups["version"])
|
||||
if candidate, ok := captureGroups["candidate"]; ok && candidate != "" {
|
||||
cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate)
|
||||
}
|
||||
|
||||
return cpe.New(cpeString, cpe.GeneratedSource)
|
||||
return generic.NewCataloger(binaryCatalogerName).
|
||||
WithParserByMimeTypes(
|
||||
newGoBinaryCataloger(opts).parseGoBinary,
|
||||
mimetype.ExecutableMIMETypeSet.List()...,
|
||||
).
|
||||
WithProcessors(stdlibProcessor)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,92 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_PackageCataloger_Binary(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expectedPkgs []string
|
||||
expectedRels []string
|
||||
}{
|
||||
{
|
||||
name: "simple module with dependencies",
|
||||
fixture: "image-small",
|
||||
expectedPkgs: []string{
|
||||
"anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/andybalholm/brotli @ v1.0.1 (/run-me)",
|
||||
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me)",
|
||||
"github.com/golang/snappy @ v0.0.2 (/run-me)",
|
||||
"github.com/klauspost/compress @ v1.11.4 (/run-me)",
|
||||
"github.com/klauspost/pgzip @ v1.2.5 (/run-me)",
|
||||
"github.com/mholt/archiver/v3 @ v3.5.1 (/run-me)",
|
||||
"github.com/nwaples/rardecode @ v1.1.0 (/run-me)",
|
||||
"github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me)",
|
||||
"github.com/ulikunitz/xz @ v0.5.9 (/run-me)",
|
||||
"github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me)",
|
||||
"stdlib @ go1.22.4 (/run-me)",
|
||||
},
|
||||
expectedRels: []string{
|
||||
"github.com/andybalholm/brotli @ v1.0.1 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/golang/snappy @ v0.0.2 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/klauspost/compress @ v1.11.4 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/klauspost/pgzip @ v1.2.5 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/mholt/archiver/v3 @ v3.5.1 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/nwaples/rardecode @ v1.1.0 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/ulikunitz/xz @ v0.5.9 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
"stdlib @ go1.22.4 (/run-me) [dependency-of] anchore.io/not/real @ (devel) (/run-me)",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partially built binary",
|
||||
// the difference is the build flags used to build the binary... they will not reference the module directly
|
||||
// see the dockerfile for details
|
||||
fixture: "image-not-a-module",
|
||||
expectedPkgs: []string{
|
||||
"command-line-arguments @ (devel) (/run-me)", // this is the difference!
|
||||
"github.com/andybalholm/brotli @ v1.0.1 (/run-me)",
|
||||
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me)",
|
||||
"github.com/golang/snappy @ v0.0.2 (/run-me)",
|
||||
"github.com/klauspost/compress @ v1.11.4 (/run-me)",
|
||||
"github.com/klauspost/pgzip @ v1.2.5 (/run-me)",
|
||||
"github.com/mholt/archiver/v3 @ v3.5.1 (/run-me)",
|
||||
"github.com/nwaples/rardecode @ v1.1.0 (/run-me)",
|
||||
"github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me)",
|
||||
"github.com/ulikunitz/xz @ v0.5.9 (/run-me)",
|
||||
"github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me)",
|
||||
"stdlib @ go1.22.4 (/run-me)",
|
||||
},
|
||||
expectedRels: []string{
|
||||
"github.com/andybalholm/brotli @ v1.0.1 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/golang/snappy @ v0.0.2 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/klauspost/compress @ v1.11.4 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/klauspost/pgzip @ v1.2.5 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/mholt/archiver/v3 @ v3.5.1 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/nwaples/rardecode @ v1.1.0 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/pierrec/lz4/v4 @ v4.1.2 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/ulikunitz/xz @ v0.5.9 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
"stdlib @ go1.22.4 (/run-me) [dependency-of] command-line-arguments @ (devel) (/run-me)",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
WithImageResolver(t, test.fixture).
|
||||
ExpectsPackageStrings(test.expectedPkgs).
|
||||
ExpectsRelationshipStrings(test.expectedRels).
|
||||
TestCataloger(t, NewGoModuleBinaryCataloger(DefaultCatalogerConfig()))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_Mod_Cataloger_Globs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -41,6 +41,7 @@ type MainModuleVersionConfig struct {
|
||||
func DefaultCatalogerConfig() CatalogerConfig {
|
||||
g := CatalogerConfig{
|
||||
MainModuleVersion: DefaultMainModuleVersionConfig(),
|
||||
LocalModCacheDir: defaultGoModDir(),
|
||||
}
|
||||
|
||||
// first process the proxy settings
|
||||
@@ -67,24 +68,25 @@ func DefaultCatalogerConfig() CatalogerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
if g.LocalModCacheDir == "" {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
|
||||
if goPath == "" {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Debug("unable to determine user home dir: %v", err)
|
||||
} else {
|
||||
goPath = filepath.Join(homeDir, "go")
|
||||
}
|
||||
}
|
||||
if goPath != "" {
|
||||
g.LocalModCacheDir = filepath.Join(goPath, "pkg", "mod")
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// defaultGoModDir returns $GOPATH/pkg/mod or $HOME/go/pkg/mod based on environment variables available
|
||||
func defaultGoModDir() string {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
|
||||
if goPath == "" {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Warnf("unable to determine GOPATH or user home dir: %w", err)
|
||||
return ""
|
||||
}
|
||||
goPath = filepath.Join(homeDir, "go")
|
||||
}
|
||||
|
||||
return filepath.Join(goPath, "pkg", "mod")
|
||||
}
|
||||
|
||||
func DefaultMainModuleVersionConfig() MainModuleVersionConfig {
|
||||
return MainModuleVersionConfig{
|
||||
FromLDFlags: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
@@ -47,7 +48,7 @@ func Test_Config(t *testing.T) {
|
||||
opts: opts{},
|
||||
expected: CatalogerConfig{
|
||||
SearchLocalModCacheLicenses: false,
|
||||
LocalModCacheDir: "/go/pkg/mod",
|
||||
LocalModCacheDir: filepath.Join("/go", "pkg", "mod"),
|
||||
SearchRemoteLicenses: false,
|
||||
Proxies: []string{"https://my.proxy"},
|
||||
NoProxy: []string{"my.private", "no.proxy"},
|
||||
|
||||
@@ -21,25 +21,41 @@ import (
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/cache"
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
type goLicenses struct {
|
||||
type goLicense struct {
|
||||
Value string `json:"val,omitempty"`
|
||||
SPDXExpression string `json:"spdx,omitempty"`
|
||||
Type license.Type `json:"type,omitempty"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
Locations []string `json:"locations,omitempty"`
|
||||
}
|
||||
|
||||
type goLicenseResolver struct {
|
||||
catalogerName string
|
||||
opts CatalogerConfig
|
||||
localModCacheResolver file.WritableResolver
|
||||
localModCacheDir fs.FS
|
||||
licenseCache cache.Resolver[[]goLicense]
|
||||
lowerLicenseFileNames *strset.Set
|
||||
}
|
||||
|
||||
func newGoLicenses(catalogerName string, opts CatalogerConfig) goLicenses {
|
||||
return goLicenses{
|
||||
func newGoLicenseResolver(catalogerName string, opts CatalogerConfig) goLicenseResolver {
|
||||
var localModCacheDir fs.FS
|
||||
if opts.SearchLocalModCacheLicenses {
|
||||
localModCacheDir = os.DirFS(opts.LocalModCacheDir)
|
||||
}
|
||||
|
||||
return goLicenseResolver{
|
||||
catalogerName: catalogerName,
|
||||
opts: opts,
|
||||
localModCacheResolver: modCacheResolver(opts.LocalModCacheDir),
|
||||
localModCacheDir: localModCacheDir,
|
||||
licenseCache: cache.GetResolverCachingErrors[[]goLicense]("golang", "v1"),
|
||||
lowerLicenseFileNames: strset.New(lowercaseLicenseFiles()...),
|
||||
}
|
||||
}
|
||||
@@ -63,98 +79,95 @@ func remotesForModule(proxies []string, noProxy []string, module string) []strin
|
||||
return proxies
|
||||
}
|
||||
|
||||
func modCacheResolver(modCacheDir string) file.WritableResolver {
|
||||
var r file.WritableResolver
|
||||
func (c *goLicenseResolver) getLicenses(resolver file.Resolver, moduleName, moduleVersion string) ([]pkg.License, error) {
|
||||
// search the scan target first, ignoring local and remote sources
|
||||
goLicenses, err := c.findLicensesInSource(resolver,
|
||||
fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion),
|
||||
)
|
||||
if err != nil || len(goLicenses) > 0 {
|
||||
return toPkgLicenses(goLicenses), err
|
||||
}
|
||||
|
||||
if modCacheDir == "" {
|
||||
log.Trace("unable to determine mod cache directory, skipping mod cache resolver")
|
||||
r = fileresolver.Empty{}
|
||||
} else {
|
||||
stat, err := os.Stat(modCacheDir)
|
||||
|
||||
if os.IsNotExist(err) || stat == nil || !stat.IsDir() {
|
||||
log.Tracef("unable to open mod cache directory: %s, skipping mod cache resolver", modCacheDir)
|
||||
r = fileresolver.Empty{}
|
||||
} else {
|
||||
r = fileresolver.NewFromUnindexedDirectory(modCacheDir)
|
||||
// look in the local host mod directory...
|
||||
if c.opts.SearchLocalModCacheLicenses {
|
||||
goLicenses, err = c.getLicensesFromLocal(moduleName, moduleVersion)
|
||||
if err != nil || len(goLicenses) > 0 {
|
||||
return toPkgLicenses(goLicenses), err
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicenses(resolver file.Resolver, moduleName, moduleVersion string) (licenses []pkg.License, err error) {
|
||||
licenses, err = c.findLicenses(resolver,
|
||||
fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion),
|
||||
)
|
||||
if err != nil || len(licenses) > 0 {
|
||||
return requireCollection(licenses), err
|
||||
// download from remote sources
|
||||
if c.opts.SearchRemoteLicenses {
|
||||
goLicenses, err = c.getLicensesFromRemote(moduleName, moduleVersion)
|
||||
}
|
||||
|
||||
// look in the local host mod cache...
|
||||
licenses, err = c.getLicensesFromLocal(moduleName, moduleVersion)
|
||||
if err != nil || len(licenses) > 0 {
|
||||
return requireCollection(licenses), err
|
||||
}
|
||||
|
||||
// we did not find it yet and remote searching was enabled
|
||||
licenses, err = c.getLicensesFromRemote(moduleName, moduleVersion)
|
||||
return requireCollection(licenses), err
|
||||
return toPkgLicenses(goLicenses), err
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]pkg.License, error) {
|
||||
if !c.opts.SearchLocalModCacheLicenses {
|
||||
func (c *goLicenseResolver) getLicensesFromLocal(moduleName, moduleVersion string) ([]goLicense, error) {
|
||||
if c.localModCacheDir == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if we're running against a directory on the filesystem, it may not include the
|
||||
// user's homedir / GOPATH, so we defer to using the localModCacheResolver
|
||||
return c.findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion))
|
||||
}
|
||||
subdir := moduleDir(moduleName, moduleVersion)
|
||||
|
||||
func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]pkg.License, error) {
|
||||
if !c.opts.SearchRemoteLicenses {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proxies := remotesForModule(c.opts.Proxies, c.opts.NoProxy, moduleName)
|
||||
|
||||
fsys, err := getModule(proxies, moduleName, moduleVersion)
|
||||
// get the local subdirectory containing the specific go module
|
||||
dir, err := fs.Sub(c.localModCacheDir, subdir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dir := moduleDir(moduleName, moduleVersion)
|
||||
|
||||
// populate the mod cache with the results
|
||||
err = fs.WalkDir(fsys, ".", func(filePath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
f, err := fsys.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.localModCacheResolver.Write(file.NewLocation(path.Join(dir, filePath)), f)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Tracef("remote proxy walk failed for: %s", moduleName)
|
||||
}
|
||||
|
||||
return c.findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion))
|
||||
// if we're running against a directory on the filesystem, it may not include the
|
||||
// user's homedir / GOPATH, so we defer to using the localModCacheResolver
|
||||
// we use $GOPATH/pkg/mod to avoid leaking information about the user's system
|
||||
return c.findLicensesInFS("file://$GOPATH/pkg/mod/"+subdir+"/", dir)
|
||||
}
|
||||
|
||||
func (c *goLicenses) findLicenses(resolver file.Resolver, globMatch string) (out []pkg.License, err error) {
|
||||
out = make([]pkg.License, 0)
|
||||
if resolver == nil {
|
||||
return
|
||||
}
|
||||
func (c *goLicenseResolver) getLicensesFromRemote(moduleName, moduleVersion string) ([]goLicense, error) {
|
||||
return c.licenseCache.Resolve(fmt.Sprintf("%s/%s", moduleName, moduleVersion), func() ([]goLicense, error) {
|
||||
proxies := remotesForModule(c.opts.Proxies, c.opts.NoProxy, moduleName)
|
||||
|
||||
urlPrefix, fsys, err := getModule(proxies, moduleName, moduleVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.findLicensesInFS(urlPrefix, fsys)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *goLicenseResolver) findLicensesInFS(urlPrefix string, fsys fs.FS) ([]goLicense, error) {
|
||||
var out []goLicense
|
||||
err := fs.WalkDir(fsys, ".", func(filePath string, d fs.DirEntry, _ error) error {
|
||||
if !c.lowerLicenseFileNames.Has(strings.ToLower(d.Name())) {
|
||||
return nil
|
||||
}
|
||||
rdr, err := fsys.Open(filePath)
|
||||
if err != nil {
|
||||
log.Debugf("error opening license file %s: %v", filePath, err)
|
||||
return nil
|
||||
}
|
||||
defer internal.CloseAndLogError(rdr, filePath)
|
||||
parsed, err := licenses.Parse(rdr, file.NewLocation(filePath))
|
||||
if err != nil {
|
||||
log.Debugf("error parsing license file %s: %v", filePath, err)
|
||||
return nil
|
||||
}
|
||||
// since these licenses are found in an external fs.FS, not in the scanned source,
|
||||
// get rid of the locations but keep information about the where the license was found
|
||||
// by prepending the urlPrefix to the internal path for an accurate representation
|
||||
for _, l := range toGoLicenses(parsed) {
|
||||
l.URLs = []string{urlPrefix + filePath}
|
||||
l.Locations = nil
|
||||
out = append(out, l)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (c *goLicenseResolver) findLicensesInSource(resolver file.Resolver, globMatch string) ([]goLicense, error) {
|
||||
var out []goLicense
|
||||
locations, err := resolver.FilesByGlob(globMatch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -168,11 +181,19 @@ func (c *goLicenses) findLicenses(resolver file.Resolver, globMatch string) (out
|
||||
out = append(out, parsed...)
|
||||
}
|
||||
|
||||
return
|
||||
// if we have a directory but simply don't have any found license files, indicate this so we
|
||||
// don't re-download modules continually
|
||||
if len(locations) > 0 && len(out) == 0 {
|
||||
return nil, noLicensesFound{
|
||||
glob: globMatch,
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *goLicenses) parseLicenseFromLocation(l file.Location, resolver file.Resolver) ([]pkg.License, error) {
|
||||
var out []pkg.License
|
||||
func (c *goLicenseResolver) parseLicenseFromLocation(l file.Location, resolver file.Resolver) ([]goLicense, error) {
|
||||
var out []goLicense
|
||||
fileName := path.Base(l.RealPath)
|
||||
if c.lowerLicenseFileNames.Has(strings.ToLower(fileName)) {
|
||||
contents, err := resolver.FileContentsByLocation(l)
|
||||
@@ -185,7 +206,7 @@ func (c *goLicenses) parseLicenseFromLocation(l file.Location, resolver file.Res
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out = append(out, parsed...)
|
||||
out = append(out, toGoLicenses(parsed)...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -194,13 +215,9 @@ func moduleDir(moduleName, moduleVersion string) string {
|
||||
return fmt.Sprintf("%s@%s", processCaps(moduleName), moduleVersion)
|
||||
}
|
||||
|
||||
func moduleSearchGlob(moduleName, moduleVersion string) string {
|
||||
return fmt.Sprintf("%s/*", moduleDir(moduleName, moduleVersion))
|
||||
}
|
||||
|
||||
func requireCollection(licenses []pkg.License) []pkg.License {
|
||||
func requireCollection[T any](licenses []T) []T {
|
||||
if licenses == nil {
|
||||
return make([]pkg.License, 0)
|
||||
return make([]T, 0)
|
||||
}
|
||||
return licenses
|
||||
}
|
||||
@@ -213,18 +230,19 @@ func processCaps(s string) string {
|
||||
})
|
||||
}
|
||||
|
||||
func getModule(proxies []string, moduleName, moduleVersion string) (fsys fs.FS, err error) {
|
||||
func getModule(proxies []string, moduleName, moduleVersion string) (urlPrefix string, fsys fs.FS, err error) {
|
||||
for _, proxy := range proxies {
|
||||
u, _ := url.Parse(proxy)
|
||||
if proxy == "direct" {
|
||||
fsys, err = getModuleRepository(moduleName, moduleVersion)
|
||||
urlPrefix, fsys, err = getModuleRepository(moduleName, moduleVersion)
|
||||
continue
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "https", "http":
|
||||
fsys, err = getModuleProxy(proxy, moduleName, moduleVersion)
|
||||
urlPrefix, fsys, err = getModuleProxy(proxy, moduleName, moduleVersion)
|
||||
case "file":
|
||||
p := filepath.Join(u.Path, moduleName, "@v", moduleVersion)
|
||||
urlPrefix = path.Join("file://", p) + "/"
|
||||
fsys = os.DirFS(p)
|
||||
}
|
||||
if fsys != nil {
|
||||
@@ -234,13 +252,13 @@ func getModule(proxies []string, moduleName, moduleVersion string) (fsys fs.FS,
|
||||
return
|
||||
}
|
||||
|
||||
func getModuleProxy(proxy string, moduleName string, moduleVersion string) (out fs.FS, _ error) {
|
||||
func getModuleProxy(proxy string, moduleName string, moduleVersion string) (moduleURL string, out fs.FS, _ error) {
|
||||
u := fmt.Sprintf("%s/%s/@v/%s.zip", proxy, moduleName, moduleVersion)
|
||||
|
||||
// get the module zip
|
||||
resp, err := http.Get(u) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
@@ -250,25 +268,25 @@ func getModuleProxy(proxy string, moduleName string, moduleVersion string) (out
|
||||
// try lowercasing it; some packages have mixed casing that really messes up the proxy
|
||||
resp, err = http.Get(u) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
|
||||
return "", nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// read the zip
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
out, err = zip.NewReader(bytes.NewReader(b), resp.ContentLength)
|
||||
versionPath := findVersionPath(out, ".")
|
||||
out = getSubFS(out, versionPath)
|
||||
|
||||
return out, err
|
||||
return u + "#" + versionPath + "/", out, err
|
||||
}
|
||||
|
||||
func findVersionPath(f fs.FS, dir string) string {
|
||||
@@ -288,26 +306,111 @@ func findVersionPath(f fs.FS, dir string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func getModuleRepository(moduleName string, moduleVersion string) (fs.FS, error) {
|
||||
func getModuleRepository(moduleName string, moduleVersion string) (string, fs.FS, error) {
|
||||
repoName := moduleName
|
||||
parts := strings.Split(moduleName, "/")
|
||||
if len(parts) > 2 {
|
||||
repoName = fmt.Sprintf("%s/%s/%s", parts[0], parts[1], parts[2])
|
||||
}
|
||||
|
||||
// see if there's a hash and use that if so, otherwise use a tag
|
||||
splitVersion := strings.Split(moduleVersion, "-")
|
||||
var cloneRefName plumbing.ReferenceName
|
||||
refPath := ""
|
||||
if len(splitVersion) < 3 {
|
||||
tagName := splitVersion[0]
|
||||
cloneRefName = plumbing.NewTagReferenceName(tagName)
|
||||
refPath = "/tags/" + tagName
|
||||
}
|
||||
|
||||
f := memfs.New()
|
||||
buf := &bytes.Buffer{}
|
||||
_, err := git.Clone(memory.NewStorage(), f, &git.CloneOptions{
|
||||
URL: fmt.Sprintf("https://%s", repoName),
|
||||
ReferenceName: plumbing.NewTagReferenceName(moduleVersion), // FIXME version might be a SHA
|
||||
repoURL := fmt.Sprintf("https://%s", repoName)
|
||||
r, err := git.Clone(memory.NewStorage(), f, &git.CloneOptions{
|
||||
URL: repoURL,
|
||||
ReferenceName: cloneRefName,
|
||||
SingleBranch: true,
|
||||
Depth: 1,
|
||||
Progress: buf,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w -- %s", err, buf.String())
|
||||
return "", nil, fmt.Errorf("%w -- %s", err, buf.String())
|
||||
}
|
||||
|
||||
return billyFSAdapter{fs: f}, nil
|
||||
if len(splitVersion) > 2 {
|
||||
sha := splitVersion[len(splitVersion)-1]
|
||||
hash, err := r.ResolveRevision(plumbing.Revision(sha))
|
||||
if err != nil || hash == nil {
|
||||
log.Tracef("unable to resolve hash %s: %v", sha, err)
|
||||
} else {
|
||||
w, err := r.Worktree()
|
||||
if err != nil {
|
||||
log.Tracef("unable to get worktree, using default: %v", err)
|
||||
}
|
||||
err = w.Checkout(&git.CheckoutOptions{
|
||||
Hash: *hash,
|
||||
})
|
||||
if err != nil {
|
||||
log.Tracef("unable to checkout commit, using default: %v", err)
|
||||
} else {
|
||||
refPath = "/refs/" + hash.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return repoURL + refPath + "/", billyFSAdapter{fs: f}, err
|
||||
}
|
||||
|
||||
type noLicensesFound struct {
|
||||
glob string
|
||||
}
|
||||
|
||||
func (l noLicensesFound) Error() string {
|
||||
return fmt.Sprintf("unable to find license information matching: %s", l.glob)
|
||||
}
|
||||
|
||||
var _ error = (*noLicensesFound)(nil)
|
||||
|
||||
func toPkgLicenses(goLicenses []goLicense) []pkg.License {
|
||||
var out []pkg.License
|
||||
for _, l := range goLicenses {
|
||||
out = append(out, pkg.License{
|
||||
Value: l.Value,
|
||||
SPDXExpression: l.SPDXExpression,
|
||||
Type: l.Type,
|
||||
URLs: l.URLs,
|
||||
Locations: toPkgLocations(l.Locations),
|
||||
})
|
||||
}
|
||||
return requireCollection(out)
|
||||
}
|
||||
|
||||
func toPkgLocations(goLocations []string) file.LocationSet {
|
||||
out := file.NewLocationSet()
|
||||
for _, l := range goLocations {
|
||||
out.Add(file.NewLocation(l))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toGoLicenses(pkgLicenses []pkg.License) []goLicense {
|
||||
var out []goLicense
|
||||
for _, l := range pkgLicenses {
|
||||
out = append(out, goLicense{
|
||||
Value: l.Value,
|
||||
SPDXExpression: l.SPDXExpression,
|
||||
Type: l.Type,
|
||||
URLs: l.URLs,
|
||||
Locations: toGoLocations(l.Locations),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toGoLocations(locations file.LocationSet) []string {
|
||||
var out []string
|
||||
for _, l := range locations.ToSlice() {
|
||||
out = append(out, l.RealPath)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -36,7 +37,8 @@ func Test_LocalLicenseSearch(t *testing.T) {
|
||||
Value: "Apache-2.0",
|
||||
SPDXExpression: "Apache-2.0",
|
||||
Type: license.Concluded,
|
||||
Locations: file.NewLocationSet(loc1),
|
||||
URLs: []string{"file://$GOPATH/pkg/mod/" + loc1.RealPath},
|
||||
Locations: file.NewLocationSet(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -46,7 +48,8 @@ func Test_LocalLicenseSearch(t *testing.T) {
|
||||
Value: "MIT",
|
||||
SPDXExpression: "MIT",
|
||||
Type: license.Concluded,
|
||||
Locations: file.NewLocationSet(loc2),
|
||||
URLs: []string{"file://$GOPATH/pkg/mod/" + loc2.RealPath},
|
||||
Locations: file.NewLocationSet(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -56,7 +59,8 @@ func Test_LocalLicenseSearch(t *testing.T) {
|
||||
Value: "Apache-2.0",
|
||||
SPDXExpression: "Apache-2.0",
|
||||
Type: license.Concluded,
|
||||
Locations: file.NewLocationSet(loc3),
|
||||
URLs: []string{"file://$GOPATH/pkg/mod/" + loc3.RealPath},
|
||||
Locations: file.NewLocationSet(),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -66,11 +70,11 @@ func Test_LocalLicenseSearch(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
l := newGoLicenses(
|
||||
l := newGoLicenseResolver(
|
||||
"",
|
||||
CatalogerConfig{
|
||||
SearchLocalModCacheLicenses: true,
|
||||
LocalModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
|
||||
LocalModCacheDir: filepath.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
|
||||
},
|
||||
)
|
||||
licenses, err := l.getLicenses(fileresolver.Empty{}, test.name, test.version)
|
||||
@@ -97,7 +101,7 @@ func Test_RemoteProxyLicenseSearch(t *testing.T) {
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
testDir := path.Join(wd, "test-fixtures", "licenses", "pkg", "mod", processCaps(modPath)+"@"+modVersion)
|
||||
testDir := filepath.Join(wd, "test-fixtures", "licenses", "pkg", "mod", processCaps(modPath)+"@"+modVersion)
|
||||
|
||||
archive := zip.NewWriter(buf)
|
||||
|
||||
@@ -106,9 +110,9 @@ func Test_RemoteProxyLicenseSearch(t *testing.T) {
|
||||
for _, f := range entries {
|
||||
// the zip files downloaded contain a path to the repo that somewhat matches where it ends up on disk,
|
||||
// so prefix entries with something similar
|
||||
writer, err := archive.Create(path.Join("github.com/something/some@version", f.Name()))
|
||||
writer, err := archive.Create(path.Join(moduleDir(modPath, modVersion), f.Name()))
|
||||
require.NoError(t, err)
|
||||
contents, err := os.ReadFile(path.Join(testDir, f.Name()))
|
||||
contents, err := os.ReadFile(filepath.Join(testDir, f.Name()))
|
||||
require.NoError(t, err)
|
||||
_, err = writer.Write(contents)
|
||||
require.NoError(t, err)
|
||||
@@ -136,7 +140,8 @@ func Test_RemoteProxyLicenseSearch(t *testing.T) {
|
||||
Value: "Apache-2.0",
|
||||
SPDXExpression: "Apache-2.0",
|
||||
Type: license.Concluded,
|
||||
Locations: file.NewLocationSet(loc1),
|
||||
URLs: []string{server.URL + "/github.com/someorg/somename/@v/v0.3.2.zip#" + loc1.RealPath},
|
||||
Locations: file.NewLocationSet(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -146,21 +151,20 @@ func Test_RemoteProxyLicenseSearch(t *testing.T) {
|
||||
Value: "MIT",
|
||||
SPDXExpression: "MIT",
|
||||
Type: license.Concluded,
|
||||
Locations: file.NewLocationSet(loc2),
|
||||
URLs: []string{server.URL + "/github.com/CapORG/CapProject/@v/v4.111.5.zip#" + loc2.RealPath},
|
||||
Locations: file.NewLocationSet(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
modDir := path.Join(t.TempDir())
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
l := newGoLicenses(
|
||||
|
||||
l := newGoLicenseResolver(
|
||||
"",
|
||||
CatalogerConfig{
|
||||
SearchRemoteLicenses: true,
|
||||
Proxies: []string{server.URL},
|
||||
LocalModCacheDir: modDir,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(resolver file.Resolver, dep *debu
|
||||
dep = dep.Replace
|
||||
}
|
||||
|
||||
licenses, err := c.licenses.getLicenses(resolver, dep.Path, dep.Version)
|
||||
licenses, err := c.licenseResolver.getLicenses(resolver, dep.Path, dep.Version)
|
||||
if err != nil {
|
||||
log.Tracef("error getting licenses for golang package: %s %v", dep.Path, err)
|
||||
}
|
||||
|
||||
@@ -38,21 +38,21 @@ var (
|
||||
// inject the correct version into the main module of the build process
|
||||
|
||||
knownBuildFlagPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?m)\.([gG]it)?([bB]uild)?[vV]er(sion)?=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||
regexp.MustCompile(`(?m)\.([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||
regexp.MustCompile(`(?m)\.[a-zA-Z0-9]*([rR]elease)?([gG]it)?([bB]uild)?[vV]er(sion)?=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||
regexp.MustCompile(`(?m)\.[a-zA-Z0-9]*([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||
}
|
||||
)
|
||||
|
||||
const devel = "(devel)"
|
||||
|
||||
type goBinaryCataloger struct {
|
||||
licenses goLicenses
|
||||
licenseResolver goLicenseResolver
|
||||
mainModuleVersion MainModuleVersionConfig
|
||||
}
|
||||
|
||||
func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger {
|
||||
return &goBinaryCataloger{
|
||||
licenses: newGoLicenses(binaryCatalogerName, opts),
|
||||
licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts),
|
||||
mainModuleVersion: opts.MainModuleVersion,
|
||||
}
|
||||
}
|
||||
@@ -69,17 +69,38 @@ func (c *goBinaryCataloger) parseGoBinary(_ context.Context, resolver file.Resol
|
||||
mods := scanFile(unionReader, reader.RealPath)
|
||||
internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
|
||||
|
||||
var rels []artifact.Relationship
|
||||
for _, mod := range mods {
|
||||
pkgs = append(pkgs, c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch, unionReader)...)
|
||||
var depPkgs []pkg.Package
|
||||
mainPkg, depPkgs := c.buildGoPkgInfo(resolver, reader.Location, mod, mod.arch, unionReader)
|
||||
if mainPkg != nil {
|
||||
rels = createModuleRelationships(*mainPkg, depPkgs)
|
||||
pkgs = append(pkgs, *mainPkg)
|
||||
}
|
||||
pkgs = append(pkgs, depPkgs...)
|
||||
}
|
||||
|
||||
return pkgs, nil, nil
|
||||
return pkgs, rels, nil
|
||||
}
|
||||
|
||||
func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) []pkg.Package {
|
||||
func createModuleRelationships(main pkg.Package, deps []pkg.Package) []artifact.Relationship {
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
for _, dep := range deps {
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: dep,
|
||||
To: main,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
})
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
|
||||
func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file.Location, mod *extendedBuildInfo, arch string, reader io.ReadSeekCloser) (*pkg.Package, []pkg.Package) {
|
||||
var pkgs []pkg.Package
|
||||
if mod == nil {
|
||||
return pkgs
|
||||
return nil, pkgs
|
||||
}
|
||||
|
||||
var empty debug.Module
|
||||
@@ -110,13 +131,12 @@ func (c *goBinaryCataloger) buildGoPkgInfo(resolver file.Resolver, location file
|
||||
}
|
||||
|
||||
if mod.Main == empty {
|
||||
return pkgs
|
||||
return nil, pkgs
|
||||
}
|
||||
|
||||
main := c.makeGoMainPackage(resolver, mod, arch, location, reader)
|
||||
pkgs = append(pkgs, main)
|
||||
|
||||
return pkgs
|
||||
return &main, pkgs
|
||||
}
|
||||
|
||||
func (c *goBinaryCataloger) makeGoMainPackage(resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package {
|
||||
|
||||
@@ -964,7 +964,10 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
c := newGoBinaryCataloger(DefaultCatalogerConfig())
|
||||
reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader(test.binaryContent)))
|
||||
require.NoError(t, err)
|
||||
pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch, reader)
|
||||
mainPkg, pkgs := c.buildGoPkgInfo(fileresolver.Empty{}, location, test.mod, test.mod.arch, reader)
|
||||
if mainPkg != nil {
|
||||
pkgs = append(pkgs, *mainPkg)
|
||||
}
|
||||
require.Len(t, pkgs, len(test.expected))
|
||||
for i, p := range pkgs {
|
||||
pkgtest.AssertPackagesEqual(t, test.expected[i], p)
|
||||
@@ -1077,6 +1080,18 @@ func Test_extractVersionFromLDFlags(t *testing.T) {
|
||||
wantMajorVersion: "4",
|
||||
wantFullVersion: "v4.5.7",
|
||||
},
|
||||
{
|
||||
name: "TiDB 7.5.0 ldflags",
|
||||
ldflags: `build -ldflags="-X \"github.com/pingcap/tidb/pkg/parser/mysql.TiDBReleaseVersion=v7.5.0\" -X \"github.com/pingcap/tidb/pkg/util/versioninfo.TiDBBuildTS=2023-11-24 08:51:04\" -X \"github.com/pingcap/tidb/pkg/util/versioninfo.TiDBGitHash=069631e2ecfedc000ffb92c67207bea81380f020\" -X \"github.com/pingcap/tidb/pkg/util/versioninfo.TiDBGitBranch=heads/refs/tags/v7.5.0\" -X \"github.com/pingcap/tidb/pkg/util/versioninfo.TiDBEdition=Community\" "`,
|
||||
wantMajorVersion: "7",
|
||||
wantFullVersion: "v7.5.0",
|
||||
},
|
||||
{
|
||||
name: "TiDB 6.1.7 ldflags",
|
||||
ldflags: `build -ldflags="-X \"github.com/pingcap/tidb/parser/mysql.TiDBReleaseVersion=v6.1.7\" -X \"github.com/pingcap/tidb/util/versioninfo.TiDBBuildTS=2023-07-04 12:06:03\" -X \"github.com/pingcap/tidb/util/versioninfo.TiDBGitHash=613ecc5f731b2843e1d53a43915e2cd8da795936\" -X \"github.com/pingcap/tidb/util/versioninfo.TiDBGitBranch=heads/refs/tags/v6.1.7\" -X \"github.com/pingcap/tidb/util/versioninfo.TiDBEdition=Community\" "`,
|
||||
wantMajorVersion: "6",
|
||||
wantFullVersion: "v6.1.7",
|
||||
},
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// negative cases
|
||||
{
|
||||
|
||||
@@ -19,7 +19,13 @@ import (
|
||||
)
|
||||
|
||||
type goModCataloger struct {
|
||||
licenses goLicenses
|
||||
licenseResolver goLicenseResolver
|
||||
}
|
||||
|
||||
func newGoModCataloger(opts CatalogerConfig) *goModCataloger {
|
||||
return &goModCataloger{
|
||||
licenseResolver: newGoLicenseResolver(modFileCatalogerName, opts),
|
||||
}
|
||||
}
|
||||
|
||||
// parseGoModFile takes a go.mod and lists all packages discovered.
|
||||
@@ -44,7 +50,7 @@ func (c *goModCataloger) parseGoModFile(_ context.Context, resolver file.Resolve
|
||||
}
|
||||
|
||||
for _, m := range f.Require {
|
||||
licenses, err := c.licenses.getLicenses(resolver, m.Mod.Path, m.Mod.Version)
|
||||
licenses, err := c.licenseResolver.getLicenses(resolver, m.Mod.Path, m.Mod.Version)
|
||||
if err != nil {
|
||||
log.Tracef("error getting licenses for package: %s %v", m.Mod.Path, err)
|
||||
}
|
||||
@@ -65,7 +71,7 @@ func (c *goModCataloger) parseGoModFile(_ context.Context, resolver file.Resolve
|
||||
|
||||
// remove any old packages and replace with new ones...
|
||||
for _, m := range f.Replace {
|
||||
licenses, err := c.licenses.getLicenses(resolver, m.New.Path, m.New.Version)
|
||||
licenses, err := c.licenseResolver.getLicenses(resolver, m.New.Path, m.New.Version)
|
||||
if err != nil {
|
||||
log.Tracef("error getting licenses for package: %s %v", m.New.Path, err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
@@ -91,10 +92,11 @@ func TestParseGoMod(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
c := goModCataloger{}
|
||||
c := newGoModCataloger(DefaultCatalogerConfig())
|
||||
pkgtest.NewCatalogTester().
|
||||
FromFile(t, test.fixture).
|
||||
Expects(test.expected, nil).
|
||||
WithResolver(fileresolver.Empty{}).
|
||||
TestParser(t, c.parseGoModFile)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func stdlibProcessor(pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
compilerPkgs, newRelationships := stdlibPackageAndRelationships(pkgs)
|
||||
return append(pkgs, compilerPkgs...), append(relationships, newRelationships...), err
|
||||
}
|
||||
|
||||
func stdlibPackageAndRelationships(pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) {
|
||||
var goCompilerPkgs []pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
totalLocations := file.NewLocationSet()
|
||||
for _, goPkg := range pkgs {
|
||||
mValue, ok := goPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// go binary packages should only contain a single location
|
||||
for _, location := range goPkg.Locations.ToSlice() {
|
||||
if totalLocations.Contains(location) {
|
||||
continue
|
||||
}
|
||||
|
||||
stdLibPkg := newGoStdLib(mValue.GoCompiledVersion, goPkg.Locations)
|
||||
if stdLibPkg != nil {
|
||||
goCompilerPkgs = append(goCompilerPkgs, *stdLibPkg)
|
||||
totalLocations.Add(location)
|
||||
}
|
||||
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: *stdLibPkg,
|
||||
To: goPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
})
|
||||
}
|
||||
}
|
||||
return goCompilerPkgs, relationships
|
||||
}
|
||||
|
||||
func newGoStdLib(version string, location file.LocationSet) *pkg.Package {
|
||||
stdlibCpe, err := generateStdlibCpe(version)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
goCompilerPkg := &pkg.Package{
|
||||
Name: "stdlib",
|
||||
Version: version,
|
||||
PURL: packageURL("stdlib", strings.TrimPrefix(version, "go")),
|
||||
CPEs: []cpe.CPE{stdlibCpe},
|
||||
Locations: location,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||
GoCompiledVersion: version,
|
||||
},
|
||||
}
|
||||
goCompilerPkg.SetID()
|
||||
|
||||
return goCompilerPkg
|
||||
}
|
||||
|
||||
func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) {
|
||||
// GoCompiledVersion when pulled from a binary is prefixed by go
|
||||
version = strings.TrimPrefix(version, "go")
|
||||
|
||||
// we also need to trim starting from the first +<metadata> to
|
||||
// correctly extract potential rc candidate information for cpe generation
|
||||
// ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned
|
||||
after, _, found := strings.Cut("+", version)
|
||||
if found {
|
||||
version = after
|
||||
}
|
||||
|
||||
// extracting <version> and <candidate>
|
||||
// https://regex101.com/r/985GsI/1
|
||||
captureGroups := internal.MatchNamedCaptureGroups(versionCandidateGroups, version)
|
||||
vr, ok := captureGroups["version"]
|
||||
if !ok || vr == "" {
|
||||
return stdlibCpe, fmt.Errorf("could not match candidate version for: %s", version)
|
||||
}
|
||||
|
||||
cpeString := fmt.Sprintf("cpe:2.3:a:golang:go:%s:-:*:*:*:*:*:*", captureGroups["version"])
|
||||
if candidate, ok := captureGroups["candidate"]; ok && candidate != "" {
|
||||
cpeString = fmt.Sprintf("cpe:2.3:a:golang:go:%s:%s:*:*:*:*:*:*", vr, candidate)
|
||||
}
|
||||
|
||||
return cpe.New(cpeString, cpe.GeneratedSource)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/cmptest"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_stdlibPackageAndRelationships(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgs []pkg.Package
|
||||
wantPkgs int
|
||||
wantRels int
|
||||
}{
|
||||
{
|
||||
name: "no packages",
|
||||
},
|
||||
{
|
||||
name: "ignore non-go-binary packages",
|
||||
pkgs: []pkg.Package{
|
||||
{
|
||||
Name: "not-go",
|
||||
Version: "1.0.0",
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
},
|
||||
},
|
||||
wantPkgs: 0,
|
||||
wantRels: 0,
|
||||
},
|
||||
{
|
||||
name: "with go-binary packages -- missing location",
|
||||
pkgs: []pkg.Package{
|
||||
{
|
||||
Name: "github.com/something/go",
|
||||
Version: "1.0.0",
|
||||
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||
GoCompiledVersion: "go1.22.2",
|
||||
MainModule: "github.com/something/go",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPkgs: 0,
|
||||
wantRels: 0,
|
||||
},
|
||||
{
|
||||
name: "with go-binary packages",
|
||||
pkgs: []pkg.Package{
|
||||
{
|
||||
Name: "github.com/something/go",
|
||||
Version: "1.0.0",
|
||||
Locations: file.NewLocationSet(file.NewLocation("/bin/my-app")),
|
||||
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||
GoCompiledVersion: "go1.22.2",
|
||||
MainModule: "github.com/something/go",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPkgs: 1,
|
||||
wantRels: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotPkgs, gotRels := stdlibPackageAndRelationships(tt.pkgs)
|
||||
assert.Len(t, gotPkgs, tt.wantPkgs)
|
||||
assert.Len(t, gotRels, tt.wantRels)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_stdlibPackageAndRelationships_values(t *testing.T) {
|
||||
loc := file.NewLocation("/bin/my-app")
|
||||
locSet := file.NewLocationSet(loc)
|
||||
p := pkg.Package{
|
||||
Name: "github.com/something/go",
|
||||
Version: "1.0.0",
|
||||
Locations: locSet,
|
||||
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||
GoCompiledVersion: "go1.22.2",
|
||||
MainModule: "github.com/something/go",
|
||||
},
|
||||
}
|
||||
p.SetID()
|
||||
|
||||
expectedPkg := pkg.Package{
|
||||
Name: "stdlib",
|
||||
Version: "go1.22.2",
|
||||
PURL: packageURL("stdlib", "1.22.2"),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicense("BSD-3-Clause")),
|
||||
CPEs: []cpe.CPE{
|
||||
{
|
||||
Attributes: cpe.MustAttributes("cpe:2.3:a:golang:go:1.22.2:-:*:*:*:*:*:*"),
|
||||
Source: "syft-generated",
|
||||
},
|
||||
},
|
||||
Locations: locSet,
|
||||
Metadata: pkg.GolangBinaryBuildinfoEntry{
|
||||
GoCompiledVersion: "go1.22.2",
|
||||
},
|
||||
}
|
||||
|
||||
expectedPkg.SetID()
|
||||
|
||||
expectedRel := artifact.Relationship{
|
||||
From: expectedPkg,
|
||||
To: p,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
}
|
||||
|
||||
gotPkgs, gotRels := stdlibPackageAndRelationships([]pkg.Package{p})
|
||||
require.Len(t, gotPkgs, 1)
|
||||
|
||||
gotPkg := gotPkgs[0]
|
||||
if d := cmp.Diff(expectedPkg, gotPkg, cmptest.DefaultCommonOptions()...); d != "" {
|
||||
t.Errorf("unexpected package (-want +got): %s", d)
|
||||
}
|
||||
|
||||
require.Len(t, gotRels, 1)
|
||||
gotRel := gotRels[0]
|
||||
|
||||
if d := cmp.Diff(expectedRel, gotRel, cmptest.DefaultCommonOptions()...); d != "" {
|
||||
t.Errorf("unexpected relationship (-want +got): %s", d)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/run-me
|
||||
@@ -0,0 +1,25 @@
|
||||
FROM --platform=linux/amd64 golang:1.22.4 AS builder
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY main.go main.go
|
||||
|
||||
# building with "." vs "main.go" is a difference in the buildinfo section
|
||||
# specifically with main.go the buildinfo section will contain the following:
|
||||
#
|
||||
# path command-line-arguments
|
||||
#
|
||||
# instead of
|
||||
#
|
||||
# mod anchore.io/not/real
|
||||
#
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o run-me main.go
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /app/run-me /run-me
|
||||
ENTRYPOINT ["/run-me"]
|
||||
@@ -0,0 +1,17 @@
|
||||
module anchore.io/not/real
|
||||
|
||||
go 1.22.1
|
||||
|
||||
require github.com/mholt/archiver/v3 v3.5.1
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/golang/snappy v0.0.2 // indirect
|
||||
github.com/klauspost/compress v1.11.4 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/ulikunitz/xz v0.5.9 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
|
||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import "github.com/mholt/archiver/v3"
|
||||
|
||||
func main() {
|
||||
|
||||
z := archiver.Zip{
|
||||
MkdirAll: true,
|
||||
SelectiveCompression: true,
|
||||
ContinueOnError: false,
|
||||
OverwriteExisting: false,
|
||||
ImplicitTopLevelFolder: false,
|
||||
}
|
||||
|
||||
err := z.Archive([]string{"main.go"}, "test.zip")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/run-me
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM --platform=linux/amd64 golang:1.22.4 AS builder
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY main.go main.go
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o run-me .
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /app/run-me /run-me
|
||||
ENTRYPOINT ["/run-me"]
|
||||
@@ -0,0 +1,17 @@
|
||||
module anchore.io/not/real
|
||||
|
||||
go 1.22.1
|
||||
|
||||
require github.com/mholt/archiver/v3 v3.5.1
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/golang/snappy v0.0.2 // indirect
|
||||
github.com/klauspost/compress v1.11.4 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.2 // indirect
|
||||
github.com/ulikunitz/xz v0.5.9 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
|
||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import "github.com/mholt/archiver/v3"
|
||||
|
||||
func main() {
|
||||
|
||||
z := archiver.Zip{
|
||||
MkdirAll: true,
|
||||
SelectiveCompression: true,
|
||||
ContinueOnError: false,
|
||||
OverwriteExisting: false,
|
||||
ImplicitTopLevelFolder: false,
|
||||
}
|
||||
|
||||
err := z.Archive([]string{"main.go"}, "test.zip")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -102,20 +102,29 @@ func normalizeCPE(cpe *wfn.Attributes) *wfn.Attributes {
|
||||
}
|
||||
|
||||
const (
|
||||
prefixForNPMPackages = "https://www.npmjs.com/package/"
|
||||
prefixForRubyGems = "https://rubygems.org/gems/"
|
||||
prefixForRubyGemsHTTP = "http://rubygems.org/gems/"
|
||||
prefixForNativeRubyGems = "https://github.com/ruby/"
|
||||
prefixForPyPIPackages = "https://pypi.org/project/"
|
||||
prefixForJenkinsPlugins = "https://plugins.jenkins.io/"
|
||||
prefixForJenkinsPluginsGitHub = "https://github.com/jenkinsci/"
|
||||
prefixForRustCrates = "https://crates.io/crates/"
|
||||
prefixForPHPPear = "https://pear.php.net/"
|
||||
prefixForPHPPearHTTP = "http://pear.php.net/"
|
||||
prefixForPHPPecl = "https://pecl.php.net/"
|
||||
prefixForPHPPeclHTTP = "http://pecl.php.net/"
|
||||
prefixForPHPComposer = "https://packagist.org/packages/"
|
||||
prefixForGoModules = "https://pkg.go.dev/"
|
||||
prefixForNPMPackages = "https://www.npmjs.com/package/"
|
||||
prefixForRubyGems = "https://rubygems.org/gems/"
|
||||
prefixForRubyGemsHTTP = "http://rubygems.org/gems/"
|
||||
prefixForNativeRubyGems = "https://github.com/ruby/"
|
||||
prefixForPyPIPackages = "https://pypi.org/project/"
|
||||
prefixForJenkinsPlugins = "https://plugins.jenkins.io/"
|
||||
prefixForJenkinsPluginsGitHub = "https://github.com/jenkinsci/"
|
||||
prefixForRustCrates = "https://crates.io/crates/"
|
||||
prefixForPHPPear = "https://pear.php.net/"
|
||||
prefixForPHPPearHTTP = "http://pear.php.net/"
|
||||
prefixForPHPPecl = "https://pecl.php.net/"
|
||||
prefixForPHPPeclHTTP = "http://pecl.php.net/"
|
||||
prefixForPHPComposer = "https://packagist.org/packages/"
|
||||
prefixForGoModules = "https://pkg.go.dev/"
|
||||
prefixForWordpressPlugins = "https://wordpress.org/plugins/"
|
||||
prefixForWordpressPluginsTracBrowser = "https://plugins.trac.wordpress.org/browser/"
|
||||
prefixForWordpressPluginsTracLog = "https://plugins.trac.wordpress.org/log/"
|
||||
prefixForWordpressPluginsGitHubArchive = "https://github.com/wp-plugins/"
|
||||
prefixForWordpressPluginsWordfence = "https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/"
|
||||
prefixForWordpressThemes = "https://wordpress.org/themes/"
|
||||
prefixForWordpressThemesTracBrowser = "https://themes.trac.wordpress.org/browser/"
|
||||
prefixForWordpressThemesTracLog = "https://themes.trac.wordpress.org/log/"
|
||||
prefixForWordpressThemesWordfence = "https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-themes/"
|
||||
)
|
||||
|
||||
// indexCPEList creates an index of CPEs by ecosystem.
|
||||
@@ -164,6 +173,12 @@ func indexCPEList(list CpeList) *dictionary.Indexed {
|
||||
|
||||
case strings.HasPrefix(ref, prefixForGoModules):
|
||||
addEntryForGoModulePackage(indexed, ref, cpeItemName)
|
||||
|
||||
case strings.HasPrefix(ref, prefixForWordpressPlugins), strings.HasPrefix(ref, prefixForWordpressPluginsTracBrowser), strings.HasPrefix(ref, prefixForWordpressPluginsTracLog), strings.HasPrefix(ref, prefixForWordpressPluginsGitHubArchive), strings.HasPrefix(ref, prefixForWordpressPluginsWordfence):
|
||||
addEntryForWordpressPlugin(indexed, ref, cpeItemName)
|
||||
|
||||
case strings.HasPrefix(ref, prefixForWordpressThemes), strings.HasPrefix(ref, prefixForWordpressThemesTracBrowser), strings.HasPrefix(ref, prefixForWordpressThemesTracLog), strings.HasPrefix(ref, prefixForWordpressThemesWordfence):
|
||||
addEntryForWordpressTheme(indexed, ref, cpeItemName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +198,37 @@ func updateIndex(indexed *dictionary.Indexed, ecosystem string, pkgName string,
|
||||
indexed.EcosystemPackages[ecosystem][pkgName].Add(cpe)
|
||||
}
|
||||
|
||||
func addEntryForWordpressPlugin(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressPlugins)
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressPluginsTracBrowser)
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressPluginsTracLog)
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressPluginsGitHubArchive)
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressPluginsWordfence)
|
||||
ref = strings.Split(ref, "?")[0]
|
||||
ref = strings.Split(ref, "/")[0]
|
||||
if ref == "" {
|
||||
return
|
||||
}
|
||||
|
||||
updateIndex(indexed, dictionary.EcosystemWordpressPlugins, ref, cpeItemName)
|
||||
}
|
||||
|
||||
func addEntryForWordpressTheme(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressThemes)
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressThemesTracBrowser)
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressThemesTracLog)
|
||||
ref = strings.TrimPrefix(ref, prefixForWordpressThemesWordfence)
|
||||
ref = strings.Split(ref, "?")[0]
|
||||
ref = strings.Split(ref, "/")[0]
|
||||
if ref == "" {
|
||||
return
|
||||
}
|
||||
|
||||
updateIndex(indexed, dictionary.EcosystemWordpressThemes, ref, cpeItemName)
|
||||
}
|
||||
|
||||
func addEntryForRustCrate(indexed *dictionary.Indexed, ref string, cpeItemName string) {
|
||||
// Prune off the non-package-name parts of the URL
|
||||
ref = strings.TrimPrefix(ref, prefixForRustCrates)
|
||||
|
||||
@@ -243,6 +243,123 @@ func Test_addEntryFuncs(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressPlugin",
|
||||
addEntryFunc: addEntryForWordpressPlugin,
|
||||
inputRef: "https://wordpress.org/plugins/armadillo/releases",
|
||||
inputCpeItemName: "cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressPlugins: {
|
||||
"armadillo": dictionary.NewSet("cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressPlugin from Trac Browser",
|
||||
addEntryFunc: addEntryForWordpressPlugin,
|
||||
inputRef: "https://plugins.trac.wordpress.org/browser/armadillo/something",
|
||||
inputCpeItemName: "cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressPlugins: {
|
||||
"armadillo": dictionary.NewSet("cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressPlugin from Trac Log",
|
||||
addEntryFunc: addEntryForWordpressPlugin,
|
||||
inputRef: "https://plugins.trac.wordpress.org/log/armadillo/log",
|
||||
inputCpeItemName: "cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressPlugins: {
|
||||
"armadillo": dictionary.NewSet("cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressPlugin from GitHub wp-plugins archive",
|
||||
addEntryFunc: addEntryForWordpressPlugin,
|
||||
inputRef: "https://github.com/wp-plugins/armadillo/something",
|
||||
inputCpeItemName: "cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressPlugins: {
|
||||
"armadillo": dictionary.NewSet("cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressPlugin wordfence",
|
||||
addEntryFunc: addEntryForWordpressPlugin,
|
||||
inputRef: "https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/armadillo/skjfhskdjhf/12344",
|
||||
inputCpeItemName: "cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressPlugins: {
|
||||
"armadillo": dictionary.NewSet("cpe:2.3:a:armadillo:armadillo:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressTheme",
|
||||
addEntryFunc: addEntryForWordpressTheme,
|
||||
inputRef: "https://wordpress.org/themes/basic/releases",
|
||||
inputCpeItemName: "cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressThemes: {
|
||||
"basic": dictionary.NewSet("cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressTheme from Trac Browser",
|
||||
addEntryFunc: addEntryForWordpressTheme,
|
||||
inputRef: "https://themes.trac.wordpress.org/browser/basic/something",
|
||||
inputCpeItemName: "cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressThemes: {
|
||||
"basic": dictionary.NewSet("cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressTheme from Trac Log",
|
||||
addEntryFunc: addEntryForWordpressTheme,
|
||||
inputRef: "https://themes.trac.wordpress.org/log/basic/log",
|
||||
inputCpeItemName: "cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressThemes: {
|
||||
"basic": dictionary.NewSet("cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "addEntryForWordpressTheme wordfence",
|
||||
addEntryFunc: addEntryForWordpressTheme,
|
||||
inputRef: "https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-themes/basic/skjfhskdjhf/12344",
|
||||
inputCpeItemName: "cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*",
|
||||
expectedIndexed: dictionary.Indexed{
|
||||
EcosystemPackages: map[string]dictionary.Packages{
|
||||
dictionary.EcosystemWordpressThemes: {
|
||||
"basic": dictionary.NewSet("cpe:2.3:a:basic:basic:1.23:*:*:*:*:wordpress:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Vendored
+52
@@ -106,6 +106,58 @@
|
||||
"unicycle": [
|
||||
"cpe:2.3:a:unicycle_project:unicycle:*:*:*:*:*:rust:*:*"
|
||||
]
|
||||
},
|
||||
"wordpress_plugins": {
|
||||
"0mk-shortener": [
|
||||
"cpe:2.3:a:0mk_shortener_project:0mk_shortener:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"jobboardwp": [
|
||||
"cpe:2.3:a:ultimatemember:jobboardwp:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"nofollow": [
|
||||
"cpe:2.3:a:ultimate_nofollow_project:ultimate_nofollow:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"petfinder-listings": [
|
||||
"cpe:2.3:a:unboxinteractive:petfinder-listings:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-instagram-feed": [
|
||||
"cpe:2.3:a:ultimate_instagram_feed_project:ultimate_instagram_feed:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-member": [
|
||||
"cpe:2.3:a:ultimatemember:ultimate_member:*:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:ultimatemember:user_profile_\\\u0026_membership:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-noindex-nofollow-tool-ii": [
|
||||
"cpe:2.3:a:ultimate_noindex_nofollow_tool_ii_project:ultimate_noindex_nofollow_tool_ii:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-sms-notifications": [
|
||||
"cpe:2.3:a:ultimatesmsnotifications:ultimate_sms_notifications_for_woocommerce:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-weather-plugin": [
|
||||
"cpe:2.3:a:ultimate-weather_project:ultimate-weather:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"ultimate-wp-query-search-filter": [
|
||||
"cpe:2.3:a:ultimate_wp_query_search_filter_project:ultimate_wp_query_search_filter:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"uncanny-automator": [
|
||||
"cpe:2.3:a:uncannyowl:uncanny_automator:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"uncanny-learndash-toolkit": [
|
||||
"cpe:2.3:a:uncannyowl:uncanny_toolkit_for_learndash:*:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:uncannyowl:uncanny_toolkit_for_learndash:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"unconfirmed": [
|
||||
"cpe:2.3:a:unconfirmed_project:unconfirmed:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"underconstruction": [
|
||||
"cpe:2.3:a:underconstruction_project:underconstruction:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-cleanfix": [
|
||||
"cpe:2.3:a:undolog:wp_cleanfix:*:*:*:*:*:wordpress:*:*"
|
||||
],
|
||||
"wp-ultra-simple-paypal-shopping-cart": [
|
||||
"cpe:2.3:a:ultra-prod:wordpress_ultra_simple_paypal_shopping_cart:*:*:*:*:*:wordpress:*:*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -25093,6 +25093,14 @@
|
||||
</references>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:yaml_project:yaml:2.3.0:*:*:*:*:go:*:*"/>
|
||||
</cpe-item>
|
||||
<cpe-item name="cpe:/a:0mk_shortener_project:0mk_shortener:0.2::~~~wordpress~~">
|
||||
<title xml:lang="en-US">0mk Shortener Project 0mk Shortener 0.2 for WordPress</title>
|
||||
<references>
|
||||
<reference href="https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/0mk-shortener/0mk-shortener-02-cross-site-request-forgery-to-stored-cross-site-scripting">Advisory</reference>
|
||||
<reference href="https://wordpress.org/plugins/0mk-shortener/">Project</reference>
|
||||
</references>
|
||||
<cpe-23:cpe23-item name="cpe:2.3:a:0mk_shortener_project:0mk_shortener:0.2:*:*:*:*:wordpress:*:*"/>
|
||||
</cpe-item>
|
||||
</cpe-list>
|
||||
</cpe-list>
|
||||
</cpe-list>
|
||||
|
||||
@@ -8,15 +8,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
EcosystemNPM = "npm"
|
||||
EcosystemRubyGems = "rubygems"
|
||||
EcosystemPyPI = "pypi"
|
||||
EcosystemPHPPear = "php_pear"
|
||||
EcosystemPHPPecl = "php_pecl"
|
||||
EcosystemPHPComposer = "php_composer"
|
||||
EcosystemJenkinsPlugins = "jenkins_plugins"
|
||||
EcosystemRustCrates = "rust_crates"
|
||||
EcosystemGoModules = "go_modules"
|
||||
EcosystemNPM = "npm"
|
||||
EcosystemRubyGems = "rubygems"
|
||||
EcosystemPyPI = "pypi"
|
||||
EcosystemPHPPear = "php_pear"
|
||||
EcosystemPHPPecl = "php_pecl"
|
||||
EcosystemPHPComposer = "php_composer"
|
||||
EcosystemJenkinsPlugins = "jenkins_plugins"
|
||||
EcosystemRustCrates = "rust_crates"
|
||||
EcosystemGoModules = "go_modules"
|
||||
EcosystemWordpressPlugins = "wordpress_plugins"
|
||||
EcosystemWordpressThemes = "wordpress_themes"
|
||||
)
|
||||
|
||||
type Indexed struct {
|
||||
|
||||
@@ -96,6 +96,13 @@ func FromDictionaryFind(p pkg.Package) ([]cpe.CPE, bool) {
|
||||
case pkg.GoModulePkg:
|
||||
cpes, ok = dict.EcosystemPackages[dictionary.EcosystemGoModules][p.Name]
|
||||
|
||||
case pkg.WordpressPluginPkg:
|
||||
metadata, valid := p.Metadata.(pkg.WordpressPluginEntry)
|
||||
if !valid {
|
||||
return parsedCPEs, false
|
||||
}
|
||||
cpes, ok = dict.EcosystemPackages[dictionary.EcosystemWordpressPlugins][metadata.PluginInstallDirectory]
|
||||
|
||||
default:
|
||||
// The dictionary doesn't support this package type yet.
|
||||
return parsedCPEs, false
|
||||
@@ -129,23 +136,26 @@ func FromDictionaryFind(p pkg.Package) ([]cpe.CPE, bool) {
|
||||
func FromPackageAttributes(p pkg.Package) []cpe.CPE {
|
||||
vendors := candidateVendors(p)
|
||||
products := candidateProducts(p)
|
||||
targetSWs := candidateTargetSw(p)
|
||||
if len(products) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := strset.New()
|
||||
cpes := make([]cpe.Attributes, 0)
|
||||
for _, product := range products {
|
||||
for _, vendor := range vendors {
|
||||
// prevent duplicate entries...
|
||||
key := fmt.Sprintf("%s|%s|%s", product, vendor, p.Version)
|
||||
if keys.Has(key) {
|
||||
continue
|
||||
}
|
||||
keys.Add(key)
|
||||
// add a new entry...
|
||||
if c := newCPE(product, vendor, p.Version, cpe.Any); c != nil {
|
||||
cpes = append(cpes, *c)
|
||||
for _, ts := range targetSWs {
|
||||
for _, product := range products {
|
||||
for _, vendor := range vendors {
|
||||
// prevent duplicate entries...
|
||||
key := fmt.Sprintf("%s|%s|%s|%s", product, vendor, p.Version, ts)
|
||||
if keys.Has(key) {
|
||||
continue
|
||||
}
|
||||
keys.Add(key)
|
||||
// add a new entry...
|
||||
if c := newCPE(product, vendor, p.Version, ts); c != nil {
|
||||
cpes = append(cpes, *c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +172,13 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE {
|
||||
return result
|
||||
}
|
||||
|
||||
func candidateTargetSw(p pkg.Package) []string {
|
||||
if p.Type == pkg.WordpressPluginPkg {
|
||||
return []string{"wordpress"}
|
||||
}
|
||||
return []string{cpe.Any}
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func candidateVendors(p pkg.Package) []string {
|
||||
// in ecosystems where the packaging metadata does not have a clear field to indicate a vendor (or a field that
|
||||
|
||||
@@ -719,6 +719,31 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
||||
"cpe:2.3:a:ruby_rake:ruby_rake:2.7.6-r0:*:*:*:*:*:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wordpress plugin",
|
||||
p: pkg.Package{
|
||||
Name: "WP Coder",
|
||||
Version: "2.5.1",
|
||||
Type: pkg.WordpressPluginPkg,
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "wp-coder",
|
||||
Author: "Wow-Company",
|
||||
AuthorURI: "https://wow-estore.com",
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:wow-company:wp-coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow-company:wp_coder:2.5.1:*:*:*:*:wordpress:*:*", // this is the correct CPE relative to CVE-2021-25053
|
||||
"cpe:2.3:a:wow-estore:wp-coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow-estore:wp_coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow:wp-coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow:wp_coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow_company:wp-coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow_company:wp_coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow_estore:wp-coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
"cpe:2.3:a:wow_estore:wp_coder:2.5.1:*:*:*:*:wordpress:*:*",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -21,12 +21,18 @@ func candidateVendorsForWordpressPlugin(p pkg.Package) fieldCandidateSet {
|
||||
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
if metadata.Author != "" {
|
||||
vendors.addValue(strings.ToLower(metadata.Author))
|
||||
}
|
||||
|
||||
if metadata.AuthorURI != "" {
|
||||
matchMap := internal.MatchNamedCaptureGroups(vendorFromURLRegexp, metadata.AuthorURI)
|
||||
if vendor, ok := matchMap["vendor"]; ok && vendor != "" {
|
||||
vendors.addValue(vendor)
|
||||
vendors.addValue(strings.ToLower(vendor))
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if len(vendors) == 0 {
|
||||
// add plugin_name + _project as a vendor if no Author URI found
|
||||
vendors.addValue(fmt.Sprintf("%s_project", normalizeWordpressPluginName(p.Name)))
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func Test_candidateVendorsForWordpressPlugin(t *testing.T) {
|
||||
AuthorURI: "https://automattic.com/wordpress-plugins/",
|
||||
},
|
||||
},
|
||||
expected: []string{"automattic"},
|
||||
expected: []string{"automattic - anti-spam team", "automattic"},
|
||||
},
|
||||
{
|
||||
name: "All-in-One WP Migration",
|
||||
@@ -47,7 +47,7 @@ func Test_candidateVendorsForWordpressPlugin(t *testing.T) {
|
||||
AuthorURI: "https://bookingultrapro.com/",
|
||||
},
|
||||
},
|
||||
expected: []string{"bookingultrapro"},
|
||||
expected: []string{"booking ultra pro", "bookingultrapro"},
|
||||
},
|
||||
{
|
||||
name: "Coming Soon Chop Chop",
|
||||
@@ -58,7 +58,7 @@ func Test_candidateVendorsForWordpressPlugin(t *testing.T) {
|
||||
AuthorURI: "https://www.chop-chop.org",
|
||||
},
|
||||
},
|
||||
expected: []string{"chop-chop"},
|
||||
expected: []string{"chop-chop.org", "chop-chop"},
|
||||
},
|
||||
{
|
||||
name: "Access Code Feeder",
|
||||
@@ -71,6 +71,22 @@ func Test_candidateVendorsForWordpressPlugin(t *testing.T) {
|
||||
// When a plugin as no `Author URI` use plugin_name + _project as a vendor
|
||||
expected: []string{"access_code_feeder_project"},
|
||||
},
|
||||
{
|
||||
name: "WP Coder",
|
||||
pkg: pkg.Package{
|
||||
Name: "WP Coder",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "wp-coder",
|
||||
Author: "Wow-Company",
|
||||
AuthorURI: "https://wow-estore.com/",
|
||||
},
|
||||
},
|
||||
// found in the wild https://plugins.trac.wordpress.org/browser/wp-coder/tags/2.5.1/wp-coder.php
|
||||
expected: []string{
|
||||
"wow-company", // this is the correct answer relative to CPEs registered on CVE-2021-25053
|
||||
"wow-estore",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
@@ -14,6 +15,14 @@ import (
|
||||
// requires for itself. These strings can represent anything from file paths, package names, or any other concept
|
||||
// that is useful for dependency resolution within that packing ecosystem.
|
||||
type Specification struct {
|
||||
ProvidesRequires
|
||||
|
||||
// Variants allows for specifying multiple sets of provides/requires for a single package. This is useful
|
||||
// in cases when you have conditional optional dependencies for a package.
|
||||
Variants []ProvidesRequires
|
||||
}
|
||||
|
||||
type ProvidesRequires struct {
|
||||
// Provides holds a list of abstract resources that this package provides for other packages.
|
||||
Provides []string
|
||||
|
||||
@@ -37,55 +46,70 @@ func Processor(s Specifier) generic.Processor {
|
||||
}
|
||||
}
|
||||
|
||||
rels = append(rels, resolve(s, pkgs)...)
|
||||
rels = append(rels, Resolve(s, pkgs)...)
|
||||
return pkgs, rels, err
|
||||
}
|
||||
}
|
||||
|
||||
// resolve will create relationships between packages based on the dependency claims of each package.
|
||||
func resolve(specifier Specifier, pkgs []pkg.Package) (relationships []artifact.Relationship) {
|
||||
pkgsProvidingResource := make(map[string][]artifact.ID)
|
||||
// Resolve will create relationships between packages based on the dependency claims of each package.
|
||||
func Resolve(specifier Specifier, pkgs []pkg.Package) (relationships []artifact.Relationship) {
|
||||
pkgsProvidingResource := make(map[string]internal.Set[artifact.ID])
|
||||
|
||||
pkgsByID := make(map[artifact.ID]pkg.Package)
|
||||
specsByPkg := make(map[artifact.ID]Specification)
|
||||
specsByPkg := make(map[artifact.ID][]ProvidesRequires)
|
||||
|
||||
for _, p := range pkgs {
|
||||
id := p.ID()
|
||||
pkgsByID[id] = p
|
||||
specsByPkg[id] = specifier(p)
|
||||
for _, resource := range deduplicate(specifier(p).Provides) {
|
||||
pkgsProvidingResource[resource] = append(pkgsProvidingResource[resource], id)
|
||||
}
|
||||
specsByPkg[id] = allProvides(pkgsProvidingResource, id, specifier(p))
|
||||
}
|
||||
|
||||
seen := strset.New()
|
||||
for _, dependantPkg := range pkgs {
|
||||
spec := specsByPkg[dependantPkg.ID()]
|
||||
for _, resource := range deduplicate(spec.Requires) {
|
||||
for _, providingPkgID := range pkgsProvidingResource[resource] {
|
||||
// prevent creating duplicate relationships
|
||||
pairKey := string(providingPkgID) + "-" + string(dependantPkg.ID())
|
||||
if seen.Has(pairKey) {
|
||||
continue
|
||||
specs := specsByPkg[dependantPkg.ID()]
|
||||
for _, spec := range specs {
|
||||
for _, resource := range deduplicate(spec.Requires) {
|
||||
for providingPkgID := range pkgsProvidingResource[resource] {
|
||||
// prevent creating duplicate relationships
|
||||
pairKey := string(providingPkgID) + "-" + string(dependantPkg.ID())
|
||||
if seen.Has(pairKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
providingPkg := pkgsByID[providingPkgID]
|
||||
|
||||
relationships = append(relationships,
|
||||
artifact.Relationship{
|
||||
From: providingPkg,
|
||||
To: dependantPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
)
|
||||
|
||||
seen.Add(pairKey)
|
||||
}
|
||||
|
||||
providingPkg := pkgsByID[providingPkgID]
|
||||
|
||||
relationships = append(relationships,
|
||||
artifact.Relationship{
|
||||
From: providingPkg,
|
||||
To: dependantPkg,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
)
|
||||
|
||||
seen.Add(pairKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return relationships
|
||||
}
|
||||
|
||||
func allProvides(pkgsProvidingResource map[string]internal.Set[artifact.ID], id artifact.ID, spec Specification) []ProvidesRequires {
|
||||
prs := []ProvidesRequires{spec.ProvidesRequires}
|
||||
prs = append(prs, spec.Variants...)
|
||||
|
||||
for _, pr := range prs {
|
||||
for _, resource := range deduplicate(pr.Provides) {
|
||||
if pkgsProvidingResource[resource] == nil {
|
||||
pkgsProvidingResource[resource] = internal.NewSet[artifact.ID]()
|
||||
}
|
||||
pkgsProvidingResource[resource].Add(id)
|
||||
}
|
||||
}
|
||||
|
||||
return prs
|
||||
}
|
||||
|
||||
func deduplicate(ss []string) []string {
|
||||
// note: we sort the set such that multiple invocations of this function will be deterministic
|
||||
set := strset.New(ss...)
|
||||
|
||||
@@ -44,6 +44,19 @@ func Test_resolve(t *testing.T) {
|
||||
"b": /* depends on */ {"a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find relationships between packages with variants",
|
||||
s: newSpecifierBuilder().
|
||||
WithProvides(a /* provides */, "a-resource").
|
||||
WithRequires(b /* requires */, "a[variant]").
|
||||
WithProvides(c /* provides */, "c-resource").
|
||||
WithVariant(a /* provides */, "variant" /* which requires */, "c-resource").
|
||||
Specifier(),
|
||||
want: map[string][]string{
|
||||
"b":/* depends on */ {"a"},
|
||||
"a":/* depends on */ {"c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deduplicates provider keys",
|
||||
s: newSpecifierBuilder().
|
||||
@@ -71,7 +84,7 @@ func Test_resolve(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
relationships := resolve(tt.s, subjects)
|
||||
relationships := Resolve(tt.s, subjects)
|
||||
if d := cmp.Diff(tt.want, abstractRelationships(t, relationships)); d != "" {
|
||||
t.Errorf("unexpected relationships (-want +got):\n%s", d)
|
||||
}
|
||||
@@ -82,12 +95,14 @@ func Test_resolve(t *testing.T) {
|
||||
type specifierBuilder struct {
|
||||
provides map[string][]string
|
||||
requires map[string][]string
|
||||
variants map[string]map[string][]string
|
||||
}
|
||||
|
||||
func newSpecifierBuilder() *specifierBuilder {
|
||||
return &specifierBuilder{
|
||||
provides: make(map[string][]string),
|
||||
requires: make(map[string][]string),
|
||||
variants: make(map[string]map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,11 +116,30 @@ func (m *specifierBuilder) WithRequires(p pkg.Package, requires ...string) *spec
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *specifierBuilder) WithVariant(p pkg.Package, variantName string, requires ...string) *specifierBuilder {
|
||||
if _, ok := m.variants[p.Name]; !ok {
|
||||
m.variants[p.Name] = make(map[string][]string)
|
||||
}
|
||||
m.variants[p.Name][variantName] = append(m.variants[p.Name][variantName], requires...)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m specifierBuilder) Specifier() Specifier {
|
||||
return func(p pkg.Package) Specification {
|
||||
var prs []ProvidesRequires
|
||||
for variantName, requires := range m.variants[p.Name] {
|
||||
prs = append(prs, ProvidesRequires{
|
||||
Provides: []string{p.Name + "[" + variantName + "]"},
|
||||
Requires: requires,
|
||||
})
|
||||
}
|
||||
|
||||
return Specification{
|
||||
Provides: m.provides[p.Name],
|
||||
Requires: m.requires[p.Name],
|
||||
ProvidesRequires: ProvidesRequires{
|
||||
Provides: m.provides[p.Name],
|
||||
Requires: m.requires[p.Name],
|
||||
},
|
||||
Variants: prs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package pkgtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -40,6 +42,7 @@ type CatalogTester struct {
|
||||
compareOptions []cmp.Option
|
||||
locationComparer cmptest.LocationComparer
|
||||
licenseComparer cmptest.LicenseComparer
|
||||
packageStringer func(pkg.Package) string
|
||||
customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship)
|
||||
}
|
||||
|
||||
@@ -48,6 +51,7 @@ func NewCatalogTester() *CatalogTester {
|
||||
wantErr: require.NoError,
|
||||
locationComparer: cmptest.DefaultLocationComparer,
|
||||
licenseComparer: cmptest.DefaultLicenseComparer,
|
||||
packageStringer: stringPackage,
|
||||
ignoreUnfulfilledPathResponses: map[string][]string{
|
||||
"FilesByPath": {
|
||||
// most catalogers search for a linux release, which will not be fulfilled in testing
|
||||
@@ -187,6 +191,23 @@ func (p *CatalogTester) Expects(pkgs []pkg.Package, relationships []artifact.Rel
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *CatalogTester) WithPackageStringer(fn func(pkg.Package) string) *CatalogTester {
|
||||
p.packageStringer = fn
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *CatalogTester) ExpectsPackageStrings(expected []string) *CatalogTester {
|
||||
return p.ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) {
|
||||
diffPackages(t, expected, pkgs, p.packageStringer)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *CatalogTester) ExpectsRelationshipStrings(expected []string) *CatalogTester {
|
||||
return p.ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
|
||||
diffRelationships(t, expected, relationships, pkgs, p.packageStringer)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *CatalogTester) ExpectsResolverPathResponses(locations []string) *CatalogTester {
|
||||
p.expectedPathResponses = locations
|
||||
return p
|
||||
@@ -347,3 +368,70 @@ func AssertPackagesEqual(t *testing.T, a, b pkg.Package) {
|
||||
t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func diffPackages(t *testing.T, expected []string, actual []pkg.Package, pkgStringer func(pkg.Package) string) {
|
||||
t.Helper()
|
||||
sort.Strings(expected)
|
||||
if d := cmp.Diff(expected, stringPackages(actual, pkgStringer)); d != "" {
|
||||
t.Errorf("unexpected package strings (-want, +got): %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func diffRelationships(t *testing.T, expected []string, actual []artifact.Relationship, pkgs []pkg.Package, pkgStringer func(pkg.Package) string) {
|
||||
t.Helper()
|
||||
pkgsByID := make(map[artifact.ID]pkg.Package)
|
||||
for _, p := range pkgs {
|
||||
pkgsByID[p.ID()] = p
|
||||
}
|
||||
sort.Strings(expected)
|
||||
if d := cmp.Diff(expected, stringRelationships(actual, pkgsByID, pkgStringer)); d != "" {
|
||||
t.Errorf("unexpected relationship strings (-want, +got): %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func stringRelationships(relationships []artifact.Relationship, nameLookup map[artifact.ID]pkg.Package, pkgStringer func(pkg.Package) string) []string {
|
||||
var result []string
|
||||
for _, r := range relationships {
|
||||
var fromName, toName string
|
||||
{
|
||||
fromPkg, ok := nameLookup[r.From.ID()]
|
||||
if !ok {
|
||||
fromName = string(r.From.ID())
|
||||
} else {
|
||||
fromName = pkgStringer(fromPkg)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
toPkg, ok := nameLookup[r.To.ID()]
|
||||
if !ok {
|
||||
toName = string(r.To.ID())
|
||||
} else {
|
||||
toName = pkgStringer(toPkg)
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, fromName+" ["+string(r.Type)+"] "+toName)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func stringPackages(pkgs []pkg.Package, pkgStringer func(pkg.Package) string) []string {
|
||||
var result []string
|
||||
for _, p := range pkgs {
|
||||
result = append(result, pkgStringer(p))
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func stringPackage(p pkg.Package) string {
|
||||
locs := p.Locations.ToSlice()
|
||||
var loc string
|
||||
if len(locs) > 0 {
|
||||
loc = p.Locations.ToSlice()[0].RealPath
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,27 @@ func TestParseRockspec(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fixture: "test-fixtures/rockspec/luasyslog-2.0.1-1.rockspec",
|
||||
ExpectedPkg: pkg.Package{
|
||||
Name: "luasyslog",
|
||||
Version: "2.0.1-1",
|
||||
PURL: "pkg:luarocks/luasyslog@2.0.1-1",
|
||||
Type: pkg.LuaRocksPkg,
|
||||
Language: pkg.Lua,
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocations("MIT/X11", file.NewLocation("test-fixtures/rockspec/luasyslog-2.0.1-1.rockspec")),
|
||||
),
|
||||
Metadata: pkg.LuaRocksPackage{
|
||||
Name: "luasyslog",
|
||||
Version: "2.0.1-1",
|
||||
License: "MIT/X11",
|
||||
Homepage: "https://github.com/lunarmodules/luasyslog",
|
||||
Description: "Syslog logging for Lua",
|
||||
URL: "git://github.com/lunarmodules/luasyslog.git",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user