mirror of
https://github.com/anchore/syft.git
synced 2026-10-13 14:47:22 +02:00
Compare commits
71
Commits
+5
-5
@@ -1,7 +1,7 @@
|
||||
tools:
|
||||
- name: binny
|
||||
version:
|
||||
want: v0.6.2
|
||||
want: v0.6.3
|
||||
method: github-release
|
||||
with:
|
||||
repo: anchore/binny
|
||||
@@ -29,7 +29,7 @@ tools:
|
||||
|
||||
- name: cosign
|
||||
version:
|
||||
want: v2.2.1
|
||||
want: v2.2.2
|
||||
method: github-release
|
||||
with:
|
||||
repo: sigstore/cosign
|
||||
@@ -43,7 +43,7 @@ tools:
|
||||
|
||||
- name: goreleaser
|
||||
version:
|
||||
want: v1.22.1
|
||||
want: v1.23.0
|
||||
method: github-release
|
||||
with:
|
||||
repo: goreleaser/goreleaser
|
||||
@@ -83,14 +83,14 @@ tools:
|
||||
|
||||
- name: task
|
||||
version:
|
||||
want: v3.31.0
|
||||
want: v3.33.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: go-task/task
|
||||
|
||||
- name: gh
|
||||
version:
|
||||
want: v2.39.2
|
||||
want: v2.40.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: cli/cli
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
- uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
|
||||
with:
|
||||
name: benchmark-test-results
|
||||
path: test/results/**/*
|
||||
|
||||
@@ -39,13 +39,13 @@ jobs:
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 #v5.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 #v2.22.8
|
||||
uses: github/codeql-action/init@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 #v3.22.12
|
||||
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@407ffafae6a767df3e0230c3df91b6443ae8df75 #v2.22.8
|
||||
uses: github/codeql-action/autobuild@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 #v3.22.12
|
||||
|
||||
# ℹ️ 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@407ffafae6a767df3e0230c3df91b6443ae8df75 #v2.22.8
|
||||
uses: github/codeql-action/analyze@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 #v3.22.12
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
echo -n "commit=$(git rev-parse HEAD)" | tee -a $GITHUB_OUTPUT
|
||||
|
||||
- name: Update Homebrew formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@d3667e5ae14df19579e4414897498e3e88f2f458 # v3.10.0
|
||||
uses: dawidd6/action-homebrew-bump-formula@75ed025ff3ad1d617862838b342b06d613a0ddf3 # v3.10.1
|
||||
with:
|
||||
token: ${{ secrets.HOMEBREW_TOKEN }}
|
||||
org: anchore
|
||||
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- uses: anchore/sbom-action@fd74a6fb98a204a1ad35bbfae0122c1a302ff88b #v0.15.0
|
||||
- uses: anchore/sbom-action@719133684c7d294116626d1344fe64f0d2ff3e9e #v0.15.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
artifact-name: sbom.spdx.json
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||
|
||||
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0
|
||||
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 #v5.0.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
stable: ${{ env.GO_STABLE_VERSION }}
|
||||
|
||||
@@ -121,7 +121,7 @@ syft <image> --scope all-layers
|
||||
|
||||
### Supported sources
|
||||
|
||||
Syft can generate a SBOM from a variety of sources:
|
||||
Syft can generate an SBOM from a variety of sources:
|
||||
|
||||
```
|
||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||
@@ -656,6 +656,10 @@ python:
|
||||
# when given an arbitrary constraint will be used (even if that version may not be available/published).
|
||||
guess-unpinned-requirements: false
|
||||
|
||||
javascript:
|
||||
search-remote-licenses: false
|
||||
npm-base-url: "https://registry.npmjs.org"
|
||||
|
||||
file-contents:
|
||||
cataloger:
|
||||
# enable/disable cataloging of file contents
|
||||
|
||||
+9
-6
@@ -77,22 +77,25 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com
|
||||
|
||||
// since root is aliased as the packages cmd we need to construct this command first
|
||||
// we also need the command to have information about the `root` options because of this alias
|
||||
packagesCmd := commands.Packages(app)
|
||||
scanCmd := commands.Scan(app)
|
||||
|
||||
// rootCmd is currently an alias for the packages command
|
||||
rootCmd := commands.Root(app, packagesCmd)
|
||||
// root is currently an alias for the scan command
|
||||
rootCmd := commands.Root(app, scanCmd)
|
||||
|
||||
// add sub-commands
|
||||
rootCmd.AddCommand(
|
||||
packagesCmd,
|
||||
scanCmd,
|
||||
commands.Packages(app, scanCmd), // this is currently an alias for the scan command
|
||||
commands.Attest(app),
|
||||
commands.Convert(app),
|
||||
clio.VersionCommand(id),
|
||||
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
|
||||
)
|
||||
|
||||
// explicitly set Cobra output to the real stdout to write things like errors and help
|
||||
rootCmd.SetOut(out)
|
||||
// note: we would direct cobra to use our writer explicitly with rootCmd.SetOut(out) , however this causes
|
||||
// deprecation warnings to be shown to stdout via the writer instead of stderr. This is unfortunate since this
|
||||
// does not appear to be the correct behavior on cobra's part https://github.com/spf13/cobra/issues/1708 .
|
||||
// In the future this functionality should be restored.
|
||||
|
||||
return app, rootCmd
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func Attest(app clio.Application) *cobra.Command {
|
||||
"appName": id.Name,
|
||||
"command": "attest",
|
||||
}),
|
||||
Args: validatePackagesArgs,
|
||||
Args: validateScanArgs,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
|
||||
@@ -1,255 +1,33 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
|
||||
`
|
||||
|
||||
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
|
||||
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
|
||||
`
|
||||
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
`
|
||||
packagesSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
|
||||
|
||||
packagesHelp = packagesExample + packagesSchemeHelp
|
||||
)
|
||||
|
||||
type packagesOptions struct {
|
||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func defaultPackagesOptions() *packagesOptions {
|
||||
return &packagesOptions{
|
||||
Output: options.DefaultOutput(),
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Catalog: options.DefaultCatalog(),
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func Packages(app clio.Application) *cobra.Command {
|
||||
func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command {
|
||||
id := app.ID()
|
||||
|
||||
opts := defaultPackagesOptions()
|
||||
opts := defaultScanOptions()
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
Example: internal.Tprintf(packagesHelp, map[string]interface{}{
|
||||
"appName": id.Name,
|
||||
"command": "packages",
|
||||
}),
|
||||
Args: validatePackagesArgs,
|
||||
cmd := app.SetupCommand(&cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: scanCmd.Short,
|
||||
Long: scanCmd.Long,
|
||||
Args: scanCmd.Args,
|
||||
Example: scanCmd.Example,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runPackages(id, opts, args[0])
|
||||
return runScan(id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func validatePackagesArgs(cmd *cobra.Command, args []string) error {
|
||||
return validateArgs(cmd, args, "an image/directory argument is required")
|
||||
}
|
||||
cmd.Deprecated = "use `syft scan` instead"
|
||||
|
||||
func validateArgs(cmd *cobra.Command, args []string, error string) error {
|
||||
if len(args) == 0 {
|
||||
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
|
||||
if err := cmd.Help(); err != nil {
|
||||
return fmt.Errorf("unable to display help: %w", err)
|
||||
}
|
||||
return fmt.Errorf(error)
|
||||
}
|
||||
|
||||
return cobra.MaximumNArgs(1)(cmd, args)
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error {
|
||||
writer, err := opts.SBOMWriter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := getSource(&opts.Catalog, userInput)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if src != nil {
|
||||
if err := src.Close(); err != nil {
|
||||
log.Tracef("unable to close source: %+v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s, err := generateSBOM(id, src, &opts.Catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("no SBOM produced for %q", userInput)
|
||||
}
|
||||
|
||||
if err := writer.Write(*s); err != nil {
|
||||
return fmt.Errorf("failed to write SBOM: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
|
||||
detection, err := source.Detect(
|
||||
userInput,
|
||||
source.DetectConfig{
|
||||
DefaultImageSource: opts.DefaultImagePullSource,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not deteremine source: %w", err)
|
||||
}
|
||||
|
||||
for _, filter := range filters {
|
||||
if err := filter(detection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var platform *image.Platform
|
||||
|
||||
if opts.Platform != "" {
|
||||
platform, err = image.NewPlatform(opts.Platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid platform: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
hashers, err := file.Hashers(opts.Source.File.Digests...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hash: %w", err)
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(
|
||||
source.DetectionSourceConfig{
|
||||
Alias: source.Alias{
|
||||
Name: opts.Source.Name,
|
||||
Version: opts.Source.Version,
|
||||
},
|
||||
RegistryOptions: opts.Registry.ToOptions(),
|
||||
Platform: platform,
|
||||
Exclude: source.ExcludeConfig{
|
||||
Paths: opts.Exclusions,
|
||||
},
|
||||
DigestAlgorithms: hashers,
|
||||
BasePath: opts.BasePath,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if userInput == "power-user" {
|
||||
bus.Notify("Note: the 'power-user' command has been removed.")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
}
|
||||
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
|
||||
tasks, err := eventloop.Tasks(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Describe(),
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: id.Name,
|
||||
Version: id.Version,
|
||||
Configuration: opts,
|
||||
},
|
||||
}
|
||||
|
||||
err = buildRelationships(&s, src, tasks)
|
||||
|
||||
return &s, err
|
||||
}
|
||||
|
||||
func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error {
|
||||
var errs error
|
||||
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
relationships = append(relationships, c)
|
||||
go func(task eventloop.Task) {
|
||||
err := eventloop.RunTask(task, &s.Artifacts, src, c)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
|
||||
for _, c := range cs {
|
||||
for n := range c {
|
||||
relationships = append(relationships, n)
|
||||
}
|
||||
}
|
||||
|
||||
return relationships
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command {
|
||||
id := app.ID()
|
||||
|
||||
opts := defaultPackagesOptions()
|
||||
opts := defaultScanOptions()
|
||||
|
||||
return app.SetupRootCommand(&cobra.Command{
|
||||
Use: fmt.Sprintf("%s [SOURCE]", app.ID().Name),
|
||||
@@ -25,7 +25,7 @@ func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runPackages(id, opts, args[0])
|
||||
return runScan(id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
scanExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
|
||||
`
|
||||
|
||||
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
|
||||
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
|
||||
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
|
||||
`
|
||||
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||
`
|
||||
scanSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
|
||||
|
||||
scanHelp = scanExample + scanSchemeHelp
|
||||
)
|
||||
|
||||
type scanOptions struct {
|
||||
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func defaultScanOptions() *scanOptions {
|
||||
return &scanOptions{
|
||||
Output: options.DefaultOutput(),
|
||||
UpdateCheck: options.DefaultUpdateCheck(),
|
||||
Catalog: options.DefaultCatalog(),
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func Scan(app clio.Application) *cobra.Command {
|
||||
id := app.ID()
|
||||
|
||||
opts := defaultScanOptions()
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
Use: "scan [SOURCE]",
|
||||
Short: "Generate an SBOM",
|
||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||
Example: internal.Tprintf(scanHelp, map[string]interface{}{
|
||||
"appName": id.Name,
|
||||
"command": "scan",
|
||||
}),
|
||||
Args: validateScanArgs,
|
||||
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||
defer restoreStdout()
|
||||
|
||||
return runScan(id, opts, args[0])
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func validateScanArgs(cmd *cobra.Command, args []string) error {
|
||||
return validateArgs(cmd, args, "an image/directory argument is required")
|
||||
}
|
||||
|
||||
func validateArgs(cmd *cobra.Command, args []string, error string) error {
|
||||
if len(args) == 0 {
|
||||
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
|
||||
if err := cmd.Help(); err != nil {
|
||||
return fmt.Errorf("unable to display help: %w", err)
|
||||
}
|
||||
return fmt.Errorf(error)
|
||||
}
|
||||
|
||||
return cobra.MaximumNArgs(1)(cmd, args)
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func runScan(id clio.Identification, opts *scanOptions, userInput string) error {
|
||||
writer, err := opts.SBOMWriter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, err := getSource(&opts.Catalog, userInput)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if src != nil {
|
||||
if err := src.Close(); err != nil {
|
||||
log.Tracef("unable to close source: %+v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s, err := generateSBOM(id, src, &opts.Catalog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("no SBOM produced for %q", userInput)
|
||||
}
|
||||
|
||||
if err := writer.Write(*s); err != nil {
|
||||
return fmt.Errorf("failed to write SBOM: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
|
||||
detection, err := source.Detect(
|
||||
userInput,
|
||||
source.DetectConfig{
|
||||
DefaultImageSource: opts.DefaultImagePullSource,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not deteremine source: %w", err)
|
||||
}
|
||||
|
||||
for _, filter := range filters {
|
||||
if err := filter(detection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var platform *image.Platform
|
||||
|
||||
if opts.Platform != "" {
|
||||
platform, err = image.NewPlatform(opts.Platform)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid platform: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
hashers, err := file.Hashers(opts.Source.File.Digests...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid hash: %w", err)
|
||||
}
|
||||
|
||||
src, err := detection.NewSource(
|
||||
source.DetectionSourceConfig{
|
||||
Alias: source.Alias{
|
||||
Name: opts.Source.Name,
|
||||
Version: opts.Source.Version,
|
||||
},
|
||||
RegistryOptions: opts.Registry.ToOptions(),
|
||||
Platform: platform,
|
||||
Exclude: source.ExcludeConfig{
|
||||
Paths: opts.Exclusions,
|
||||
},
|
||||
DigestAlgorithms: hashers,
|
||||
BasePath: opts.BasePath,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if userInput == "power-user" {
|
||||
bus.Notify("Note: the 'power-user' command has been removed.")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||
}
|
||||
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
|
||||
tasks, err := eventloop.Tasks(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := sbom.SBOM{
|
||||
Source: src.Describe(),
|
||||
Descriptor: sbom.Descriptor{
|
||||
Name: id.Name,
|
||||
Version: id.Version,
|
||||
Configuration: opts,
|
||||
},
|
||||
}
|
||||
|
||||
err = buildRelationships(&s, src, tasks)
|
||||
|
||||
return &s, err
|
||||
}
|
||||
|
||||
func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error {
|
||||
var errs error
|
||||
|
||||
var relationships []<-chan artifact.Relationship
|
||||
for _, task := range tasks {
|
||||
c := make(chan artifact.Relationship)
|
||||
relationships = append(relationships, c)
|
||||
go func(task eventloop.Task) {
|
||||
err := eventloop.RunTask(task, &s.Artifacts, src, c)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
|
||||
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
|
||||
for _, c := range cs {
|
||||
for n := range c {
|
||||
relationships = append(relationships, n)
|
||||
}
|
||||
}
|
||||
|
||||
return relationships
|
||||
}
|
||||
@@ -14,8 +14,10 @@ import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
binaryCataloger "github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
javaCataloger "github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||
javascriptCataloger "github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
pythonCataloger "github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
@@ -26,6 +28,7 @@ type Catalog struct {
|
||||
Package pkg `yaml:"package" json:"package" mapstructure:"package"`
|
||||
Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"`
|
||||
Java java `yaml:"java" json:"java" mapstructure:"java"`
|
||||
Javascript javascript `yaml:"javascript" json:"javascript" mapstructure:"javascript"`
|
||||
LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
|
||||
Python python `yaml:"python" json:"python" mapstructure:"python"`
|
||||
FileMetadata fileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
|
||||
@@ -145,6 +148,10 @@ func (cfg Catalog) ToCatalogerConfig() cataloger.Config {
|
||||
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
|
||||
},
|
||||
cfg.Java.MaxParentRecursiveDepth),
|
||||
Javascript: javascriptCataloger.DefaultCatalogerConfig().
|
||||
WithSearchRemoteLicenses(cfg.Javascript.SearchRemoteLicenses).
|
||||
WithNpmBaseURL(cfg.Javascript.NpmBaseURL),
|
||||
Binary: binaryCataloger.DefaultCatalogerConfig(),
|
||||
Python: pythonCataloger.CatalogerConfig{
|
||||
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package options
|
||||
|
||||
type javascript struct {
|
||||
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
NpmBaseURL string `json:"npm-base-url" yaml:"npm-base-url" mapstructure:"npm-base-url"`
|
||||
}
|
||||
@@ -1,16 +1,24 @@
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_task_in_progress - 1]
|
||||
some task title [some value]
|
||||
⠙ Cataloging contents
|
||||
⠙ some task title ━━━━━━━━━━━━━━━━━━━━ [some stage]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_in_progress - 1]
|
||||
└── some task title [some value]
|
||||
⠙ Cataloging contents
|
||||
└── ⠙ some task title ━━━━━━━━━━━━━━━━━━━━ [some stage]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete - 1]
|
||||
✔ └── some task done [some value]
|
||||
⠙ Cataloging contents
|
||||
└── ✔ some task done [some stage]
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_--_hide_stage - 1]
|
||||
⠙ Cataloging contents
|
||||
└── ✔ some task done
|
||||
---
|
||||
|
||||
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_with_removal - 1]
|
||||
|
||||
⠙ Cataloging contents
|
||||
---
|
||||
|
||||
@@ -100,18 +100,21 @@ func TestHandler_handleAttestationStarted(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
models, _ := handler.Handle(event)
|
||||
require.Len(t, models, 2)
|
||||
|
||||
t.Run("task line", func(t *testing.T) {
|
||||
tsk, ok := models[0].(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
@@ -119,11 +122,15 @@ func TestHandler_handleAttestationStarted(t *testing.T) {
|
||||
t.Run("log", func(t *testing.T) {
|
||||
log, ok := models[1].(attestLogFrame)
|
||||
require.True(t, ok)
|
||||
got := runModel(t, log, tt.iterations, attestLogFrameTickMsg{
|
||||
|
||||
gotModel := runModel(t, log, tt.iterations, attestLogFrameTickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: log.sequence,
|
||||
ID: log.id,
|
||||
}, log.reader.running)
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -3,70 +3,121 @@ package ui
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/google/uuid"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/bubbly/bubbles/tree"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
var _ progress.Stager = (*catalogerTaskStageAdapter)(nil)
|
||||
// we standardize how rows are instantiated to ensure consistency in the appearance across the UI
|
||||
type taskModelFactory func(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model
|
||||
|
||||
type catalogerTaskStageAdapter struct {
|
||||
mon *monitor.CatalogerTask
|
||||
var _ tea.Model = (*catalogerTaskModel)(nil)
|
||||
|
||||
type catalogerTaskModel struct {
|
||||
model tree.Model
|
||||
modelFactory taskModelFactory
|
||||
}
|
||||
|
||||
func newCatalogerTaskStageAdapter(mon *monitor.CatalogerTask) *catalogerTaskStageAdapter {
|
||||
return &catalogerTaskStageAdapter{
|
||||
mon: mon,
|
||||
func newCatalogerTaskTreeModel(f taskModelFactory) *catalogerTaskModel {
|
||||
t := tree.NewModel()
|
||||
t.Padding = " "
|
||||
t.RootsWithoutPrefix = true
|
||||
return &catalogerTaskModel{
|
||||
modelFactory: f,
|
||||
model: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (c catalogerTaskStageAdapter) Stage() string {
|
||||
return c.mon.GetValue()
|
||||
type newCatalogerTaskRowEvent struct {
|
||||
info monitor.GenericTask
|
||||
prog progress.StagedProgressable
|
||||
}
|
||||
|
||||
func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) []tea.Model {
|
||||
mon, err := syftEventParsers.ParseCatalogerTaskStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
func (cts catalogerTaskModel) Init() tea.Cmd {
|
||||
return cts.model.Init()
|
||||
}
|
||||
|
||||
func (cts catalogerTaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
event, ok := msg.(newCatalogerTaskRowEvent)
|
||||
if !ok {
|
||||
model, cmd := cts.model.Update(msg)
|
||||
cts.model = model.(tree.Model)
|
||||
|
||||
return cts, cmd
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if mon.SubStatus {
|
||||
// TODO: support list of sub-statuses, not just a single leaf
|
||||
prefix = "└── "
|
||||
}
|
||||
info, prog := event.info, event.prog
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
tsk := cts.modelFactory(
|
||||
taskprogress.Title{
|
||||
// TODO: prefix should not be part of the title, but instead a separate field that is aware of the tree structure
|
||||
Default: prefix + mon.Title,
|
||||
Running: prefix + mon.Title,
|
||||
Success: prefix + mon.TitleOnCompletion,
|
||||
Default: info.Title.Default,
|
||||
Running: info.Title.WhileRunning,
|
||||
Success: info.Title.OnSuccess,
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Progressable: mon.GetMonitor(),
|
||||
Stager: newCatalogerTaskStageAdapter(mon),
|
||||
},
|
||||
),
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
// TODO: this isn't ideal since the model stays around after it is no longer needed, but it works for now
|
||||
tsk.HideOnSuccess = mon.RemoveOnCompletion
|
||||
tsk.HideStageOnSuccess = false
|
||||
tsk.HideProgressOnSuccess = false
|
||||
if info.Context != "" {
|
||||
tsk.Context = []string{info.Context}
|
||||
}
|
||||
|
||||
tsk.TitleStyle = lipgloss.NewStyle()
|
||||
// TODO: this is a hack to get the spinner to not show up, but ideally the component would support making the spinner optional
|
||||
tsk.Spinner.Spinner.Frames = []string{" "}
|
||||
tsk.HideOnSuccess = info.HideOnSuccess
|
||||
tsk.HideStageOnSuccess = info.HideStageOnSuccess
|
||||
tsk.HideProgressOnSuccess = true
|
||||
|
||||
return []tea.Model{tsk}
|
||||
if info.ParentID != "" {
|
||||
tsk.TitleStyle = lipgloss.NewStyle()
|
||||
}
|
||||
|
||||
if err := cts.model.Add(info.ParentID, info.ID, tsk); err != nil {
|
||||
log.WithFields("error", err).Error("unable to add cataloger task to tree model")
|
||||
}
|
||||
|
||||
return cts, tsk.Init()
|
||||
}
|
||||
|
||||
func (cts catalogerTaskModel) View() string {
|
||||
return cts.model.View()
|
||||
}
|
||||
|
||||
func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {
|
||||
mon, info, err := syftEventParsers.ParseCatalogerTaskStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var models []tea.Model
|
||||
|
||||
// only create the new cataloger task tree once to manage all cataloger task events
|
||||
m.onNewCatalogerTask.Do(func() {
|
||||
models = append(models, newCatalogerTaskTreeModel(m.newTaskProgress))
|
||||
})
|
||||
|
||||
// we need to update the cataloger task model with a new row. We should never update the model outside of the
|
||||
// bubbletea update-render event loop. Instead, we return a command that will be executed by the bubbletea runtime,
|
||||
// producing a message that is passed to the cataloger task model. This is the prescribed way to update models
|
||||
// in bubbletea.
|
||||
|
||||
if info.ID == "" {
|
||||
// ID is optional from the consumer perspective, but required internally
|
||||
info.ID = uuid.Must(uuid.NewRandom()).String()
|
||||
}
|
||||
|
||||
cmd := func() tea.Msg {
|
||||
// this message will cause the cataloger task model to add a new row to the output based on the given task
|
||||
// information and progress data.
|
||||
return newCatalogerTaskRowEvent{
|
||||
info: *info,
|
||||
prog: mon,
|
||||
}
|
||||
}
|
||||
|
||||
return models, cmd
|
||||
}
|
||||
|
||||
@@ -2,19 +2,22 @@ package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
)
|
||||
|
||||
func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
title := monitor.Title{
|
||||
Default: "some task title",
|
||||
OnSuccess: "some task done",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
@@ -23,99 +26,171 @@ func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||
{
|
||||
name: "cataloging task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: false,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
value.Manual.Add(50)
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
value.Manual.Add(50)
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: false,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
src.SetCompleted()
|
||||
value.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete -- hide stage",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
|
||||
value.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: false,
|
||||
HideStageOnSuccess: true,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging sub task complete with removal",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
src := &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: true,
|
||||
Title: "some task title",
|
||||
TitleOnCompletion: "some task done",
|
||||
value := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage("some stage"),
|
||||
Manual: progress.NewManual(100),
|
||||
}
|
||||
|
||||
src.SetValue("some value")
|
||||
src.SetCompleted()
|
||||
value.SetCompleted()
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: src,
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: monitor.GenericTask{
|
||||
Title: title,
|
||||
HideOnSuccess: true,
|
||||
HideStageOnSuccess: false,
|
||||
ID: "my-id",
|
||||
ParentID: "top-level-task",
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
// need to be able to get the initial newCatalogerTaskRowEvent + initialize the nested taskprogress model
|
||||
if tt.iterations == 0 {
|
||||
tt.iterations = 2
|
||||
}
|
||||
|
||||
e := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Catalog contents",
|
||||
WhileRunning: "Cataloging contents",
|
||||
OnSuccess: "Cataloged contents",
|
||||
},
|
||||
ID: "top-level-task",
|
||||
}
|
||||
|
||||
// note: this line / event is not under test, only needed to show a sub status
|
||||
kickoffEvent := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(""),
|
||||
Manual: progress.NewManual(-1),
|
||||
}
|
||||
|
||||
models, cmd := handler.Handle(
|
||||
partybus.Event{
|
||||
Type: syftEvent.CatalogerTaskStarted,
|
||||
Source: info,
|
||||
Value: progress.StagedProgressable(kickoffEvent),
|
||||
},
|
||||
)
|
||||
require.Len(t, models, 1)
|
||||
require.NotNil(t, cmd)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
tr, ok := model.(*catalogerTaskModel)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
gotModel := runModel(t, tr, tt.iterations, cmd())
|
||||
|
||||
models, cmd = handler.Handle(e)
|
||||
require.Len(t, models, 0)
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
gotModel = runModel(t, gotModel, tt.iterations, cmd())
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -80,18 +80,21 @@ func TestHandler_handleFetchImage(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
models, _ := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFileDigestsCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
prog, err := syftEventParsers.ParseFileDigestsCatalogingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog file digests",
|
||||
Running: "Cataloging file digests",
|
||||
Success: "Cataloged file digests",
|
||||
}, taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFileDigestsCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,18 +80,21 @@ func TestHandler_handleFileIndexingStarted(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
models, _ := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
)
|
||||
|
||||
func (m *Handler) handleFileMetadataCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
prog, err := syftEventParsers.ParseFileMetadataCatalogingStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog file metadata",
|
||||
Running: "Cataloging file metadata",
|
||||
Success: "Cataloged file metadata",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(prog),
|
||||
)
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
func TestHandler_handleFileMetadataCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
mon := struct {
|
||||
progress.Progressable
|
||||
progress.Stager
|
||||
}{
|
||||
Progressable: prog,
|
||||
Stager: &progress.Stage{
|
||||
Current: "current",
|
||||
},
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
var _ progress.StagedProgressable = (*packageCatalogerProgressAdapter)(nil)
|
||||
|
||||
type packageCatalogerProgressAdapter struct {
|
||||
monitor *cataloger.Monitor
|
||||
monitors []progress.Monitorable
|
||||
}
|
||||
|
||||
func newPackageCatalogerProgressAdapter(monitor *cataloger.Monitor) packageCatalogerProgressAdapter {
|
||||
return packageCatalogerProgressAdapter{
|
||||
monitor: monitor,
|
||||
monitors: []progress.Monitorable{
|
||||
monitor.FilesProcessed,
|
||||
monitor.PackagesDiscovered,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Stage() string {
|
||||
return fmt.Sprintf("%d packages", p.monitor.PackagesDiscovered.Current())
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Current() int64 {
|
||||
return p.monitor.PackagesDiscovered.Current()
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Error() error {
|
||||
completedMonitors := 0
|
||||
for _, monitor := range p.monitors {
|
||||
err := monitor.Error()
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if progress.IsErrCompleted(err) {
|
||||
completedMonitors++
|
||||
continue
|
||||
}
|
||||
// something went wrong
|
||||
return err
|
||||
}
|
||||
if completedMonitors == len(p.monitors) && len(p.monitors) > 0 {
|
||||
return p.monitors[0].Error()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p packageCatalogerProgressAdapter) Size() int64 {
|
||||
// this is an inherently unknown value (indeterminate total number of packages to discover)
|
||||
return -1
|
||||
}
|
||||
|
||||
func (m *Handler) handlePackageCatalogerStarted(e partybus.Event) []tea.Model {
|
||||
monitor, err := syftEventParsers.ParsePackageCatalogerStarted(e)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Warn("unable to parse event")
|
||||
return nil
|
||||
}
|
||||
|
||||
tsk := m.newTaskProgress(
|
||||
taskprogress.Title{
|
||||
Default: "Catalog packages",
|
||||
Running: "Cataloging packages",
|
||||
Success: "Cataloged packages",
|
||||
},
|
||||
taskprogress.WithStagedProgressable(
|
||||
newPackageCatalogerProgressAdapter(monitor),
|
||||
),
|
||||
)
|
||||
|
||||
tsk.HideStageOnSuccess = false
|
||||
|
||||
return []tea.Model{tsk}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/gkampitakis/go-snaps/snaps"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
func TestHandler_handlePackageCatalogerStarted(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
eventFn func(*testing.T) partybus.Event
|
||||
iterations int
|
||||
}{
|
||||
{
|
||||
name: "cataloging in progress",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: progress.NewManual(-1),
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging only files complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(50)
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
files.SetCompleted()
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging only packages complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cataloging complete",
|
||||
eventFn: func(t *testing.T) partybus.Event {
|
||||
prog := &progress.Manual{}
|
||||
prog.SetTotal(100)
|
||||
prog.Set(100)
|
||||
prog.SetCompleted()
|
||||
|
||||
files := progress.NewManual(-1)
|
||||
files.SetCompleted()
|
||||
|
||||
mon := cataloger.Monitor{
|
||||
FilesProcessed: files,
|
||||
PackagesDiscovered: prog,
|
||||
}
|
||||
|
||||
return partybus.Event{
|
||||
Type: syftEvent.PackageCatalogerStarted,
|
||||
Value: mon,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
event := tt.eventFn(t)
|
||||
handler := New(DefaultHandlerConfig())
|
||||
handler.WindowSize = tea.WindowSizeMsg{
|
||||
Width: 100,
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -98,18 +98,21 @@ func TestHandler_handleReadImage(t *testing.T) {
|
||||
Height: 80,
|
||||
}
|
||||
|
||||
models := handler.Handle(event)
|
||||
models, _ := handler.Handle(event)
|
||||
require.Len(t, models, 1)
|
||||
model := models[0]
|
||||
|
||||
tsk, ok := model.(taskprogress.Model)
|
||||
require.True(t, ok)
|
||||
|
||||
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||
Time: time.Now(),
|
||||
Sequence: tsk.Sequence(),
|
||||
ID: tsk.ID(),
|
||||
})
|
||||
|
||||
got := gotModel.View()
|
||||
|
||||
t.Log(got)
|
||||
snaps.MatchSnapshot(t, got)
|
||||
})
|
||||
|
||||
+19
-13
@@ -29,6 +29,8 @@ type Handler struct {
|
||||
Config HandlerConfig
|
||||
|
||||
bubbly.EventHandler
|
||||
|
||||
onNewCatalogerTask *sync.Once
|
||||
}
|
||||
|
||||
func DefaultHandlerConfig() HandlerConfig {
|
||||
@@ -41,28 +43,32 @@ func New(cfg HandlerConfig) *Handler {
|
||||
d := bubbly.NewEventDispatcher()
|
||||
|
||||
h := &Handler{
|
||||
EventHandler: d,
|
||||
Running: &sync.WaitGroup{},
|
||||
Config: cfg,
|
||||
EventHandler: d,
|
||||
Running: &sync.WaitGroup{},
|
||||
Config: cfg,
|
||||
onNewCatalogerTask: &sync.Once{},
|
||||
}
|
||||
|
||||
// register all supported event types with the respective handler functions
|
||||
d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{
|
||||
stereoscopeEvent.PullDockerImage: h.handlePullDockerImage,
|
||||
stereoscopeEvent.PullContainerdImage: h.handlePullContainerdImage,
|
||||
stereoscopeEvent.ReadImage: h.handleReadImage,
|
||||
stereoscopeEvent.FetchImage: h.handleFetchImage,
|
||||
syftEvent.PackageCatalogerStarted: h.handlePackageCatalogerStarted,
|
||||
syftEvent.FileDigestsCatalogerStarted: h.handleFileDigestsCatalogerStarted,
|
||||
syftEvent.FileMetadataCatalogerStarted: h.handleFileMetadataCatalogerStarted,
|
||||
syftEvent.FileIndexingStarted: h.handleFileIndexingStarted,
|
||||
syftEvent.AttestationStarted: h.handleAttestationStarted,
|
||||
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
|
||||
stereoscopeEvent.PullDockerImage: simpleHandler(h.handlePullDockerImage),
|
||||
stereoscopeEvent.PullContainerdImage: simpleHandler(h.handlePullContainerdImage),
|
||||
stereoscopeEvent.ReadImage: simpleHandler(h.handleReadImage),
|
||||
stereoscopeEvent.FetchImage: simpleHandler(h.handleFetchImage),
|
||||
syftEvent.FileIndexingStarted: simpleHandler(h.handleFileIndexingStarted),
|
||||
syftEvent.AttestationStarted: simpleHandler(h.handleAttestationStarted),
|
||||
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
|
||||
})
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func simpleHandler(fn func(partybus.Event) []tea.Model) bubbly.EventHandlerFn {
|
||||
return func(e partybus.Event) ([]tea.Model, tea.Cmd) {
|
||||
return fn(e), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Handler) OnMessage(msg tea.Msg) {
|
||||
if msg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.WindowSize = msg
|
||||
|
||||
@@ -4,12 +4,11 @@ import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, h ...*sync.WaitGroup) string {
|
||||
func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, h ...*sync.WaitGroup) tea.Model {
|
||||
t.Helper()
|
||||
if iterations == 0 {
|
||||
iterations = 1
|
||||
@@ -37,34 +36,21 @@ func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, h ...*
|
||||
cmd = tea.Batch(nextCmds...)
|
||||
}
|
||||
|
||||
return m.View()
|
||||
return m
|
||||
}
|
||||
|
||||
func flatten(p tea.Msg) (msgs []tea.Msg) {
|
||||
if reflect.TypeOf(p).Name() == "batchMsg" {
|
||||
partials := extractBatchMessages(p)
|
||||
for _, m := range partials {
|
||||
msgs = append(msgs, flatten(m)...)
|
||||
func flatten(ps ...tea.Msg) (msgs []tea.Msg) {
|
||||
for _, p := range ps {
|
||||
if bm, ok := p.(tea.BatchMsg); ok {
|
||||
for _, m := range bm {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, flatten(m())...)
|
||||
}
|
||||
} else {
|
||||
msgs = []tea.Msg{p}
|
||||
}
|
||||
} else {
|
||||
msgs = []tea.Msg{p}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func extractBatchMessages(m tea.Msg) (ret []tea.Msg) {
|
||||
sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil)))
|
||||
value := reflect.ValueOf(m) // note: this is technically unaddressable
|
||||
|
||||
// make our own instance that is addressable
|
||||
valueCopy := reflect.New(value.Type()).Elem()
|
||||
valueCopy.Set(value)
|
||||
|
||||
cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem()
|
||||
for i := 0; i < cmds.Len(); i++ {
|
||||
item := cmds.Index(i)
|
||||
r := item.Call(nil)
|
||||
ret = append(ret, r[0].Interface().(tea.Msg))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -155,7 +155,11 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
for _, newModel := range m.handler.Handle(msg) {
|
||||
models, cmd := m.handler.Handle(msg)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
for _, newModel := range models {
|
||||
if newModel == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -3,25 +3,25 @@ module github.com/anchore/syft
|
||||
go 1.21.0
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.7.2
|
||||
github.com/CycloneDX/cyclonedx-go v0.8.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
|
||||
github.com/acobaugh/osrelease v0.1.0
|
||||
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461
|
||||
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9
|
||||
github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc
|
||||
github.com/anchore/fangs v0.0.0-20231103141714-84c94dc43a2e
|
||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b
|
||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501
|
||||
github.com/anchore/stereoscope v0.0.0-20231117203853-3610f4ef3e83
|
||||
github.com/anchore/stereoscope v0.0.0-20231220161148-590920dabc54
|
||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/charmbracelet/bubbletea v0.24.2
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/lipgloss v0.9.1
|
||||
github.com/dave/jennifer v1.7.0
|
||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||
@@ -32,12 +32,12 @@ require (
|
||||
github.com/github/go-spdx/v2 v2.2.0
|
||||
github.com/gkampitakis/go-snaps v0.4.12
|
||||
github.com/go-git/go-billy/v5 v5.5.0
|
||||
github.com/go-git/go-git/v5 v5.10.1
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-test/deep v1.1.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-containerregistry v0.16.1
|
||||
github.com/google/go-containerregistry v0.17.0
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
@@ -53,7 +53,7 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/saferwall/pe v1.4.7
|
||||
github.com/saferwall/pe v1.4.8
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
|
||||
github.com/sanity-io/litter v1.5.5
|
||||
github.com/sassoftware/go-rpmutils v0.2.0
|
||||
@@ -73,7 +73,7 @@ require (
|
||||
golang.org/x/mod v0.14.0
|
||||
golang.org/x/net v0.19.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.27.0
|
||||
modernc.org/sqlite v1.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -85,7 +85,7 @@ require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||
github.com/adrg/xdg v0.4.0 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
@@ -98,7 +98,7 @@ require (
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/containerd/containerd v1.7.8 // indirect
|
||||
github.com/containerd/containerd v1.7.11 // indirect
|
||||
github.com/containerd/continuity v0.4.2 // indirect
|
||||
github.com/containerd/fifo v1.1.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
@@ -117,12 +117,13 @@ require (
|
||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||
github.com/gkampitakis/ciinfo v0.3.0 // indirect
|
||||
github.com/gkampitakis/go-diff v1.3.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
@@ -203,11 +204,13 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
|
||||
go.opentelemetry.io/otel v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.19.0 // indirect
|
||||
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.16.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
|
||||
@@ -58,8 +58,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
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/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ=
|
||||
github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk=
|
||||
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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
@@ -74,8 +74,8 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/hcsshim v0.11.1 h1:hJ3s7GbWlGK4YVV92sO88BQSyF4ZLVy7/awqOlPxFbA=
|
||||
github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJNJLECr5JQg=
|
||||
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
|
||||
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
@@ -89,12 +89,12 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 h1:xGu4/uMWucwWV0YV3fpFIQZ6KVfS/Wfhmma8t0s0vRo=
|
||||
github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461/go.mod h1:Ger02eh5NpPm2IqkPAy396HU1KlK3BhOeCljDYXySSk=
|
||||
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9 h1:p0ZIe0htYOX284Y4axJaGBvXHU0VCCzLN5Wf5XbKStU=
|
||||
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9/go.mod h1:3ZsFB9tzW3vl4gEiUeuSOMDnwroWxIxJelOOHUp8dSw=
|
||||
github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc h1:A1KFO+zZZmbNlz1+WKsCF0RKVx6XRoxsAG3lrqH9hUQ=
|
||||
github.com/anchore/clio v0.0.0-20231016125544-c98a83e1c7fc/go.mod h1:QeWvNzxsrUNxcs6haQo3OtISfXUXW0qAuiG4EQiz0GU=
|
||||
github.com/anchore/fangs v0.0.0-20231103141714-84c94dc43a2e h1:O8ZubApaSl7dRzKNvyfGq9cLIPLQ5v3Iz0Y3huHKCgg=
|
||||
github.com/anchore/fangs v0.0.0-20231103141714-84c94dc43a2e/go.mod h1:yPsN3NUGhU5dcBtYBa1dMNzGu1yT5ZAfSjKq9DY4aV8=
|
||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b h1:L/djgY7ZbZ/38+wUtdkk398W3PIBJLkt1N8nU/7e47A=
|
||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b/go.mod h1:TLcE0RE5+8oIx2/NPWem/dq1DeaMoC+fPEH7hoSzPLo=
|
||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw=
|
||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||
@@ -107,8 +107,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
|
||||
github.com/anchore/stereoscope v0.0.0-20231117203853-3610f4ef3e83 h1:mxGIOmj+asEm8LUkPTG3/v0hi27WIlDVjiEVsUB9eqY=
|
||||
github.com/anchore/stereoscope v0.0.0-20231117203853-3610f4ef3e83/go.mod h1:GKAnytSVV1hoqB5r5Gd9M5Ph3Rzqq0zPdEJesewjC2w=
|
||||
github.com/anchore/stereoscope v0.0.0-20231220161148-590920dabc54 h1:i2YK5QEs9H2YB3B2zv+AGR44ves0nmAGOD07lMphH14=
|
||||
github.com/anchore/stereoscope v0.0.0-20231220161148-590920dabc54/go.mod h1:IylG7ofLoUKHwS1XDF6rPhOmaE3GgpAgsMdvvYfooTU=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
@@ -147,8 +147,8 @@ 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.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
|
||||
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
|
||||
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
|
||||
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
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.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
@@ -178,8 +178,8 @@ github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHq
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/containerd v1.7.8 h1:RkwgOW3AVUT3H/dyT0W03Dc8AzlpMG65lX48KftOFSM=
|
||||
github.com/containerd/containerd v1.7.8/go.mod h1:L/Hn9qylJtUFT7cPeM0Sr3fATj+WjHwRQ0lyrYk3OPY=
|
||||
github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw=
|
||||
github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE=
|
||||
github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
|
||||
github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
|
||||
@@ -261,6 +261,8 @@ github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
|
||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
@@ -288,8 +290,8 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+
|
||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk=
|
||||
github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg=
|
||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -298,8 +300,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=
|
||||
@@ -368,8 +370,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
|
||||
github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
|
||||
github.com/google/go-containerregistry v0.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk=
|
||||
github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
|
||||
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
|
||||
@@ -397,8 +399,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S3
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
@@ -664,8 +666,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/saferwall/pe v1.4.7 h1:A+G3DxX49paJ5OsxBfHKskhyDtmTjShlDmBd81IsHlQ=
|
||||
github.com/saferwall/pe v1.4.7/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s=
|
||||
github.com/saferwall/pe v1.4.8 h1:ey/L8FGBMrJ1Xh+Rltj1MAFPZ4LOQYGJqNa5B1Na6B0=
|
||||
github.com/saferwall/pe v1.4.8/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s=
|
||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
|
||||
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
||||
@@ -817,10 +819,14 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
|
||||
go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
|
||||
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
|
||||
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
|
||||
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
|
||||
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
|
||||
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
|
||||
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
|
||||
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
|
||||
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
@@ -848,8 +854,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.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
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=
|
||||
@@ -1353,8 +1359,8 @@ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8=
|
||||
modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
|
||||
@@ -2,10 +2,12 @@ package bus
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/redact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
)
|
||||
|
||||
func Exit() {
|
||||
@@ -33,3 +35,18 @@ func Notify(message string) {
|
||||
Value: message,
|
||||
})
|
||||
}
|
||||
|
||||
func StartCatalogerTask(info monitor.GenericTask, size int64, initialStage string) *monitor.CatalogerTaskProgress {
|
||||
t := &monitor.CatalogerTaskProgress{
|
||||
AtomicStage: progress.NewAtomicStage(initialStage),
|
||||
Manual: progress.NewManual(size),
|
||||
}
|
||||
|
||||
Publish(partybus.Event{
|
||||
Type: event.CatalogerTaskStarted,
|
||||
Source: info,
|
||||
Value: progress.StagedProgressable(t),
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -4,3 +4,10 @@ type ArchiveSearchConfig struct {
|
||||
IncludeIndexedArchives bool `yaml:"include-indexed-archives" json:"include-indexed-archives" mapstructure:"include-indexed-archives"`
|
||||
IncludeUnindexedArchives bool `yaml:"include-unindexed-archives" json:"include-unindexed-archives" mapstructure:"include-unindexed-archives"`
|
||||
}
|
||||
|
||||
func DefaultArchiveSearchConfig() ArchiveSearchConfig {
|
||||
return ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: true,
|
||||
IncludeUnindexedArchives: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,6 @@ const (
|
||||
|
||||
// Events from the syft library
|
||||
|
||||
// PackageCatalogerStarted is a partybus event that occurs when the package cataloging has begun
|
||||
PackageCatalogerStarted partybus.EventType = typePrefix + "-package-cataloger-started-event"
|
||||
|
||||
// FileMetadataCatalogerStarted is a partybus event that occurs when the file metadata cataloging has begun
|
||||
FileMetadataCatalogerStarted partybus.EventType = typePrefix + "-file-metadata-cataloger-started-event"
|
||||
|
||||
// FileDigestsCatalogerStarted is a partybus event that occurs when the file digests cataloging has begun
|
||||
FileDigestsCatalogerStarted partybus.EventType = typePrefix + "-file-digests-cataloger-started-event"
|
||||
|
||||
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
||||
FileIndexingStarted partybus.EventType = typePrefix + "-file-indexing-started-event"
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
)
|
||||
|
||||
// TODO: this should be refactored to support read-only/write-only access using idioms of the progress lib
|
||||
|
||||
type CatalogerTask struct {
|
||||
prog *progress.Manual
|
||||
// Title
|
||||
Title string
|
||||
// TitleOnCompletion a string to use as title when completed
|
||||
TitleOnCompletion string
|
||||
// SubStatus indicates this progress should be rendered as a sub-item
|
||||
SubStatus bool
|
||||
// RemoveOnCompletion indicates this progress line will be removed when completed
|
||||
RemoveOnCompletion bool
|
||||
// value is the value to display -- not public as SetValue needs to be called to initialize this progress
|
||||
value string
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) init() {
|
||||
e.prog = progress.NewManual(-1)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.CatalogerTaskStarted,
|
||||
Source: e,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) SetCompleted() {
|
||||
if e.prog != nil {
|
||||
e.prog.SetCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) SetValue(value string) {
|
||||
if e.prog == nil {
|
||||
e.init()
|
||||
}
|
||||
e.value = value
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) GetValue() string {
|
||||
return e.value
|
||||
}
|
||||
|
||||
func (e *CatalogerTask) GetMonitor() *progress.Manual {
|
||||
return e.prog
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-progress"
|
||||
)
|
||||
|
||||
type CatalogerTaskProgress struct {
|
||||
*progress.AtomicStage
|
||||
*progress.Manual
|
||||
}
|
||||
@@ -18,6 +18,19 @@ type Title struct {
|
||||
}
|
||||
|
||||
type GenericTask struct {
|
||||
Title Title
|
||||
Context string
|
||||
|
||||
// required fields
|
||||
|
||||
Title Title
|
||||
|
||||
// optional format fields
|
||||
|
||||
HideOnSuccess bool
|
||||
HideStageOnSuccess bool
|
||||
|
||||
// optional fields
|
||||
|
||||
ID string
|
||||
ParentID string
|
||||
Context string
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||
)
|
||||
|
||||
type ErrBadPayload struct {
|
||||
@@ -40,45 +39,6 @@ func checkEventType(actual, expected partybus.EventType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParsePackageCatalogerStarted(e partybus.Event) (*cataloger.Monitor, error) {
|
||||
if err := checkEventType(e.Type, event.PackageCatalogerStarted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
monitor, ok := e.Value.(cataloger.Monitor)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return &monitor, nil
|
||||
}
|
||||
|
||||
func ParseFileMetadataCatalogingStarted(e partybus.Event) (progress.StagedProgressable, error) {
|
||||
if err := checkEventType(e.Type, event.FileMetadataCatalogerStarted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prog, ok := e.Value.(progress.StagedProgressable)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return prog, nil
|
||||
}
|
||||
|
||||
func ParseFileDigestsCatalogingStarted(e partybus.Event) (progress.StagedProgressable, error) {
|
||||
if err := checkEventType(e.Type, event.FileDigestsCatalogerStarted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prog, ok := e.Value.(progress.StagedProgressable)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return prog, nil
|
||||
}
|
||||
|
||||
func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgressable, error) {
|
||||
if err := checkEventType(e.Type, event.FileIndexingStarted); err != nil {
|
||||
return "", nil, err
|
||||
@@ -97,17 +57,24 @@ func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgress
|
||||
return path, prog, nil
|
||||
}
|
||||
|
||||
func ParseCatalogerTaskStarted(e partybus.Event) (*monitor.CatalogerTask, error) {
|
||||
func ParseCatalogerTaskStarted(e partybus.Event) (progress.StagedProgressable, *monitor.GenericTask, error) {
|
||||
if err := checkEventType(e.Type, event.CatalogerTaskStarted); err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
source, ok := e.Source.(*monitor.CatalogerTask)
|
||||
var mon progress.StagedProgressable
|
||||
|
||||
source, ok := e.Source.(monitor.GenericTask)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Source", e.Source)
|
||||
return nil, nil, newPayloadErr(e.Type, "Source", e.Source)
|
||||
}
|
||||
|
||||
return source, nil
|
||||
mon, ok = e.Value.(progress.StagedProgressable)
|
||||
if !ok {
|
||||
mon = nil
|
||||
}
|
||||
|
||||
return mon, &source, nil
|
||||
}
|
||||
|
||||
func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progressable, *monitor.GenericTask, error) {
|
||||
|
||||
@@ -3,16 +3,16 @@ package filedigest
|
||||
import (
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
intCataloger "github.com/anchore/syft/syft/file/cataloger/internal"
|
||||
)
|
||||
@@ -37,13 +37,19 @@ func (i *Cataloger) Catalog(resolver file.Resolver, coordinates ...file.Coordina
|
||||
locations = intCataloger.AllRegularFiles(resolver)
|
||||
} else {
|
||||
for _, c := range coordinates {
|
||||
locations = append(locations, file.NewLocationFromCoordinates(c))
|
||||
locs, err := resolver.FilesByPath(c.RealPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get file locations for path %q: %w", c.RealPath, err)
|
||||
}
|
||||
locations = append(locations, locs...)
|
||||
}
|
||||
}
|
||||
|
||||
stage, prog := digestsCatalogingProgress(int64(len(locations)))
|
||||
prog := digestsCatalogingProgress(int64(len(locations)))
|
||||
for _, location := range locations {
|
||||
stage.Current = location.RealPath
|
||||
prog.Increment()
|
||||
prog.AtomicStage.Set(location.Path())
|
||||
|
||||
result, err := i.catalogLocation(resolver, location)
|
||||
|
||||
if errors.Is(err, ErrUndigestableFile) {
|
||||
@@ -61,8 +67,12 @@ func (i *Cataloger) Catalog(resolver file.Resolver, coordinates ...file.Coordina
|
||||
prog.Increment()
|
||||
results[location.Coordinates] = result
|
||||
}
|
||||
|
||||
log.Debugf("file digests cataloger processed %d files", prog.Current())
|
||||
|
||||
prog.AtomicStage.Set(fmt.Sprintf("%s digests", humanize.Comma(prog.Current())))
|
||||
prog.SetCompleted()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -91,20 +101,14 @@ func (i *Cataloger) catalogLocation(resolver file.Resolver, location file.Locati
|
||||
return digests, nil
|
||||
}
|
||||
|
||||
func digestsCatalogingProgress(locations int64) (*progress.Stage, *progress.Manual) {
|
||||
stage := &progress.Stage{}
|
||||
prog := progress.NewManual(locations)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.FileDigestsCatalogerStarted,
|
||||
Value: struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Stager: progress.Stager(stage),
|
||||
Progressable: prog,
|
||||
func digestsCatalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Catalog file digests",
|
||||
WhileRunning: "Cataloging file digests",
|
||||
OnSuccess: "Cataloged file digests",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return stage, prog
|
||||
return bus.StartCatalogerTask(info, locations, "")
|
||||
}
|
||||
|
||||
@@ -160,3 +160,47 @@ func TestDigestsCataloger_MixFileTypes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileDigestCataloger_GivenCoordinates(t *testing.T) {
|
||||
testImage := "image-file-type-mix"
|
||||
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
c := NewCataloger([]crypto.Hash{crypto.SHA256})
|
||||
|
||||
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resolver, err := src.FileResolver(source.SquashedScope)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
exists bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
path: "/file-1.txt",
|
||||
exists: true,
|
||||
expected: "b089629781f05ef805b4511e93717f2ffa4c9d991771d5cbfa4b7242b4ef5fff",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.path, func(t *testing.T) {
|
||||
_, ref, err := img.SquashedTree().File(stereoscopeFile.Path(test.path))
|
||||
require.NoError(t, err)
|
||||
|
||||
l := file.NewLocationFromImage(test.path, *ref.Reference, img)
|
||||
|
||||
// note: an important difference between this test and the previous is that this test is using a list
|
||||
// of specific coordinates to catalog
|
||||
actual, err := c.Catalog(resolver, l.Coordinates)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actual, 1)
|
||||
|
||||
assert.Equal(t, test.expected, actual[l.Coordinates][0].Value, "mismatched digests")
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package filemetadata
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
"fmt"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
@@ -20,7 +21,6 @@ func NewCataloger() *Cataloger {
|
||||
func (i *Cataloger) Catalog(resolver file.Resolver, coordinates ...file.Coordinates) (map[file.Coordinates]file.Metadata, error) {
|
||||
results := make(map[file.Coordinates]file.Metadata)
|
||||
var locations <-chan file.Location
|
||||
|
||||
if len(coordinates) == 0 {
|
||||
locations = resolver.AllLocations()
|
||||
} else {
|
||||
@@ -43,36 +43,35 @@ func (i *Cataloger) Catalog(resolver file.Resolver, coordinates ...file.Coordina
|
||||
}()
|
||||
}
|
||||
|
||||
stage, prog := metadataCatalogingProgress(int64(len(locations)))
|
||||
prog := metadataCatalogingProgress(int64(len(locations)))
|
||||
for location := range locations {
|
||||
stage.Current = location.RealPath
|
||||
prog.Increment()
|
||||
prog.AtomicStage.Set(location.Path())
|
||||
|
||||
metadata, err := resolver.FileMetadataByLocation(location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results[location.Coordinates] = metadata
|
||||
prog.Increment()
|
||||
}
|
||||
|
||||
log.Debugf("file metadata cataloger processed %d files", prog.Current())
|
||||
|
||||
prog.AtomicStage.Set(fmt.Sprintf("%s locations", humanize.Comma(prog.Current())))
|
||||
prog.SetCompleted()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func metadataCatalogingProgress(locations int64) (*progress.Stage, *progress.Manual) {
|
||||
stage := &progress.Stage{}
|
||||
prog := progress.NewManual(locations)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.FileMetadataCatalogerStarted,
|
||||
Value: struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Stager: progress.Stager(stage),
|
||||
Progressable: prog,
|
||||
func metadataCatalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Catalog file metadata",
|
||||
WhileRunning: "Cataloging file metadata",
|
||||
OnSuccess: "Cataloged file metadata",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return stage, prog
|
||||
return bus.StartCatalogerTask(info, locations, "")
|
||||
}
|
||||
|
||||
@@ -168,7 +168,6 @@ func TestFileMetadataCataloger_GivenCoordinates(t *testing.T) {
|
||||
path string
|
||||
exists bool
|
||||
expected file.Metadata
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
path: "/file-1.txt",
|
||||
|
||||
@@ -249,9 +249,22 @@ func extractDescriptor(meta *cyclonedx.Metadata) (desc sbom.Descriptor) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range *meta.Tools {
|
||||
desc.Name = t.Name
|
||||
desc.Version = t.Version
|
||||
// handle 1.5 component element
|
||||
if meta.Tools.Components != nil {
|
||||
for _, t := range *meta.Tools.Components {
|
||||
desc.Name = t.Name
|
||||
desc.Version = t.Version
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handle pre-1.5 tool element
|
||||
if meta.Tools.Tools != nil {
|
||||
for _, t := range *meta.Tools.Tools {
|
||||
desc.Name = t.Name
|
||||
desc.Version = t.Version
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -114,11 +114,14 @@ func formatCPE(cpeString string) string {
|
||||
func toBomDescriptor(name, version string, srcMetadata source.Description) *cyclonedx.Metadata {
|
||||
return &cyclonedx.Metadata{
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Tools: &[]cyclonedx.Tool{
|
||||
{
|
||||
Vendor: "anchore",
|
||||
Name: name,
|
||||
Version: version,
|
||||
Tools: &cyclonedx.ToolsChoice{
|
||||
Components: &[]cyclonedx.Component{
|
||||
{
|
||||
Type: cyclonedx.ComponentTypeApplication,
|
||||
Author: "anchore",
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: toBomProperties(srcMetadata),
|
||||
|
||||
@@ -168,13 +168,14 @@ func Test_toBomDescriptor(t *testing.T) {
|
||||
want: &cyclonedx.Metadata{
|
||||
Timestamp: "",
|
||||
Lifecycles: nil,
|
||||
Tools: &[]cyclonedx.Tool{
|
||||
{
|
||||
Vendor: "anchore",
|
||||
Name: "test-image",
|
||||
Version: "1.0.0",
|
||||
Hashes: nil,
|
||||
ExternalReferences: nil,
|
||||
Tools: &cyclonedx.ToolsChoice{
|
||||
Components: &[]cyclonedx.Component{
|
||||
{
|
||||
Type: cyclonedx.ComponentTypeApplication,
|
||||
Author: "anchore",
|
||||
Name: "test-image",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
Authors: nil,
|
||||
|
||||
+10
-7
@@ -6,13 +6,16 @@
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "timestamp:redacted",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "v0.42.0-bogus"
|
||||
}
|
||||
],
|
||||
"tools": {
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"author": "anchore",
|
||||
"name": "syft",
|
||||
"version": "v0.42.0-bogus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"component": {
|
||||
"bom-ref":"redacted",
|
||||
"type": "file",
|
||||
|
||||
@@ -6,13 +6,16 @@
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "timestamp:redacted",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "v0.42.0-bogus"
|
||||
}
|
||||
],
|
||||
"tools": {
|
||||
"components": [
|
||||
{
|
||||
"type": "application",
|
||||
"author": "anchore",
|
||||
"name": "syft",
|
||||
"version": "v0.42.0-bogus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"component": {
|
||||
"bom-ref":"redacted",
|
||||
"type": "container",
|
||||
|
||||
+7
-5
@@ -3,11 +3,13 @@
|
||||
<metadata>
|
||||
<timestamp>redacted</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>v0.42.0-bogus</version>
|
||||
</tool>
|
||||
<components>
|
||||
<component type="application">
|
||||
<author>anchore</author>
|
||||
<name>syft</name>
|
||||
<version>v0.42.0-bogus</version>
|
||||
</component>
|
||||
</components>
|
||||
</tools>
|
||||
<component bom-ref="redacted" type="file">
|
||||
<name>some/path</name>
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
<metadata>
|
||||
<timestamp>redacted</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>v0.42.0-bogus</version>
|
||||
</tool>
|
||||
<components>
|
||||
<component type="application">
|
||||
<author>anchore</author>
|
||||
<name>syft</name>
|
||||
<version>v0.42.0-bogus</version>
|
||||
</component>
|
||||
</components>
|
||||
</tools>
|
||||
<component bom-ref="redacted" type="container">
|
||||
<name>user-image-input</name>
|
||||
|
||||
@@ -297,10 +297,15 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg := DefaultEncoderConfig()
|
||||
cfg.Pretty = true
|
||||
enc, err := NewFormatEncoderWithConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: s,
|
||||
Format: NewFormatEncoder(),
|
||||
Format: enc,
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: true,
|
||||
|
||||
@@ -640,6 +640,9 @@ func TestAllLayersResolver_AllLocations(t *testing.T) {
|
||||
// this isn't important for the test, so we remove them.
|
||||
paths.Remove("/proc", "/sys", "/dev", "/etc")
|
||||
|
||||
// Remove cache created by Mac Rosetta when emulating different arches
|
||||
paths.Remove("/.cache/rosetta", "/.cache")
|
||||
|
||||
pathsList := paths.List()
|
||||
sort.Strings(pathsList)
|
||||
|
||||
|
||||
@@ -547,6 +547,9 @@ func TestSquashResolver_AllLocations(t *testing.T) {
|
||||
// this isn't important for the test, so we remove them.
|
||||
paths.Remove("/proc", "/sys", "/dev", "/etc")
|
||||
|
||||
// Remove cache created by Mac Rosetta when emulating different arches
|
||||
paths.Remove("/.cache/rosetta", "/.cache")
|
||||
|
||||
pathsList := paths.List()
|
||||
sort.Strings(pathsList)
|
||||
|
||||
|
||||
@@ -12,8 +12,20 @@ import (
|
||||
|
||||
const catalogerName = "binary-cataloger"
|
||||
|
||||
func NewCataloger() pkg.Cataloger {
|
||||
return &Cataloger{}
|
||||
type CatalogerConfig struct {
|
||||
Classifiers []Classifier
|
||||
}
|
||||
|
||||
func DefaultCatalogerConfig() CatalogerConfig {
|
||||
return CatalogerConfig{
|
||||
Classifiers: DefaultClassifiers(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewCataloger(cfg CatalogerConfig) pkg.Cataloger {
|
||||
return &Cataloger{
|
||||
classifiers: cfg.Classifiers,
|
||||
}
|
||||
}
|
||||
|
||||
// Cataloger is the cataloger responsible for surfacing evidence of a very limited set of binary files,
|
||||
@@ -21,7 +33,9 @@ func NewCataloger() pkg.Cataloger {
|
||||
// binary, but rather the specific set that has been curated to be important, predominantly related to toolchain-
|
||||
// related runtimes like Python, Go, Java, or Node. Some exceptions can be made for widely-used binaries such
|
||||
// as busybox.
|
||||
type Cataloger struct{}
|
||||
type Cataloger struct {
|
||||
classifiers []Classifier
|
||||
}
|
||||
|
||||
// Name returns a string that uniquely describes the Cataloger
|
||||
func (c Cataloger) Name() string {
|
||||
@@ -34,7 +48,7 @@ func (c Cataloger) Catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Re
|
||||
var packages []pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
for _, cls := range defaultClassifiers {
|
||||
for _, cls := range c.classifiers {
|
||||
log.WithFields("classifier", cls.Class).Trace("cataloging binaries")
|
||||
newPkgs, err := catalog(resolver, cls)
|
||||
if err != nil {
|
||||
@@ -71,7 +85,7 @@ func mergePackages(target *pkg.Package, extra *pkg.Package) {
|
||||
target.Metadata = meta
|
||||
}
|
||||
|
||||
func catalog(resolver file.Resolver, cls classifier) (packages []pkg.Package, err error) {
|
||||
func catalog(resolver file.Resolver, cls Classifier) (packages []pkg.Package, err error) {
|
||||
locations, err := resolver.FilesByGlob(cls.FileGlob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -540,6 +540,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
expected: pkg.Package{
|
||||
Name: "busybox",
|
||||
Version: "3.33.3",
|
||||
PURL: "pkg:generic/busybox@3.33.3",
|
||||
Locations: locations("["), // note: busybox is a link to [
|
||||
Metadata: metadata("busybox-binary", "[", "busybox"),
|
||||
},
|
||||
@@ -715,6 +716,18 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("consul-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "positive-erlang-25.3.2.7",
|
||||
fixtureDir: "test-fixtures/classifiers/positive/erlang-25.3.2.7",
|
||||
expected: pkg.Package{
|
||||
Name: "erlang",
|
||||
Version: "25.3.2.7",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/erlang@25.3.2.7",
|
||||
Locations: locations("erlexec"),
|
||||
Metadata: metadata("erlang-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "positive-nginx-1.25.1",
|
||||
fixtureDir: "test-fixtures/classifiers/positive/nginx-1.25.1",
|
||||
@@ -751,11 +764,23 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
|
||||
Metadata: metadata("bash-binary"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "positive-openssl-3.1.4",
|
||||
fixtureDir: "test-fixtures/classifiers/positive/openssl-3.1.4",
|
||||
expected: pkg.Package{
|
||||
Name: "openssl",
|
||||
Version: "3.1.4",
|
||||
Type: "binary",
|
||||
PURL: "pkg:generic/openssl@3.1.4",
|
||||
Locations: locations("openssl"),
|
||||
Metadata: metadata("openssl-binary"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCataloger()
|
||||
c := NewCataloger(DefaultCatalogerConfig())
|
||||
|
||||
src, err := source.NewFromDirectoryPath(test.fixtureDir)
|
||||
require.NoError(t, err)
|
||||
@@ -785,6 +810,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||
expected: pkg.Package{
|
||||
Name: "busybox",
|
||||
Version: "1.35.0",
|
||||
PURL: "pkg:generic/busybox@1.35.0",
|
||||
Locations: locations("/bin/["),
|
||||
Metadata: metadata("busybox-binary", "/bin/[", "/bin/busybox"),
|
||||
},
|
||||
@@ -793,7 +819,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCataloger()
|
||||
c := NewCataloger(DefaultCatalogerConfig())
|
||||
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
|
||||
src, err := source.NewFromStereoscopeImageObject(img, test.fixtureImage, nil)
|
||||
@@ -824,7 +850,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
|
||||
c := NewCataloger()
|
||||
c := NewCataloger(DefaultCatalogerConfig())
|
||||
|
||||
src, err := source.NewFromDirectoryPath("test-fixtures/classifiers/negative")
|
||||
assert.NoError(t, err)
|
||||
@@ -837,6 +863,129 @@ func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
|
||||
assert.Equal(t, 0, len(actualResults))
|
||||
}
|
||||
|
||||
func Test_Cataloger_CustomClassifiers(t *testing.T) {
|
||||
defaultClassifers := DefaultClassifiers()
|
||||
|
||||
golangExpected := pkg.Package{
|
||||
Name: "go",
|
||||
Version: "1.14",
|
||||
PURL: "pkg:generic/go@1.14",
|
||||
Locations: locations("go"),
|
||||
Metadata: metadata("go-binary"),
|
||||
}
|
||||
customExpected := pkg.Package{
|
||||
Name: "foo",
|
||||
Version: "1.2.3",
|
||||
PURL: "pkg:generic/foo@1.2.3",
|
||||
Locations: locations("foo"),
|
||||
Metadata: metadata("foo-binary"),
|
||||
}
|
||||
fooClassifier := Classifier{
|
||||
Class: "foo-binary",
|
||||
FileGlob: "**/foo",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)foobar\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "foo",
|
||||
PURL: mustPURL("pkg:generic/foo@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config CatalogerConfig
|
||||
fixtureDir string
|
||||
expected *pkg.Package
|
||||
}{
|
||||
{
|
||||
name: "empty-negative",
|
||||
config: CatalogerConfig{
|
||||
Classifiers: []Classifier{},
|
||||
},
|
||||
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "default-positive",
|
||||
config: CatalogerConfig{
|
||||
Classifiers: defaultClassifers,
|
||||
},
|
||||
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
|
||||
expected: &golangExpected,
|
||||
},
|
||||
{
|
||||
name: "nodefault-negative",
|
||||
config: CatalogerConfig{
|
||||
Classifiers: []Classifier{fooClassifier},
|
||||
},
|
||||
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "default-extended-positive",
|
||||
config: CatalogerConfig{
|
||||
Classifiers: append(
|
||||
append([]Classifier{}, defaultClassifers...),
|
||||
fooClassifier,
|
||||
),
|
||||
},
|
||||
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
|
||||
expected: &golangExpected,
|
||||
},
|
||||
{
|
||||
name: "default-cutsom-negative",
|
||||
config: CatalogerConfig{
|
||||
|
||||
Classifiers: append(
|
||||
append([]Classifier{}, defaultClassifers...),
|
||||
Classifier{
|
||||
Class: "foo-binary",
|
||||
FileGlob: "**/foo",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)not there`),
|
||||
Package: "foo",
|
||||
PURL: mustPURL("pkg:generic/foo@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
),
|
||||
},
|
||||
fixtureDir: "test-fixtures/classifiers/positive/custom",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "default-cutsom-positive",
|
||||
config: CatalogerConfig{
|
||||
Classifiers: append(
|
||||
append([]Classifier{}, defaultClassifers...),
|
||||
fooClassifier,
|
||||
),
|
||||
},
|
||||
fixtureDir: "test-fixtures/classifiers/positive/custom",
|
||||
expected: &customExpected,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := NewCataloger(test.config)
|
||||
|
||||
src, err := source.NewFromDirectoryPath(test.fixtureDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
resolver, err := src.FileResolver(source.SquashedScope)
|
||||
require.NoError(t, err)
|
||||
|
||||
packages, _, err := c.Catalog(resolver)
|
||||
require.NoError(t, err)
|
||||
|
||||
if test.expected == nil {
|
||||
assert.Equal(t, 0, len(packages))
|
||||
} else {
|
||||
require.Len(t, packages, 1)
|
||||
|
||||
assertPackagesAreEqual(t, *test.expected, packages[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func locations(locations ...string) file.LocationSet {
|
||||
var locs []file.Location
|
||||
for _, s := range locations {
|
||||
@@ -929,8 +1078,8 @@ func assertPackagesAreEqual(t *testing.T, expected pkg.Package, p pkg.Package) {
|
||||
cmp.Transformer("Locations", func(l file.LocationSet) []file.Location {
|
||||
return l.ToSlice()
|
||||
}),
|
||||
cmpopts.IgnoreUnexported(pkg.Package{}, file.Location{}),
|
||||
cmpopts.IgnoreFields(pkg.Package{}, "CPEs", "FoundBy", "MetadataType", "Type"),
|
||||
cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationData{}),
|
||||
cmpopts.IgnoreFields(pkg.Package{}, "CPEs", "FoundBy", "Type", "Locations", "Licenses"),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -993,7 +1142,7 @@ func (p *panicyResolver) FileMetadataByLocation(_ file.Location) (file.Metadata,
|
||||
var _ file.Resolver = (*panicyResolver)(nil)
|
||||
|
||||
func Test_Cataloger_ResilientToErrors(t *testing.T) {
|
||||
c := NewCataloger()
|
||||
c := NewCataloger(DefaultCatalogerConfig())
|
||||
|
||||
resolver := &panicyResolver{}
|
||||
_, _, err := c.Catalog(resolver)
|
||||
|
||||
@@ -20,11 +20,9 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
|
||||
)
|
||||
|
||||
var emptyPURL = packageurl.PackageURL{}
|
||||
|
||||
// classifier is a generic package classifier that can be used to match a package definition
|
||||
// to a file that meets the given content criteria of the evidenceMatcher.
|
||||
type classifier struct {
|
||||
// Classifier is a generic package classifier that can be used to match a package definition
|
||||
// to a file that meets the given content criteria of the EvidenceMatcher.
|
||||
type Classifier struct {
|
||||
Class string
|
||||
|
||||
// FileGlob is a selector to narrow down file inspection using the **/glob* syntax
|
||||
@@ -32,19 +30,13 @@ type classifier struct {
|
||||
|
||||
// EvidenceMatcher is what will be used to match against the file in the source
|
||||
// location. If the matcher returns a package, the file will be considered a candidate.
|
||||
EvidenceMatcher evidenceMatcher
|
||||
EvidenceMatcher EvidenceMatcher
|
||||
|
||||
// Information below is used to specify the Package information when returned
|
||||
|
||||
// Package is the name to use for the package
|
||||
Package string
|
||||
|
||||
// Language is the language to classify this package as
|
||||
Language pkg.Language
|
||||
|
||||
// Type is the package type to use for the package
|
||||
Type pkg.Type
|
||||
|
||||
// PURL is the Package URL to use when generating a package
|
||||
PURL packageurl.PackageURL
|
||||
|
||||
@@ -52,11 +44,11 @@ type classifier struct {
|
||||
CPEs []cpe.CPE
|
||||
}
|
||||
|
||||
// evidenceMatcher is a function called to catalog Packages that match some sort of evidence
|
||||
type evidenceMatcher func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error)
|
||||
// EvidenceMatcher is a function called to catalog Packages that match some sort of evidence
|
||||
type EvidenceMatcher func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error)
|
||||
|
||||
func evidenceMatchers(matchers ...evidenceMatcher) evidenceMatcher {
|
||||
return func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error) {
|
||||
func evidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher {
|
||||
return func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error) {
|
||||
for _, matcher := range matchers {
|
||||
match, err := matcher(resolver, classifier, location)
|
||||
if err != nil {
|
||||
@@ -70,9 +62,9 @@ func evidenceMatchers(matchers ...evidenceMatcher) evidenceMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate string) evidenceMatcher {
|
||||
func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate string) EvidenceMatcher {
|
||||
pat := regexp.MustCompile(fileNamePattern)
|
||||
return func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error) {
|
||||
return func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error) {
|
||||
if !pat.MatchString(location.RealPath) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -116,9 +108,9 @@ func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate stri
|
||||
}
|
||||
}
|
||||
|
||||
func fileContentsVersionMatcher(pattern string) evidenceMatcher {
|
||||
func FileContentsVersionMatcher(pattern string) EvidenceMatcher {
|
||||
pat := regexp.MustCompile(pattern)
|
||||
return func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error) {
|
||||
return func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error) {
|
||||
contents, err := getContents(resolver, location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get read contents for file: %w", err)
|
||||
@@ -136,9 +128,9 @@ func fileContentsVersionMatcher(pattern string) evidenceMatcher {
|
||||
}
|
||||
|
||||
//nolint:gocognit
|
||||
func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher evidenceMatcher) evidenceMatcher {
|
||||
func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher EvidenceMatcher) EvidenceMatcher {
|
||||
pat := regexp.MustCompile(sharedLibraryPattern)
|
||||
return func(resolver file.Resolver, classifier classifier, location file.Location) (packages []pkg.Package, _ error) {
|
||||
return func(resolver file.Resolver, classifier Classifier, location file.Location) (packages []pkg.Package, _ error) {
|
||||
libs, err := sharedLibraries(resolver, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -13,16 +13,16 @@ func Test_ClassifierCPEs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
classifier classifier
|
||||
classifier Classifier
|
||||
cpes []string
|
||||
}{
|
||||
{
|
||||
name: "no CPEs",
|
||||
fixture: "test-fixtures/version.txt",
|
||||
classifier: classifier{
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
CPEs: []cpe.CPE{},
|
||||
},
|
||||
cpes: nil,
|
||||
@@ -30,10 +30,10 @@ func Test_ClassifierCPEs(t *testing.T) {
|
||||
{
|
||||
name: "one CPE",
|
||||
fixture: "test-fixtures/version.txt",
|
||||
classifier: classifier{
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
@@ -45,10 +45,10 @@ func Test_ClassifierCPEs(t *testing.T) {
|
||||
{
|
||||
name: "multiple CPEs",
|
||||
fixture: "test-fixtures/version.txt",
|
||||
classifier: classifier{
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"),
|
||||
|
||||
@@ -2,309 +2,351 @@ package binary
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var defaultClassifiers = []classifier{
|
||||
{
|
||||
Class: "python-binary",
|
||||
FileGlob: "**/python*",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
// try to find version information from libpython shared libraries
|
||||
sharedLibraryLookup(
|
||||
`^libpython[0-9]+(?:\.[0-9]+)+[a-z]?\.so.*$`,
|
||||
libpythonMatcher),
|
||||
// check for version information in the binary
|
||||
fileNameTemplateVersionMatcher(
|
||||
`(?:.*/|^)python(?P<version>[0-9]+(?:\.[0-9]+)+)$`,
|
||||
pythonVersionTemplate),
|
||||
),
|
||||
Package: "python",
|
||||
PURL: mustPURL("pkg:generic/python@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"),
|
||||
//nolint:funlen
|
||||
func DefaultClassifiers() []Classifier {
|
||||
return []Classifier{
|
||||
{
|
||||
Class: "python-binary",
|
||||
FileGlob: "**/python*",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
// try to find version information from libpython shared libraries
|
||||
sharedLibraryLookup(
|
||||
`^libpython[0-9]+(?:\.[0-9]+)+[a-z]?\.so.*$`,
|
||||
libpythonMatcher),
|
||||
// check for version information in the binary
|
||||
fileNameTemplateVersionMatcher(
|
||||
`(?:.*/|^)python(?P<version>[0-9]+(?:\.[0-9]+)+)$`,
|
||||
pythonVersionTemplate),
|
||||
),
|
||||
Package: "python",
|
||||
PURL: mustPURL("pkg:generic/python@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Class: "python-binary-lib",
|
||||
FileGlob: "**/libpython*.so*",
|
||||
EvidenceMatcher: libpythonMatcher,
|
||||
Package: "python",
|
||||
PURL: mustPURL("pkg:generic/python@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"),
|
||||
{
|
||||
Class: "python-binary-lib",
|
||||
FileGlob: "**/libpython*.so*",
|
||||
EvidenceMatcher: libpythonMatcher,
|
||||
Package: "python",
|
||||
PURL: mustPURL("pkg:generic/python@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:python_software_foundation:python:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:python:python:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Class: "go-binary",
|
||||
FileGlob: "**/go",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)\x00`),
|
||||
Package: "go",
|
||||
PURL: mustPURL("pkg:generic/go@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "helm",
|
||||
FileGlob: "**/helm",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)\x00v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`),
|
||||
Package: "helm",
|
||||
PURL: mustPURL("pkg:golang/helm.sh/helm@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:helm:helm:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "redis-binary",
|
||||
FileGlob: "**/redis-server",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
fileContentsVersionMatcher(`(?s)payload %5.*?(?P<version>\d.\d\.\d\d*)[a-z0-9]{12,15}-[0-9]{19}`),
|
||||
fileContentsVersionMatcher(`(?s)\x00(?P<version>\d.\d\.\d\d*)[a-z0-9]{12}-[0-9]{19}\x00.*?payload %5`),
|
||||
),
|
||||
Package: "redis",
|
||||
PURL: mustPURL("pkg:generic/redis@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:redislabs:redis:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "java-binary-openjdk",
|
||||
FileGlob: "**/java",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL]
|
||||
// [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL]
|
||||
`(?m)\x00openjdk\x00java\x00(?P<release>[0-9]+[.0-9]*)\x00(?P<version>[0-9]+[^\x00]+)\x00`),
|
||||
Package: "java",
|
||||
PURL: mustPURL("pkg:generic/java@version"),
|
||||
// TODO the updates might need to be part of the CPE, like: 1.8.0:update152
|
||||
CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "java-binary-ibm",
|
||||
FileGlob: "**/java",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// [NUL]java[NUL]1.8[NUL][NUL][NUL][NUL]1.8.0-foreman_2022_09_22_15_30-b00[NUL]
|
||||
`(?m)\x00java\x00(?P<release>[0-9]+[.0-9]+)\x00{4}(?P<version>[0-9]+[-._a-zA-Z0-9]+)\x00`),
|
||||
Package: "java",
|
||||
PURL: mustPURL("pkg:generic/java@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ibm:java:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "java-binary-oracle",
|
||||
FileGlob: "**/java",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// [NUL]19.0.1+10-21[NUL]
|
||||
`(?m)\x00(?P<version>[0-9]+[.0-9]+[+][-0-9]+)\x00`),
|
||||
Package: "java",
|
||||
PURL: mustPURL("pkg:generic/java@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:oracle:jre:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "nodejs-binary",
|
||||
FileGlob: "**/node",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)node\.js\/v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "node",
|
||||
Language: pkg.JavaScript,
|
||||
PURL: mustPURL("pkg:generic/node@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:nodejs:node.js:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "go-binary-hint",
|
||||
FileGlob: "**/VERSION",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)`),
|
||||
Package: "go",
|
||||
PURL: mustPURL("pkg:generic/go@version"),
|
||||
},
|
||||
{
|
||||
Class: "busybox-binary",
|
||||
FileGlob: "**/busybox",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)BusyBox\s+v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "busybox",
|
||||
CPEs: singleCPE("cpe:2.3:a:busybox:busybox:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "haproxy-binary",
|
||||
FileGlob: "**/haproxy",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
fileContentsVersionMatcher(`(?m)HA-Proxy version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
fileContentsVersionMatcher(`(?m)(?P<version>[0-9]+\.[0-9]+\.[0-9]+)-[0-9a-zA-Z]{7}.+HAProxy version`),
|
||||
),
|
||||
Package: "haproxy",
|
||||
PURL: mustPURL("pkg:generic/haproxy@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:haproxy:haproxy:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "perl-binary",
|
||||
FileGlob: "**/perl",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "perl",
|
||||
PURL: mustPURL("pkg:generic/perl@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "php-cli-binary",
|
||||
FileGlob: "**/php*",
|
||||
EvidenceMatcher: fileNameTemplateVersionMatcher(
|
||||
`(.*/|^)php[0-9]*$`,
|
||||
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
|
||||
Package: "php-cli",
|
||||
PURL: mustPURL("pkg:generic/php-cli@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "php-fpm-binary",
|
||||
FileGlob: "**/php-fpm*",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
|
||||
Package: "php-fpm",
|
||||
PURL: mustPURL("pkg:generic/php-fpm@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "php-apache-binary",
|
||||
FileGlob: "**/libphp*.so",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
|
||||
Package: "libphp",
|
||||
PURL: mustPURL("pkg:generic/php@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "httpd-binary",
|
||||
FileGlob: "**/httpd",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)Apache\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "httpd",
|
||||
PURL: mustPURL("pkg:generic/httpd@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "memcached-binary",
|
||||
FileGlob: "**/memcached",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
`(?m)memcached\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "memcached",
|
||||
PURL: mustPURL("pkg:generic/memcached@version"),
|
||||
},
|
||||
{
|
||||
Class: "traefik-binary",
|
||||
FileGlob: "**/traefik",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// [NUL]v1.7.34[NUL]
|
||||
// [NUL]2.9.6[NUL]
|
||||
`(?m)\x00v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-alpha[0-9]|-beta[0-9]|-rc[0-9])?)\x00`),
|
||||
Package: "traefik",
|
||||
PURL: mustPURL("pkg:generic/traefik@version"),
|
||||
},
|
||||
{
|
||||
Class: "postgresql-binary",
|
||||
FileGlob: "**/postgres",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// [NUL]PostgreSQL 15beta4
|
||||
// [NUL]PostgreSQL 15.1
|
||||
// [NUL]PostgreSQL 9.6.24
|
||||
// ?PostgreSQL 9.5alpha1
|
||||
`(?m)(\x00|\?)PostgreSQL (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
|
||||
Package: "postgresql",
|
||||
PURL: mustPURL("pkg:generic/postgresql@version"),
|
||||
},
|
||||
{
|
||||
Class: "mysql-binary",
|
||||
FileGlob: "**/mysql",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// ../../mysql-8.0.34
|
||||
// /mysql-5.6.51/bld/client
|
||||
`(?m).*/mysql-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
|
||||
Package: "mysql",
|
||||
PURL: mustPURL("pkg:generic/mysql@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "mariadb-binary",
|
||||
FileGlob: "**/mariadb",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// 10.6.15-MariaDB
|
||||
`(?m)(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)-MariaDB`),
|
||||
Package: "mariadb",
|
||||
PURL: mustPURL("pkg:generic/mariadb@version"),
|
||||
},
|
||||
{
|
||||
Class: "rust-standard-library-linux",
|
||||
FileGlob: "**/libstd-????????????????.so",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// clang LLVM (rustc version 1.48.0 (7eac88abb 2020-11-16))
|
||||
`(?m)(\x00)clang LLVM \(rustc version (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
|
||||
Package: "rust",
|
||||
PURL: mustPURL("pkg:generic/rust@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:rust-lang:rust:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "rust-standard-library-macos",
|
||||
FileGlob: "**/libstd-????????????????.dylib",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// c 1.48.0 (7eac88abb 2020-11-16)
|
||||
`(?m)c (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
|
||||
Package: "rust",
|
||||
PURL: mustPURL("pkg:generic/rust@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:rust-lang:rust:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "ruby-binary",
|
||||
FileGlob: "**/ruby",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
rubyMatcher,
|
||||
sharedLibraryLookup(
|
||||
// try to find version information from libruby shared libraries
|
||||
`^libruby\.so.*$`,
|
||||
rubyMatcher),
|
||||
),
|
||||
Package: "ruby",
|
||||
PURL: mustPURL("pkg:generic/ruby@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ruby-lang:ruby:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "consul-binary",
|
||||
FileGlob: "**/consul",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// NOTE: This is brittle and may not work for past or future versions
|
||||
`CONSUL_VERSION: (?P<version>\d+\.\d+\.\d+)`,
|
||||
),
|
||||
Package: "consul",
|
||||
PURL: mustPURL("pkg:golang/github.com/hashicorp/consul@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:hashicorp:consul:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "nginx-binary",
|
||||
FileGlob: "**/nginx",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// [NUL]nginx version: nginx/1.25.1 - fetches '1.25.1'
|
||||
// [NUL]nginx version: openresty/1.21.4.1 - fetches '1.21.4' as this is the nginx version part
|
||||
`(?m)(\x00|\?)nginx version: [^\/]+\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(?:\+\d+)?(?:-\d+)?)`,
|
||||
),
|
||||
Package: "nginx",
|
||||
PURL: mustPURL("pkg:generic/nginx@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*"),
|
||||
{
|
||||
Class: "go-binary",
|
||||
FileGlob: "**/go",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)\x00`),
|
||||
Package: "go",
|
||||
PURL: mustPURL("pkg:generic/go@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:golang:go:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Class: "bash-binary",
|
||||
FileGlob: "**/bash",
|
||||
EvidenceMatcher: fileContentsVersionMatcher(
|
||||
// @(#)Bash version 5.2.15(1) release GNU
|
||||
// @(#)Bash version 5.2.0(1) alpha GNU
|
||||
// @(#)Bash version 5.2.0(1) beta GNU
|
||||
// @(#)Bash version 5.2.0(1) rc4 GNU
|
||||
`(?m)@\(#\)Bash version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)\([0-9]\) [a-z0-9]+ GNU`,
|
||||
),
|
||||
Package: "bash",
|
||||
PURL: mustPURL("pkg:generic/bash@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:gnu:bash:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "julia-binary",
|
||||
FileGlob: "**/libjulia-internal.so",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)__init__\x00(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00verify`),
|
||||
Package: "julia",
|
||||
PURL: mustPURL("pkg:generic/julia@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:julialang:julia:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "helm",
|
||||
FileGlob: "**/helm",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)\x00v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`),
|
||||
Package: "helm",
|
||||
PURL: mustPURL("pkg:golang/helm.sh/helm@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:helm:helm:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "redis-binary",
|
||||
FileGlob: "**/redis-server",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
FileContentsVersionMatcher(`(?s)payload %5.*?(?P<version>\d.\d\.\d\d*)[a-z0-9]{12,15}-[0-9]{19}`),
|
||||
FileContentsVersionMatcher(`(?s)\x00(?P<version>\d.\d\.\d\d*)[a-z0-9]{12}-[0-9]{19}\x00.*?payload %5`),
|
||||
),
|
||||
Package: "redis",
|
||||
PURL: mustPURL("pkg:generic/redis@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:redislabs:redis:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "java-binary-openjdk",
|
||||
FileGlob: "**/java",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL]
|
||||
// [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL]
|
||||
`(?m)\x00openjdk\x00java\x00(?P<release>[0-9]+[.0-9]*)\x00(?P<version>[0-9]+[^\x00]+)\x00`),
|
||||
Package: "java",
|
||||
PURL: mustPURL("pkg:generic/java@version"),
|
||||
// TODO the updates might need to be part of the CPE, like: 1.8.0:update152
|
||||
CPEs: singleCPE("cpe:2.3:a:oracle:openjdk:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "java-binary-ibm",
|
||||
FileGlob: "**/java",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// [NUL]java[NUL]1.8[NUL][NUL][NUL][NUL]1.8.0-foreman_2022_09_22_15_30-b00[NUL]
|
||||
`(?m)\x00java\x00(?P<release>[0-9]+[.0-9]+)\x00{4}(?P<version>[0-9]+[-._a-zA-Z0-9]+)\x00`),
|
||||
Package: "java",
|
||||
PURL: mustPURL("pkg:generic/java@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ibm:java:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "java-binary-oracle",
|
||||
FileGlob: "**/java",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// [NUL]19.0.1+10-21[NUL]
|
||||
`(?m)\x00(?P<version>[0-9]+[.0-9]+[+][-0-9]+)\x00`),
|
||||
Package: "java",
|
||||
PURL: mustPURL("pkg:generic/java@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:oracle:jre:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "nodejs-binary",
|
||||
FileGlob: "**/node",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)node\.js\/v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "node",
|
||||
PURL: mustPURL("pkg:generic/node@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:nodejs:node.js:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "go-binary-hint",
|
||||
FileGlob: "**/VERSION",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)`),
|
||||
Package: "go",
|
||||
PURL: mustPURL("pkg:generic/go@version"),
|
||||
},
|
||||
{
|
||||
Class: "busybox-binary",
|
||||
FileGlob: "**/busybox",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)BusyBox\s+v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "busybox",
|
||||
PURL: mustPURL("pkg:generic/busybox@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:busybox:busybox:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "haproxy-binary",
|
||||
FileGlob: "**/haproxy",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
FileContentsVersionMatcher(`(?m)HA-Proxy version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
FileContentsVersionMatcher(`(?m)(?P<version>[0-9]+\.[0-9]+\.[0-9]+)-[0-9a-zA-Z]{7}.+HAProxy version`),
|
||||
),
|
||||
Package: "haproxy",
|
||||
PURL: mustPURL("pkg:generic/haproxy@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:haproxy:haproxy:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "perl-binary",
|
||||
FileGlob: "**/perl",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "perl",
|
||||
PURL: mustPURL("pkg:generic/perl@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "php-cli-binary",
|
||||
FileGlob: "**/php*",
|
||||
EvidenceMatcher: fileNameTemplateVersionMatcher(
|
||||
`(.*/|^)php[0-9]*$`,
|
||||
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
|
||||
Package: "php-cli",
|
||||
PURL: mustPURL("pkg:generic/php-cli@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "php-fpm-binary",
|
||||
FileGlob: "**/php-fpm*",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
|
||||
Package: "php-fpm",
|
||||
PURL: mustPURL("pkg:generic/php-fpm@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "php-apache-binary",
|
||||
FileGlob: "**/libphp*.so",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
|
||||
Package: "libphp",
|
||||
PURL: mustPURL("pkg:generic/php@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "php-composer-binary",
|
||||
FileGlob: "**/composer*",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)'pretty_version'\s*=>\s*'(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)'`),
|
||||
Package: "composer",
|
||||
PURL: mustPURL("pkg:generic/composer@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:getcomposer:composer:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "httpd-binary",
|
||||
FileGlob: "**/httpd",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)Apache\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "httpd",
|
||||
PURL: mustPURL("pkg:generic/httpd@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "memcached-binary",
|
||||
FileGlob: "**/memcached",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
`(?m)memcached\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
|
||||
Package: "memcached",
|
||||
PURL: mustPURL("pkg:generic/memcached@version"),
|
||||
},
|
||||
{
|
||||
Class: "traefik-binary",
|
||||
FileGlob: "**/traefik",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// [NUL]v1.7.34[NUL]
|
||||
// [NUL]2.9.6[NUL]
|
||||
`(?m)\x00v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-alpha[0-9]|-beta[0-9]|-rc[0-9])?)\x00`),
|
||||
Package: "traefik",
|
||||
PURL: mustPURL("pkg:generic/traefik@version"),
|
||||
},
|
||||
{
|
||||
Class: "postgresql-binary",
|
||||
FileGlob: "**/postgres",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// [NUL]PostgreSQL 15beta4
|
||||
// [NUL]PostgreSQL 15.1
|
||||
// [NUL]PostgreSQL 9.6.24
|
||||
// ?PostgreSQL 9.5alpha1
|
||||
`(?m)(\x00|\?)PostgreSQL (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
|
||||
Package: "postgresql",
|
||||
PURL: mustPURL("pkg:generic/postgresql@version"),
|
||||
},
|
||||
{
|
||||
Class: "mysql-binary",
|
||||
FileGlob: "**/mysql",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// ../../mysql-8.0.34
|
||||
// /mysql-5.6.51/bld/client
|
||||
`(?m).*/mysql-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
|
||||
Package: "mysql",
|
||||
PURL: mustPURL("pkg:generic/mysql@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:oracle:mysql:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "mariadb-binary",
|
||||
FileGlob: "**/mariadb",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// 10.6.15-MariaDB
|
||||
`(?m)(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)-MariaDB`),
|
||||
Package: "mariadb",
|
||||
PURL: mustPURL("pkg:generic/mariadb@version"),
|
||||
},
|
||||
{
|
||||
Class: "rust-standard-library-linux",
|
||||
FileGlob: "**/libstd-????????????????.so",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// clang LLVM (rustc version 1.48.0 (7eac88abb 2020-11-16))
|
||||
`(?m)(\x00)clang LLVM \(rustc version (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
|
||||
Package: "rust",
|
||||
PURL: mustPURL("pkg:generic/rust@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:rust-lang:rust:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "rust-standard-library-macos",
|
||||
FileGlob: "**/libstd-????????????????.dylib",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// c 1.48.0 (7eac88abb 2020-11-16)
|
||||
`(?m)c (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
|
||||
Package: "rust",
|
||||
PURL: mustPURL("pkg:generic/rust@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:rust-lang:rust:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "ruby-binary",
|
||||
FileGlob: "**/ruby",
|
||||
EvidenceMatcher: evidenceMatchers(
|
||||
rubyMatcher,
|
||||
sharedLibraryLookup(
|
||||
// try to find version information from libruby shared libraries
|
||||
`^libruby\.so.*$`,
|
||||
rubyMatcher),
|
||||
),
|
||||
Package: "ruby",
|
||||
PURL: mustPURL("pkg:generic/ruby@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:ruby-lang:ruby:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "erlang-binary",
|
||||
FileGlob: "**/erlexec",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/
|
||||
`(?m)\<artificial\>\x00/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+?)/erts/`,
|
||||
),
|
||||
Package: "erlang",
|
||||
PURL: mustPURL("pkg:generic/erlang@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:erlang:erlang\\/otp:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "consul-binary",
|
||||
FileGlob: "**/consul",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// NOTE: This is brittle and may not work for past or future versions
|
||||
`CONSUL_VERSION: (?P<version>\d+\.\d+\.\d+)`,
|
||||
),
|
||||
Package: "consul",
|
||||
PURL: mustPURL("pkg:golang/github.com/hashicorp/consul@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:hashicorp:consul:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "nginx-binary",
|
||||
FileGlob: "**/nginx",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// [NUL]nginx version: nginx/1.25.1 - fetches '1.25.1'
|
||||
// [NUL]nginx version: openresty/1.21.4.1 - fetches '1.21.4' as this is the nginx version part
|
||||
`(?m)(\x00|\?)nginx version: [^\/]+\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(?:\+\d+)?(?:-\d+)?)`,
|
||||
),
|
||||
Package: "nginx",
|
||||
PURL: mustPURL("pkg:generic/nginx@version"),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*"),
|
||||
cpe.Must("cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Class: "bash-binary",
|
||||
FileGlob: "**/bash",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// @(#)Bash version 5.2.15(1) release GNU
|
||||
// @(#)Bash version 5.2.0(1) alpha GNU
|
||||
// @(#)Bash version 5.2.0(1) beta GNU
|
||||
// @(#)Bash version 5.2.0(1) rc4 GNU
|
||||
`(?m)@\(#\)Bash version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)\([0-9]\) [a-z0-9]+ GNU`,
|
||||
),
|
||||
Package: "bash",
|
||||
PURL: mustPURL("pkg:generic/bash@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:gnu:bash:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
{
|
||||
Class: "openssl-binary",
|
||||
FileGlob: "**/openssl",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(
|
||||
// [NUL]OpenSSL 3.1.4'
|
||||
`\x00OpenSSL (?P<version>[0-9]+\.[0-9]+\.[0-9]+(-alpha[0-9]|-beta[0-9]|-rc[0-9])?)`,
|
||||
),
|
||||
Package: "openssl",
|
||||
PURL: mustPURL("pkg:generic/openssl@version"),
|
||||
CPEs: singleCPE("cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// in both binaries and shared libraries, the version pattern is [NUL]3.11.2[NUL]
|
||||
@@ -315,7 +357,7 @@ var libpythonMatcher = fileNameTemplateVersionMatcher(
|
||||
pythonVersionTemplate,
|
||||
)
|
||||
|
||||
var rubyMatcher = fileContentsVersionMatcher(
|
||||
var rubyMatcher = FileContentsVersionMatcher(
|
||||
// ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux]
|
||||
// ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [x86_64-linux]
|
||||
`(?m)ruby (?P<version>[0-9]+\.[0-9]+\.[0-9]+(p[0-9]+)?) `)
|
||||
|
||||
@@ -3,12 +3,15 @@ package binary
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func newPackage(classifier classifier, location file.Location, matchMetadata map[string]string) *pkg.Package {
|
||||
var emptyPURL = packageurl.PackageURL{}
|
||||
|
||||
func newPackage(classifier Classifier, location file.Location, matchMetadata map[string]string) *pkg.Package {
|
||||
version, ok := matchMetadata["version"]
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -42,20 +45,12 @@ func newPackage(classifier classifier, location file.Location, matchMetadata map
|
||||
},
|
||||
}
|
||||
|
||||
if classifier.Type != "" {
|
||||
p.Type = classifier.Type
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(classifier.PURL, emptyPURL) {
|
||||
purl := classifier.PURL
|
||||
purl.Version = version
|
||||
p.PURL = purl.ToString()
|
||||
}
|
||||
|
||||
if classifier.Language != "" {
|
||||
p.Language = classifier.Language
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return &p
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
blah
|
||||
foobar 1.2.3
|
||||
baz
|
||||
Binary file not shown.
Binary file not shown.
Executable → Regular
Binary file not shown.
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -6,14 +6,14 @@ import (
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -35,21 +35,6 @@ type catalogResult struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
// newMonitor creates a new Monitor object and publishes the object on the bus as a PackageCatalogerStarted event.
|
||||
func newMonitor() (*progress.Manual, *progress.Manual) {
|
||||
filesProcessed := progress.Manual{}
|
||||
packagesDiscovered := progress.Manual{}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.PackageCatalogerStarted,
|
||||
Value: Monitor{
|
||||
FilesProcessed: progress.Monitorable(&filesProcessed),
|
||||
PackagesDiscovered: progress.Monitorable(&packagesDiscovered),
|
||||
},
|
||||
})
|
||||
return &filesProcessed, &packagesDiscovered
|
||||
}
|
||||
|
||||
func runCataloger(cataloger pkg.Cataloger, resolver file.Resolver) (catalogerResult *catalogResult, err error) {
|
||||
// handle individual cataloger panics
|
||||
defer func() {
|
||||
@@ -101,6 +86,7 @@ func runCataloger(cataloger pkg.Cataloger, resolver file.Resolver) (catalogerRes
|
||||
}
|
||||
catalogerResult.Packages = append(catalogerResult.Packages, p)
|
||||
}
|
||||
|
||||
catalogerResult.Relationships = append(catalogerResult.Relationships, relationships...)
|
||||
log.WithFields("cataloger", cataloger.Name()).Trace("cataloging complete")
|
||||
return catalogerResult, err
|
||||
@@ -116,9 +102,7 @@ func Catalog(resolver file.Resolver, _ *linux.Release, parallelism int, cataloge
|
||||
catalog := pkg.NewCollection()
|
||||
var allRelationships []artifact.Relationship
|
||||
|
||||
filesProcessed, packagesDiscovered := newMonitor()
|
||||
defer filesProcessed.SetCompleted()
|
||||
defer packagesDiscovered.SetCompleted()
|
||||
prog := monitorPackageCatalogingTask()
|
||||
|
||||
// perform analysis, accumulating errors for each failed analysis
|
||||
var errs error
|
||||
@@ -131,10 +115,11 @@ func Catalog(resolver file.Resolver, _ *linux.Release, parallelism int, cataloge
|
||||
|
||||
jobs := make(chan pkg.Cataloger, nCatalogers)
|
||||
results := make(chan *catalogResult, nCatalogers)
|
||||
discoveredPackages := make(chan int64, nCatalogers)
|
||||
|
||||
waitGroup := sync.WaitGroup{}
|
||||
|
||||
var totalPackagesDiscovered int64
|
||||
|
||||
for i := 0; i < parallelism; i++ {
|
||||
waitGroup.Add(1)
|
||||
|
||||
@@ -148,20 +133,16 @@ func Catalog(resolver file.Resolver, _ *linux.Release, parallelism int, cataloge
|
||||
// ensure we set the error to be aggregated
|
||||
result.Error = err
|
||||
|
||||
discoveredPackages <- result.Discovered
|
||||
prog.Add(result.Discovered)
|
||||
totalPackagesDiscovered += result.Discovered
|
||||
count := humanize.Comma(totalPackagesDiscovered)
|
||||
prog.AtomicStage.Set(fmt.Sprintf("%s packages", count))
|
||||
|
||||
results <- result
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// dynamically show updated discovered package status
|
||||
go func() {
|
||||
for discovered := range discoveredPackages {
|
||||
packagesDiscovered.Add(discovered)
|
||||
}
|
||||
}()
|
||||
|
||||
// Enqueue the jobs
|
||||
for _, cataloger := range catalogers {
|
||||
jobs <- cataloger
|
||||
@@ -171,7 +152,6 @@ func Catalog(resolver file.Resolver, _ *linux.Release, parallelism int, cataloge
|
||||
// Wait for the jobs to finish
|
||||
waitGroup.Wait()
|
||||
close(results)
|
||||
close(discoveredPackages)
|
||||
|
||||
// collect the results
|
||||
for result := range results {
|
||||
@@ -186,6 +166,12 @@ func Catalog(resolver file.Resolver, _ *linux.Release, parallelism int, cataloge
|
||||
|
||||
allRelationships = append(allRelationships, pkg.NewRelationships(catalog)...)
|
||||
|
||||
if errs != nil {
|
||||
prog.SetError(errs)
|
||||
} else {
|
||||
prog.SetCompleted()
|
||||
}
|
||||
|
||||
return catalog, allRelationships, errs
|
||||
}
|
||||
|
||||
@@ -228,3 +214,16 @@ func packageFileOwnershipRelationships(p pkg.Package, resolver file.PathResolver
|
||||
}
|
||||
return relationships, nil
|
||||
}
|
||||
|
||||
func monitorPackageCatalogingTask() *monitor.CatalogerTaskProgress {
|
||||
info := monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Catalog packages",
|
||||
WhileRunning: "Cataloging packages",
|
||||
OnSuccess: "Cataloged packages",
|
||||
},
|
||||
HideOnSuccess: false,
|
||||
}
|
||||
|
||||
return bus.StartCatalogerTask(info, -1, "")
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger {
|
||||
return filterCatalogers([]pkg.Cataloger{
|
||||
arch.NewDBCataloger(),
|
||||
alpine.NewDBCataloger(),
|
||||
binary.NewCataloger(),
|
||||
binary.NewCataloger(cfg.Binary),
|
||||
cpp.NewConanInfoCataloger(),
|
||||
debian.NewDBCataloger(),
|
||||
dotnet.NewDotnetPortableExecutableCataloger(),
|
||||
@@ -68,7 +68,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
|
||||
return filterCatalogers([]pkg.Cataloger{
|
||||
arch.NewDBCataloger(),
|
||||
alpine.NewDBCataloger(),
|
||||
binary.NewCataloger(),
|
||||
binary.NewCataloger(cfg.Binary),
|
||||
cpp.NewConanCataloger(),
|
||||
dart.NewPubspecLockCataloger(),
|
||||
debian.NewDBCataloger(),
|
||||
@@ -83,9 +83,9 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
|
||||
haskell.NewHackageCataloger(),
|
||||
java.NewArchiveCataloger(cfg.JavaConfig()),
|
||||
java.NewGradleLockfileCataloger(),
|
||||
java.NewPomCataloger(),
|
||||
java.NewPomCataloger(cfg.JavaConfig()),
|
||||
java.NewNativeImageCataloger(),
|
||||
javascript.NewLockCataloger(),
|
||||
javascript.NewLockCataloger(cfg.Javascript),
|
||||
nix.NewStoreCataloger(),
|
||||
php.NewComposerLockCataloger(),
|
||||
gentoo.NewPortageCataloger(),
|
||||
@@ -107,7 +107,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
|
||||
return filterCatalogers([]pkg.Cataloger{
|
||||
arch.NewDBCataloger(),
|
||||
alpine.NewDBCataloger(),
|
||||
binary.NewCataloger(),
|
||||
binary.NewCataloger(cfg.Binary),
|
||||
cpp.NewConanCataloger(),
|
||||
dart.NewPubspecLockCataloger(),
|
||||
debian.NewDBCataloger(),
|
||||
@@ -122,9 +122,9 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
|
||||
haskell.NewHackageCataloger(),
|
||||
java.NewArchiveCataloger(cfg.JavaConfig()),
|
||||
java.NewGradleLockfileCataloger(),
|
||||
java.NewPomCataloger(),
|
||||
java.NewPomCataloger(cfg.JavaConfig()),
|
||||
java.NewNativeImageCataloger(),
|
||||
javascript.NewLockCataloger(),
|
||||
javascript.NewLockCataloger(cfg.Javascript),
|
||||
javascript.NewPackageCataloger(),
|
||||
kernel.NewLinuxKernelCataloger(cfg.LinuxKernel),
|
||||
nix.NewStoreCataloger(),
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"dependency-track": "cpe:2.3:a:jenkins:owasp_dependency-track:*:*:*:*:*:jenkins:*:*",
|
||||
"deploy": "cpe:2.3:a:jenkins:deploy:*:*:*:*:*:jenkins:*:*",
|
||||
"description-column": "cpe:2.3:a:jenkins:description_column:*:*:*:*:*:jenkins:*:*",
|
||||
"dingding-json-pusher": "cpe:2.3:a:jenkins:dingding_json_pusher:*:*:*:*:*:jenkins:*:*",
|
||||
"dingding-notifications": "cpe:2.3:a:jenkins:dingding:*:*:*:*:*:jenkins:*:*",
|
||||
"docker": "cpe:2.3:a:jenkins:docker:*:*:*:*:*:jenkins:*:*",
|
||||
"dry": "cpe:2.3:a:jenkins:dry:*:*:*:*:*:jenkins:*:*",
|
||||
@@ -115,6 +116,7 @@
|
||||
"google-kubernetes-engine": "cpe:2.3:a:jenkins:google_kubernetes_engine:*:*:*:*:*:jenkins:*:*",
|
||||
"google-oauth": "cpe:2.3:a:jenkins:google_oauth_credentials:*:*:*:*:*:jenkins:*:*",
|
||||
"google-play-android-publisher": "cpe:2.3:a:jenkins:google-play-android-publisher:*:*:*:*:*:jenkins:*:*",
|
||||
"gradle": "cpe:2.3:a:jenkins:gradle:*:*:*:*:*:jenkins:*:*",
|
||||
"groovy": "cpe:2.3:a:jenkins:groovy:*:*:*:*:*:jenkins:*:*",
|
||||
"harvest": "cpe:2.3:a:jenkins:harvest_scm:*:*:*:*:*:jenkins:*:*",
|
||||
"hipchat": "cpe:2.3:a:atlassian:hipchat:*:*:*:*:*:jenkins:*:*",
|
||||
@@ -173,6 +175,7 @@
|
||||
"nerrvana": "cpe:2.3:a:jenkins:nerrvana:*:*:*:*:*:jenkins:*:*",
|
||||
"nested-view": "cpe:2.3:a:jenkins:nested_view:*:*:*:*:*:jenkins:*:*",
|
||||
"netsparker-cloud-scan": "cpe:2.3:a:jenkins:netsparker_cloud_scan:*:*:*:*:*:jenkins:*:*",
|
||||
"nexus-platform": "cpe:2.3:a:jenkins:nexus_platform:*:*:*:*:*:jenkins:*:*",
|
||||
"ontrack": "cpe:2.3:a:jenkins:ontrack:*:*:*:*:*:jenkins:*:*",
|
||||
"openshift-pipeline": "cpe:2.3:a:jenkins:openshift_pipeline:*:*:*:*:*:jenkins:*:*",
|
||||
"ownership": "cpe:2.3:a:jenkins:job_and_node_ownership:*:*:*:*:*:jenkins:*:*",
|
||||
@@ -288,6 +291,7 @@
|
||||
"@awsui/components-react": "cpe:2.3:a:amazon:awsui\\/components-react:*:*:*:*:*:node.js:*:*",
|
||||
"@azure/ms-rest-nodeauth": "cpe:2.3:a:microsoft:ms-rest-nodeauth:*:*:*:*:*:node.js:*:*",
|
||||
"@backstage/plugin-auth-backend": "cpe:2.3:a:linuxfoundation:auth_backend:*:*:*:*:*:node.js:*:*",
|
||||
"@evershop/evershop": "cpe:2.3:a:evershop:evershop:*:*:*:*:*:node.js:*:*",
|
||||
"@fastly/js-compute": "cpe:2.3:a:fastly:js-compute:*:*:*:*:*:node.js:*:*",
|
||||
"@firebase/util": "cpe:2.3:a:google:firebase\\/util:*:*:*:*:*:node.js:*:*",
|
||||
"@github/paste-markdown": "cpe:2.3:a:paste-markdown_project:paste-markdown:*:*:*:*:*:node.js:*:*",
|
||||
@@ -295,9 +299,12 @@
|
||||
"@nuxt/devalue": "cpe:2.3:a:nuxtjs:\\@nuxt\\/devalue:*:*:*:*:*:node.js:*:*",
|
||||
"@progfay/scrapbox-parser": "cpe:2.3:a:scrapbox-parser_project:scrapbox-parser:*:*:*:*:*:node.js:*:*",
|
||||
"@rkesters/gnuplot": "cpe:2.3:a:gnuplot_project:gnuplot:*:*:*:*:*:node.js:*:*",
|
||||
"@sap/xssec": "cpe:2.3:a:sap:\\@sap\\/xssec:*:*:*:*:*:node.js:*:*",
|
||||
"@sentry/astro": "cpe:2.3:a:sentry:astro:*:*:*:*:*:node.js:*:*",
|
||||
"@solana/pay": "cpe:2.3:a:solanalabs:pay:*:*:*:*:*:*:*:*",
|
||||
"@strikeentco/set": "cpe:2.3:a:set_project:set:*:*:*:*:*:node.js:*:*",
|
||||
"@thi.ng/egf": "cpe:2.3:a:\\@thi.ng\\/egf_project:\\@thi.ng\\/egf:*:*:*:*:*:node.js:*:*",
|
||||
"@vue/devtools": "cpe:2.3:a:vuejs:devtools:*:*:*:*:*:node.js:*:*",
|
||||
"@zxcvbn-ts/core": "cpe:2.3:a:zxcvbn-ts_project:zxcvbn-ts:*:*:*:*:*:node.js:*:*",
|
||||
"Proto": "cpe:2.3:a:proto_project:proto:*:*:*:*:*:node.js:*:*",
|
||||
"Templ8": "cpe:2.3:a:templ8_project:templ8:*:*:*:*:*:node.js:*:*",
|
||||
@@ -389,6 +396,7 @@
|
||||
"conf-cfg-ini": "cpe:2.3:a:conf-cfg-ini_project:conf-cfg-ini:*:*:*:*:*:node.js:*:*",
|
||||
"confinit": "cpe:2.3:a:confinit_project:confinit:*:*:*:*:*:node.js:*:*",
|
||||
"confucious": "cpe:2.3:a:realseriousgames:confucious:*:*:*:*:*:node.js:*:*",
|
||||
"connect-multiparty": "cpe:2.3:a:connect-multiparty_project:connect-multiparty:*:*:*:*:*:*:*:*",
|
||||
"connect-pg-simple": "cpe:2.3:a:connect-pg-simple_project:connect-pg-simple:*:*:*:*:*:node.js:*:*",
|
||||
"connection-tester": "cpe:2.3:a:connection-tester_project:connection-tester:*:*:*:*:*:node.js:*:*",
|
||||
"console-io": "cpe:2.3:a:console-io_project:console-io:*:*:*:*:*:node.js:*:*",
|
||||
@@ -1084,11 +1092,15 @@
|
||||
"reqmgr2": "cpe:2.3:a:reqmgr2_project:reqmgr2:*:*:*:*:*:python:*:*",
|
||||
"reqmon": "cpe:2.3:a:reqmon_project:reqmon:*:*:*:*:*:python:*:*",
|
||||
"requests-xml": "cpe:2.3:a:requests-xml_project:requests-xml:*:*:*:*:*:python:*:*",
|
||||
"rondolu-yt-concate": "cpe:2.3:a:rondolu-yt-concate_project:rondolu-yt-concate:*:*:*:*:*:pypi:*:*",
|
||||
"rope": "cpe:2.3:a:rope_project:rope:*:*:*:*:*:python:*:*",
|
||||
"rply": "cpe:2.3:a:rply_project:rply:*:*:*:*:*:*:*:*",
|
||||
"rsa": "cpe:2.3:a:python:rsa:*:*:*:*:*:python:*:*",
|
||||
"ruamel.yaml": "cpe:2.3:a:ruamel.yaml_project:ruamel.yaml:*:*:*:*:*:*:*:*",
|
||||
"sap-xssec": "cpe:2.3:a:sap:sap-xssec:*:*:*:*:*:python:*:*",
|
||||
"scoptrial": "cpe:2.3:a:scoptrial_project:scoptrial:*:*:*:*:*:pypi:*:*",
|
||||
"simiki": "cpe:2.3:a:simiki_project:simiki:*:*:*:*:*:*:*:*",
|
||||
"sixfab-power-python-api": "cpe:2.3:a:sixfab-tool_project:sixfab-tool:*:*:*:*:*:pypi:*:*",
|
||||
"slashify": "cpe:2.3:a:google:slashify:*:*:*:*:*:node.js:*:*",
|
||||
"sopel-plugins.channelmgnt": "cpe:2.3:a:mirahezebots:channelmgnt:*:*:*:*:*:sopel:*:*",
|
||||
"spacy": "cpe:2.3:a:explosion:spacy:*:*:*:*:*:python:*:*",
|
||||
@@ -1219,6 +1231,7 @@
|
||||
"bzip2": "cpe:2.3:a:bzip2_project:bzip2:*:*:*:*:*:rust:*:*",
|
||||
"cache": "cpe:2.3:a:cache_project:cache:*:*:*:*:*:rust:*:*",
|
||||
"cached": "cpe:2.3:a:cached_project:cached:*:*:*:*:*:rust:*:*",
|
||||
"candid": "cpe:2.3:a:dfinity:candid:*:*:*:*:*:rust:*:*",
|
||||
"cbox": "cpe:2.3:a:cbox_project:cbox:*:*:*:*:*:rust:*:*",
|
||||
"ckb": "cpe:2.3:a:nervos:ckb:*:*:*:*:*:rust:*:*",
|
||||
"conduit-hyper": "cpe:2.3:a:conduit-hyper_project:conduit-hyper:*:*:*:*:*:rust:*:*",
|
||||
|
||||
@@ -294,7 +294,7 @@ func GetManifestFieldGroupIDs(manifest *pkg.JavaManifest, fields []string) (grou
|
||||
}
|
||||
|
||||
func cleanGroupID(groupID string) string {
|
||||
return strings.TrimSpace(removeOSCIDirectives(groupID))
|
||||
return strings.TrimSpace(strings.Split(removeOSCIDirectives(groupID), "#")[0])
|
||||
}
|
||||
|
||||
func removeOSCIDirectives(groupID string) string {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -161,6 +161,17 @@ func Test_groupIDsFromJavaPackage(t *testing.T) {
|
||||
},
|
||||
expects: []string{"io.jenkins-ci.plugin.thing"},
|
||||
},
|
||||
{
|
||||
name: "clean # suffixes",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
GroupID: "org.elasticsearch.plugin#parent-join;6.8.15",
|
||||
},
|
||||
},
|
||||
},
|
||||
expects: []string{"org.elasticsearch.plugin"},
|
||||
},
|
||||
{
|
||||
name: "from artifactID",
|
||||
pkg: pkg.Package{
|
||||
|
||||
@@ -2,8 +2,10 @@ package cataloger
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
)
|
||||
@@ -15,6 +17,8 @@ type Config struct {
|
||||
LinuxKernel kernel.LinuxKernelCatalogerConfig
|
||||
Python python.CatalogerConfig
|
||||
Java java.ArchiveCatalogerConfig
|
||||
Javascript javascript.CatalogerConfig
|
||||
Binary binary.CatalogerConfig
|
||||
Catalogers []string
|
||||
Parallelism int
|
||||
ExcludeBinaryOverlapByOwnership bool
|
||||
@@ -27,6 +31,8 @@ func DefaultConfig() Config {
|
||||
LinuxKernel: kernel.DefaultLinuxCatalogerConfig(),
|
||||
Python: python.DefaultCatalogerConfig(),
|
||||
Java: java.DefaultArchiveCatalogerConfig(),
|
||||
Javascript: javascript.DefaultCatalogerConfig(),
|
||||
Binary: binary.DefaultCatalogerConfig(),
|
||||
ExcludeBinaryOverlapByOwnership: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,23 +58,14 @@ func parseDotnetPortableExecutable(_ file.Resolver, _ *generic.Environment, f fi
|
||||
func buildDotNetPackage(versionResources map[string]string, f file.LocationReadCloser) (dnpkg pkg.Package, err error) {
|
||||
name := findName(versionResources)
|
||||
if name == "" {
|
||||
return dnpkg, fmt.Errorf("unable to find FileDescription, or ProductName in PE file: %s", f.RealPath)
|
||||
return dnpkg, fmt.Errorf("unable to find PE name in file: %s", f.RealPath)
|
||||
}
|
||||
|
||||
version := findVersion(versionResources)
|
||||
if strings.TrimSpace(version) == "" {
|
||||
return dnpkg, fmt.Errorf("unable to find FileVersion in PE file: %s", f.RealPath)
|
||||
if version == "" {
|
||||
return dnpkg, fmt.Errorf("unable to find PE version in file: %s", f.RealPath)
|
||||
}
|
||||
|
||||
purl := packageurl.NewPackageURL(
|
||||
packageurl.TypeNuget, // See explanation in syft/pkg/cataloger/dotnet/package.go as to why this was chosen.
|
||||
"",
|
||||
name,
|
||||
version,
|
||||
nil,
|
||||
"",
|
||||
).ToString()
|
||||
|
||||
metadata := pkg.DotnetPortableExecutableEntry{
|
||||
AssemblyVersion: versionResources["Assembly Version"],
|
||||
LegalCopyright: versionResources["LegalCopyright"],
|
||||
@@ -91,7 +82,7 @@ func buildDotNetPackage(versionResources map[string]string, f file.LocationReadC
|
||||
Locations: file.NewLocationSet(f.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Type: pkg.DotnetPkg,
|
||||
Language: pkg.Dotnet,
|
||||
PURL: purl,
|
||||
PURL: portableExecutablePackageURL(name, version),
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
@@ -100,30 +91,128 @@ func buildDotNetPackage(versionResources map[string]string, f file.LocationReadC
|
||||
return dnpkg, nil
|
||||
}
|
||||
|
||||
func findVersion(versionResources map[string]string) string {
|
||||
for _, key := range []string{"FileVersion"} {
|
||||
if version, ok := versionResources[key]; ok {
|
||||
if strings.TrimSpace(version) == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(version)
|
||||
if len(fields) > 0 {
|
||||
return fields[0]
|
||||
}
|
||||
func portableExecutablePackageURL(name, version string) string {
|
||||
return packageurl.NewPackageURL(
|
||||
packageurl.TypeNuget, // See explanation in syft/pkg/cataloger/dotnet/package.go as to why this was chosen.
|
||||
"",
|
||||
name,
|
||||
version,
|
||||
nil,
|
||||
"",
|
||||
).ToString()
|
||||
}
|
||||
|
||||
func extractVersion(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
|
||||
out := ""
|
||||
|
||||
// some example versions are: "1, 0, 0, 0", "Release 73" or "4.7.4076.0 built by: NET472REL1LAST_B"
|
||||
// so try to split it and take the first parts that look numeric
|
||||
for i, f := range strings.Fields(version) {
|
||||
// if the output already has a number but the current segment does not have a number,
|
||||
// return what we found for the version
|
||||
if containsNumber(out) && !containsNumber(f) {
|
||||
return out
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
out = f
|
||||
} else {
|
||||
out += " " + f
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func findVersion(versionResources map[string]string) string {
|
||||
productVersion := extractVersion(versionResources["ProductVersion"])
|
||||
fileVersion := extractVersion(versionResources["FileVersion"])
|
||||
|
||||
if productVersion == "" {
|
||||
return fileVersion
|
||||
}
|
||||
|
||||
productVersionDetail := punctuationCount(productVersion)
|
||||
fileVersionDetail := punctuationCount(fileVersion)
|
||||
|
||||
if containsNumber(productVersion) && productVersionDetail >= fileVersionDetail {
|
||||
return productVersion
|
||||
}
|
||||
|
||||
if containsNumber(fileVersion) && fileVersionDetail > 0 {
|
||||
return fileVersion
|
||||
}
|
||||
|
||||
if containsNumber(productVersion) {
|
||||
return productVersion
|
||||
}
|
||||
|
||||
if containsNumber(fileVersion) {
|
||||
return fileVersion
|
||||
}
|
||||
|
||||
return productVersion
|
||||
}
|
||||
|
||||
func containsNumber(s string) bool {
|
||||
return numberRegex.MatchString(s)
|
||||
}
|
||||
|
||||
func punctuationCount(s string) int {
|
||||
return len(versionPunctuationRegex.FindAllString(s, -1))
|
||||
}
|
||||
|
||||
var (
|
||||
// spaceRegex includes nbsp (#160) considered to be a space character
|
||||
spaceRegex = regexp.MustCompile(`[\s\xa0]+`)
|
||||
numberRegex = regexp.MustCompile(`\d`)
|
||||
versionPunctuationRegex = regexp.MustCompile(`[.,]+`)
|
||||
)
|
||||
|
||||
func findName(versionResources map[string]string) string {
|
||||
// PE files found in the wild _not_ authored by Microsoft seem to use ProductName as a clear
|
||||
// identifier of the software
|
||||
nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"}
|
||||
|
||||
if isMicrosoft(versionResources) {
|
||||
// Microsoft seems to be consistent using the FileDescription, with a few that are blank and have
|
||||
// fallbacks to ProductName last, as this is often something very broad like "Microsoft Windows"
|
||||
nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"}
|
||||
}
|
||||
|
||||
for _, field := range nameFields {
|
||||
value := spaceNormalize(versionResources[field])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func findName(versionResources map[string]string) string {
|
||||
for _, key := range []string{"FileDescription", "ProductName"} {
|
||||
if name, ok := versionResources[key]; ok {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
continue
|
||||
}
|
||||
trimmed := strings.TrimSpace(name)
|
||||
return regexp.MustCompile(`[^a-zA-Z0-9.]+`).ReplaceAllString(trimmed, "")
|
||||
}
|
||||
// normalizes a string to a trimmed version with all contigous whitespace collapsed to a single space character
|
||||
func spaceNormalize(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
// ensure valid utf8 text
|
||||
value = strings.ToValidUTF8(value, "")
|
||||
// consolidate all space characters
|
||||
value = spaceRegex.ReplaceAllString(value, " ")
|
||||
// remove other non-space, non-printable characters
|
||||
value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "")
|
||||
// consolidate all space characters again in case other non-printables were in-between
|
||||
value = spaceRegex.ReplaceAllString(value, " ")
|
||||
// finally, remove any remaining surrounding whitespace
|
||||
value = strings.TrimSpace(value)
|
||||
return value
|
||||
}
|
||||
|
||||
func isMicrosoft(versionResources map[string]string) bool {
|
||||
return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") ||
|
||||
strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft")
|
||||
}
|
||||
|
||||
@@ -32,12 +32,8 @@ func TestParseDotnetPortableExecutable(t *testing.T) {
|
||||
"Assembly Version": "3.14.2.11",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "ActiveDirectoryAuthenticationLibrary",
|
||||
Version: "3.14.40721.0918",
|
||||
Locations: file.NewLocationSet(file.NewLocation("").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Type: pkg.DotnetPkg,
|
||||
Language: pkg.Dotnet,
|
||||
PURL: "pkg:nuget/ActiveDirectoryAuthenticationLibrary@3.14.40721.0918",
|
||||
Name: "Active Directory Authentication Library",
|
||||
Version: "3.14.40721.0918",
|
||||
Metadata: pkg.DotnetPortableExecutableEntry{
|
||||
AssemblyVersion: "3.14.2.11",
|
||||
LegalCopyright: "Copyright (c) Microsoft Corporation. All rights reserved.",
|
||||
@@ -52,7 +48,7 @@ func TestParseDotnetPortableExecutable(t *testing.T) {
|
||||
name: "dotnet package with malformed field and extended version",
|
||||
versionResources: map[string]string{
|
||||
"CompanyName": "Microsoft Corporation",
|
||||
"FileDescription": "äbFileVersion",
|
||||
"FileDescription": "äbFile\xa0\xa1Versi on",
|
||||
"FileVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
|
||||
"InternalName": "äbFileVersion",
|
||||
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
|
||||
@@ -61,13 +57,9 @@ func TestParseDotnetPortableExecutable(t *testing.T) {
|
||||
"ProductVersion": "4.6.25512.01 built by: dlab-DDVSOWINAGE016. Commit Hash: d0d5c7b49271cadb6d97de26d8e623e98abdc8db",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "bFileVersion",
|
||||
Name: "äbFileVersi on",
|
||||
Version: "4.6.25512.01",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Type: pkg.DotnetPkg,
|
||||
Language: pkg.Dotnet,
|
||||
PURL: "pkg:nuget/bFileVersion@4.6.25512.01",
|
||||
PURL: "pkg:nuget/%C3%A4bFileVersi%20on@4.6.25512.01",
|
||||
Metadata: pkg.DotnetPortableExecutableEntry{
|
||||
LegalCopyright: "© Microsoft Corporation. All rights reserved.",
|
||||
InternalName: "äb\x01FileVersion",
|
||||
@@ -77,16 +69,208 @@ func TestParseDotnetPortableExecutable(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "System.Data.Linq.dll",
|
||||
versionResources: map[string]string{
|
||||
"CompanyName": "Microsoft Corporation",
|
||||
"FileDescription": "System.Data.Linq.dll",
|
||||
"FileVersion": "4.7.3190.0 built by: NET472REL1LAST_C",
|
||||
"InternalName": "System.Data.Linq.dll",
|
||||
"LegalCopyright": "© Microsoft Corporation. All rights reserved.",
|
||||
"OriginalFilename": "System.Data.Linq.dll",
|
||||
"ProductName": "Microsoft® .NET Framework",
|
||||
"ProductVersion": "4.7.3190.0",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "System.Data.Linq.dll",
|
||||
Version: "4.7.3190.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "curl",
|
||||
versionResources: map[string]string{
|
||||
"CompanyName": "curl, https://curl.se/",
|
||||
"FileDescription": "The curl executable",
|
||||
"FileVersion": "8.4.0",
|
||||
"InternalName": "curl",
|
||||
"LegalCopyright": "© Daniel Stenberg, <daniel@haxx.se>.",
|
||||
"OriginalFilename": "curl.exe",
|
||||
"ProductName": "The curl executable",
|
||||
"ProductVersion": "8.4.0",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "The curl executable",
|
||||
Version: "8.4.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Prometheus",
|
||||
versionResources: map[string]string{
|
||||
"AssemblyVersion": "8.0.0.0",
|
||||
"CompanyName": "",
|
||||
"FileDescription": "",
|
||||
"FileVersion": "8.0.1",
|
||||
"InternalName": "Prometheus.AspNetCore.dll",
|
||||
"OriginalFilename": "Prometheus.AspNetCore.dll",
|
||||
"ProductName": "",
|
||||
"ProductVersion": "8.0.1",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "Prometheus.AspNetCore.dll",
|
||||
Version: "8.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Hidden Input",
|
||||
versionResources: map[string]string{
|
||||
"FileDescription": "Reads from stdin without leaking info to the terminal and outputs back to stdout",
|
||||
"FileVersion": "1, 0, 0, 0",
|
||||
"InternalName": "hiddeninput",
|
||||
"LegalCopyright": "Jordi Boggiano - 2012",
|
||||
"OriginalFilename": "hiddeninput.exe",
|
||||
"ProductName": "Hidden Input",
|
||||
"ProductVersion": "1, 0, 0, 0",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "Hidden Input",
|
||||
Version: "1, 0, 0, 0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SQLite3",
|
||||
versionResources: map[string]string{
|
||||
"CompanyName": "SQLite Development Team",
|
||||
"FileDescription": "SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine.",
|
||||
"FileVersion": "3.23.2",
|
||||
"InternalName": "sqlite3",
|
||||
"LegalCopyright": "http://www.sqlite.org/copyright.html",
|
||||
"ProductName": "SQLite",
|
||||
"ProductVersion": "3.23.2",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "SQLite",
|
||||
Version: "3.23.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Brave Browser",
|
||||
versionResources: map[string]string{
|
||||
"CompanyName": "Brave Software, Inc.",
|
||||
"FileDescription": "Brave Browser",
|
||||
"FileVersion": "80.1.7.92",
|
||||
"InternalName": "chrome_exe",
|
||||
"LegalCopyright": "Copyright 2016 The Brave Authors. All rights reserved.",
|
||||
"OriginalFilename": "chrome.exe",
|
||||
"ProductName": "Brave Browser",
|
||||
"ProductVersion": "80.1.7.92",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "Brave Browser",
|
||||
Version: "80.1.7.92",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Better product version",
|
||||
versionResources: map[string]string{
|
||||
"FileDescription": "Better version",
|
||||
"FileVersion": "80.1.7",
|
||||
"ProductVersion": "80.1.7.92",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "Better version",
|
||||
Version: "80.1.7.92",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Better file version",
|
||||
versionResources: map[string]string{
|
||||
"FileDescription": "Better version",
|
||||
"FileVersion": "80.1.7.92",
|
||||
"ProductVersion": "80.1.7",
|
||||
},
|
||||
expectedPackage: pkg.Package{
|
||||
Name: "Better version",
|
||||
Version: "80.1.7.92",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
location := file.NewLocation("")
|
||||
f := file.LocationReadCloser{
|
||||
Location: file.NewLocation(""),
|
||||
Location: location,
|
||||
}
|
||||
got, err := buildDotNetPackage(tc.versionResources, f)
|
||||
assert.NoErrorf(t, err, "failed to build package from version resources: %+v", tc.versionResources)
|
||||
|
||||
// ignore certain metadata
|
||||
if tc.expectedPackage.Metadata == nil {
|
||||
got.Metadata = nil
|
||||
}
|
||||
// set known defaults
|
||||
if tc.expectedPackage.Type == "" {
|
||||
tc.expectedPackage.Type = pkg.DotnetPkg
|
||||
}
|
||||
if tc.expectedPackage.Language == "" {
|
||||
tc.expectedPackage.Language = pkg.Dotnet
|
||||
}
|
||||
if tc.expectedPackage.PURL == "" {
|
||||
tc.expectedPackage.PURL = portableExecutablePackageURL(tc.expectedPackage.Name, tc.expectedPackage.Version)
|
||||
}
|
||||
tc.expectedPackage.Locations = file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
|
||||
|
||||
pkgtest.AssertPackagesEqual(t, tc.expectedPackage, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_extractVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "1, 0, 0, 0",
|
||||
expected: "1, 0, 0, 0",
|
||||
},
|
||||
{
|
||||
input: "Release 73",
|
||||
expected: "Release 73",
|
||||
},
|
||||
{
|
||||
input: "4.7.4076.0 built by: NET472REL1LAST_B",
|
||||
expected: "4.7.4076.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
got := extractVersion(test.input)
|
||||
assert.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_spaceNormalize(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
expected: "some spaces apart",
|
||||
input: " some spaces\n\t\t \n\rapart\n",
|
||||
},
|
||||
{
|
||||
expected: "söme ¡nvalid characters",
|
||||
input: "\rsöme \u0001¡nvalid\t characters\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.expected, func(t *testing.T) {
|
||||
got := spaceNormalize(test.input)
|
||||
assert.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package erlang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -12,6 +13,8 @@ type erlangNode struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
var errSkipComments = errors.New("")
|
||||
|
||||
func (e erlangNode) Slice() []erlangNode {
|
||||
out, ok := e.value.([]erlangNode)
|
||||
if ok {
|
||||
@@ -56,6 +59,10 @@ func parseErlang(reader io.Reader) (erlangNode, error) {
|
||||
i := 0
|
||||
for i < len(data) {
|
||||
item, err := parseErlangBlock(data, &i)
|
||||
if err == errSkipComments {
|
||||
skipWhitespace(data, &i)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return node(nil), fmt.Errorf("%w\n%s", err, printError(data, i))
|
||||
}
|
||||
@@ -141,11 +148,26 @@ func parseErlangNode(data []byte, i *int) (erlangNode, error) {
|
||||
c := data[*i]
|
||||
switch c {
|
||||
case '[', '{':
|
||||
offset := *i + 1
|
||||
skipWhitespace(data, &offset)
|
||||
c2 := data[offset]
|
||||
|
||||
// Add support for empty lists
|
||||
if (c == '[' && c2 == ']') || (c == '{' && c2 == '}') {
|
||||
*i = offset + 1
|
||||
return node(nil), nil
|
||||
}
|
||||
|
||||
return parseErlangList(data, i)
|
||||
case '"':
|
||||
fallthrough
|
||||
case '\'':
|
||||
return parseErlangString(data, i)
|
||||
case '<':
|
||||
return parseErlangAngleString(data, i)
|
||||
case '%':
|
||||
parseErlangComment(data, i)
|
||||
return node(nil), errSkipComments
|
||||
}
|
||||
|
||||
if isLiteral(c) {
|
||||
@@ -205,7 +227,7 @@ func parseErlangString(data []byte, i *int) (erlangNode, error) {
|
||||
buf.WriteByte(c)
|
||||
*i++
|
||||
}
|
||||
return node(buf.String()), nil
|
||||
return node(nil), fmt.Errorf("unterminated string at %d", *i)
|
||||
}
|
||||
|
||||
func parseErlangList(data []byte, i *int) (erlangNode, error) {
|
||||
@@ -216,6 +238,10 @@ func parseErlangList(data []byte, i *int) (erlangNode, error) {
|
||||
for *i < len(data) {
|
||||
item, err := parseErlangNode(data, i)
|
||||
if err != nil {
|
||||
if err == errSkipComments {
|
||||
skipWhitespace(data, i)
|
||||
continue
|
||||
}
|
||||
return node(nil), err
|
||||
}
|
||||
out.value = append(out.value.([]erlangNode), item)
|
||||
@@ -225,6 +251,9 @@ func parseErlangList(data []byte, i *int) (erlangNode, error) {
|
||||
case ',':
|
||||
*i++
|
||||
continue
|
||||
case '%':
|
||||
// Starts a new comment node
|
||||
continue
|
||||
case ']', '}':
|
||||
*i++
|
||||
return out, nil
|
||||
@@ -234,3 +263,19 @@ func parseErlangList(data []byte, i *int) (erlangNode, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseErlangComment(data []byte, i *int) {
|
||||
for *i < len(data) {
|
||||
c := data[*i]
|
||||
|
||||
*i++
|
||||
|
||||
// Rest of a line is a comment. Deals with CR, LF and CR/LF
|
||||
if c == '\n' {
|
||||
break
|
||||
} else if c == '\r' && data[*i] == '\n' {
|
||||
*i++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,21 @@ func Test_parseErlang(t *testing.T) {
|
||||
{<<"bcrypt">>, <<"3418821BC17CE6E96A4A77D1A88D7485BF783E212069FACFC79510AFBFF95352">>},
|
||||
{<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]}
|
||||
].`,
|
||||
},
|
||||
{
|
||||
name: "empty list",
|
||||
content: `
|
||||
{test, [
|
||||
{with_space, [ ]},
|
||||
{without_space, []}
|
||||
]}`,
|
||||
},
|
||||
{
|
||||
name: "valid strings",
|
||||
content: `
|
||||
{strings, [
|
||||
"foo", 'bar'
|
||||
]}`,
|
||||
},
|
||||
{
|
||||
name: "invalid string content",
|
||||
@@ -46,6 +61,14 @@ func Test_parseErlang(t *testing.T) {
|
||||
{"1.2.0
|
||||
">>},
|
||||
].`,
|
||||
},
|
||||
{
|
||||
name: "string mismach",
|
||||
wantErr: require.Error,
|
||||
content: `
|
||||
{bad_string, [
|
||||
'foo"
|
||||
]}`,
|
||||
},
|
||||
{
|
||||
name: "invalid content",
|
||||
@@ -54,6 +77,25 @@ func Test_parseErlang(t *testing.T) {
|
||||
{"1.2.0"}.
|
||||
].`,
|
||||
},
|
||||
{
|
||||
name: "valid comments",
|
||||
content: `
|
||||
{ comments, [
|
||||
{ foo, bar },
|
||||
%% this is a comment
|
||||
% this is also a comment
|
||||
{ hello, 'bar' }, %%inline comment
|
||||
{ baz }
|
||||
]}`,
|
||||
},
|
||||
{
|
||||
name: "starts with a comments",
|
||||
content: `
|
||||
%% starts with comment
|
||||
{ comments, [
|
||||
{ foo, bar }
|
||||
]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event/monitor"
|
||||
@@ -31,7 +32,6 @@ import (
|
||||
type goLicenses struct {
|
||||
opts CatalogerConfig
|
||||
localModCacheResolver file.WritableResolver
|
||||
progress *monitor.CatalogerTask
|
||||
lowerLicenseFileNames *strset.Set
|
||||
}
|
||||
|
||||
@@ -39,11 +39,6 @@ func newGoLicenses(opts CatalogerConfig) goLicenses {
|
||||
return goLicenses{
|
||||
opts: opts,
|
||||
localModCacheResolver: modCacheResolver(opts.LocalModCacheDir),
|
||||
progress: &monitor.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: true,
|
||||
Title: "Downloading go mod",
|
||||
},
|
||||
lowerLicenseFileNames: strset.New(lowercaseLicenseFiles()...),
|
||||
}
|
||||
}
|
||||
@@ -123,7 +118,16 @@ func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]
|
||||
|
||||
proxies := remotesForModule(c.opts.Proxies, c.opts.NoProxy, moduleName)
|
||||
|
||||
fsys, err := getModule(c.progress, proxies, moduleName, moduleVersion)
|
||||
prog := bus.StartCatalogerTask(monitor.GenericTask{
|
||||
Title: monitor.Title{
|
||||
Default: "Download go mod",
|
||||
WhileRunning: "Downloading go mod",
|
||||
OnSuccess: "Downloaded go mod",
|
||||
},
|
||||
HideOnSuccess: true,
|
||||
}, -1, "")
|
||||
|
||||
fsys, err := getModule(prog, proxies, moduleName, moduleVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -206,7 +210,7 @@ func processCaps(s string) string {
|
||||
})
|
||||
}
|
||||
|
||||
func getModule(progress *monitor.CatalogerTask, proxies []string, moduleName, moduleVersion string) (fsys fs.FS, err error) {
|
||||
func getModule(progress *monitor.CatalogerTaskProgress, proxies []string, moduleName, moduleVersion string) (fsys fs.FS, err error) {
|
||||
for _, proxy := range proxies {
|
||||
u, _ := url.Parse(proxy)
|
||||
if proxy == "direct" {
|
||||
@@ -218,7 +222,7 @@ func getModule(progress *monitor.CatalogerTask, proxies []string, moduleName, mo
|
||||
fsys, err = getModuleProxy(progress, proxy, moduleName, moduleVersion)
|
||||
case "file":
|
||||
p := filepath.Join(u.Path, moduleName, "@v", moduleVersion)
|
||||
progress.SetValue(fmt.Sprintf("file: %s", p))
|
||||
progress.AtomicStage.Set(fmt.Sprintf("file: %s", p))
|
||||
fsys = os.DirFS(p)
|
||||
}
|
||||
if fsys != nil {
|
||||
@@ -228,9 +232,9 @@ func getModule(progress *monitor.CatalogerTask, proxies []string, moduleName, mo
|
||||
return
|
||||
}
|
||||
|
||||
func getModuleProxy(progress *monitor.CatalogerTask, proxy string, moduleName string, moduleVersion string) (out fs.FS, _ error) {
|
||||
func getModuleProxy(progress *monitor.CatalogerTaskProgress, proxy string, moduleName string, moduleVersion string) (out fs.FS, _ error) {
|
||||
u := fmt.Sprintf("%s/%s/@v/%s.zip", proxy, moduleName, moduleVersion)
|
||||
progress.SetValue(u)
|
||||
progress.AtomicStage.Set(u)
|
||||
|
||||
// get the module zip
|
||||
resp, err := http.Get(u) //nolint:gosec
|
||||
@@ -241,7 +245,7 @@ func getModuleProxy(progress *monitor.CatalogerTask, proxy string, moduleName st
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
u = fmt.Sprintf("%s/%s/@v/%s.zip", proxy, strings.ToLower(moduleName), moduleVersion)
|
||||
progress.SetValue(u)
|
||||
progress.AtomicStage.Set(u)
|
||||
|
||||
// try lowercasing it; some packages have mixed casing that really messes up the proxy
|
||||
resp, err = http.Get(u) //nolint:gosec
|
||||
@@ -284,14 +288,14 @@ func findVersionPath(f fs.FS, dir string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func getModuleRepository(progress *monitor.CatalogerTask, moduleName string, moduleVersion string) (fs.FS, error) {
|
||||
func getModuleRepository(progress *monitor.CatalogerTaskProgress, moduleName string, moduleVersion 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])
|
||||
}
|
||||
|
||||
progress.SetValue(fmt.Sprintf("git: %s", repoName))
|
||||
progress.AtomicStage.Set(fmt.Sprintf("git: %s", repoName))
|
||||
|
||||
f := memfs.New()
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
@@ -63,6 +63,9 @@ func parseStackLock(_ file.Resolver, _ *generic.Environment, reader file.Locatio
|
||||
}
|
||||
|
||||
for _, pack := range lockFile.Packages {
|
||||
if pack.Completed.Hackage == "" {
|
||||
continue
|
||||
}
|
||||
pkgName, pkgVersion, pkgHash := parseStackPackageEncoding(pack.Completed.Hackage)
|
||||
pkgs = append(
|
||||
pkgs,
|
||||
@@ -80,13 +83,20 @@ func parseStackLock(_ file.Resolver, _ *generic.Environment, reader file.Locatio
|
||||
|
||||
return pkgs, nil, nil
|
||||
}
|
||||
|
||||
func parseStackPackageEncoding(pkgEncoding string) (name, version, hash string) {
|
||||
lastDashIdx := strings.LastIndex(pkgEncoding, "-")
|
||||
if lastDashIdx == -1 {
|
||||
name = pkgEncoding
|
||||
return
|
||||
}
|
||||
name = pkgEncoding[:lastDashIdx]
|
||||
remainingEncoding := pkgEncoding[lastDashIdx+1:]
|
||||
encodingSplits := strings.Split(remainingEncoding, "@")
|
||||
version = encodingSplits[0]
|
||||
startHash, endHash := strings.Index(encodingSplits[1], ":")+1, strings.Index(encodingSplits[1], ",")
|
||||
hash = encodingSplits[1][startHash:endHash]
|
||||
if len(encodingSplits) > 1 {
|
||||
startHash, endHash := strings.Index(encodingSplits[1], ":")+1, strings.Index(encodingSplits[1], ",")
|
||||
hash = encodingSplits[1][startHash:endHash]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,6 +67,19 @@ packages:
|
||||
sha256: 557c438345de19f82bf01d676100da2a191ef06f624e7a4b90b09ac17cbb52a5
|
||||
original:
|
||||
hackage: ptr-0.16.8.2@sha256:708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac,3959
|
||||
- completed:
|
||||
commit: a5847301404583e16d55cd4d051b8e605d704fbc
|
||||
git: https://github.com/runtimeverification/haskell-backend.git
|
||||
name: kore
|
||||
pantry-tree:
|
||||
sha256: 30a502eda589be5af735b1b59760ce3e0235c0cae8961978a46b3564dd8db32b
|
||||
size: 44685
|
||||
subdir: kore
|
||||
version: 0.60.0.0
|
||||
original:
|
||||
commit: a5847301404583e16d55cd4d051b8e605d704fbc
|
||||
git: https://github.com/runtimeverification/haskell-backend.git
|
||||
subdir: kore
|
||||
snapshots:
|
||||
- completed:
|
||||
size: 618951
|
||||
|
||||
@@ -3,15 +3,9 @@ package java
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vifraa/gopom"
|
||||
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
@@ -263,19 +257,13 @@ func findLicenseFromJavaMetadata(name string, manifest *pkg.JavaManifest, versio
|
||||
if gID := groupIDFromJavaMetadata(name, pkg.JavaArchive{Manifest: manifest}); gID != "" {
|
||||
groupID = gID
|
||||
}
|
||||
pomLicenses, err := recursivelyFindLicensesFromParentPom(groupID, name, version, j.cfg)
|
||||
if err != nil {
|
||||
log.Tracef("unable to get parent pom from Maven central: %v", err)
|
||||
}
|
||||
pomLicenses := recursivelyFindLicensesFromParentPom(groupID, name, version, j.cfg)
|
||||
|
||||
if len(pomLicenses) == 0 {
|
||||
// Try removing the last part of the groupId, as sometimes it duplicates the artifactId
|
||||
packages := strings.Split(groupID, ".")
|
||||
groupID = strings.Join(packages[:len(packages)-1], ".")
|
||||
pomLicenses, err = recursivelyFindLicensesFromParentPom(groupID, name, version, j.cfg)
|
||||
if err != nil {
|
||||
log.Tracef("unable to get parent pom from Maven central: %v", err)
|
||||
}
|
||||
pomLicenses = recursivelyFindLicensesFromParentPom(groupID, name, version, j.cfg)
|
||||
}
|
||||
|
||||
if len(pomLicenses) > 0 {
|
||||
@@ -320,8 +308,18 @@ func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo() (name, versi
|
||||
if version == "" && pomProjectObject != nil {
|
||||
version = pomProjectObject.Version
|
||||
}
|
||||
if pomProjectObject != nil && j.cfg.UseNetwork {
|
||||
findPomLicenses(pomProjectObject, j.cfg)
|
||||
if j.cfg.UseNetwork {
|
||||
if pomProjectObject == nil {
|
||||
// If we have no pom.xml, check maven central using pom.properties
|
||||
parentLicenses := recursivelyFindLicensesFromParentPom(pomPropertiesObject.GroupID, pomPropertiesObject.ArtifactID, pomPropertiesObject.Version, j.cfg)
|
||||
if len(parentLicenses) > 0 {
|
||||
for _, licenseName := range parentLicenses {
|
||||
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
findPomLicenses(pomProjectObject, j.cfg)
|
||||
}
|
||||
}
|
||||
|
||||
if pomProjectObject != nil {
|
||||
@@ -341,16 +339,12 @@ func artifactIDMatchesFilename(artifactID, fileName string) bool {
|
||||
func findPomLicenses(pomProjectObject *parsedPomProject, cfg ArchiveCatalogerConfig) {
|
||||
// If we don't have any licenses until now, and if we have a parent Pom, then we'll check the parent pom in maven central for licenses.
|
||||
if pomProjectObject != nil && pomProjectObject.Parent != nil && len(pomProjectObject.Licenses) == 0 {
|
||||
parentLicenses, err := recursivelyFindLicensesFromParentPom(
|
||||
parentLicenses := recursivelyFindLicensesFromParentPom(
|
||||
pomProjectObject.Parent.GroupID,
|
||||
pomProjectObject.Parent.ArtifactID,
|
||||
pomProjectObject.Parent.Version,
|
||||
cfg)
|
||||
if err != nil {
|
||||
// We don't want to abort here as the parent pom might not exist in Maven Central, we'll just log the error
|
||||
log.Tracef("unable to get parent pom from Maven central: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(parentLicenses) > 0 {
|
||||
for _, licenseName := range parentLicenses {
|
||||
pomProjectObject.Licenses = append(pomProjectObject.Licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
|
||||
@@ -359,96 +353,6 @@ func findPomLicenses(pomProjectObject *parsedPomProject, cfg ArchiveCatalogerCon
|
||||
}
|
||||
}
|
||||
|
||||
func formatMavenPomURL(groupID, artifactID, version, mavenBaseURL string) (requestURL string, err error) {
|
||||
// groupID needs to go from maven.org -> maven/org
|
||||
urlPath := strings.Split(groupID, ".")
|
||||
artifactPom := fmt.Sprintf("%s-%s.pom", artifactID, version)
|
||||
urlPath = append(urlPath, artifactID, version, artifactPom)
|
||||
|
||||
// ex:"https://repo1.maven.org/maven2/groupID/artifactID/artifactPom
|
||||
requestURL, err = url.JoinPath(mavenBaseURL, urlPath...)
|
||||
if err != nil {
|
||||
return requestURL, fmt.Errorf("could not construct maven url: %w", err)
|
||||
}
|
||||
return requestURL, err
|
||||
}
|
||||
|
||||
func recursivelyFindLicensesFromParentPom(groupID, artifactID, version string, cfg ArchiveCatalogerConfig) ([]string, error) {
|
||||
var licenses []string
|
||||
// As there can be nested parent poms, we'll recursively check for licenses until we reach the max depth
|
||||
for i := 0; i < cfg.MaxParentRecursiveDepth; i++ {
|
||||
parentPom, err := getPomFromMavenRepo(groupID, artifactID, version, cfg.MavenBaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentLicenses := parseLicensesFromPom(parentPom)
|
||||
if len(parentLicenses) > 0 || parentPom == nil || parentPom.Parent == nil {
|
||||
licenses = parentLicenses
|
||||
break
|
||||
}
|
||||
|
||||
groupID = *parentPom.Parent.GroupID
|
||||
artifactID = *parentPom.Parent.ArtifactID
|
||||
version = *parentPom.Parent.Version
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
func getPomFromMavenRepo(groupID, artifactID, version, mavenBaseURL string) (*gopom.Project, error) {
|
||||
requestURL, err := formatMavenPomURL(groupID, artifactID, version, mavenBaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Tracef("trying to fetch parent pom from Maven central %s", requestURL)
|
||||
|
||||
mavenRequest, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to format request for Maven central: %w", err)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(mavenRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get pom from Maven central: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Errorf("unable to close body: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err)
|
||||
}
|
||||
|
||||
pom, err := decodePomXML(strings.NewReader(string(bytes)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err)
|
||||
}
|
||||
|
||||
return &pom, nil
|
||||
}
|
||||
|
||||
func parseLicensesFromPom(pom *gopom.Project) []string {
|
||||
var licenses []string
|
||||
if pom != nil && pom.Licenses != nil {
|
||||
for _, license := range *pom.Licenses {
|
||||
if license.Name != nil {
|
||||
licenses = append(licenses, *license.Name)
|
||||
} else if license.URL != nil {
|
||||
licenses = append(licenses, *license.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return licenses
|
||||
}
|
||||
|
||||
// discoverPkgsFromAllMavenFiles parses Maven POM properties/xml for a given
|
||||
// parent package, returning all listed Java packages found for each pom
|
||||
// properties discovered and potentially updating the given parentPkg with new
|
||||
@@ -693,10 +597,22 @@ func newPackageFromMavenData(pomProperties pkg.JavaPomProperties, parsedPomProje
|
||||
|
||||
var pkgPomProject *pkg.JavaPomProject
|
||||
licenses := make([]pkg.License, 0)
|
||||
if parsedPomProject != nil {
|
||||
if cfg.UseNetwork {
|
||||
|
||||
if cfg.UseNetwork {
|
||||
if parsedPomProject == nil {
|
||||
// If we have no pom.xml, check maven central using pom.properties
|
||||
parentLicenses := recursivelyFindLicensesFromParentPom(pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version, cfg)
|
||||
if len(parentLicenses) > 0 {
|
||||
for _, licenseName := range parentLicenses {
|
||||
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
findPomLicenses(parsedPomProject, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
if parsedPomProject != nil {
|
||||
pkgPomProject = parsedPomProject.JavaPomProject
|
||||
licenses = append(licenses, parsedPomProject.Licenses...)
|
||||
}
|
||||
|
||||
@@ -31,9 +31,11 @@ func NewArchiveCataloger(cfg ArchiveCatalogerConfig) *generic.Cataloger {
|
||||
|
||||
// NewPomCataloger returns a cataloger capable of parsing dependencies from a pom.xml file.
|
||||
// Pom files list dependencies that maybe not be locally installed yet.
|
||||
func NewPomCataloger() pkg.Cataloger {
|
||||
func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger {
|
||||
gap := newGenericArchiveParserAdapter(cfg)
|
||||
|
||||
return generic.NewCataloger("java-pom-cataloger").
|
||||
WithParserByGlobs(parserPomXML, "**/pom.xml")
|
||||
WithParserByGlobs(gap.parserPomXML, "**/pom.xml")
|
||||
}
|
||||
|
||||
// NewGradleLockfileCataloger returns a cataloger capable of parsing dependencies from a gradle.lockfile file.
|
||||
|
||||
@@ -89,7 +89,15 @@ func Test_POMCataloger_Globs(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, test.fixture).
|
||||
ExpectsResolverContentQueries(test.expected).
|
||||
TestCataloger(t, NewPomCataloger())
|
||||
TestCataloger(t,
|
||||
NewPomCataloger(
|
||||
ArchiveCatalogerConfig{
|
||||
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: true,
|
||||
IncludeUnindexedArchives: true,
|
||||
},
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ type ArchiveCatalogerConfig struct {
|
||||
|
||||
func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig {
|
||||
return ArchiveCatalogerConfig{
|
||||
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: true,
|
||||
IncludeUnindexedArchives: false,
|
||||
},
|
||||
ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(),
|
||||
UseNetwork: false,
|
||||
MavenBaseURL: mavenBaseURL,
|
||||
MaxParentRecursiveDepth: 5,
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package java
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vifraa/gopom"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func formatMavenPomURL(groupID, artifactID, version, mavenBaseURL string) (requestURL string, err error) {
|
||||
// groupID needs to go from maven.org -> maven/org
|
||||
urlPath := strings.Split(groupID, ".")
|
||||
artifactPom := fmt.Sprintf("%s-%s.pom", artifactID, version)
|
||||
urlPath = append(urlPath, artifactID, version, artifactPom)
|
||||
|
||||
// ex:"https://repo1.maven.org/maven2/groupID/artifactID/artifactPom
|
||||
requestURL, err = url.JoinPath(mavenBaseURL, urlPath...)
|
||||
if err != nil {
|
||||
return requestURL, fmt.Errorf("could not construct maven url: %w", err)
|
||||
}
|
||||
return requestURL, err
|
||||
}
|
||||
|
||||
// An artifact can have its version defined in a parent's DependencyManagement section
|
||||
func recursivelyFindVersionFromParentPom(groupID, artifactID, parentGroupID, parentArtifactID, parentVersion string, cfg ArchiveCatalogerConfig) string {
|
||||
// As there can be nested parent poms, we'll recursively check for the version until we reach the max depth
|
||||
for i := 0; i < cfg.MaxParentRecursiveDepth; i++ {
|
||||
parentPom, err := getPomFromMavenRepo(parentGroupID, parentArtifactID, parentVersion, cfg.MavenBaseURL)
|
||||
if err != nil {
|
||||
// We don't want to abort here as the parent pom might not exist in Maven Central, we'll just log the error
|
||||
log.Tracef("unable to get parent pom from Maven central: %v", err)
|
||||
break
|
||||
}
|
||||
if parentPom != nil && parentPom.DependencyManagement != nil {
|
||||
for _, dependency := range *parentPom.DependencyManagement.Dependencies {
|
||||
if groupID == *dependency.GroupID && artifactID == *dependency.ArtifactID && dependency.Version != nil {
|
||||
return *dependency.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
if parentPom == nil || parentPom.Parent == nil {
|
||||
break
|
||||
}
|
||||
parentGroupID = *parentPom.Parent.GroupID
|
||||
parentArtifactID = *parentPom.Parent.ArtifactID
|
||||
parentVersion = *parentPom.Parent.Version
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func recursivelyFindLicensesFromParentPom(groupID, artifactID, version string, cfg ArchiveCatalogerConfig) []string {
|
||||
var licenses []string
|
||||
// As there can be nested parent poms, we'll recursively check for licenses until we reach the max depth
|
||||
for i := 0; i < cfg.MaxParentRecursiveDepth; i++ {
|
||||
parentPom, err := getPomFromMavenRepo(groupID, artifactID, version, cfg.MavenBaseURL)
|
||||
if err != nil {
|
||||
// We don't want to abort here as the parent pom might not exist in Maven Central, we'll just log the error
|
||||
log.Tracef("unable to get parent pom from Maven central: %v", err)
|
||||
return []string{}
|
||||
}
|
||||
parentLicenses := parseLicensesFromPom(parentPom)
|
||||
if len(parentLicenses) > 0 || parentPom == nil || parentPom.Parent == nil {
|
||||
licenses = parentLicenses
|
||||
break
|
||||
}
|
||||
|
||||
groupID = *parentPom.Parent.GroupID
|
||||
artifactID = *parentPom.Parent.ArtifactID
|
||||
version = *parentPom.Parent.Version
|
||||
}
|
||||
|
||||
return licenses
|
||||
}
|
||||
|
||||
func getPomFromMavenRepo(groupID, artifactID, version, mavenBaseURL string) (*gopom.Project, error) {
|
||||
requestURL, err := formatMavenPomURL(groupID, artifactID, version, mavenBaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Tracef("trying to fetch parent pom from Maven central %s", requestURL)
|
||||
|
||||
mavenRequest, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to format request for Maven central: %w", err)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(mavenRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get pom from Maven central: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Errorf("unable to close body: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err)
|
||||
}
|
||||
|
||||
pom, err := decodePomXML(strings.NewReader(string(bytes)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err)
|
||||
}
|
||||
|
||||
return &pom, nil
|
||||
}
|
||||
|
||||
func parseLicensesFromPom(pom *gopom.Project) []string {
|
||||
var licenses []string
|
||||
if pom != nil && pom.Licenses != nil {
|
||||
for _, license := range *pom.Licenses {
|
||||
if license.Name != nil {
|
||||
licenses = append(licenses, *license.Name)
|
||||
} else if license.URL != nil {
|
||||
licenses = append(licenses, *license.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return licenses
|
||||
}
|
||||
@@ -153,6 +153,22 @@ func extractNameFromApacheMavenBundlePlugin(manifest *pkg.JavaManifest) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractNameFromArchiveFilename(a archiveFilename) string {
|
||||
if strings.Contains(a.name, ".") {
|
||||
// special case: this *might* be a group id + artifact id. By convention artifact ids do not have "." in them;
|
||||
// however, there are some specific exceptions like with the artifacts under
|
||||
// https://repo1.maven.org/maven2/org/eclipse/platform/
|
||||
if strings.HasPrefix(a.name, "org.eclipse.") {
|
||||
return a.name
|
||||
}
|
||||
|
||||
fields := strings.Split(a.name, ".")
|
||||
return fields[len(fields)-1]
|
||||
}
|
||||
|
||||
return a.name
|
||||
}
|
||||
|
||||
func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
|
||||
name := extractNameFromApacheMavenBundlePlugin(manifest)
|
||||
if name != "" {
|
||||
@@ -160,13 +176,9 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
|
||||
}
|
||||
|
||||
// the filename tends to be the next-best reference for the package name
|
||||
if filenameObj.name != "" {
|
||||
if strings.Contains(filenameObj.name, ".") {
|
||||
// special case: this *might* be a group id + artifact id. By convention artifact ids do not have "." in them.
|
||||
fields := strings.Split(filenameObj.name, ".")
|
||||
return fields[len(fields)-1]
|
||||
}
|
||||
return filenameObj.name
|
||||
name = extractNameFromArchiveFilename(filenameObj)
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
|
||||
// remaining fields in the manifest is a bit of a free-for-all depending on the build tooling used and package maintainer preferences
|
||||
|
||||
@@ -209,6 +209,16 @@ func TestSelectName(t *testing.T) {
|
||||
archive: newJavaArchiveFilename("/something/com.atlassian.gadgets.atlassian-gadgets-api.jar"),
|
||||
expected: "atlassian-gadgets-api",
|
||||
},
|
||||
{
|
||||
desc: "Skip stripping groupId prefix from archive filename for org.eclipse",
|
||||
manifest: pkg.JavaManifest{
|
||||
Main: map[string]string{
|
||||
"Automatic-Module-Name": "org.eclipse.ant.core",
|
||||
},
|
||||
},
|
||||
archive: newJavaArchiveFilename("/something/org.eclipse.ant.core-3.7.0.jar"),
|
||||
expected: "org.eclipse.ant.core",
|
||||
},
|
||||
{
|
||||
// example: pkg:maven/com.google.oauth-client/google-oauth-client@1.25.0
|
||||
desc: "skip Apache Maven Bundle Plugin logic if symbolic name is same as vendor id",
|
||||
|
||||
@@ -24,7 +24,7 @@ const pomXMLGlob = "*pom.xml"
|
||||
|
||||
var propertyMatcher = regexp.MustCompile("[$][{][^}]+[}]")
|
||||
|
||||
func parserPomXML(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
func (gap genericArchiveParserAdapter) parserPomXML(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
pom, err := decodePomXML(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -36,6 +36,7 @@ func parserPomXML(_ file.Resolver, _ *generic.Environment, reader file.LocationR
|
||||
p := newPackageFromPom(
|
||||
pom,
|
||||
dep,
|
||||
gap.cfg,
|
||||
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||
)
|
||||
if p.Name == "" {
|
||||
@@ -97,7 +98,7 @@ func newPomProject(path string, p gopom.Project, location file.Location) *parsed
|
||||
}
|
||||
}
|
||||
|
||||
func newPackageFromPom(pom gopom.Project, dep gopom.Dependency, locations ...file.Location) pkg.Package {
|
||||
func newPackageFromPom(pom gopom.Project, dep gopom.Dependency, cfg ArchiveCatalogerConfig, locations ...file.Location) pkg.Package {
|
||||
m := pkg.JavaArchive{
|
||||
PomProperties: &pkg.JavaPomProperties{
|
||||
GroupID: resolveProperty(pom, dep.GroupID, "groupId"),
|
||||
@@ -109,10 +110,32 @@ func newPackageFromPom(pom gopom.Project, dep gopom.Dependency, locations ...fil
|
||||
name := safeString(dep.ArtifactID)
|
||||
version := resolveProperty(pom, dep.Version, "version")
|
||||
|
||||
licenses := make([]pkg.License, 0)
|
||||
if cfg.UseNetwork {
|
||||
if version == "" {
|
||||
// If we have no version then let's try to get it from a parent pom DependencyManagement section
|
||||
version = recursivelyFindVersionFromParentPom(*dep.GroupID, *dep.ArtifactID, *pom.Parent.GroupID, *pom.Parent.ArtifactID, *pom.Parent.Version, cfg)
|
||||
}
|
||||
if version != "" {
|
||||
parentLicenses := recursivelyFindLicensesFromParentPom(
|
||||
m.PomProperties.GroupID,
|
||||
m.PomProperties.ArtifactID,
|
||||
version,
|
||||
cfg)
|
||||
|
||||
if len(parentLicenses) > 0 {
|
||||
for _, licenseName := range parentLicenses {
|
||||
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Locations: file.NewLocationSet(locations...),
|
||||
Licenses: pkg.NewLicenseSet(licenses...),
|
||||
PURL: packageURL(name, version, m),
|
||||
Language: pkg.Java,
|
||||
Type: pkg.JavaPkg, // TODO: should we differentiate between packages from jar/war/zip versus packages from a pom.xml that were not installed yet?
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vifraa/gopom"
|
||||
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -61,7 +62,15 @@ func Test_parserPomXML(t *testing.T) {
|
||||
for i := range test.expected {
|
||||
test.expected[i].Locations.Add(file.NewLocation(test.input))
|
||||
}
|
||||
pkgtest.TestFileParser(t, test.input, parserPomXML, test.expected, nil)
|
||||
|
||||
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{
|
||||
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: true,
|
||||
IncludeUnindexedArchives: true,
|
||||
},
|
||||
})
|
||||
|
||||
pkgtest.TestFileParser(t, test.input, gap.parserPomXML, test.expected, nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -276,7 +285,14 @@ func Test_parseCommonsTextPomXMLProject(t *testing.T) {
|
||||
for i := range test.expected {
|
||||
test.expected[i].Locations.Add(file.NewLocation(test.input))
|
||||
}
|
||||
pkgtest.TestFileParser(t, test.input, parserPomXML, test.expected, nil)
|
||||
|
||||
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{
|
||||
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
|
||||
IncludeIndexedArchives: true,
|
||||
IncludeUnindexedArchives: true,
|
||||
},
|
||||
})
|
||||
pkgtest.TestFileParser(t, test.input, gap.parserPomXML, test.expected, nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@ func NewPackageCataloger() pkg.Cataloger {
|
||||
}
|
||||
|
||||
// NewLockCataloger returns a new cataloger object for NPM (and NPM-adjacent, such as yarn) lock files.
|
||||
func NewLockCataloger() pkg.Cataloger {
|
||||
func NewLockCataloger(cfg CatalogerConfig) pkg.Cataloger {
|
||||
yarnLockAdapter := newGenericYarnLockAdapter(cfg)
|
||||
return generic.NewCataloger("javascript-lock-cataloger").
|
||||
WithParserByGlobs(parsePackageLock, "**/package-lock.json").
|
||||
WithParserByGlobs(parseYarnLock, "**/yarn.lock").
|
||||
WithParserByGlobs(yarnLockAdapter.parseYarnLock, "**/yarn.lock").
|
||||
WithParserByGlobs(parsePnpmLock, "**/pnpm-lock.yaml")
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func Test_JavascriptCataloger(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, "test-fixtures/pkg-lock").
|
||||
Expects(expectedPkgs, nil).
|
||||
TestCataloger(t, NewLockCataloger())
|
||||
TestCataloger(t, NewLockCataloger(CatalogerConfig{}))
|
||||
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ func Test_LockCataloger_Globs(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, test.fixture).
|
||||
ExpectsResolverContentQueries(test.expected).
|
||||
TestCataloger(t, NewLockCataloger())
|
||||
TestCataloger(t, NewLockCataloger(CatalogerConfig{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user