Skip to content

Commit

Permalink
Adds: support for harbor registry vulnerability fetching (#1015)
Browse files Browse the repository at this point in the history
* Adds: support for harbor vuln fetching

* code nits: renamed env variables

Co-authored-by: Suvarna Rokade <[email protected]>
  • Loading branch information
Rchanger and Suvarna Rokade authored Oct 6, 2021
1 parent c11052e commit 56b6964
Show file tree
Hide file tree
Showing 7 changed files with 833 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Terrascan is a static code analyzer for Infrastructure as Code. Terrascan allows
* Scanning of [Kubernetes](https://runterrascan.io/docs/usage/command_line_mode/#scanning-for-a-specific-iac-provider) (JSON/YAML), [Helm](https://runterrascan.io/docs/usage/command_line_mode/#scanning-a-helm-chart) v3, and [Kustomize](https://runterrascan.io/docs/usage/command_line_mode/#scanning-a-kustomize-chart)
* Scanning of [Dockerfiles](https://runterrascan.io/docs/usage/command_line_mode/#scanning-a-dockerfile)
* Support for [AWS](https://runterrascan.io/docs/policies/aws/), [Azure](https://runterrascan.io/docs/policies/azure/), [GCP](https://runterrascan.io/docs/policies/gcp/), [Kubernetes](https://runterrascan.io/docs/policies/k8s/), [Dockerfile](https://runterrascan.io/docs/policies/docker/), and [GitHub](https://runterrascan.io/docs/policies/github/)
* Integrates with docker image vulnerability scanning for AWS, Azure, GCP container registries.
* Integrates with docker image vulnerability scanning for AWS, Azure, GCP, Harbor container registries.

## Quick Start

Expand Down
10 changes: 9 additions & 1 deletion docs/usage/command_line_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Terrascan can display vulnerabilities for Docker images present in the IaC files
$ terrascan scan -i <IaC Provider> --find-vuln
```

This command looks for all the Docker images present in the IaC files being scanned and retrieves any vulnerabilities as reported by it's container registry. Supported container registries include: AWS Elastic Container Registry (ECR), Azure Container Registry, Google Container Registry, and Google Artifact Registry.
This command looks for all the Docker images present in the IaC files being scanned and retrieves any vulnerabilities as reported by it's container registry. Supported container registries include: AWS Elastic Container Registry (ECR), Azure Container Registry, Google Container Registry, Google Artifact Registry and Harbor Container Registry.

The following environment variables are required when connecting to the container registries:

Expand Down Expand Up @@ -223,6 +223,14 @@ Terrascan also requires the password to the registry set into the `AZURE_ACR_PAS
``` Bash
az acr credential show --name RegistryName
```
#### Harbor Container Registry

When integrating vulnerability results from Harbor, Terrascan requires the `HARBOR_REGISTRY_USERNAME`, `HARBOR_REGISTRY_PASSWORD`,`HARBOR_REGISTRY_CACERT`, `HARBOR_SKIP_TLS`, and `HARBOR_REGISTRY_DOMAIN` environment variables.

Terrascan requires `HARBOR_REGISTRY_DOMAIN` environment variable to identify domain of harobor registry.

In case using terrascan for development or testing purpose then to skip tls verification set `HARBOR_SKIP_TLS` environment variable to `true`.


### Resource Config
While scanning a IaC, Terrascan loads all the IaC files, creates a list of resource configs and then processes this list to report violations. For debugging purposes, you can print this resource configs list as an output by using the `--config-only` flag to the `terrascan scan` command.
Expand Down
55 changes: 55 additions & 0 deletions pkg/iac-providers/output/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,58 @@ func (v *Vulnerability) PrepareFromGCRImageScan(gcpVulnerability *grafeaspb.Occu
vendorCVSS[nvd] = cvss
v.CVSS = vendorCVSS
}

//PrepareFromHarborImageScan - prepares vulnerability object from Harbor image scan findings
func (v *Vulnerability) PrepareFromHarborImageScan(vulnerability map[string]interface{}) {
if id, ok := vulnerability["id"]; ok {
v.VulnerabilityID, _ = id.(string)
}
if severity, ok := vulnerability["severity"]; ok {
v.Severity, _ = severity.(string)
}
if description, ok := vulnerability["description"]; ok {
v.Description, _ = description.(string)
}
if pkgName, ok := vulnerability["package"]; ok {
v.PkgName, _ = pkgName.(string)
}
if version, ok := vulnerability["version"]; ok {
v.InstalledVersion, _ = version.(string)
}
if fixedVersion, ok := vulnerability["fix_version"]; ok {
v.FixedVersion, _ = fixedVersion.(string)
}
if attr, ok := vulnerability["vendor_attributes"]; ok {
cvss := CVSS{}
data, _ := attr.(map[string]interface{})
cvss.PrepareFromHarborImageScanAttribute(data)
v.CVSS = VendorCVSS{
nvd: cvss,
}
}
if v.VulnerabilityID != "" {
v.PrimaryURL = primaryURL + v.VulnerabilityID
}
}

// PrepareFromHarborImageScanAttribute prepares cvss object from harbor image scan attribute
func (cvss *CVSS) PrepareFromHarborImageScanAttribute(attr map[string]interface{}) {
if tempCvss, ok := attr["CVSS"]; ok {
temp := tempCvss.(map[string]interface{})
if tempNvd, ok := temp[nvd]; ok {
nvdMap, _ := tempNvd.(map[string]interface{})
if v2Vector, ok := nvdMap["V2Vector"]; ok {
cvss.V2Vector, _ = v2Vector.(string)
}
if v2Score, ok := nvdMap["V2Score"]; ok {
cvss.V2Score, _ = v2Score.(float64)
}
if v3Vector, ok := nvdMap["V3Vector"]; ok {
cvss.V3Vector, _ = v3Vector.(string)
}
if v3Score, ok := nvdMap["V3Score"]; ok {
cvss.V3Score, _ = v3Score.(float64)
}
}
}
}
63 changes: 63 additions & 0 deletions pkg/iac-providers/output/vulnerability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,66 @@ func TestVulnerabilityPrepareFromGCRImageScan(t *testing.T) {
})
}
}

func TestVulnerability_PrepareFromHarborImageScan(t *testing.T) {

type args struct {
vulnerability map[string]interface{}
}
tests := []struct {
name string
args args
want *Vulnerability
}{
{
name: "complete vulnerability object",
args: args{
vulnerability: map[string]interface{}{
"id": "CVE-2021-36159",
"package": "apk-tools",
"version": "2.10.5-r1",
"fix_version": "2.10.7-r0",
"severity": "Critical",
"description": "libfetch before 2021-07-26, as used",
"vendor_attributes": map[string]interface{}{
"CVSS": map[string]interface{}{
"nvd": map[string]interface{}{
"V2Score": 6.4,
"V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:P",
"V3Score": 9.1,
"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H",
},
},
},
},
},
want: &Vulnerability{
VulnerabilityID: "CVE-2021-36159",
PkgName: "apk-tools",
InstalledVersion: "2.10.5-r1",
FixedVersion: "2.10.7-r0",
PrimaryURL: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-36159",
Description: "libfetch before 2021-07-26, as used",
Severity: "Critical",
CVSS: VendorCVSS{
"nvd": CVSS{
V2Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:P",
V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H",
V2Score: 6.4,
V3Score: 9.1,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := &Vulnerability{}
v.PrepareFromHarborImageScan(tt.args.vulnerability)
if !reflect.DeepEqual(v, tt.want) {
t.Errorf("PrepareFromHarborImageScan() got = %v, want %v", v, tt.want)
return
}
})
}
}
231 changes: 231 additions & 0 deletions pkg/vulnerability/harbor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
Copyright (C) 2020 Accurics, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package vulnerability

import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"

"github.com/accurics/terrascan/pkg/iac-providers/output"
"go.uber.org/zap"
)

const (
registryUsername = "HARBOR_REGISTRY_USERNAME"
registryPassword = "HARBOR_REGISTRY_PASSWORD"
vulnerabilityHeader = "X-Accept-Vulnerabilities"
vulnerabilityHeaderValue = "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
skipTLSVerify = "HARBOR_SKIP_TLS"
certificate = "HARBOR_REGISTRY_CACERT"
registryDomain = "HARBOR_REGISTRY_DOMAIN"
)

//ServerCaller interface with client methods
type ServerCaller interface {
Do(req *http.Request) (*http.Response, error)
}

// harborScanner holds external harbor registry methods
type harborScanner interface {
prepareRequest(serverURL string) (*http.Request, error)
sendRequest(caller ServerCaller, req *http.Request) ([]byte, error)
getClient() *http.Client
}

//scanner implementor for harborScanner interface
type hscanner struct{}

//Harbor Harbor container registry
type Harbor struct {
scanner harborScanner
}

func init() {
RegisterContainerRegistry("harbor", &Harbor{
scanner: hscanner{},
})
}

//prepareRequest prepares http request
func (hscanner) prepareRequest(serverURL string) (*http.Request, error) {
username := os.Getenv(registryUsername)
password := os.Getenv(registryPassword)

if username == "" || password == "" {
errorMsg := fmt.Errorf("incomplete authentication details")
zap.S().Error(errorMsg)
return nil, errorMsg
}

req, err := http.NewRequest(http.MethodGet, serverURL, nil)
if err != nil {
errorMsg := fmt.Errorf("error creating harbor server request: %v", err)
zap.S().Error(errorMsg)
return nil, errorMsg
}

req.Header.Add("accept", "application/json")
req.Header.Add(vulnerabilityHeader, vulnerabilityHeaderValue)

req.SetBasicAuth(username, password)

return req, nil
}

//getClient prepares http client
func (hscanner) getClient() *http.Client {
tlsConfig := &tls.Config{}

skipTLS := os.Getenv(skipTLSVerify)
if skipTLS != "" {
if strings.EqualFold(skipTLS, "true") {
tlsConfig.InsecureSkipVerify = true
}
}

cert := os.Getenv(certificate)
if cert != "" {
caCert, err := ioutil.ReadFile(cert)
if err != nil {
errorMsg := fmt.Errorf("client certificate not found")
zap.S().Error(errorMsg)
} else {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
}
}

return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
}

//sendRequest sends request to harbor
func (hscanner) sendRequest(caller ServerCaller, req *http.Request) ([]byte, error) {
res, err := caller.Do(req)
if err != nil {
errorMsg := fmt.Errorf("error calling harbor server: %v", err)
zap.S().Error(errorMsg)
return nil, errorMsg
}

defer res.Body.Close()

body, err := ioutil.ReadAll(res.Body)
if err != nil {
errorMsg := fmt.Errorf("error reading response from harbor: %v", err)
zap.S().Error(errorMsg)
return nil, err
}

if res.StatusCode != http.StatusOK {
errMsg := fmt.Errorf(string(body))
return nil, errMsg
}

return body, nil
}

//CheckRegistry verify provided image belongs to harbor registry
func (h *Harbor) checkRegistry(image string) bool {
reg := os.Getenv(registryDomain)
host := GetDomain(image)
return reg != "" && strings.EqualFold(reg, host)
}

// GetVulnerabilities - get vulnerabilities from harbor registry
func (h *Harbor) getVulnerabilities(container output.ContainerDetails, options map[string]interface{}) (vulnerabilities []output.Vulnerability) {
results, err := h.ScanImage(container.Image)
if err != nil {
zap.S().Errorf("error finding vulnerabilities for image %s : %v", container.Image, err)
return
}
if temp, ok := results[vulnerabilityHeaderValue]; ok {
if tempData, ok := temp.(map[string]interface{}); ok {
if tempResults, ok := tempData["vulnerabilities"]; ok {
mapResults, _ := tempResults.([]interface{})
for _, result := range mapResults {
vulnerability := output.Vulnerability{}
if data, ok := result.(map[string]interface{}); ok {
vulnerability.PrepareFromHarborImageScan(data)
vulnerabilities = append(vulnerabilities, vulnerability)
}
}
}
}
}

return
}

// ScanImage get the image scan result from harbor registry
func (h *Harbor) ScanImage(image string) (result map[string]interface{}, err error) {
imageDetails := ImageDetails{}
imageDetails = GetImageDetails(image, imageDetails)
if imageDetails.Tag == "" {
imageDetails.Tag = defaultTagValue
}
reference := imageDetails.Tag
if imageDetails.Digest != "" {
reference = imageDetails.Digest
}

project, repository := getProjectNameAndRepository(imageDetails.Repository)

serverURL := fmt.Sprintf("https://%s/api/v2.0/projects/%s/repositories/%s/artifacts/%s/additions/vulnerabilities",
imageDetails.Registry,
project,
repository,
reference)

client := h.scanner.getClient()

req, err := h.scanner.prepareRequest(serverURL)
if err != nil {
return nil, err
}

data, err := h.scanner.sendRequest(client, req)
if err != nil {
return nil, err
}

if err := json.Unmarshal(data, &result); err != nil {
errorMsg := fmt.Errorf("error unmarshaling harbor server response : %v", err)
zap.S().Error(errorMsg)
return nil, errorMsg
}
return result, nil
}

//getProjectNameAndRepository fetch project and repo name
func getProjectNameAndRepository(name string) (string, string) {
parts := strings.Split(name, "/")
if len(parts) >= 2 {
return parts[0], parts[1]
}
return "", ""
}
Loading

0 comments on commit 56b6964

Please sign in to comment.