Skip to content

Commit

Permalink
feat(aqua-enterprise): add support for filesystem scanning (aquasecur…
Browse files Browse the repository at this point in the history
  • Loading branch information
deven0t authored Feb 24, 2022
1 parent 6372525 commit d38dd59
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 64 deletions.
33 changes: 17 additions & 16 deletions cmd/scanner-aqua/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,7 +30,7 @@ func main() {
}

func run() error {
opt := options{}
opt := cli.Options{}

rootCmd := &cobra.Command{
Use: "scanner",
Expand All @@ -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)
Expand All @@ -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
}
Expand Down
40 changes: 37 additions & 3 deletions pkg/plugin/aqua/client/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand All @@ -18,6 +19,7 @@ var ErrUnauthorized = errors.New("unauthorized")

type client struct {
baseURL string
authHeader string
authorization Authorization
httpClient *http.Client
}
Expand All @@ -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
Expand All @@ -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,
}
Expand All @@ -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{
Expand All @@ -76,7 +110,7 @@ func NewClient(baseURL string, authorization Authorization) *Client {
registries: &Registries{
client: client,
},
}
}, nil
}

func (c *Client) Images() ImagesInterface {
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugin/aqua/client/client_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
46 changes: 38 additions & 8 deletions pkg/plugin/aqua/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"},
Expand All @@ -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))
})
})

Expand All @@ -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))
})
})
})
Expand Down Expand Up @@ -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"},
Expand All @@ -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))
})
})

Expand All @@ -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))
})
})
})
Expand Down
49 changes: 32 additions & 17 deletions pkg/plugin/aqua/scanner/api/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit d38dd59

Please sign in to comment.