From d38dd5905fc38de8221a3432c100fab98d028c8b Mon Sep 17 00:00:00 2001 From: Devend Date: Thu, 24 Feb 2022 18:17:03 +0530 Subject: [PATCH] feat(aqua-enterprise): add support for filesystem scanning (#985) --- cmd/scanner-aqua/main.go | 33 +++++----- pkg/plugin/aqua/client/client.go | 40 ++++++++++++- .../aqua/client/client_integration_test.go | 2 +- pkg/plugin/aqua/client/client_test.go | 46 +++++++++++--- pkg/plugin/aqua/scanner/api/scanner.go | 49 +++++++++------ pkg/plugin/aqua/scanner/cli/scanner.go | 60 +++++++++++++------ pkg/plugin/aqua/scanner/cli/types.go | 18 ++++++ 7 files changed, 184 insertions(+), 64 deletions(-) diff --git a/cmd/scanner-aqua/main.go b/cmd/scanner-aqua/main.go index ad5fa8c81..57bf06658 100644 --- a/cmd/scanner-aqua/main.go +++ b/cmd/scanner-aqua/main.go @@ -18,14 +18,10 @@ const ( hostFlag = "host" userFlag = "user" passwordFlag = "password" + registryFlag = "registry" + commandFlag = "command" ) -type options struct { - version string - baseURL string - credentials client.UsernameAndPassword -} - // main is the entrypoint of the executable command used by Aqua vulnerabilityreport.Plugin. func main() { if err := run(); err != nil { @@ -34,7 +30,7 @@ func main() { } func run() error { - opt := options{} + opt := cli.Options{} rootCmd := &cobra.Command{ Use: "scanner", @@ -50,10 +46,12 @@ func run() error { }, } - rootCmd.Flags().StringVarP(&opt.version, versionFlag, "V", "", "Version of Aqua") - rootCmd.Flags().StringVarP(&opt.baseURL, hostFlag, "H", "", "Aqua management console address (required)") - rootCmd.Flags().StringVarP(&opt.credentials.Username, userFlag, "U", "", "Aqua management console username (required)") - rootCmd.Flags().StringVarP(&opt.credentials.Password, passwordFlag, "P", "", "Aqua management console password (required)") + rootCmd.Flags().StringVarP(&opt.Version, versionFlag, "V", "", "Version of Aqua") + rootCmd.Flags().StringVarP(&opt.BaseURL, hostFlag, "H", "", "Aqua management console address (required)") + rootCmd.Flags().StringVarP(&opt.Credentials.Username, userFlag, "U", "", "Aqua management console username (required)") + rootCmd.Flags().StringVarP(&opt.Credentials.Password, passwordFlag, "P", "", "Aqua management console password (required)") + rootCmd.Flags().StringVarP(&opt.RegistryName, registryFlag, "R", "", "Registry name from Aqua management console") + rootCmd.Flags().StringVarP(&opt.Command, commandFlag, "C", "image", "Command mode to use for scanner eg image/fs") _ = rootCmd.MarkFlagRequired(versionFlag) _ = rootCmd.MarkFlagRequired(hostFlag) @@ -65,19 +63,22 @@ func run() error { // scan scans the specified image reference. Firstly, attempt to download a vulnerability // report with Aqua REST API call. If the report is not found, execute the `scannercli scan` command. -func scan(opt options, imageRef string) (report v1alpha1.VulnerabilityReportData, err error) { - clientset := client.NewClient(opt.baseURL, client.Authorization{ - Basic: &opt.credentials, +func scan(opt cli.Options, imageRef string) (report v1alpha1.VulnerabilityReportData, err error) { + clientset, err := client.NewClient(opt.BaseURL, client.Authorization{ + Basic: &opt.Credentials, }) + if err != nil { + return + } - report, err = api.NewScanner(opt.version, clientset).Scan(imageRef) + report, err = api.NewScanner(opt, clientset).Scan(imageRef) if err == nil { return } if !errors.Is(err, client.ErrNotFound) { return } - report, err = cli.NewScanner(opt.version, opt.baseURL, opt.credentials).Scan(imageRef) + report, err = cli.NewScanner(opt).Scan(imageRef) if err != nil { return } diff --git a/pkg/plugin/aqua/client/client.go b/pkg/plugin/aqua/client/client.go index 45ee02b5c..b226ae79a 100644 --- a/pkg/plugin/aqua/client/client.go +++ b/pkg/plugin/aqua/client/client.go @@ -1,6 +1,7 @@ package client import ( + "encoding/base64" "encoding/json" "errors" "fmt" @@ -18,6 +19,7 @@ var ErrUnauthorized = errors.New("unauthorized") type client struct { baseURL string + authHeader string authorization Authorization httpClient *http.Client } @@ -30,11 +32,39 @@ func (c *client) newGetRequest(url string) (*http.Request, error) { req.Header.Add("Content-Type", "application/json; charset=UTF-8") req.Header.Add("User-Agent", userAgent) if auth := c.authorization.Basic; auth != nil { - req.SetBasicAuth(auth.Username, auth.Password) + req.Header.Add(c.authHeader, fmt.Sprintf("Basic %s", + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", auth.Username, auth.Password))))) } return req, nil } +// Aqua api has custom auth header, auth header can be get using /api endpoint +func (c *client) getAuthHeader() error { + url := fmt.Sprintf("%s/api", c.baseURL) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json; charset=UTF-8") + req.Header.Add("User-Agent", userAgent) + resp, err := c.httpClient.Do(req) + if err != nil || resp == nil { + return err + } + var apiInfo map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&apiInfo) + if err != nil { + return err + } + if header, ok := apiInfo["authorization_header"].(string); ok { + c.authHeader = header + } else { + // Default fallback + c.authHeader = "Authorization" + } + return nil +} + // Clientset defines methods of the Aqua API client. type Clientset interface { Registries() RegistriesInterface @@ -59,7 +89,7 @@ type Client struct { } // NewClient constructs a new API client with the specified base URL and authorization details. -func NewClient(baseURL string, authorization Authorization) *Client { +func NewClient(baseURL string, authorization Authorization) (*Client, error) { httpClient := &http.Client{ Timeout: defaultTimeout, } @@ -68,6 +98,10 @@ func NewClient(baseURL string, authorization Authorization) *Client { authorization: authorization, httpClient: httpClient, } + err := client.getAuthHeader() + if err != nil { + return &Client{}, fmt.Errorf("failed to get authorization header %w", err) + } return &Client{ images: &Images{ @@ -76,7 +110,7 @@ func NewClient(baseURL string, authorization Authorization) *Client { registries: &Registries{ client: client, }, - } + }, nil } func (c *Client) Images() ImagesInterface { diff --git a/pkg/plugin/aqua/client/client_integration_test.go b/pkg/plugin/aqua/client/client_integration_test.go index 44bbe0c1a..8317e7f85 100644 --- a/pkg/plugin/aqua/client/client_integration_test.go +++ b/pkg/plugin/aqua/client/client_integration_test.go @@ -12,7 +12,7 @@ func TestClient(t *testing.T) { t.Skip("Run this test manually") } - c := NewClient("http://aqua.domain", Authorization{ + c, _ := NewClient("http://aqua.domain", Authorization{ Basic: &UsernameAndPassword{"administrator", "Password12345"}}) t.Run("Should list registries", func(t *testing.T) { diff --git a/pkg/plugin/aqua/client/client_test.go b/pkg/plugin/aqua/client/client_test.go index b2537bded..e4695fb62 100644 --- a/pkg/plugin/aqua/client/client_test.go +++ b/pkg/plugin/aqua/client/client_test.go @@ -16,14 +16,44 @@ var _ = Describe("The Aqua API client", func() { BeforeEach(func() { server = NewServer() - aquaClient = client.NewClient(server.URL(), client.Authorization{ + server.AppendHandlers( + CombineHandlers( + VerifyRequest("GET", "/api"), + RespondWith(http.StatusOK, `{"authorization_header":"Authorization"}`), + ), + ) + aquaClient, _ = client.NewClient(server.URL(), client.Authorization{ Basic: &client.UsernameAndPassword{ Username: "administrator", - Password: "Password1", + Password: "bdclz", }, }) }) + Describe("get auth header", func() { + var server *Server + BeforeEach(func() { + server = NewServer() + server.AppendHandlers(CombineHandlers( + VerifyRequest("GET", "/api"), + RespondWith(http.StatusOK, `{"authorization_header":"Authorization"}`), + ), + ) + }) + Context("when the request succeeds", func() { + It("should make a request to fetch registries", func() { + _, err := client.NewClient(server.URL(), client.Authorization{ + Basic: &client.UsernameAndPassword{ + Username: "administrator", + Password: "bdclz", + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(server.ReceivedRequests()).To(HaveLen(1)) + }) + }) + }) + Describe("fetching registries", func() { var returnedRegistries []client.RegistryResponse var statusCode int @@ -36,7 +66,7 @@ var _ = Describe("The Aqua API client", func() { server.AppendHandlers( CombineHandlers( VerifyRequest("GET", "/api/v1/registries"), - VerifyBasicAuth("administrator", "Password1"), + VerifyBasicAuth("administrator", "bdclz"), VerifyMimeType("application/json"), VerifyHeader(http.Header{ "User-Agent": []string{"StarboardSecurityOperator"}, @@ -55,7 +85,7 @@ var _ = Describe("The Aqua API client", func() { registries, err := aquaClient.Registries().List() Expect(err).ToNot(HaveOccurred()) Expect(registries).To(Equal(returnedRegistries)) - Expect(server.ReceivedRequests()).To(HaveLen(1)) + Expect(server.ReceivedRequests()).To(HaveLen(2)) }) }) @@ -67,7 +97,7 @@ var _ = Describe("The Aqua API client", func() { It("should return error", func() { _, err := aquaClient.Registries().List() Expect(err).To(MatchError(client.ErrUnauthorized)) - Expect(server.ReceivedRequests()).To(HaveLen(1)) + Expect(server.ReceivedRequests()).To(HaveLen(2)) }) }) }) @@ -117,7 +147,7 @@ var _ = Describe("The Aqua API client", func() { server.AppendHandlers( CombineHandlers( VerifyRequest("GET", "/api/v2/images/Harbor/library/nginx/1.17/vulnerabilities"), - VerifyBasicAuth("administrator", "Password1"), + VerifyBasicAuth("administrator", "bdclz"), VerifyMimeType("application/json"), VerifyHeader(http.Header{ "User-Agent": []string{"StarboardSecurityOperator"}, @@ -136,7 +166,7 @@ var _ = Describe("The Aqua API client", func() { vulnerabilities, err := aquaClient.Images().Vulnerabilities("Harbor", "library/nginx", "1.17") Expect(err).ToNot(HaveOccurred()) Expect(vulnerabilities).To(Equal(returnedVulnerabilities)) - Expect(server.ReceivedRequests()).To(HaveLen(1)) + Expect(server.ReceivedRequests()).To(HaveLen(2)) }) }) @@ -148,7 +178,7 @@ var _ = Describe("The Aqua API client", func() { It("should return error", func() { _, err := aquaClient.Images().Vulnerabilities("Harbor", "library/nginx", "1.17") Expect(err).To(MatchError(client.ErrUnauthorized)) - Expect(server.ReceivedRequests()).To(HaveLen(1)) + Expect(server.ReceivedRequests()).To(HaveLen(2)) }) }) }) diff --git a/pkg/plugin/aqua/scanner/api/scanner.go b/pkg/plugin/aqua/scanner/api/scanner.go index effec23fb..c74dc6aa0 100644 --- a/pkg/plugin/aqua/scanner/api/scanner.go +++ b/pkg/plugin/aqua/scanner/api/scanner.go @@ -6,6 +6,7 @@ import ( "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" "github.com/aquasecurity/starboard/pkg/plugin/aqua/client" + "github.com/aquasecurity/starboard/pkg/plugin/aqua/scanner/cli" "github.com/google/go-containerregistry/pkg/name" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -15,23 +16,48 @@ const ( ) type Scanner struct { - version string + options cli.Options clientset client.Clientset } -func NewScanner(version string, clientset client.Clientset) *Scanner { +func NewScanner(options cli.Options, clientset client.Clientset) *Scanner { return &Scanner{ - version: version, + options: options, clientset: clientset, } } func (s *Scanner) Scan(imageRef string) (v1alpha1.VulnerabilityReportData, error) { - registries, err := s.clientset.Registries().List() + registryName, err := s.getRegistryName(imageRef) + if err != nil { + return v1alpha1.VulnerabilityReportData{}, err + } + reference, err := name.ParseReference(imageRef) + if err != nil { + return v1alpha1.VulnerabilityReportData{}, err + } + repo := reference.Context().RepositoryStr() + if cli.Command(s.options.Command) == cli.Filesystem { + // in case of fs command, full repo name required for Aqua console + repo = reference.Context().RegistryStr() + "/" + reference.Context().RepositoryStr() + } + vulnerabilities, err := s.clientset.Images().Vulnerabilities(registryName, repo, reference.Identifier()) if err != nil { return v1alpha1.VulnerabilityReportData{}, err } + return s.convert(reference, vulnerabilities) +} + +func (s *Scanner) getRegistryName(imageRef string) (string, error) { + if s.options.RegistryName != "" { + return s.options.RegistryName, nil + } + registries, err := s.clientset.Registries().List() + if err != nil { + return "", err + } + var registryName string for _, r := range registries { for _, p := range r.Prefixes { @@ -46,18 +72,7 @@ func (s *Scanner) Scan(imageRef string) (v1alpha1.VulnerabilityReportData, error // Fallback to ad hoc scans registry registryName = adHocScansRegistry } - - reference, err := name.ParseReference(imageRef) - if err != nil { - return v1alpha1.VulnerabilityReportData{}, err - } - - vulnerabilities, err := s.clientset.Images().Vulnerabilities(registryName, reference.Context().RepositoryStr(), reference.Identifier()) - if err != nil { - return v1alpha1.VulnerabilityReportData{}, err - } - - return s.convert(reference, vulnerabilities) + return registryName, nil } func (s *Scanner) convert(ref name.Reference, response client.VulnerabilitiesResponse) (v1alpha1.VulnerabilityReportData, error) { @@ -90,7 +105,7 @@ func (s *Scanner) convert(ref name.Reference, response client.VulnerabilitiesRes Scanner: v1alpha1.Scanner{ Name: "Aqua CSP", Vendor: "Aqua Security", - Version: s.version, + Version: s.options.Version, }, Registry: v1alpha1.Registry{ Server: ref.Context().RegistryStr(), diff --git a/pkg/plugin/aqua/scanner/cli/scanner.go b/pkg/plugin/aqua/scanner/cli/scanner.go index 591914c73..ba26d1a2a 100644 --- a/pkg/plugin/aqua/scanner/cli/scanner.go +++ b/pkg/plugin/aqua/scanner/cli/scanner.go @@ -9,37 +9,27 @@ import ( "time" "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" - "github.com/aquasecurity/starboard/pkg/plugin/aqua/client" "github.com/google/go-containerregistry/pkg/name" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type Scanner struct { - version string - baseURL string - credentials client.UsernameAndPassword + scanOptions Options } -func NewScanner(version string, baseURL string, credentials client.UsernameAndPassword) *Scanner { +func NewScanner(options Options) *Scanner { return &Scanner{ - version: version, - baseURL: baseURL, - credentials: credentials, + options, } } func (s *Scanner) Scan(imageRef string) (report v1alpha1.VulnerabilityReportData, err error) { - args := []string{ - "scan", - "--checkonly", - "--dockerless", - fmt.Sprintf("--host=%s", s.baseURL), - fmt.Sprintf("--user=%s", s.credentials.Username), - fmt.Sprintf("--password=%s", s.credentials.Password), - "--local", - imageRef, + var command *exec.Cmd + if Command(s.scanOptions.Command) == Filesystem { + command = s.getFSScanCommand(imageRef) + } else { + command = s.getImageScanCommand(imageRef) } - command := exec.Command("scannercli", args...) var stderr bytes.Buffer command.Stderr = &stderr @@ -57,6 +47,38 @@ func (s *Scanner) Scan(imageRef string) (report v1alpha1.VulnerabilityReportData return s.convert(imageRef, aquaReport) } +func (s *Scanner) getImageScanCommand(imageRef string) *exec.Cmd { + args := []string{ + "scan", + "--checkonly", + "--dockerless", + fmt.Sprintf("--host=%s", s.scanOptions.BaseURL), + fmt.Sprintf("--user=%s", s.scanOptions.Credentials.Username), + fmt.Sprintf("--password=%s", s.scanOptions.Credentials.Password), + "--local", + imageRef, + } + command := exec.Command("scannercli", args...) + return command +} + +func (s *Scanner) getFSScanCommand(imageRef string) *exec.Cmd { + args := []string{ + "scan", + "--checkonly", + fmt.Sprintf("--host=%s", s.scanOptions.BaseURL), + fmt.Sprintf("--user=%s", s.scanOptions.Credentials.Username), + fmt.Sprintf("--password=%s", s.scanOptions.Credentials.Password), + fmt.Sprintf("--registry=%s", s.scanOptions.RegistryName), + fmt.Sprintf("--image-name=%s", imageRef), + "--register", + "--fs-scan", + "/", + } + command := exec.Command("/var/aqua/scannercli", args...) + return command +} + func (s *Scanner) convert(imageRef string, aquaReport ScanReport) (report v1alpha1.VulnerabilityReportData, err error) { items := make([]v1alpha1.Vulnerability, 0) @@ -103,7 +125,7 @@ func (s *Scanner) convert(imageRef string, aquaReport ScanReport) (report v1alph Scanner: v1alpha1.Scanner{ Name: "Aqua CSP", Vendor: "Aqua Security", - Version: s.version, + Version: s.scanOptions.Version, }, Registry: v1alpha1.Registry{ Server: ref.Context().RegistryStr(), diff --git a/pkg/plugin/aqua/scanner/cli/types.go b/pkg/plugin/aqua/scanner/cli/types.go index b838a8fbe..9e0eaac60 100644 --- a/pkg/plugin/aqua/scanner/cli/types.go +++ b/pkg/plugin/aqua/scanner/cli/types.go @@ -1,5 +1,7 @@ package cli +import "github.com/aquasecurity/starboard/pkg/plugin/aqua/client" + type ResourceType int const ( @@ -8,6 +10,22 @@ const ( Package ) +// Command to scan image or filesystem. +type Command string + +const ( + Filesystem Command = "filesystem" + Image Command = "image" +) + +type Options struct { + Version string + BaseURL string + Credentials client.UsernameAndPassword + RegistryName string + Command string +} + type ScanReport struct { Image string `json:"image"` Registry string `json:"registry"`