Skip to content

Commit

Permalink
adds support to display passed rules (#572)
Browse files Browse the repository at this point in the history
* changes to show passed rules

* show passed for server mode

* modified applicable e2e tests, added e2e test for --show-passed

* --show-passed flag's help modified
  • Loading branch information
patilpankaj212 committed Mar 5, 2021
1 parent 065e010 commit 6728908
Show file tree
Hide file tree
Showing 28 changed files with 581 additions and 95 deletions.
14 changes: 11 additions & 3 deletions pkg/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ type ScanOptions struct {

// severity is the level of severity of policy violations that should be reported
severity string
// Verbose indicates whether to display all fields in default human readlbe output
Verbose bool

// verbose indicates whether to display all fields in default human readlbe output
verbose bool

// showPassedRules indicates whether to display passed rules or not
showPassedRules bool
}

// NewScanOptions returns a new pointer to ScanOptions
Expand Down Expand Up @@ -212,7 +216,11 @@ func (s *ScanOptions) downloadRemoteRepository(tempDir string) error {

func (s ScanOptions) writeResults(results runtime.Output) error {
// add verbose flag to the scan summary
results.Violations.ViolationStore.Summary.ShowViolationDetails = s.Verbose
results.Violations.ViolationStore.Summary.ShowViolationDetails = s.verbose

if !s.showPassedRules {
results.Violations.ViolationStore.PassedRules = nil
}

outputWriter := NewOutputWriter(s.UseColors)

Expand Down
9 changes: 9 additions & 0 deletions pkg/cli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ func TestRun(t *testing.T) {
outputType: "human",
},
},
{
name: "normal k8s run with successful output for junit-xml with passed tests",
scanOptions: &ScanOptions{
policyType: []string{"k8s"},
iacDirPath: kustomizeTestDirPath,
outputType: "junit-xml",
showPassedRules: true,
},
},
{
name: "config-only flag terraform",
scanOptions: &ScanOptions{
Expand Down
3 changes: 2 additions & 1 deletion pkg/cli/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ func init() {
scanCmd.Flags().BoolVarP(&scanOptions.configOnly, "config-only", "", false, "will output resource config (should only be used for debugging purposes)")
// 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)")
scanCmd.Flags().BoolVarP(&scanOptions.verbose, "verbose", "v", false, "will show violations with details (applicable for default output)")
scanCmd.Flags().StringSliceVarP(&scanOptions.scanRules, "scan-rules", "", []string{}, "one or more rules to scan (example: --scan-rules=\"ruleID1,ruleID2\")")
scanCmd.Flags().StringSliceVarP(&scanOptions.skipRules, "skip-rules", "", []string{}, "one or more rules to skip while scanning (example: --skip-rules=\"ruleID1,ruleID2\")")
scanCmd.Flags().StringVar(&scanOptions.severity, "severity", "", "minimum severity level of the policy violations to be reported by terrascan")
scanCmd.Flags().BoolVarP(&scanOptions.showPassedRules, "show-passed", "", false, "display passed rules, along with violations")
RegisterCommand(rootCmd, scanCmd)
}
17 changes: 17 additions & 0 deletions pkg/http-server/file-scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
scanRules = []string{}
skipRules = []string{}
configOnly = false
showPassed = false
)

// parse multipart form, 10 << 20 specifies maximum upload of 10 MB files
Expand Down Expand Up @@ -107,6 +108,18 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
}
}

// read show_passed from the form data
showPassedValue := r.FormValue("show_passed")
if showPassedValue != "" {
showPassed, err = strconv.ParseBool(showPassedValue)
if err != nil {
errMsg := fmt.Sprintf("error while reading 'show_passed' value. error: '%v'", err)
zap.S().Error(errMsg)
apiErrorResponse(w, errMsg, http.StatusBadRequest)
return
}
}

if scanRulesValue != "" {
scanRules = strings.Split(scanRulesValue, ",")
}
Expand Down Expand Up @@ -143,6 +156,10 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {

var output interface{}

if !showPassed {
normalized.Violations.ViolationStore.PassedRules = nil
}

// if config only, return resource config else return violations
if configOnly {
output = normalized.ResourceConfig
Expand Down
32 changes: 32 additions & 0 deletions pkg/http-server/file-scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func TestUpload(t *testing.T) {
severity string
configOnly bool
invalidConfigOnly bool
showPassed bool
invalidShowPassed bool
wantStatus int
}{
{
Expand Down Expand Up @@ -199,6 +201,24 @@ func TestUpload(t *testing.T) {
wantStatus: http.StatusBadRequest,
invalidConfigOnly: true,
},
{
name: "test for show passed attribute",
path: testFilePath,
param: testParamName,
iacType: testIacType,
cloudType: testCloudType,
showPassed: true,
wantStatus: http.StatusOK,
},
{
name: "test for invalid show_passed value",
path: testFilePath,
param: testParamName,
iacType: testIacType,
cloudType: testCloudType,
invalidShowPassed: true,
wantStatus: http.StatusBadRequest,
},
}

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

if !tt.invalidShowPassed {
if err = writer.WriteField("show_passed", strconv.FormatBool(tt.showPassed)); err != nil {
writer.Close()
t.Error(err)
}
} else {
if err = writer.WriteField("show_passed", "invalid"); err != nil {
writer.Close()
t.Error(err)
}
}

writer.Close()

// http request of the type "/v1/{iacType}/{iacVersion}/{cloudType}/file/scan"
Expand Down
5 changes: 5 additions & 0 deletions pkg/http-server/remote-repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type scanRemoteRepoReq struct {
ScanRules []string `json:"scan_rules"`
SkipRules []string `json:"skip_rules"`
Severity string `json:"severity"`
ShowPassed bool `json:"show_passed"`
d downloader.Downloader
}

Expand Down Expand Up @@ -129,6 +130,10 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType
return output, err
}

if !s.ShowPassed {
results.Violations.ViolationStore.PassedRules = nil
}

// if config only, return only config else return only violations
if s.ConfigOnly {
output = results.ResourceConfig
Expand Down
15 changes: 15 additions & 0 deletions pkg/http-server/remote-repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ func TestScanRemoteRepoHandler(t *testing.T) {
remoteType string
scanRules []string
skipRules []string
showPassed bool
configOnly bool
wantStatus int
}{
{
Expand Down Expand Up @@ -145,6 +147,17 @@ func TestScanRemoteRepoHandler(t *testing.T) {
skipRules: []string{"AWS.CloudFront.Network Security.Low.0568"},
wantStatus: http.StatusOK,
},
{
name: "test show passed rules and config only",
iacType: testIacType,
iacVersion: testIacVersion,
cloudType: testCloudType,
remoteURL: validRepo,
remoteType: "git",
showPassed: true,
configOnly: true,
wantStatus: http.StatusOK,
},
}

for _, tt := range table {
Expand All @@ -161,6 +174,8 @@ func TestScanRemoteRepoHandler(t *testing.T) {
RemoteType: tt.remoteType,
ScanRules: tt.scanRules,
SkipRules: tt.skipRules,
ShowPassed: tt.showPassed,
ConfigOnly: tt.configOnly,
}
reqBody, _ := json.Marshal(s)

Expand Down
15 changes: 15 additions & 0 deletions pkg/policy/opa/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,19 @@ func (e *Engine) reportViolation(regoData *RegoData, resource *output.ResourceCo
}
}

// reportPassed Adds a passed rule which wasn't violated by all the resources
func (e *Engine) reportPassed(regoData *RegoData) {
passedRule := results.PassedRule{
RuleName: regoData.Metadata.Name,
Description: regoData.Metadata.Description,
RuleID: regoData.Metadata.ReferenceID,
Severity: regoData.Metadata.Severity,
Category: regoData.Metadata.Category,
}

e.results.ViolationStore.AddPassedRule(&passedRule)
}

// Evaluate Executes compiled OPA queries against the input JSON data
func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput, error) {
// Keep track of how long it takes to evaluate the policies
Expand All @@ -343,6 +356,8 @@ func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput,
resourceViolations := rs[0].Expressions[0].Value.([]interface{})
if len(resourceViolations) == 0 {
zap.S().Debug("query executed but found no violations", zap.Error(err), zap.String("rule", "'"+k+"'"))
// add the passed rule
e.reportPassed(e.regoDataMap[k])
continue
}

Expand Down
1 change: 1 addition & 0 deletions pkg/policy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func (me EngineOutput) AsViolationStore() results.ViolationStore {
return results.ViolationStore{
Violations: me.Violations,
SkippedViolations: me.SkippedViolations,
PassedRules: me.PassedRules,
Summary: me.Summary,
}
}
11 changes: 11 additions & 0 deletions pkg/results/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func NewViolationStore() *ViolationStore {
return &ViolationStore{
Violations: []*Violation{},
SkippedViolations: []*Violation{},
PassedRules: []*PassedRule{},
}
}

Expand All @@ -42,3 +43,13 @@ func (s *ViolationStore) GetResults(isSkipped bool) []*Violation {
}
return s.Violations
}

// AddPassedRule Adds individual passed rule into the violation store
func (s *ViolationStore) AddPassedRule(rule *PassedRule) {
s.PassedRules = append(s.PassedRules, rule)
}

// GetPassedRules Retrieves all passed rules from the violation store
func (s *ViolationStore) GetPassedRules() []*PassedRule {
return s.PassedRules
}
20 changes: 16 additions & 4 deletions pkg/results/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,21 @@ type Violation struct {
LineNumber int `json:"line" yaml:"line" xml:"line,attr"`
}

// PassedRule contains information of a passed rule
type PassedRule struct {
RuleName string `json:"rule_name" yaml:"rule_name" xml:"rule_name,attr"`
Description string `json:"description" yaml:"description" xml:"description,attr"`
RuleID string `json:"rule_id" yaml:"rule_id" xml:"rule_id,attr"`
Severity string `json:"severity" yaml:"severity" xml:"severity,attr"`
Category string `json:"category" yaml:"category" xml:"category,attr"`
}

// ViolationStore Storage area for violation data
type ViolationStore struct {
Violations []*Violation `json:"violations" yaml:"violations" xml:"violations>violation"`
SkippedViolations []*Violation `json:"skipped_violations" yaml:"skipped_violations" xml:"skipped_violations>violation"`
Summary ScanSummary `json:"scan_summary" yaml:"scan_summary" xml:"scan_summary"`
PassedRules []*PassedRule `json:"passed_rules,omitempty" yaml:"passed_rules,omitempty" xml:"passed_rules>passed_rule,omitempty"`
Violations []*Violation `json:"violations" yaml:"violations" xml:"violations>violation"`
SkippedViolations []*Violation `json:"skipped_violations" yaml:"skipped_violations" xml:"skipped_violations>violation"`
Summary ScanSummary `json:"scan_summary" yaml:"scan_summary" xml:"scan_summary"`
}

// ScanSummary will hold the default scan summary data
Expand All @@ -55,14 +65,16 @@ type ScanSummary struct {
LowCount int `json:"low" yaml:"low" xml:"low,attr"`
MediumCount int `json:"medium" yaml:"medium" xml:"medium,attr"`
HighCount int `json:"high" yaml:"high" xml:"high,attr"`
TotalTime int64 `json:"-" yaml:"-" xml:"-"`
// field TotalTime is added for junit-xml output
TotalTime int64 `json:"-" yaml:"-" xml:"-"`
}

// Add adds two ViolationStores
func (vs ViolationStore) Add(extra ViolationStore) ViolationStore {
// Just concatenate the slices, since order shouldn't be important
vs.Violations = append(vs.Violations, extra.Violations...)
vs.SkippedViolations = append(vs.SkippedViolations, extra.SkippedViolations...)
vs.PassedRules = append(vs.PassedRules, extra.PassedRules...)

// Add the scan summary
vs.Summary.LowCount += extra.Summary.LowCount
Expand Down
20 changes: 19 additions & 1 deletion pkg/writer/human_readable.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ const (
humanReadbleFormat supportedFormat = "human"

defaultTemplate string = `
{{if (gt (len .ViolationStore.Violations) 0) }}
{{if (gt (len .ViolationStore.PassedRules) 0) }}
Passed Rules -
{{range $index, $element := .ViolationStore.PassedRules}}
{{passedRules $element | printf "%s"}}
-----------------------------------------------------------------------
{{end}}
{{end}}
{{- if (gt (len .ViolationStore.Violations) 0) }}
Violation Details -
{{- $showDetails := .ViolationStore.Summary.ShowViolationDetails}}
{{range $index, $element := .ViolationStore.Violations}}
Expand Down Expand Up @@ -68,6 +75,7 @@ func HumanReadbleWriter(data interface{}, writer io.Writer) error {
"defaultViolations": defaultViolations,
"detailedViolations": detailedViolations,
"scanSummary": scanSummary,
"passedRules": passedRules,
}).Parse(defaultTemplate)
if err != nil {
zap.S().Errorf("failed to write human readable output. error: '%v'", err)
Expand Down Expand Up @@ -125,3 +133,13 @@ func scanSummary(s results.ScanSummary) string {
"High", s.HighCount)
return out
}

func passedRules(v results.PassedRule) string {
out := fmt.Sprintf("%-15v:\t%s\n\t%-15v:\t%s\n\t%-15v:\t%s\n\t%-15v:\t%s\n\t%-15v:\t%s\n\t",
"Rule ID", v.RuleID,
"Rule Name", v.RuleName,
"Description", v.Description,
"Severity", v.Severity,
"Category", v.Category)
return out
}

0 comments on commit 6728908

Please sign in to comment.