mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 10:36:45 +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)
|
# location to write the log file (default is not to have a log file)
|
||||||
# same as SYFT_LOG_FILE env var
|
# same as SYFT_LOG_FILE env var
|
||||||
file: ""
|
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
|
### Adding an SBOM to an image as an attestation using Syft
|
||||||
|
|||||||
@ -19,13 +19,7 @@ type PackagesOptions struct {
|
|||||||
OutputTemplatePath string
|
OutputTemplatePath string
|
||||||
File string
|
File string
|
||||||
Platform string
|
Platform string
|
||||||
Host string
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Dockerfile string
|
|
||||||
Exclude []string
|
Exclude []string
|
||||||
OverwriteExistingImage bool
|
|
||||||
ImportTimeout uint
|
|
||||||
Catalogers []string
|
Catalogers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,30 +41,12 @@ func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
|||||||
cmd.Flags().StringVarP(&o.Platform, "platform", "", "",
|
cmd.Flags().StringVarP(&o.Platform, "platform", "", "",
|
||||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
"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,
|
cmd.Flags().StringArrayVarP(&o.Exclude, "exclude", "", nil,
|
||||||
"exclude paths from being scanned using a glob expression")
|
"exclude paths from being scanned using a glob expression")
|
||||||
|
|
||||||
cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil,
|
cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil,
|
||||||
"enable one or more package catalogers")
|
"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)
|
return bindPackageConfigOptions(cmd.Flags(), v)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,31 +81,5 @@ func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
|||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ package packages
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/wagoodman/go-partybus"
|
"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/eventloop"
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/anchore"
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
"github.com/anchore/syft/internal/log"
|
"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)
|
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{
|
bus.Publish(partybus.Event{
|
||||||
Type: event.Exit,
|
Type: event.Exit,
|
||||||
Value: func() error { return writer.Write(*s) },
|
Value: func() error { return writer.Write(*s) },
|
||||||
@ -144,55 +134,6 @@ func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []art
|
|||||||
return relationships
|
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 {
|
func validateOutputOptions(app *config.Application) error {
|
||||||
var usesTemplateOutput bool
|
var usesTemplateOutput bool
|
||||||
for _, o := range app.Outputs {
|
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/acobaugh/osrelease v0.1.0
|
||||||
github.com/adrg/xdg v0.3.3
|
github.com/adrg/xdg v0.3.3
|
||||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
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-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
|
||||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||||
github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7
|
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/bmatcuk/doublestar/v4 v4.0.2
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/facebookincubator/nvdtools v0.1.4
|
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.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||||
github.com/aliyun/credentials-go v1.2.3 h1:Vmodnr52Rz1mcbwn0kzMhLRKb6soizewuKXdfZiNemU=
|
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/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 h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
|
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=
|
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/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/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 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/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/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=
|
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
|
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
|
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
|
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"`
|
Dev development `yaml:"dev" json:"dev" mapstructure:"dev"`
|
||||||
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
|
||||||
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
|
Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"`
|
||||||
@ -112,7 +111,6 @@ func (cfg *Application) parseConfigValues() error {
|
|||||||
|
|
||||||
// parse application config options
|
// parse application config options
|
||||||
for _, optionFn := range []func() error{
|
for _, optionFn := range []func() error{
|
||||||
cfg.parseUploadOptions,
|
|
||||||
cfg.parseLogLevelOption,
|
cfg.parseLogLevelOption,
|
||||||
cfg.parseFile,
|
cfg.parseFile,
|
||||||
} {
|
} {
|
||||||
@ -136,13 +134,6 @@ func (cfg *Application) parseConfigValues() error {
|
|||||||
return nil
|
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 {
|
func (cfg *Application) parseLogLevelOption() error {
|
||||||
switch {
|
switch {
|
||||||
case cfg.Quiet:
|
case cfg.Quiet:
|
||||||
|
|||||||
@ -132,48 +132,6 @@ func TestPackagesCmdFlags(t *testing.T) {
|
|||||||
assertSuccessfulReturnCode,
|
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
|
// 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",
|
name: "catalog-single-go-binary",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user