Skip to content

Commit

Permalink
adds e2e tests for api server (#585)
Browse files Browse the repository at this point in the history
* 1. server e2e tests
2. fix minor defects
3. make api server port configurable

* fix failing tests

* incorporate review comment
  • Loading branch information
patilpankaj212 committed Mar 5, 2021
1 parent 6728908 commit bbb4a56
Show file tree
Hide file tree
Showing 14 changed files with 902 additions and 15 deletions.
4 changes: 2 additions & 2 deletions pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ Initializes Terrascan and clones policies from the Terrascan GitHub repository.
SilenceErrors: true,
}

func initial(cmd *cobra.Command, args []string, isScanCmd bool) error {
func initial(cmd *cobra.Command, args []string, nonInitCmd bool) error {
// initialize terrascan
if err := initialize.Run(isScanCmd); err != nil {
if err := initialize.Run(nonInitCmd); err != nil {
zap.S().Errorf("failed to initialize terrascan. error : %v", err)
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var serverCmd = &cobra.Command{
Run Terrascan as an API server that inspects incoming IaC (Infrastructure-as-Code) files and returns the scan results.
`,
PreRun: func(cmd *cobra.Command, args []string) {
initial(cmd, args, false)
initial(cmd, args, true)
},
Run: server,
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config-reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func NewTerrascanConfigReader(fileName string) (*TerrascanConfigReader, error) {
// return error if file doesn't exist
_, err := os.Stat(fileName)
if err != nil {
zap.S().Error("config file: %s, doesn't exist", fileName)
zap.S().Errorf("config file: %s, doesn't exist", fileName)
return configReader, ErrNotPresent
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/http-server/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ const (

// APIVersion - default api version for REST endpoints
APIVersion = "v1"

// TerrascanServerPort allows user to configure server at a port other than default
TerrascanServerPort = "TERRASCAN_SERVER_PORT"
)
20 changes: 19 additions & 1 deletion pkg/http-server/file-scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strconv"
"strings"

"github.com/accurics/terrascan/pkg/config"
"github.com/accurics/terrascan/pkg/runtime"
"github.com/accurics/terrascan/pkg/utils"
"github.com/gorilla/mux"
Expand Down Expand Up @@ -139,7 +140,7 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
tempFile.Name(), "", "", []string{"./testdata/testpolicies"}, scanRules, skipRules, severity)
} else {
executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType,
tempFile.Name(), "", "", []string{}, scanRules, skipRules, severity)
tempFile.Name(), "", "", getPolicyPathFromEnv(), scanRules, skipRules, severity)
}
if err != nil {
zap.S().Error(err)
Expand Down Expand Up @@ -178,3 +179,20 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
// return that we have successfully uploaded our file!
apiResponse(w, string(j), http.StatusOK)
}

// getPolicyPathFromEnv reads the TERRASCAN_CONFIG env variable (if present) and returns the policy path
func getPolicyPathFromEnv() []string {
policyPath := []string{}

// read policy path from TERRASCAN_CONFIG env variable
terrascanConfigFile := os.Getenv("TERRASCAN_CONFIG")
if terrascanConfigFile != "" {
terrascanConfigReader, err := config.NewTerrascanConfigReader(terrascanConfigFile)
if err != nil {
zap.S().Errorf("error while reading config file, %s; err %v", terrascanConfigFile, err)
} else {
policyPath = append(policyPath, terrascanConfigReader.GetPolicyConfig().RepoPath)
}
}
return policyPath
}
2 changes: 1 addition & 1 deletion pkg/http-server/remote-repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (g *APIHandler) scanRemoteRepo(w http.ResponseWriter, r *http.Request) {
results, err = s.ScanRemoteRepo(iacType, iacVersion, cloudType, []string{"./testdata/testpolicies"})

} else {
results, err = s.ScanRemoteRepo(iacType, iacVersion, cloudType, []string{})
results, err = s.ScanRemoteRepo(iacType, iacVersion, cloudType, getPolicyPathFromEnv())
}
if err != nil {
apiErrorResponse(w, err.Error(), http.StatusBadRequest)
Expand Down
13 changes: 9 additions & 4 deletions pkg/http-server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ func Start() {
// get all routes
routes := server.Routes()

serverPort := os.Getenv(TerrascanServerPort)
if serverPort == "" {
serverPort = GatewayDefaultPort
}

// register routes and start the http server
server.start(routes)
server.start(routes, serverPort)
}

// start http server
func (g *APIServer) start(routes []*Route) {
func (g *APIServer) start(routes []*Route, port string) {

var (
err error
Expand All @@ -58,7 +63,7 @@ func (g *APIServer) start(routes []*Route) {

// start http server
server := &http.Server{
Addr: ":" + GatewayDefaultPort,
Addr: ":" + port,
Handler: router,
}

Expand All @@ -68,7 +73,7 @@ func (g *APIServer) start(routes []*Route) {
logger.Fatal(err)
}
}()
logger.Infof("http server listening at port %v", GatewayDefaultPort)
logger.Infof("http server listening at port %v", port)

// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/server/golden/server_typo_help.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Error: unknown command "servre" for "terrascan"

Did you mean this?
server

Run 'terrascan --help' for usage.
245 changes: 245 additions & 0 deletions test/e2e/server/server_file_scan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package server_test

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"

"github.com/accurics/terrascan/pkg/iac-providers/output"
"github.com/accurics/terrascan/pkg/policy"
serverUtils "github.com/accurics/terrascan/test/e2e/server"
"github.com/accurics/terrascan/test/helper"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/gexec"
)

var _ = Describe("Server File Scan", func() {

var session *gexec.Session
var outWriter, errWriter io.Writer = gbytes.NewBuffer(), gbytes.NewBuffer()
port := "9012"

Context("file scan tests", func() {

JustBeforeEach(func() {
os.Setenv(terrascanServerPort, port)
})
JustAfterEach(func() {
os.Setenv(terrascanServerPort, "")
})

// launches a server session on port 9012
It("should start a new server session", func() {
session = helper.RunCommand(terrascanBinaryPath, outWriter, errWriter, serverUtils.ServerCommand)
Eventually(session.Err, serverUtils.ServerCommandTimeout).Should(gbytes.Say(fmt.Sprintf("http server listening at port %s", port)))
})

Context("terraform file scan", func() {
requestURL := fmt.Sprintf("%s:%s/v1/terraform/v12/all/local/file/scan", host, port)
When("iac file violates aws_db_instance_violation", func() {
It("should return violations successfully", func() {
iacFilePath, err := filepath.Abs(filepath.Join("..", "test_data", "iac", "aws", "aws_db_instance_violation", "main.tf"))
Expect(err).NotTo(HaveOccurred())

goldenFilePath, err := filepath.Abs(filepath.Join("..", "scan", "golden", "terraform_scans", "aws", "aws_db_instance_violations", "aws_db_instance_json.txt"))
Expect(err).NotTo(HaveOccurred())

responseBytes := serverUtils.MakeFileScanRequest(iacFilePath, requestURL, nil, http.StatusOK)
serverUtils.CompareResponseAndGoldenOutput(goldenFilePath, responseBytes)
})
})
When("iac file violates aws_ami", func() {
It("should return violations successfully", func() {
iacFilePath, err := filepath.Abs(filepath.Join("..", "test_data", "iac", "aws", "aws_ami_violation", "main.tf"))
Expect(err).NotTo(HaveOccurred())

goldenFilePath, err := filepath.Abs(filepath.Join("..", "scan", "golden", "terraform_scans", "aws", "aws_ami_violations", "aws_ami_violation_json.txt"))
Expect(err).NotTo(HaveOccurred())

responseBytes := serverUtils.MakeFileScanRequest(iacFilePath, requestURL, nil, http.StatusOK)
serverUtils.CompareResponseAndGoldenOutput(goldenFilePath, responseBytes)
})
})

Context("iac provider or iac version is invalid", func() {
errMessage := "iac type or version not supported"
iacFilePath, err := filepath.Abs(filepath.Join("..", "test_data", "iac", "aws", "aws_db_instance_violation", "main.tf"))

When("iac provider is invalid", func() {
It("should get and error response", func() {
requestURL := fmt.Sprintf("%s:%s/v1/%s/v12/all/local/file/scan", host, port, "terra")
Expect(err).NotTo(HaveOccurred())

serverUtils.MakeFileScanRequest(iacFilePath, requestURL, nil, http.StatusBadRequest)
Eventually(session.Err).Should(gbytes.Say(errMessage))
})
})
When("iac version is invalid", func() {
It("should get and error response", func() {
requestURL := fmt.Sprintf("%s:%s/v1/terraform/%s/all/local/file/scan", host, port, "v2")
Expect(err).NotTo(HaveOccurred())

serverUtils.MakeFileScanRequest(iacFilePath, requestURL, nil, http.StatusBadRequest)
Eventually(session.Err).Should(gbytes.Say(errMessage))
})
})
})

Context("body attributes present in the request", func() {
awsAmiIacFilePath, _ := filepath.Abs(filepath.Join("..", "test_data", "iac", "aws", "aws_ami_violation", "main.tf"))

When("config_only attribute is set", func() {

It("should receive resource config in response", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["config_only"] = "true"

responseBytes := serverUtils.MakeFileScanRequest(awsAmiIacFilePath, requestURL, bodyAttrs, http.StatusOK)

var responseResourceConfig output.AllResourceConfigs
err := json.Unmarshal(responseBytes, &responseResourceConfig)
Expect(err).NotTo(HaveOccurred())

Expect(responseResourceConfig).To(HaveLen(1))
// the iac file only contains aws_ami resource
Expect(responseResourceConfig).To(HaveKey("aws_ami"))
})
})

When("show_passed attribute is set", func() {
It("should receive passed rules along with violations", func() {
iacFilePath, err := filepath.Abs(filepath.Join("..", "test_data", "iac", "aws", "aws_db_instance_violation", "main.tf"))
Expect(err).NotTo(HaveOccurred())

goldenFilePath, err := filepath.Abs(filepath.Join("..", "scan", "golden", "terraform_scans", "aws", "aws_db_instance_violations", "aws_db_instance_json_show_passed.txt"))
Expect(err).NotTo(HaveOccurred())

bodyAttrs := make(map[string]string)
bodyAttrs["show_passed"] = "true"
responseBytes := serverUtils.MakeFileScanRequest(iacFilePath, requestURL, bodyAttrs, http.StatusOK)
serverUtils.CompareResponseAndGoldenOutput(goldenFilePath, responseBytes)
})
})

Context("unknown body attributes are present", func() {
Context("api server ignores unknown attributes", func() {
It("should receive violations and 200 OK resopnse", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["unknown_attribute"] = "someValue"

serverUtils.MakeFileScanRequest(awsAmiIacFilePath, requestURL, bodyAttrs, http.StatusOK)
})
})
})

Context("body attributes have invalid value", func() {
When("config_only has invalid value", func() {
It("should receive an error", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["config_only"] = "invalid"

serverUtils.MakeFileScanRequest(awsAmiIacFilePath, requestURL, bodyAttrs, http.StatusBadRequest)
Eventually(session.Err, serverUtils.ServerCommandTimeout).Should(gbytes.Say(`error while reading 'config_only' value. error: 'strconv.ParseBool: parsing "invalid": invalid syntax'`))
})
})

When("show_passed has invalid value", func() {
It("should receive an error", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["show_passed"] = "invalid"

serverUtils.MakeFileScanRequest(awsAmiIacFilePath, requestURL, bodyAttrs, http.StatusBadRequest)
Eventually(session.Err, serverUtils.ServerCommandTimeout).Should(gbytes.Say(`error while reading 'show_passed' value. error: 'strconv.ParseBool: parsing "invalid": invalid syntax'`))
})
})
})
})
})

Context("k8s file scan", func() {
It("should return violations successfully", func() {
Skip("add test case when https://github.com/accurics/terrascan/issues/584 is fixed")
})
})

Context("rules filtering options for file scan", func() {
requestURL := fmt.Sprintf("%s:%s/v1/terraform/v12/all/local/file/scan", host, port)
iacFilePath, _ := filepath.Abs(filepath.Join("..", "test_data", "iac", "aws", "aws_db_instance_violation", "main.tf"))

When("scan_rules is used", func() {
It("should receive violations and 200 OK resopnse", func() {

bodyAttrs := make(map[string]string)
bodyAttrs["scan_rules"] = "AWS.RDS.DS.High.1041"
responseBytes := serverUtils.MakeFileScanRequest(iacFilePath, requestURL, bodyAttrs, http.StatusOK)

var responseEngineOutput policy.EngineOutput
err := json.Unmarshal(responseBytes, &responseEngineOutput)
Expect(err).NotTo(HaveOccurred())

Expect(responseEngineOutput.ViolationStore.Summary.TotalPolicies).To(BeIdenticalTo(1))
})
})

When("skip_rules is used", func() {

It("should receive violations and 200 OK response", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["skip_rules"] = "AWS.RDS.DataSecurity.High.0577"
responseBytes := serverUtils.MakeFileScanRequest(iacFilePath, requestURL, bodyAttrs, http.StatusOK)

var responseEngineOutput policy.EngineOutput
err := json.Unmarshal(responseBytes, &responseEngineOutput)
Expect(err).NotTo(HaveOccurred())

// There are total 7 rules in the test policies directory, out of which 1 is skipped
Expect(responseEngineOutput.ViolationStore.Summary.TotalPolicies).To(BeIdenticalTo(6))
})
})

When("scan and skip rules is used", func() {
It("should receive violations and 200 OK response", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["scan_rules"] = "AWS.RDS.DS.High.1041,AWS.AWS RDS.NS.High.0101,AWS.RDS.DataSecurity.High.0577"
bodyAttrs["skip_rules"] = "AWS.RDS.DataSecurity.High.0577"
responseBytes := serverUtils.MakeFileScanRequest(iacFilePath, requestURL, bodyAttrs, http.StatusOK)

var responseEngineOutput policy.EngineOutput
err := json.Unmarshal(responseBytes, &responseEngineOutput)
Expect(err).NotTo(HaveOccurred())

// Total rules to be validated would be (scan_rules count - skip_rules count)
Expect(responseEngineOutput.ViolationStore.Summary.TotalPolicies).To(BeIdenticalTo(2))
})
})

When("severity is used", func() {
awsAmiIacFilePath, _ := filepath.Abs(filepath.Join("..", "test_data", "iac", "aws", "aws_ami_violation", "main.tf"))

When("severity is invalid", func() {
It("should receive a 400 bad request", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["severity"] = "1"

serverUtils.MakeFileScanRequest(awsAmiIacFilePath, requestURL, bodyAttrs, http.StatusBadRequest)
Eventually(session.Err, serverUtils.ServerCommandTimeout).Should(gbytes.Say("severity level not supported"))
})
})

When("severity is valid", func() {
It("should receive violations result with 200 OK response", func() {
bodyAttrs := make(map[string]string)
bodyAttrs["severity"] = "HIGH"

serverUtils.MakeFileScanRequest(awsAmiIacFilePath, requestURL, bodyAttrs, http.StatusOK)
})
})
})
})
})
})

0 comments on commit bbb4a56

Please sign in to comment.