mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
refactor: Remove experimental Anchore Enterprise upload functionality (#1257)
This commit is contained in:
parent
d89e320dcd
commit
780e1c310c
22
README.md
22
README.md
@ -610,28 +610,6 @@ log:
|
||||
# location to write the log file (default is not to have a log file)
|
||||
# same as SYFT_LOG_FILE env var
|
||||
file: ""
|
||||
|
||||
# uploading package SBOM is exposed through the packages subcommand
|
||||
anchore:
|
||||
# (feature-preview) the Anchore Enterprise Host or URL to upload results to (supported on Enterprise 3.0+)
|
||||
# same as -H ; SYFT_ANCHORE_HOST env var
|
||||
host: ""
|
||||
|
||||
# (feature-preview) the path after the host to the Anchore External API (supported on Enterprise 3.0+)
|
||||
# same as SYFT_ANCHORE_PATH env var
|
||||
path: ""
|
||||
|
||||
# (feature-preview) the username to authenticate against Anchore Enterprise (supported on Enterprise 3.0+)
|
||||
# same as -u ; SYFT_ANCHORE_USERNAME env var
|
||||
username: ""
|
||||
|
||||
# (feature-preview) the password to authenticate against Anchore Enterprise (supported on Enterprise 3.0+)
|
||||
# same as -p ; SYFT_ANCHORE_PASSWORD env var
|
||||
password: ""
|
||||
|
||||
# (feature-preview) path to dockerfile to be uploaded with the syft results to Anchore Enterprise (supported on Enterprise 3.0+)
|
||||
# same as -d ; SYFT_ANCHORE_DOCKERFILE env var
|
||||
dockerfile: ""
|
||||
```
|
||||
|
||||
### Adding an SBOM to an image as an attestation using Syft
|
||||
|
||||
@ -19,13 +19,7 @@ type PackagesOptions struct {
|
||||
OutputTemplatePath string
|
||||
File string
|
||||
Platform string
|
||||
Host string
|
||||
Username string
|
||||
Password string
|
||||
Dockerfile string
|
||||
Exclude []string
|
||||
OverwriteExistingImage bool
|
||||
ImportTimeout uint
|
||||
Catalogers []string
|
||||
}
|
||||
|
||||
@ -47,30 +41,12 @@ func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
||||
cmd.Flags().StringVarP(&o.Platform, "platform", "", "",
|
||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
||||
|
||||
cmd.Flags().StringVarP(&o.Host, "host", "H", "",
|
||||
"the hostname or URL of the Anchore Enterprise instance to upload to")
|
||||
|
||||
cmd.Flags().StringVarP(&o.Username, "username", "u", "",
|
||||
"the username to authenticate against Anchore Enterprise")
|
||||
|
||||
cmd.Flags().StringVarP(&o.Password, "password", "p", "",
|
||||
"the password to authenticate against Anchore Enterprise")
|
||||
|
||||
cmd.Flags().StringVarP(&o.Dockerfile, "dockerfile", "d", "",
|
||||
"include dockerfile for upload to Anchore Enterprise")
|
||||
|
||||
cmd.Flags().StringArrayVarP(&o.Exclude, "exclude", "", nil,
|
||||
"exclude paths from being scanned using a glob expression")
|
||||
|
||||
cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil,
|
||||
"enable one or more package catalogers")
|
||||
|
||||
cmd.Flags().BoolVarP(&o.OverwriteExistingImage, "overwrite-existing-image", "", false,
|
||||
"overwrite an existing image during the upload to Anchore Enterprise")
|
||||
|
||||
cmd.Flags().UintVarP(&o.ImportTimeout, "import-timeout", "", 30,
|
||||
"set a timeout duration (in seconds) for the upload to Anchore Enterprise")
|
||||
|
||||
return bindPackageConfigOptions(cmd.Flags(), v)
|
||||
}
|
||||
|
||||
@ -105,31 +81,5 @@ func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := v.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("anchore.username", flags.Lookup("username")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("anchore.password", flags.Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("anchore.dockerfile", flags.Lookup("dockerfile")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("anchore.overwrite-existing-image", flags.Lookup("overwrite-existing-image")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.BindPFlag("anchore.import-timeout", flags.Lookup("import-timeout")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -3,8 +3,6 @@ package packages
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
@ -12,7 +10,6 @@ import (
|
||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/anchore"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/config"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@ -88,13 +85,6 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-
|
||||
errs <- fmt.Errorf("no SBOM produced for %q", si.UserInput)
|
||||
}
|
||||
|
||||
if app.Anchore.Host != "" {
|
||||
if err := runPackageSbomUpload(src, *s, app); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.Exit,
|
||||
Value: func() error { return writer.Write(*s) },
|
||||
@ -144,55 +134,6 @@ func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []art
|
||||
return relationships
|
||||
}
|
||||
|
||||
func runPackageSbomUpload(src *source.Source, s sbom.SBOM, app *config.Application) error {
|
||||
log.Infof("uploading results to %s", app.Anchore.Host)
|
||||
|
||||
if src.Metadata.Scheme != source.ImageScheme {
|
||||
return fmt.Errorf("unable to upload results: only images are supported")
|
||||
}
|
||||
|
||||
var dockerfileContents []byte
|
||||
if app.Anchore.Dockerfile != "" {
|
||||
if _, err := os.Stat(app.Anchore.Dockerfile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable dockerfile=%q does not exist: %w", app.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
fh, err := os.Open(app.Anchore.Dockerfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open dockerfile=%q: %w", app.Anchore.Dockerfile, err)
|
||||
}
|
||||
|
||||
dockerfileContents, err = io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read dockerfile=%q: %w", app.Anchore.Dockerfile, err)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := anchore.NewClient(anchore.Configuration{
|
||||
BaseURL: app.Anchore.Host,
|
||||
Username: app.Anchore.Username,
|
||||
Password: app.Anchore.Password,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anchore client: %w", err)
|
||||
}
|
||||
|
||||
importCfg := anchore.ImportConfig{
|
||||
ImageMetadata: src.Image.Metadata,
|
||||
SBOM: s,
|
||||
Dockerfile: dockerfileContents,
|
||||
OverwriteExistingUpload: app.Anchore.OverwriteExistingImage,
|
||||
Timeout: app.Anchore.ImportTimeout,
|
||||
}
|
||||
|
||||
if err := c.Import(context.Background(), importCfg); err != nil {
|
||||
return fmt.Errorf("failed to upload results to host=%s: %+v", app.Anchore.Host, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOutputOptions(app *config.Application) error {
|
||||
var usesTemplateOutput bool
|
||||
for _, o := range app.Outputs {
|
||||
|
||||
2
go.mod
2
go.mod
@ -8,12 +8,10 @@ require (
|
||||
github.com/acobaugh/osrelease v0.1.0
|
||||
github.com/adrg/xdg v0.3.3
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
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.20220428202044-a072fa3cb6d7
|
||||
github.com/antihax/optional v1.0.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.0.2
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/facebookincubator/nvdtools v0.1.4
|
||||
|
||||
3
go.sum
3
go.sum
@ -270,8 +270,6 @@ github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCE
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.2.3 h1:Vmodnr52Rz1mcbwn0kzMhLRKb6soizewuKXdfZiNemU=
|
||||
github.com/aliyun/credentials-go v1.2.3/go.mod h1:/KowD1cfGSLrLsH28Jr8W+xwoId0ywIy5lNzDz6O1vw=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||
@ -290,7 +288,6 @@ github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
|
||||
|
||||
@ -1,115 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
config Configuration
|
||||
client *external.APIClient
|
||||
}
|
||||
|
||||
func NewClient(cfg Configuration) (*Client, error) {
|
||||
if cfg.UserAgent == "" {
|
||||
versionInfo := version.FromBuild()
|
||||
// format: product / product-version comment
|
||||
cfg.UserAgent = fmt.Sprintf("%s / %s %s", internal.ApplicationName, versionInfo.Version, versionInfo.Platform)
|
||||
}
|
||||
|
||||
baseURL, err := prepareBaseURLForClient(cfg.BaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create client: %w", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
config: cfg,
|
||||
client: external.NewAPIClient(&external.Configuration{
|
||||
BasePath: baseURL,
|
||||
UserAgent: cfg.UserAgent,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) newRequestContext(parentContext context.Context) context.Context {
|
||||
if parentContext == nil {
|
||||
parentContext = context.Background()
|
||||
}
|
||||
return context.WithValue(
|
||||
parentContext,
|
||||
external.ContextBasicAuth,
|
||||
external.BasicAuth{
|
||||
UserName: c.config.Username,
|
||||
Password: c.config.Password,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var ErrInvalidBaseURLInput = errors.New("invalid base URL input")
|
||||
|
||||
func prepareBaseURLForClient(baseURL string) (string, error) {
|
||||
if err := checkBaseURLInput(baseURL); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
scheme, urlWithoutScheme := splitSchemeFromURL(baseURL)
|
||||
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
urlWithoutScheme = path.Clean(urlWithoutScheme)
|
||||
|
||||
const requiredSuffix = "v1"
|
||||
if path.Base(urlWithoutScheme) != requiredSuffix {
|
||||
urlWithoutScheme = path.Join(urlWithoutScheme, requiredSuffix)
|
||||
}
|
||||
|
||||
preparedBaseURL := scheme + "://" + urlWithoutScheme
|
||||
return preparedBaseURL, nil
|
||||
}
|
||||
|
||||
func checkBaseURLInput(url string) error {
|
||||
if url == "" {
|
||||
return ErrInvalidBaseURLInput
|
||||
}
|
||||
|
||||
firstCharacter := rune(url[0])
|
||||
if !(unicode.IsLetter(firstCharacter)) {
|
||||
return ErrInvalidBaseURLInput
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitSchemeFromURL(url string) (scheme, urlWithoutScheme string) {
|
||||
if hasScheme(url) {
|
||||
urlParts := strings.SplitN(url, "://", 2)
|
||||
scheme = urlParts[0]
|
||||
urlWithoutScheme = urlParts[1]
|
||||
return
|
||||
}
|
||||
|
||||
return "", url
|
||||
}
|
||||
|
||||
func hasScheme(url string) bool {
|
||||
parts := strings.Split(url, "://")
|
||||
|
||||
return len(parts) > 1
|
||||
}
|
||||
@ -1,210 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHasScheme(t *testing.T) {
|
||||
cases := []struct {
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
url: "http://localhost",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
url: "https://anchore.com:8443",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
url: "google.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
url: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.url, func(t *testing.T) {
|
||||
result := hasScheme(testCase.url)
|
||||
|
||||
if testCase.expected != result {
|
||||
t.Errorf("expected %t but got %t", testCase.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareBaseURLForClient(t *testing.T) {
|
||||
cases := []struct {
|
||||
inputURL string
|
||||
expectedURL string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
inputURL: "",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
inputURL: "localhost",
|
||||
expectedURL: "http://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/v1/",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "https://localhost/v1//",
|
||||
expectedURL: "https://localhost/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "http://something.com/platform/v1/services/anchore",
|
||||
expectedURL: "http://something.com/platform/v1/services/anchore/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "my-host:8228",
|
||||
expectedURL: "http://my-host:8228/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "v1/v1",
|
||||
expectedURL: "http://v1/v1",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
inputURL: "/v1",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
inputURL: "/imports/images",
|
||||
expectedURL: "",
|
||||
expectedErr: ErrInvalidBaseURLInput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.inputURL, func(t *testing.T) {
|
||||
resultURL, err := prepareBaseURLForClient(testCase.inputURL)
|
||||
if err != testCase.expectedErr {
|
||||
t.Errorf("expected err to be '%v' but got '%v'", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if resultURL != testCase.expectedURL {
|
||||
t.Errorf("expected URL to be '%v' but got '%v'", testCase.expectedURL, resultURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckBaseURLInput(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
input: "x",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
input: "localhost:8000",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
input: ":80",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
{
|
||||
input: "/v1",
|
||||
expected: ErrInvalidBaseURLInput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.input, func(t *testing.T) {
|
||||
resultErr := checkBaseURLInput(testCase.input)
|
||||
|
||||
if testCase.expected != resultErr {
|
||||
t.Errorf("expected err to be '%v' but got '%v'", testCase.expected, resultErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSchemeFromURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expectedScheme string
|
||||
expectedURLWithoutScheme string
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "",
|
||||
},
|
||||
{
|
||||
input: "localhost",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "localhost",
|
||||
},
|
||||
{
|
||||
input: "https://anchore.com/path",
|
||||
expectedScheme: "https",
|
||||
expectedURLWithoutScheme: "anchore.com/path",
|
||||
},
|
||||
{
|
||||
input: "tcp://host:1234",
|
||||
expectedScheme: "tcp",
|
||||
expectedURLWithoutScheme: "host:1234",
|
||||
},
|
||||
{
|
||||
input: "/hello",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "/hello",
|
||||
},
|
||||
{
|
||||
input: "://host",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "host",
|
||||
},
|
||||
{
|
||||
input: "http//localhost",
|
||||
expectedScheme: "",
|
||||
expectedURLWithoutScheme: "http//localhost",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.input, func(t *testing.T) {
|
||||
resultScheme, resultURLWithoutScheme := splitSchemeFromURL(testCase.input)
|
||||
|
||||
if testCase.expectedScheme != resultScheme {
|
||||
t.Errorf("expected scheme to be '%s' but got '%s'", testCase.expectedScheme, resultScheme)
|
||||
}
|
||||
|
||||
if testCase.expectedURLWithoutScheme != resultURLWithoutScheme {
|
||||
t.Errorf("expected urlWithoutScheme to be '%s' but got '%s'", testCase.expectedURLWithoutScheme, resultURLWithoutScheme)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,145 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/antihax/optional"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type ImportConfig struct {
|
||||
ImageMetadata image.Metadata
|
||||
SBOM sbom.SBOM
|
||||
Dockerfile []byte
|
||||
OverwriteExistingUpload bool
|
||||
Timeout uint
|
||||
}
|
||||
|
||||
func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||
stage := &progress.Stage{}
|
||||
prog := &progress.Manual{
|
||||
// this is the number of stages to expect; start + individual endpoints + stop
|
||||
Total: 6,
|
||||
}
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.ImportStarted,
|
||||
Source: source,
|
||||
Value: progress.StagedProgressable(&struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Stager: progress.Stager(stage),
|
||||
Progressable: prog,
|
||||
}),
|
||||
})
|
||||
|
||||
return stage, prog
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
stage, prog := importProgress(c.config.BaseURL)
|
||||
|
||||
timeout := time.Duration(cfg.Timeout) * time.Second
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
authedCtx := c.newRequestContext(ctxWithTimeout)
|
||||
|
||||
stage.Current = "starting session"
|
||||
startOperation, createResponse, err := c.client.ImportsApi.CreateOperation(authedCtx)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
}
|
||||
return fmt.Errorf("unable to start import session: %w: %s", err, detail)
|
||||
}
|
||||
defer createResponse.Body.Close()
|
||||
|
||||
prog.N++
|
||||
sessionID := startOperation.Uuid
|
||||
|
||||
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Package SBOM: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawManifest, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Manifest: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawConfig, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Config: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
dockerfileDigest, err := importDockerfile(authedCtx, c.client.ImportsApi, sessionID, cfg.Dockerfile, stage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to import Dockerfile: %w", err)
|
||||
}
|
||||
prog.N++
|
||||
|
||||
stage.Current = "finalizing"
|
||||
imageModel := addImageModel(cfg.ImageMetadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID)
|
||||
opts := external.AddImageOpts{
|
||||
Force: optional.NewBool(cfg.OverwriteExistingUpload),
|
||||
}
|
||||
|
||||
_, addResponse, err := c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
|
||||
if err != nil {
|
||||
var detail = "no details given"
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
detail = string(openAPIErr.Body())
|
||||
}
|
||||
return fmt.Errorf("unable to complete import session=%q: %w: %s", sessionID, err, detail)
|
||||
}
|
||||
defer addResponse.Body.Close()
|
||||
|
||||
prog.N++
|
||||
|
||||
stage.Current = ""
|
||||
prog.SetCompleted()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addImageModel(imageMetadata image.Metadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID string) external.ImageAnalysisRequest {
|
||||
var tags = make([]string, len(imageMetadata.Tags))
|
||||
for i, t := range imageMetadata.Tags {
|
||||
tags[i] = t.String()
|
||||
}
|
||||
|
||||
return external.ImageAnalysisRequest{
|
||||
Source: external.ImageSource{
|
||||
Import: &external.ImageImportManifest{
|
||||
Contents: external.ImportContentDigests{
|
||||
Packages: packageDigest,
|
||||
Manifest: manifestDigest,
|
||||
Dockerfile: dockerfileDigest,
|
||||
ImageConfig: configDigest,
|
||||
},
|
||||
Tags: tags,
|
||||
Digest: imageMetadata.ManifestDigest,
|
||||
LocalImageId: imageMetadata.ID,
|
||||
OperationUuid: sessionID,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
//nolint:dupl
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
type configImportAPI interface {
|
||||
ImportImageConfig(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func importConfig(ctx context.Context, api configImportAPI, sessionID string, config []byte, stage *progress.Stage) (string, error) {
|
||||
if len(config) > 0 {
|
||||
log.Debug("importing image config")
|
||||
stage.Current = "image config"
|
||||
|
||||
// API requires an object, but we do not verify the shape of this object locally
|
||||
var sender map[string]interface{}
|
||||
if err := json.Unmarshal(config, &sender); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
response, httpResponse, err := api.ImportImageConfig(ctx, sessionID, sender)
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import Config: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import Config: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
)
|
||||
|
||||
type mockConfigImportAPI struct {
|
||||
sessionID string
|
||||
model interface{}
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
wasCalled bool
|
||||
}
|
||||
|
||||
func (m *mockConfigImportAPI) ImportImageConfig(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.wasCalled = true
|
||||
m.model = contents
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func TestConfigImport(t *testing.T) {
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
manifestJSONStr string
|
||||
api *mockConfigImportAPI
|
||||
expectsError bool
|
||||
expectsCall bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
manifestJSONStr: `{ "key": "the-manifest-contents!" }`,
|
||||
api: &mockConfigImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "No manifest provided",
|
||||
manifestJSONStr: "",
|
||||
api: &mockConfigImportAPI{},
|
||||
expectsCall: false,
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
manifestJSONStr: `{ "key": "the-manifest-contents!" }`,
|
||||
api: &mockConfigImportAPI{
|
||||
err: fmt.Errorf("api error, something went wrong"),
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
manifestJSONStr: `{ "key": "the-manifest-contents!" }`,
|
||||
api: &mockConfigImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importConfig(context.TODO(), test.api, sessionID, []byte(test.manifestJSONStr), &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if !test.api.wasCalled && test.expectsCall {
|
||||
t.Fatalf("was not called!")
|
||||
} else if test.api.wasCalled && !test.expectsCall {
|
||||
t.Fatalf("should not have been called")
|
||||
}
|
||||
|
||||
if !test.expectsCall {
|
||||
return
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
var expected map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(test.manifestJSONStr), &expected); err != nil {
|
||||
t.Fatalf("could not unmarshal expected results")
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(test.api.model, expected) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
type dockerfileImportAPI interface {
|
||||
ImportImageDockerfile(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func importDockerfile(ctx context.Context, api dockerfileImportAPI, sessionID string, dockerfile []byte, stage *progress.Stage) (string, error) {
|
||||
if len(dockerfile) > 0 {
|
||||
log.Debug("importing dockerfile")
|
||||
stage.Current = "dockerfile"
|
||||
|
||||
response, httpResponse, err := api.ImportImageDockerfile(ctx, sessionID, string(dockerfile))
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import Dockerfile: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import Dockerfile: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
)
|
||||
|
||||
type mockDockerfileImportAPI struct {
|
||||
sessionID string
|
||||
model string
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
wasCalled bool
|
||||
}
|
||||
|
||||
func (m *mockDockerfileImportAPI) ImportImageDockerfile(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.wasCalled = true
|
||||
m.model = contents
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func TestDockerfileImport(t *testing.T) {
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dockerfile string
|
||||
api *mockDockerfileImportAPI
|
||||
expectsError bool
|
||||
expectsCall bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
dockerfile: "the-manifest-contents!",
|
||||
api: &mockDockerfileImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "No manifest provided",
|
||||
dockerfile: "",
|
||||
api: &mockDockerfileImportAPI{},
|
||||
expectsCall: false,
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
dockerfile: "the-manifest-contents!",
|
||||
api: &mockDockerfileImportAPI{
|
||||
err: fmt.Errorf("api error, something went wrong"),
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
dockerfile: "the-manifest-contents!",
|
||||
api: &mockDockerfileImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importDockerfile(context.TODO(), test.api, sessionID, []byte(test.dockerfile), &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if !test.api.wasCalled && test.expectsCall {
|
||||
t.Fatalf("was not called!")
|
||||
} else if test.api.wasCalled && !test.expectsCall {
|
||||
t.Fatalf("should not have been called")
|
||||
}
|
||||
|
||||
if !test.expectsCall {
|
||||
return
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(test.api.model, test.dockerfile) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
//nolint:dupl
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
type manifestImportAPI interface {
|
||||
ImportImageManifest(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
func importManifest(ctx context.Context, api manifestImportAPI, sessionID string, manifest []byte, stage *progress.Stage) (string, error) {
|
||||
if len(manifest) > 0 {
|
||||
log.Debug("importing image manifest")
|
||||
stage.Current = "image manifest"
|
||||
|
||||
// API requires an object, but we do not verify the shape of this object locally
|
||||
var sender map[string]interface{}
|
||||
if err := json.Unmarshal(manifest, &sender); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
response, httpResponse, err := api.ImportImageManifest(ctx, sessionID, sender)
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import Manifest: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import Manifest: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
)
|
||||
|
||||
type mockManifestImportAPI struct {
|
||||
sessionID string
|
||||
model interface{}
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
wasCalled bool
|
||||
}
|
||||
|
||||
func (m *mockManifestImportAPI) ImportImageManifest(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.wasCalled = true
|
||||
m.model = contents
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func TestManifestImport(t *testing.T) {
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest string
|
||||
api *mockManifestImportAPI
|
||||
expectsError bool
|
||||
expectsCall bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
manifest: `{ "key": "the-config-contents!" }`,
|
||||
api: &mockManifestImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "No manifest provided",
|
||||
manifest: "",
|
||||
api: &mockManifestImportAPI{},
|
||||
expectsCall: false,
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
manifest: `{ "key": "the-config-contents!" }`,
|
||||
api: &mockManifestImportAPI{
|
||||
err: fmt.Errorf("api error, something went wrong"),
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
manifest: `{ "key": "the-config-contents!" }`,
|
||||
api: &mockManifestImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
expectsCall: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importManifest(context.TODO(), test.api, sessionID, []byte(test.manifest), &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if !test.api.wasCalled && test.expectsCall {
|
||||
t.Fatalf("was not called!")
|
||||
} else if test.api.wasCalled && !test.expectsCall {
|
||||
t.Fatalf("should not have been called")
|
||||
}
|
||||
|
||||
if !test.expectsCall {
|
||||
return
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
var expected map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(test.manifest), &expected); err != nil {
|
||||
t.Fatalf("could not unmarshal expected results")
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(test.api.model, expected) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/formats/syftjson"
|
||||
syftjsonModel "github.com/anchore/syft/syft/formats/syftjson/model"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type packageSBOMImportAPI interface {
|
||||
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
|
||||
}
|
||||
|
||||
// importSBOM mirrors all elements found on the syftjson model format object relative to the anchore engine import schema.
|
||||
type importSBOM struct {
|
||||
Artifacts []syftjsonModel.Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||
ArtifactRelationships []syftjsonModel.Relationship `json:"artifactRelationships"`
|
||||
Files []syftjsonModel.File `json:"files,omitempty"` // note: must have omitempty
|
||||
Secrets []syftjsonModel.Secrets `json:"secrets,omitempty"` // note: must have omitempty
|
||||
Source syftjsonModel.Source `json:"source"` // Source represents the original object that was cataloged
|
||||
Distro external.ImportDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
|
||||
Descriptor syftjsonModel.Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
|
||||
Schema syftjsonModel.Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
|
||||
}
|
||||
|
||||
// toImportSBOMModel transforms the current sbom shape into what is needed for the current anchore import api shape.
|
||||
func toImportSBOMModel(s sbom.SBOM) importSBOM {
|
||||
m := syftjson.ToFormatModel(s)
|
||||
|
||||
var idLike string
|
||||
if len(m.Distro.IDLike) > 0 {
|
||||
idLike = m.Distro.IDLike[0]
|
||||
}
|
||||
|
||||
var version = m.Distro.VersionID // note: version is intentionally not used as the default
|
||||
if version == "" {
|
||||
version = m.Distro.Version
|
||||
}
|
||||
|
||||
var name = m.Distro.ID // note: name is intentionally not used as the default
|
||||
if name == "" {
|
||||
name = m.Distro.Name
|
||||
}
|
||||
|
||||
return importSBOM{
|
||||
Artifacts: m.Artifacts,
|
||||
ArtifactRelationships: m.ArtifactRelationships,
|
||||
Files: m.Files,
|
||||
Secrets: m.Secrets,
|
||||
Source: m.Source,
|
||||
Distro: external.ImportDistribution{
|
||||
Name: name,
|
||||
Version: version,
|
||||
IdLike: idLike,
|
||||
},
|
||||
Descriptor: m.Descriptor,
|
||||
Schema: m.Schema,
|
||||
}
|
||||
}
|
||||
|
||||
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
doc := toImportSBOMModel(s)
|
||||
|
||||
enc := json.NewEncoder(&buf)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(&doc); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode import JSON model: %w", err)
|
||||
}
|
||||
|
||||
// the model is 1:1 the JSON output of today. As the schema changes, this will need to be converted into individual mappings.
|
||||
var model external.ImagePackageManifest
|
||||
if err := json.Unmarshal(buf.Bytes(), &model); err != nil {
|
||||
return nil, fmt.Errorf("unable to convert JSON output to import model: %w", err)
|
||||
}
|
||||
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s sbom.SBOM, stage *progress.Stage) (string, error) {
|
||||
log.Debug("importing package SBOM")
|
||||
stage.Current = "package SBOM"
|
||||
|
||||
model, err := packageSbomModel(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
|
||||
}
|
||||
|
||||
response, httpResponse, err := api.ImportImagePackages(ctx, sessionID, *model)
|
||||
if err != nil {
|
||||
var openAPIErr external.GenericOpenAPIError
|
||||
if errors.As(err, &openAPIErr) {
|
||||
log.Errorf("api response: %+v", string(openAPIErr.Body()))
|
||||
}
|
||||
return "", fmt.Errorf("unable to import PackageSBOM: %w", err)
|
||||
}
|
||||
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != 200 {
|
||||
return "", fmt.Errorf("unable to import PackageSBOM: %s", httpResponse.Status)
|
||||
}
|
||||
|
||||
return response.Digest, nil
|
||||
}
|
||||
@ -1,398 +0,0 @@
|
||||
package anchore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/go-test/deep"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/client-go/pkg/external"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/formats/syftjson"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func must(c pkg.CPE, e error) pkg.CPE {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type mockPackageSBOMImportAPI struct {
|
||||
sessionID string
|
||||
model external.ImagePackageManifest
|
||||
httpResponse *http.Response
|
||||
err error
|
||||
ctx context.Context
|
||||
responseDigest string
|
||||
}
|
||||
|
||||
func (m *mockPackageSBOMImportAPI) ImportImagePackages(ctx context.Context, sessionID string, model external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error) {
|
||||
m.model = model
|
||||
m.sessionID = sessionID
|
||||
m.ctx = ctx
|
||||
if m.httpResponse == nil {
|
||||
m.httpResponse = &http.Response{}
|
||||
}
|
||||
m.httpResponse.Body = ioutils.NewReadCloserWrapper(strings.NewReader(""), func() error { return nil })
|
||||
return external.ImageImportContentResponse{Digest: m.responseDigest}, m.httpResponse, m.err
|
||||
}
|
||||
|
||||
func sbomFixture() sbom.SBOM {
|
||||
return sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
PackageCatalog: pkg.NewCatalog(pkg.Package{
|
||||
Name: "name",
|
||||
Version: "version",
|
||||
FoundBy: "foundBy",
|
||||
Locations: source.NewLocationSet(
|
||||
source.Location{
|
||||
Coordinates: source.Coordinates{
|
||||
RealPath: "path",
|
||||
FileSystemID: "layerID",
|
||||
},
|
||||
},
|
||||
),
|
||||
Licenses: []string{"license"},
|
||||
Language: pkg.Python,
|
||||
Type: pkg.PythonPkg,
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
PURL: "purl",
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "p-name",
|
||||
Version: "p-version",
|
||||
License: "p-license",
|
||||
Author: "p-author",
|
||||
AuthorEmail: "p-email",
|
||||
Platform: "p-platform",
|
||||
Files: []pkg.PythonFileRecord{
|
||||
{
|
||||
Path: "p-path",
|
||||
Digest: &pkg.PythonFileDigest{
|
||||
Algorithm: "p-alg",
|
||||
Value: "p-digest",
|
||||
},
|
||||
Size: "p-size",
|
||||
},
|
||||
},
|
||||
SitePackagesRootPath: "p-site-packages-root",
|
||||
TopLevelPackages: []string{"top-level"},
|
||||
},
|
||||
}),
|
||||
LinuxDistribution: &linux.Release{
|
||||
ID: "centos",
|
||||
Version: "8.0",
|
||||
VersionID: "8.0",
|
||||
IDLike: []string{"rhel"},
|
||||
},
|
||||
},
|
||||
Relationships: []artifact.Relationship{
|
||||
{
|
||||
From: source.NewLocation("/place1"),
|
||||
To: source.NewLocation("/place2"),
|
||||
Type: artifact.ContainsRelationship,
|
||||
},
|
||||
},
|
||||
Source: source.Metadata{
|
||||
Scheme: source.ImageScheme,
|
||||
ImageMetadata: source.ImageMetadata{
|
||||
UserInput: "user-in",
|
||||
Layers: nil,
|
||||
Size: 10,
|
||||
ManifestDigest: "sha256:digest!",
|
||||
MediaType: "mediatype!",
|
||||
Tags: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPackageSbomImport(t *testing.T) {
|
||||
sbomResult := sbomFixture()
|
||||
theModel, err := packageSbomModel(sbomResult)
|
||||
if err != nil {
|
||||
t.Fatalf("could not get sbom model: %+v", err)
|
||||
}
|
||||
|
||||
sessionID := "my-session"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
api *mockPackageSBOMImportAPI
|
||||
expectsError bool
|
||||
}{
|
||||
|
||||
{
|
||||
name: "Go case: import works",
|
||||
api: &mockPackageSBOMImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 200},
|
||||
responseDigest: "digest!",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "API returns an error",
|
||||
api: &mockPackageSBOMImportAPI{
|
||||
err: fmt.Errorf("API error, something went wrong."),
|
||||
},
|
||||
expectsError: true,
|
||||
},
|
||||
{
|
||||
name: "API HTTP-level error",
|
||||
api: &mockPackageSBOMImportAPI{
|
||||
httpResponse: &http.Response{StatusCode: 404},
|
||||
},
|
||||
expectsError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, sbomResult, &progress.Stage{})
|
||||
|
||||
// validate error handling
|
||||
if err != nil && !test.expectsError {
|
||||
t.Fatalf("did not expect an error, but got: %+v", err)
|
||||
} else if err == nil && test.expectsError {
|
||||
t.Fatalf("did expect an error, but got none")
|
||||
}
|
||||
|
||||
if digest != test.api.responseDigest {
|
||||
t.Errorf("unexpected content digest: %q != %q", digest, test.api.responseDigest)
|
||||
}
|
||||
|
||||
// validating that the mock got the right parameters (api.ImportImagePackages)
|
||||
if test.api.sessionID != sessionID {
|
||||
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
|
||||
}
|
||||
|
||||
for _, d := range deep.Equal(&test.api.model, theModel) {
|
||||
t.Errorf("model difference: %s", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type modelAssertion func(t *testing.T, model *external.ImagePackageManifest)
|
||||
|
||||
func Test_packageSbomModel(t *testing.T) {
|
||||
fix := sbomFixture()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sbom sbom.SBOM
|
||||
traits []modelAssertion
|
||||
}{
|
||||
{
|
||||
name: "distro: has single distro id-like",
|
||||
sbom: sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
LinuxDistribution: &linux.Release{
|
||||
Name: "centos-name",
|
||||
ID: "centos-id",
|
||||
IDLike: []string{
|
||||
"centos-id-like-1",
|
||||
},
|
||||
Version: "version",
|
||||
VersionID: "version-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
traits: []modelAssertion{
|
||||
hasDistroInfo("centos-id", "version-id", "centos-id-like-1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "distro: has multiple distro id-like",
|
||||
sbom: sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
LinuxDistribution: &linux.Release{
|
||||
Name: "centos-name",
|
||||
ID: "centos-id",
|
||||
IDLike: []string{
|
||||
"centos-id-like-1",
|
||||
"centos-id-like-2",
|
||||
},
|
||||
Version: "version",
|
||||
VersionID: "version-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
traits: []modelAssertion{
|
||||
hasDistroInfo("centos-id", "version-id", "centos-id-like-1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "distro: has no distro id-like",
|
||||
sbom: sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
LinuxDistribution: &linux.Release{
|
||||
Name: "centos-name",
|
||||
ID: "centos-id",
|
||||
IDLike: []string{},
|
||||
Version: "version",
|
||||
VersionID: "version-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
traits: []modelAssertion{
|
||||
hasDistroInfo("centos-id", "version-id", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "distro: has no version-id",
|
||||
sbom: sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
LinuxDistribution: &linux.Release{
|
||||
Name: "centos-name",
|
||||
ID: "centos-id",
|
||||
IDLike: []string{},
|
||||
Version: "version",
|
||||
VersionID: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
traits: []modelAssertion{
|
||||
hasDistroInfo("centos-id", "version", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "distro: has no id",
|
||||
sbom: sbom.SBOM{
|
||||
Artifacts: sbom.Artifacts{
|
||||
LinuxDistribution: &linux.Release{
|
||||
Name: "centos-name",
|
||||
ID: "",
|
||||
IDLike: []string{},
|
||||
Version: "version",
|
||||
VersionID: "version-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
traits: []modelAssertion{
|
||||
hasDistroInfo("centos-name", "version-id", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should have expected packages",
|
||||
sbom: fix,
|
||||
traits: []modelAssertion{
|
||||
func(t *testing.T, model *external.ImagePackageManifest) {
|
||||
require.Len(t, model.Artifacts, 1)
|
||||
|
||||
modelPkg := model.Artifacts
|
||||
modelBytes, err := json.Marshal(&modelPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
fixPkg := syftjson.ToFormatModel(fix).Artifacts
|
||||
fixBytes, err := json.Marshal(&fixPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(fixBytes), string(modelBytes))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should have expected relationships",
|
||||
sbom: fix,
|
||||
traits: []modelAssertion{
|
||||
func(t *testing.T, model *external.ImagePackageManifest) {
|
||||
modelPkg := model.ArtifactRelationships
|
||||
modelBytes, err := json.Marshal(&modelPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
fixPkg := syftjson.ToFormatModel(fix).ArtifactRelationships
|
||||
fixBytes, err := json.Marshal(&fixPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(fixBytes), string(modelBytes))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should have expected schema",
|
||||
sbom: fix,
|
||||
traits: []modelAssertion{
|
||||
func(t *testing.T, model *external.ImagePackageManifest) {
|
||||
modelPkg := model.Schema
|
||||
modelBytes, err := json.Marshal(&modelPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
fixPkg := syftjson.ToFormatModel(fix).Schema
|
||||
fixBytes, err := json.Marshal(&fixPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(fixBytes), string(modelBytes))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should have expected descriptor",
|
||||
sbom: fix,
|
||||
traits: []modelAssertion{
|
||||
func(t *testing.T, model *external.ImagePackageManifest) {
|
||||
modelPkg := model.Descriptor
|
||||
modelBytes, err := json.Marshal(&modelPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
fixPkg := syftjson.ToFormatModel(fix).Descriptor
|
||||
fixBytes, err := json.Marshal(&fixPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(fixBytes), string(modelBytes))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should have expected source",
|
||||
sbom: fix,
|
||||
traits: []modelAssertion{
|
||||
func(t *testing.T, model *external.ImagePackageManifest) {
|
||||
modelPkg := model.Source
|
||||
modelBytes, err := json.Marshal(&modelPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
fixPkg := syftjson.ToFormatModel(fix).Source
|
||||
fixBytes, err := json.Marshal(&fixPkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, string(fixBytes), string(modelBytes))
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := packageSbomModel(tt.sbom)
|
||||
require.NoError(t, err)
|
||||
for _, fn := range tt.traits {
|
||||
fn(t, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func hasDistroInfo(name, version, idLike string) modelAssertion {
|
||||
return func(t *testing.T, model *external.ImagePackageManifest) {
|
||||
assert.Equal(t, name, model.Distro.Name)
|
||||
assert.Equal(t, version, model.Distro.Version)
|
||||
assert.Equal(t, idLike, model.Distro.IdLike)
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package config
|
||||
|
||||
import "github.com/spf13/viper"
|
||||
|
||||
type anchore struct {
|
||||
// upload options
|
||||
Host string `yaml:"host" json:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to (setting this value enables upload)
|
||||
Path string `yaml:"path" json:"path" mapstructure:"path"` // override the engine/enterprise API upload path
|
||||
// IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
|
||||
Username string `yaml:"-" json:"-" mapstructure:"username"` // -u , username to authenticate upload
|
||||
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
|
||||
Password string `yaml:"-" json:"-" mapstructure:"password"` // -p , password to authenticate upload
|
||||
Dockerfile string `yaml:"dockerfile" json:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
|
||||
OverwriteExistingImage bool `yaml:"overwrite-existing-image" json:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload
|
||||
ImportTimeout uint `yaml:"import-timeout" json:"import-timeout" mapstructure:"import-timeout"` // --import-timeout
|
||||
// , customize the number of seconds within which the SBOM import must be completed or canceled
|
||||
}
|
||||
|
||||
func (cfg anchore) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("anchore.path", "")
|
||||
}
|
||||
@ -43,7 +43,6 @@ type Application struct {
|
||||
OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
|
||||
Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
|
||||
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
||||
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
|
||||
@ -112,7 +111,6 @@ func (cfg *Application) parseConfigValues() error {
|
||||
|
||||
// parse application config options
|
||||
for _, optionFn := range []func() error{
|
||||
cfg.parseUploadOptions,
|
||||
cfg.parseLogLevelOption,
|
||||
cfg.parseFile,
|
||||
} {
|
||||
@ -136,13 +134,6 @@ func (cfg *Application) parseConfigValues() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Application) parseUploadOptions() error {
|
||||
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
|
||||
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Application) parseLogLevelOption() error {
|
||||
switch {
|
||||
case cfg.Quiet:
|
||||
|
||||
@ -132,48 +132,6 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||
assertSuccessfulReturnCode,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "attempt-upload-on-cli-switches",
|
||||
args: []string{"packages", "-vv", "-H", "localhost:8080", "-u", "the-username", "-d", "test-fixtures/image-pkg-coverage/Dockerfile", "--overwrite-existing-image", coverageImage},
|
||||
env: map[string]string{
|
||||
"SYFT_ANCHORE_PATH": "path/to/api",
|
||||
"SYFT_ANCHORE_PASSWORD": "the-password",
|
||||
},
|
||||
assertions: []traitAssertion{
|
||||
// we cannot easily assert a successful upload behavior, so instead we are doing the next best thing
|
||||
// and asserting that the parsed configuration has the expected values and we see log entries
|
||||
// indicating an upload attempt.
|
||||
assertNotInOutput("the-username"),
|
||||
assertNotInOutput("the-password"),
|
||||
assertInOutput("uploading results to localhost:8080"),
|
||||
assertInOutput(`dockerfile: test-fixtures/image-pkg-coverage/Dockerfile`),
|
||||
assertInOutput(`overwrite-existing-image: true`),
|
||||
assertInOutput(`path: path/to/api`),
|
||||
assertInOutput(`host: localhost:8080`),
|
||||
assertFailingReturnCode, // upload can't go anywhere, so if this passes that would be surprising
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dockerfile-without-upload-is-invalid",
|
||||
args: []string{"packages", "-vv", "-d", "test-fixtures/image-pkg-coverage/Dockerfile", coverageImage},
|
||||
assertions: []traitAssertion{
|
||||
|
||||
assertNotInOutput("uploading results to localhost:8080"),
|
||||
assertInOutput("invalid application config: cannot provide dockerfile option without enabling upload"),
|
||||
assertFailingReturnCode,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "attempt-upload-with-env-host-set",
|
||||
args: []string{"packages", "-vv", coverageImage},
|
||||
env: map[string]string{
|
||||
"SYFT_ANCHORE_HOST": "localhost:8080",
|
||||
},
|
||||
assertions: []traitAssertion{
|
||||
assertInOutput("uploading results to localhost:8080"),
|
||||
assertFailingReturnCode, // upload can't go anywhere, so if this passes that would be surprising
|
||||
},
|
||||
},
|
||||
{
|
||||
// we want to make certain that syft can catalog a single go binary and get a SBOM report that is not empty
|
||||
name: "catalog-single-go-binary",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user