Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ability to output normalized resource config with errors #1134

Merged
merged 6 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions pkg/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import (
)

const (
humanOutputFormat = "human"
sarifOutputFormat = "sarif"
yamlOutputFormat = "yaml"
jsonOutputFormat = "json"
)

// ScanOptions represents scan command and its optional flags
Expand Down Expand Up @@ -66,6 +66,9 @@ type ScanOptions struct {
// configOnly will output resource config (should only be used for debugging purposes)
configOnly bool

// configWithError will output resource config and encountered errors
configWithError bool

// config file path
configFile string

Expand Down Expand Up @@ -148,13 +151,12 @@ func (s *ScanOptions) Init() error {
// validate config only for human readable output
// rest command options are validated by the executor
func (s ScanOptions) validate() error {
// human readable output doesn't support --config-only flag
// if --config-only flag is set, then exit with an error
// human readable output doesn't support --config-only and --config-with-error flag
// if --config-only/--config-with-error flag is set, then exit with an error
// asking the user to use yaml or json output format
if s.configOnly && strings.EqualFold(s.outputType, humanOutputFormat) {
return errors.New("please use yaml or json output format when using --config-only flag")
if (s.configOnly || s.configWithError) && !(strings.EqualFold(s.outputType, yamlOutputFormat) || strings.EqualFold(s.outputType, jsonOutputFormat)) {
return errors.New("please use yaml or json output format when using --config-only or --config-with-error flags")
}

return nil
}

Expand Down Expand Up @@ -207,7 +209,7 @@ func (s *ScanOptions) Run() error {
}

// executor output
results, err := executor.Execute(s.configOnly)
results, err := executor.Execute(s.configOnly, s.configWithError)
if err != nil {
return err
}
Expand Down Expand Up @@ -258,6 +260,10 @@ func (s ScanOptions) writeResults(results runtime.Output) error {
return writer.Write(s.outputType, results.ResourceConfig, outputWriter)
}

if s.configWithError {
return writer.Write(s.outputType, results, outputWriter)
}

// add verbose flag to the scan summary
results.Violations.ViolationStore.Summary.ShowViolationDetails = s.verbose

Expand Down
50 changes: 44 additions & 6 deletions pkg/cli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ func TestRun(t *testing.T) {
},
wantErr: true,
},
{
name: "config-with-error flag terraform",
scanOptions: &ScanOptions{
policyType: []string{"all"},
iacFilePath: testTerraformFilePath,
configWithError: true,
outputType: "json",
},
},
}

for _, tt := range table {
Expand Down Expand Up @@ -475,9 +484,10 @@ func TestScanOptionsInitColor(t *testing.T) {

func TestScanOptionsInit(t *testing.T) {
type fields struct {
configOnly bool
outputType string
useColors string
configOnly bool
configWithError bool
outputType string
useColors string
}
tests := []struct {
name string
Expand All @@ -501,13 +511,41 @@ func TestScanOptionsInit(t *testing.T) {
configOnly: false,
},
},
{
name: "init fail for --config-with-error with human readable output",
fields: fields{
useColors: "auto",
outputType: "human",
configWithError: true,
},
wantErr: true,
},
{
name: "init success for --config-with-error with yaml readable output",
fields: fields{
useColors: "auto",
outputType: "yaml",
configWithError: true,
},
wantErr: false,
},
{
name: "init success for --config-with-error with json readable output",
fields: fields{
useColors: "auto",
outputType: "json",
configWithError: true,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &ScanOptions{
configOnly: tt.fields.configOnly,
outputType: tt.fields.outputType,
useColors: tt.fields.useColors,
configOnly: tt.fields.configOnly,
configWithError: tt.fields.configWithError,
outputType: tt.fields.outputType,
useColors: tt.fields.useColors,
}
if err := s.Init(); (err != nil) != tt.wantErr {
t.Errorf("ScanOptions.Init() error = %v, wantErr %v", err, tt.wantErr)
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func init() {
scanCmd.Flags().StringVarP(&scanOptions.remoteType, "remote-type", "r", "", "type of remote backend (git, s3, gcs, http, terraform-registry)")
scanCmd.Flags().StringVarP(&scanOptions.remoteURL, "remote-url", "u", "", "url pointing to remote IaC repository")
scanCmd.Flags().BoolVarP(&scanOptions.configOnly, "config-only", "", false, "will output resource config (should only be used for debugging purposes)")
scanCmd.Flags().BoolVarP(&scanOptions.configWithError, "config-with-error", "", false, "will output resource config and errors (if any)")
// flag passes a string, but we normalize to bool in PreRun
scanCmd.Flags().StringVar(&scanOptions.useColors, "use-colors", "auto", "color output (auto, t, f)")
scanCmd.Flags().BoolVarP(&scanOptions.verbose, "verbose", "v", false, "will show violations with details (applicable for default output)")
Expand Down
20 changes: 17 additions & 3 deletions pkg/http-server/file-scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
showPassed = false
findVulnerabilities = false
categories = []string{}
configWithError = false
)

// parse multipart form, 10 << 20 specifies maximum upload of 10 MB files
Expand Down Expand Up @@ -133,6 +134,17 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
return
}
}
// read config_with_error from the form data
configWithErrorValue := r.FormValue("config_with_error")
if configWithErrorValue != "" {
configWithError, err = strconv.ParseBool(configWithErrorValue)
if err != nil {
errMsg := fmt.Sprintf("error while reading 'config_with_error' value. error: '%v'", err)
zap.S().Error(errMsg)
apiErrorResponse(w, errMsg, http.StatusBadRequest)
return
}
}

// read show_passed from the form data
showPassedValue := r.FormValue("show_passed")
Expand Down Expand Up @@ -176,7 +188,7 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
apiErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
normalized, err := executor.Execute(configOnly)
normalized, err := executor.Execute(configOnly, configWithError)
if err != nil {
errMsg := fmt.Sprintf("failed to scan uploaded file. error: '%v'", err)
zap.S().Error(errMsg)
Expand All @@ -186,8 +198,10 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {

var output interface{}

// if config only, return resource config else return violations
if configOnly {
// if config-with-error return config as well as dir errors,for config only, return resource config else return violations
if configWithError {
output = normalized
} else if configOnly {
output = normalized.ResourceConfig
} else {
if !showPassed {
Expand Down
33 changes: 33 additions & 0 deletions pkg/http-server/file-scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func TestUpload(t *testing.T) {
findVulnerabilities bool
notificationWebhookURL string
notificationWebhookToken string
configWithError bool
invalidConfigWithError bool
}{
{
name: "valid file scan",
Expand Down Expand Up @@ -273,6 +275,25 @@ func TestUpload(t *testing.T) {
notificationWebhookURL: "https://httpbin.org/post",
notificationWebhookToken: "token",
},
{
name: "test for config with erorr invalid",
path: testFilePath,
param: testParamName,
iacType: testIacType,
cloudType: testCloudType,
wantStatus: http.StatusBadRequest,
invalidConfigWithError: true,
},
{
name: "test for config with eror",
path: testFilePath,
param: testParamName,
iacType: testIacType,
cloudType: testCloudType,
wantStatus: http.StatusOK,
invalidConfigWithError: false,
configWithError: true,
},
}

for _, tt := range table {
Expand Down Expand Up @@ -378,6 +399,18 @@ func TestUpload(t *testing.T) {
}
}

if !tt.invalidConfigWithError {
if err = writer.WriteField("config_with_error", strconv.FormatBool(tt.configWithError)); err != nil {
writer.Close()
t.Error(err)
}
} else {
if err = writer.WriteField("config_with_error", "invalid"); err != nil {
writer.Close()
t.Error(err)
}
}

writer.Close()

// http request of the type "/v1/{iacType}/{iacVersion}/{cloudType}/file/scan"
Expand Down
9 changes: 6 additions & 3 deletions pkg/http-server/remote-repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type scanRemoteRepoReq struct {
NotificationWebhookURL string `json:"webhook_url"`
NotificationWebhookToken string `json:"webhook_token"`
RepoRef string `json:"repo-ref"`
ConfigWithError bool `json:"config_with_error"`
}

// scanRemoteRepo downloads the remote Iac repository and scans it for
Expand Down Expand Up @@ -139,15 +140,17 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType
}

// evaluate policies IaC for violations
results, err := executor.Execute(s.ConfigOnly)
results, err := executor.Execute(s.ConfigOnly, s.ConfigWithError)
if err != nil {
errMsg := fmt.Sprintf("failed to scan uploaded file. error: '%v'", err)
zap.S().Error(errMsg)
return output, isAdmissionDenied, err
}

// if config only, return only config else return only violations
if s.ConfigOnly {
// if config-with-error return config as well as dir errors,for config only, return resource config else return violations
if s.ConfigWithError {
output = results
} else if s.ConfigOnly {
output = results.ResourceConfig
} else {
// set remote url in case remote repo is scanned
Expand Down
51 changes: 32 additions & 19 deletions pkg/http-server/remote-repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,19 @@ func TestScanRemoteRepoHandler(t *testing.T) {
testCloudType := "aws"

table := []struct {
name string
iacType string
iacVersion string
cloudType string
remoteURL string
remoteType string
scanRules []string
skipRules []string
showPassed bool
configOnly bool
nonRecursive bool
wantStatus int
name string
iacType string
iacVersion string
cloudType string
remoteURL string
remoteType string
scanRules []string
skipRules []string
showPassed bool
configOnly bool
configWithError bool
nonRecursive bool
wantStatus int
}{
{
name: "empty url and type",
Expand Down Expand Up @@ -174,6 +175,17 @@ func TestScanRemoteRepoHandler(t *testing.T) {
configOnly: true,
wantStatus: http.StatusOK,
},
{
name: "test show config with error",
iacType: testIacType,
iacVersion: testIacVersion,
cloudType: testCloudType,
remoteURL: validRepo,
remoteType: "git",
showPassed: false,
configWithError: true,
wantStatus: http.StatusOK,
},
}

for _, tt := range table {
Expand All @@ -186,13 +198,14 @@ func TestScanRemoteRepoHandler(t *testing.T) {

// request body
s := scanRemoteRepoReq{
RemoteURL: tt.remoteURL,
RemoteType: tt.remoteType,
ScanRules: tt.scanRules,
SkipRules: tt.skipRules,
ShowPassed: tt.showPassed,
ConfigOnly: tt.configOnly,
NonRecursive: tt.nonRecursive,
RemoteURL: tt.remoteURL,
RemoteType: tt.remoteType,
ScanRules: tt.scanRules,
SkipRules: tt.skipRules,
ShowPassed: tt.showPassed,
ConfigOnly: tt.configOnly,
ConfigWithError: tt.configWithError,
NonRecursive: tt.nonRecursive,
}
reqBody, _ := json.Marshal(s)

Expand Down
2 changes: 1 addition & 1 deletion pkg/k8s/admission-webhook/validating-webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func (w ValidatingWebhook) scanK8sFile(filePath string) (runtime.Output, error)
return result, err
}

result, err = executor.Execute(false)
result, err = executor.Execute(false, false)
if err != nil {
zap.S().Error("failed to scan resource object. error: '%v'", err)
return result, err
Expand Down
12 changes: 11 additions & 1 deletion pkg/runtime/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/accurics/terrascan/pkg/iac-providers/output"
"github.com/accurics/terrascan/pkg/notifications"
"github.com/accurics/terrascan/pkg/policy"
res "github.com/accurics/terrascan/pkg/results"
"github.com/hashicorp/go-multierror"
)

Expand Down Expand Up @@ -204,7 +205,7 @@ func (e *Executor) initPolicyEngines() (err error) {
}

// Execute validates the inputs, processes the IaC, creates json output
func (e *Executor) Execute(configOnly bool) (results Output, err error) {
func (e *Executor) Execute(configOnly, configWithError bool) (results Output, err error) {

var merr *multierror.Error
var resourceConfig output.AllResourceConfigs
Expand Down Expand Up @@ -238,6 +239,15 @@ func (e *Executor) Execute(configOnly bool) (results Output, err error) {
results.ResourceConfig = e.fetchVulnerabilities(&results, options)
}

if configWithError {
results.Violations.ViolationStore = res.NewViolationStore()
if err := merr.ErrorOrNil(); err != nil {
sort.Sort(merr)
results.Violations.ViolationStore.AddLoadDirErrors(merr.WrappedErrors())
}
return results, nil
}

if configOnly {
return results, nil
}
Expand Down
Loading