Skip to content

Commit

Permalink
Merge pull request #501 from dev-gaur/severity_flag
Browse files Browse the repository at this point in the history
feature: add options to specify desired severity level of violations to be reported
  • Loading branch information
Devang Gaur committed Feb 11, 2021
2 parents b579bec + 58e5f45 commit 7b3593f
Show file tree
Hide file tree
Showing 23 changed files with 814 additions and 78 deletions.
4 changes: 3 additions & 1 deletion pkg/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ type ScanOptions struct {
// SkipRules is the array of rules to skip while scanning
skipRules []string

// 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
}
Expand Down Expand Up @@ -167,7 +169,7 @@ func (s *ScanOptions) Run() error {

// create a new runtime executor for processing IaC
executor, err := runtime.NewExecutor(s.iacType, s.iacVersion, s.policyType,
s.iacFilePath, s.iacDirPath, s.configFile, s.policyPath, s.scanRules, s.skipRules)
s.iacFilePath, s.iacDirPath, s.configFile, s.policyPath, s.scanRules, s.skipRules, s.severity)
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ func init() {
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")
RegisterCommand(rootCmd, scanCmd)
}
5 changes: 5 additions & 0 deletions pkg/config/config-reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ func (r TerrascanConfigReader) GetNotifications() map[string]Notifier {
func (r TerrascanConfigReader) GetRules() Rules {
return r.config.Rules
}

// GetSeverity will return the level of severity specified in the terrascan config file
func (r TerrascanConfigReader) GetSeverity() Severity {
return r.config.Severity
}
25 changes: 25 additions & 0 deletions pkg/config/config-reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ func TestNewTerrascanConfigReader(t *testing.T) {
SkipRules: []string{"rule.1"},
}

highSeverity := Severity{Level: "high"}

type args struct {
fileName string
}

tests := []struct {
name string
args args
Expand Down Expand Up @@ -107,6 +110,28 @@ func TestNewTerrascanConfigReader(t *testing.T) {
Policy: testPolicy,
Rules: testRules,
},
{
name: "valid toml config file with all fields and severity defined",
args: args{
fileName: "testdata/terrascan-config-severity.toml",
},
want: &TerrascanConfigReader{
config: TerrascanConfig{
Policy: testPolicy,
Notifications: map[string]Notifier{
"webhook1": testNotifier,
},
Rules: testRules,
Severity: highSeverity,
},
},
assertGetters: true,
notifications: map[string]Notifier{
"webhook1": testNotifier,
},
Policy: testPolicy,
Rules: testRules,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
26 changes: 26 additions & 0 deletions pkg/config/testdata/terrascan-config-severity.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[policy]
path = "custom-path"
rego_subdir = "rego-subdir"
repo_url = "https://repository/url"
branch = "branch-name"

[notifications]
[notifications.webhook1]
type = 'webhook'
[notifications.webhook1.config]
url = 'testurl1'

[rules]
scan-rules = [
"rule.1",
"rule.2",
"rule.3",
"rule.4",
"rule.5",
]
skip-rules = [
"rule.1"
]

[severity]
level = "high"
6 changes: 6 additions & 0 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ type TerrascanConfig struct {
Policy `toml:"policy,omitempty"`
Notifications map[string]Notifier `toml:"notifications,omitempty"`
Rules `toml:"rules,omitempty"`
Severity `toml:"severity,omitempty"`
}

// Severity defines the minimum level of severity of violations that you want to be reported
type Severity struct {
Level string `toml:"level"`
}

// Policy struct defines policy specific configurations
Expand Down
12 changes: 10 additions & 2 deletions pkg/http-server/file-scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"

"github.com/accurics/terrascan/pkg/runtime"
"github.com/accurics/terrascan/pkg/utils"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -89,6 +90,9 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
scanRulesValue := r.FormValue("scan_rules")
skipRulesValue := r.FormValue("skip_rules")

// severity is the minimum severity level of violations that the user want to get informed about: low, medium or high
severity := r.FormValue("severity")

if scanRulesValue != "" {
scanRules = strings.Split(scanRulesValue, ",")
}
Expand All @@ -97,14 +101,18 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
skipRules = strings.Split(skipRulesValue, ",")
}

if severity != "" {
severity = utils.EnsureUpperCaseTrimmed(severity)
}

// create a new runtime executor for scanning the uploaded file
var executor *runtime.Executor
if g.test {
executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType,
tempFile.Name(), "", "", []string{"./testdata/testpolicies"}, scanRules, skipRules)
tempFile.Name(), "", "", []string{"./testdata/testpolicies"}, scanRules, skipRules, severity)
} else {
executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType,
tempFile.Name(), "", "", []string{}, scanRules, skipRules)
tempFile.Name(), "", "", []string{}, scanRules, skipRules, severity)
}
if err != nil {
zap.S().Error(err)
Expand Down
87 changes: 85 additions & 2 deletions pkg/http-server/file-scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestUpload(t *testing.T) {
cloudType string
scanRules []string
skipRules []string
severity string
wantStatus int
}{
{
Expand All @@ -57,6 +58,15 @@ func TestUpload(t *testing.T) {
cloudType: testCloudType,
wantStatus: http.StatusOK,
},
{
name: "valid file scan default iac version, with invalid severity level input",
path: testFilePath,
param: testParamName,
iacType: testIacType,
severity: "HGIH",
cloudType: testCloudType,
wantStatus: http.StatusBadRequest,
},
{
name: "invalid iacType",
path: testFilePath,
Expand Down Expand Up @@ -102,6 +112,72 @@ func TestUpload(t *testing.T) {
"AWS.CloudFront.Logging.Medium.0567", "AWS.CloudFront.Network Security.Low.0568"},
skipRules: []string{"AWS.CloudFront.Network Security.Low.0568"},
},
{
name: "valid file scan default iac version",
path: testFilePath,
param: testParamName,
iacType: testIacType,
cloudType: testCloudType,
severity: "low ",
wantStatus: http.StatusOK,
},
{
name: "valid file scan default iac version with MEDIUM severity",
path: testFilePath,
param: testParamName,
iacType: testIacType,
cloudType: testCloudType,
severity: " MEDIUM ",
wantStatus: http.StatusOK,
},
{
name: "valid file scan default iac version with high severity",
path: testFilePath,
param: testParamName,
iacType: testIacType,
cloudType: testCloudType,
severity: "high",
wantStatus: http.StatusOK,
},
{
name: "valid file scan with scan and skip rules with low severity",
path: testFilePath,
param: testParamName,
iacType: testIacType,
iacVersion: testIacVersion,
cloudType: testCloudType,
wantStatus: http.StatusOK,
severity: "low ",
scanRules: []string{"AWS.CloudFront.EncryptionandKeyManagement.High.0407", "AWS.CloudFront.EncryptionandKeyManagement.High.0408",
"AWS.CloudFront.Logging.Medium.0567", "AWS.CloudFront.Network Security.Low.0568"},
skipRules: []string{"AWS.CloudFront.Network Security.Low.0568"},
},
{
name: "valid file scan with scan and skip rules with medium severity",
path: testFilePath,
param: testParamName,
iacType: testIacType,
iacVersion: testIacVersion,
cloudType: testCloudType,
wantStatus: http.StatusOK,
severity: " medium",
scanRules: []string{"AWS.CloudFront.EncryptionandKeyManagement.High.0407", "AWS.CloudFront.EncryptionandKeyManagement.High.0408",
"AWS.CloudFront.Logging.Medium.0567", "AWS.CloudFront.Network Security.Low.0568"},
skipRules: []string{"AWS.CloudFront.Network Security.Low.0568"},
},
{
name: "valid file scan with scan and skip rules with HIGH severity",
path: testFilePath,
param: testParamName,
iacType: testIacType,
iacVersion: testIacVersion,
cloudType: testCloudType,
wantStatus: http.StatusOK,
severity: "HIGH",
scanRules: []string{"AWS.CloudFront.EncryptionandKeyManagement.High.0407", "AWS.CloudFront.EncryptionandKeyManagement.High.0408",
"AWS.CloudFront.Logging.Medium.0567", "AWS.CloudFront.Network Security.Low.0568"},
skipRules: []string{"AWS.CloudFront.Network Security.Low.0568"},
},
}

for _, tt := range table {
Expand All @@ -114,7 +190,6 @@ func TestUpload(t *testing.T) {
t.Error(err)
}
defer file.Close()

// use buffer to store response body
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
Expand All @@ -137,6 +212,14 @@ func TestUpload(t *testing.T) {
t.Error(err)
}
}

if len(tt.severity) > 0 {
if err = writer.WriteField("severity", tt.severity); err != nil {
writer.Close()
t.Error(err)
}
}

writer.Close()

// http request of the type "/v1/{iacType}/{iacVersion}/{cloudType}/file/scan"
Expand All @@ -154,7 +237,7 @@ func TestUpload(t *testing.T) {
h.scanFile(res, req)

if res.Code != tt.wantStatus {
t.Errorf("incorrect status code, got: '%v', want: '%v', error: '%v'", res.Code, http.StatusOK, res.Body)
t.Errorf("incorrect status code, got: '%v', want: '%v', error: '%v'", res.Code, tt.wantStatus, res.Body)
}
})
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/http-server/remote-repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type scanRemoteRepoReq struct {
ConfigOnly bool `json:"config_only"`
ScanRules []string `json:"scan_rules"`
SkipRules []string `json:"skip_rules"`
Severity string `json:"severity"`
d downloader.Downloader
}

Expand Down Expand Up @@ -114,7 +115,7 @@ func (s *scanRemoteRepoReq) ScanRemoteRepo(iacType, iacVersion string, cloudType

// create a new runtime executor for scanning the remote repo
executor, err := runtime.NewExecutor(iacType, iacVersion, cloudType,
"", iacDirPath, "", policyPath, s.ScanRules, s.SkipRules)
"", iacDirPath, "", policyPath, s.ScanRules, s.SkipRules, s.Severity)
if err != nil {
zap.S().Error(err)
return output, err
Expand Down
6 changes: 3 additions & 3 deletions pkg/policy/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ package policy

// Engine Policy Engine interface
type Engine interface {
//Init method to initialize engine with policy path, scan and skip rules
Init(string, []string, []string) error
FilterRules(string, []string, []string)
//Init method to initialize engine with policy path, scan and skip rules, and severity level
Init(string, []string, []string, string) error
FilterRules(string, []string, []string, string)
Configure() error
Evaluate(EngineInput) (EngineOutput, error)
GetResults() EngineOutput
Expand Down
32 changes: 27 additions & 5 deletions pkg/policy/opa/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,16 +243,16 @@ func (e *Engine) CompileRegoFiles() error {

// Init initializes the Opa engine
// Handles loading all rules, filtering, compiling, and preparing for evaluation
func (e *Engine) Init(policyPath string, scanRules, skipRules []string) error {
func (e *Engine) Init(policyPath string, scanRules, skipRules []string, severity string) error {
e.context = context.Background()

if err := e.LoadRegoFiles(policyPath); err != nil {
zap.S().Error("error loading rego files", zap.String("policy path", policyPath), zap.Error(err))
return errInitFailed
}

// before compiling the rego files, filter the rules based on scan and skip rules supplied
e.FilterRules(policyPath, scanRules, skipRules)
// before compiling the rego files, filter the rules based on scan and skip rules, and severity level supplied
e.FilterRules(policyPath, scanRules, skipRules, severity)

// update the rule count
e.stats.ruleCount = len(e.regoDataMap)
Expand Down Expand Up @@ -419,8 +419,8 @@ func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput,
return e.results, nil
}

// FilterRules will apply the scan and skip rules
func (e *Engine) FilterRules(policyPath string, scanRules, skipRules []string) {
// FilterRules will apply the scan and skip rules, and severity level
func (e *Engine) FilterRules(policyPath string, scanRules, skipRules []string, severity string) {
// apply scan rules
if len(scanRules) > 0 {
e.filterScanRules(policyPath, scanRules)
Expand All @@ -430,6 +430,10 @@ func (e *Engine) FilterRules(policyPath string, scanRules, skipRules []string) {
if len(skipRules) > 0 {
e.filterSkipRules(policyPath, skipRules)
}

if len(severity) > 0 {
e.filterBySeverity(policyPath, severity)
}
}

func (e *Engine) filterScanRules(policyPath string, scanRules []string) {
Expand Down Expand Up @@ -465,3 +469,21 @@ func (e *Engine) filterSkipRules(policyPath string, skipRules []string) {
}
}
}

func (e *Engine) filterBySeverity(policyPath, severity string) {

// temporary map to store data from original rego data map
tempMap := make(map[string]*RegoData)
for ruleID, regoData := range e.regoDataMap {

if utils.CheckSeverity(regoData.Metadata.Severity, severity) {
tempMap[ruleID] = regoData
}
}
if len(tempMap) == 0 {
zap.S().Debugf("policy path: %s, doesn't have any rule matching the severity level : %s", policyPath, severity)
}

// the regoDataMap should only contain regoData for required minimum severity level
e.regoDataMap = tempMap
}

0 comments on commit 7b3593f

Please sign in to comment.