Skip to content

Commit

Permalink
adds support to scan sub folders for terraform iac provider (#640)
Browse files Browse the repository at this point in the history
* implement sub folder scan for terraform
* unit tests for AllResourceConfigs methods
* change --non-recursive help message
  • Loading branch information
patilpankaj212 committed May 8, 2021
1 parent 4590a3a commit dee8090
Show file tree
Hide file tree
Showing 69 changed files with 1,715 additions and 395 deletions.
5 changes: 4 additions & 1 deletion pkg/cli/run.go
Expand Up @@ -92,6 +92,9 @@ type ScanOptions struct {

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

// nonRecursive enables recursive scan for the terraform iac provider
nonRecursive bool
}

// NewScanOptions returns a new pointer to ScanOptions
Expand Down Expand Up @@ -176,7 +179,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.policyPath, s.scanRules, s.skipRules, s.categories, s.severity)
s.iacFilePath, s.iacDirPath, s.policyPath, s.scanRules, s.skipRules, s.categories, s.severity, s.nonRecursive)
if err != nil {
return err
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/cli/run_test.go
Expand Up @@ -94,6 +94,17 @@ func TestRun(t *testing.T) {
outputType: "json",
},
},
{
name: "terraform run with --non-recursive flag",
scanOptions: &ScanOptions{
iacType: "terraform",
policyType: []string{"all"},
iacDirPath: testDataDir,
outputType: "json",
nonRecursive: true,
},
wantErr: true,
},
{
name: "normal k8s run",
scanOptions: &ScanOptions{
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/scan.go
Expand Up @@ -68,5 +68,6 @@ func init() {
scanCmd.Flags().StringVar(&scanOptions.severity, "severity", "", "minimum severity level of the policy violations to be reported by terrascan")
scanCmd.Flags().StringSliceVarP(&scanOptions.categories, "categories", "", []string{}, "list of categories of violations to be reported by terrascan (example: --categories=\"category1,category2\")")
scanCmd.Flags().BoolVarP(&scanOptions.showPassedRules, "show-passed", "", false, "display passed rules, along with violations")
scanCmd.Flags().BoolVarP(&scanOptions.nonRecursive, "non-recursive", "", false, "do not scan directories and modules recursively")
RegisterCommand(rootCmd, scanCmd)
}
4 changes: 2 additions & 2 deletions pkg/http-server/file-scan.go
Expand Up @@ -151,10 +151,10 @@ func (g *APIHandler) scanFile(w http.ResponseWriter, r *http.Request) {
var executor *runtime.Executor
if g.test {
executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType,
tempFile.Name(), "", []string{"./testdata/testpolicies"}, scanRules, skipRules, categories, severity)
tempFile.Name(), "", []string{"./testdata/testpolicies"}, scanRules, skipRules, categories, severity, false)
} else {
executor, err = runtime.NewExecutor(iacType, iacVersion, cloudType,
tempFile.Name(), "", getPolicyPathFromConfig(), scanRules, skipRules, categories, severity)
tempFile.Name(), "", getPolicyPathFromConfig(), scanRules, skipRules, categories, severity, false)
}
if err != nil {
zap.S().Error(err)
Expand Down
21 changes: 11 additions & 10 deletions pkg/http-server/remote-repo.go
Expand Up @@ -35,15 +35,16 @@ import (

// scanRemoteRepoReq contains request body for remote repository scanning
type scanRemoteRepoReq struct {
RemoteType string `json:"remote_type"`
RemoteURL string `json:"remote_url"`
ConfigOnly bool `json:"config_only"`
ScanRules []string `json:"scan_rules"`
SkipRules []string `json:"skip_rules"`
Categories []string `json:"categories"`
Severity string `json:"severity"`
ShowPassed bool `json:"show_passed"`
d downloader.Downloader
RemoteType string `json:"remote_type"`
RemoteURL string `json:"remote_url"`
ConfigOnly bool `json:"config_only"`
ScanRules []string `json:"scan_rules"`
SkipRules []string `json:"skip_rules"`
Categories []string `json:"categories"`
Severity string `json:"severity"`
ShowPassed bool `json:"show_passed"`
NonRecursive bool `json:"non_recursive"`
d downloader.Downloader
}

// scanRemoteRepo downloads the remote Iac repository and scans it for
Expand Down Expand Up @@ -126,7 +127,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, s.Categories, s.Severity)
"", iacDirPath, policyPath, s.ScanRules, s.SkipRules, s.Categories, s.Severity, s.NonRecursive)
if err != nil {
zap.S().Error(err)
return output, isAdmissionDenied, err
Expand Down
46 changes: 29 additions & 17 deletions pkg/http-server/remote-repo_test.go
Expand Up @@ -92,17 +92,18 @@ 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
wantStatus int
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: "empty url and type",
Expand Down Expand Up @@ -140,6 +141,16 @@ func TestScanRemoteRepoHandler(t *testing.T) {
remoteType: "git",
wantStatus: http.StatusOK,
},
{
name: "iac type terraform with non-recursive scan",
iacType: testIacType,
iacVersion: testIacVersion,
cloudType: testCloudType,
remoteURL: validRepo,
remoteType: "git",
nonRecursive: true,
wantStatus: http.StatusOK,
},
{
name: "valid url and type with scan and skip rules",
iacType: testIacType,
Expand Down Expand Up @@ -175,12 +186,13 @@ 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,
RemoteURL: tt.remoteURL,
RemoteType: tt.remoteType,
ScanRules: tt.scanRules,
SkipRules: tt.skipRules,
ShowPassed: tt.showPassed,
ConfigOnly: tt.configOnly,
NonRecursive: tt.nonRecursive,
}
reqBody, _ := json.Marshal(s)

Expand Down
2 changes: 1 addition & 1 deletion pkg/iac-providers/helm/v3/load-dir.go
Expand Up @@ -43,7 +43,7 @@ var (
)

// LoadIacDir loads all helm charts under the specified directory
func (h *HelmV3) LoadIacDir(absRootDir string) (output.AllResourceConfigs, error) {
func (h *HelmV3) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) {

allResourcesConfig := make(map[string][]output.ResourceConfig)

Expand Down
2 changes: 1 addition & 1 deletion pkg/iac-providers/helm/v3/load-dir_test.go
Expand Up @@ -79,7 +79,7 @@ func TestLoadIacDir(t *testing.T) {

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
resources, gotErr := tt.helmv3.LoadIacDir(tt.dirPath)
resources, gotErr := tt.helmv3.LoadIacDir(tt.dirPath, false)
me, ok := gotErr.(*multierror.Error)
if !ok {
t.Errorf("expected multierror.Error, got %T", gotErr)
Expand Down
2 changes: 1 addition & 1 deletion pkg/iac-providers/interface.go
Expand Up @@ -24,5 +24,5 @@ import (
// to claim support in terrascan
type IacProvider interface {
LoadIacFile(string) (output.AllResourceConfigs, error)
LoadIacDir(string) (output.AllResourceConfigs, error)
LoadIacDir(string, bool) (output.AllResourceConfigs, error)
}
2 changes: 1 addition & 1 deletion pkg/iac-providers/kubernetes/v1/load-dir.go
Expand Up @@ -29,7 +29,7 @@ func (*K8sV1) getFileType(file string) string {
}

// LoadIacDir loads all k8s files in the current directory
func (k *K8sV1) LoadIacDir(absRootDir string) (output.AllResourceConfigs, error) {
func (k *K8sV1) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) {

allResourcesConfig := make(map[string][]output.ResourceConfig)

Expand Down
2 changes: 1 addition & 1 deletion pkg/iac-providers/kubernetes/v1/load-dir_test.go
Expand Up @@ -88,7 +88,7 @@ func TestLoadIacDir(t *testing.T) {

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
_, gotErr := tt.k8sV1.LoadIacDir(tt.dirPath)
_, gotErr := tt.k8sV1.LoadIacDir(tt.dirPath, false)
me, ok := gotErr.(*multierror.Error)
if !ok {
t.Errorf("expected multierror.Error, got %T", gotErr)
Expand Down
2 changes: 1 addition & 1 deletion pkg/iac-providers/kustomize/v3/load-dir.go
Expand Up @@ -24,7 +24,7 @@ var (
)

// LoadIacDir loads the kustomize directory and returns the ResourceConfig mapping which is evaluated by the policy engine
func (k *KustomizeV3) LoadIacDir(absRootDir string) (output.AllResourceConfigs, error) {
func (k *KustomizeV3) LoadIacDir(absRootDir string, nonRecursive bool) (output.AllResourceConfigs, error) {

allResourcesConfig := make(map[string][]output.ResourceConfig)

Expand Down
2 changes: 1 addition & 1 deletion pkg/iac-providers/kustomize/v3/load-dir_test.go
Expand Up @@ -90,7 +90,7 @@ func TestLoadIacDir(t *testing.T) {

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
resourceMap, gotErr := tt.kustomize.LoadIacDir(tt.dirPath)
resourceMap, gotErr := tt.kustomize.LoadIacDir(tt.dirPath, false)
me, ok := gotErr.(*multierror.Error)
if !ok {
t.Errorf("expected multierror.Error, got %T", gotErr)
Expand Down
50 changes: 44 additions & 6 deletions pkg/iac-providers/output/types.go
Expand Up @@ -24,12 +24,13 @@ import (

// ResourceConfig describes a resource present in IaC
type ResourceConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Source string `json:"source"`
Line int `json:"line"`
Type string `json:"type"`
Config interface{} `json:"config"`
ID string `json:"id"`
Name string `json:"name"`
Source string `json:"source"`
PlanRoot string `json:"plan_root,omitempty" yaml:"plan_root,omitempty" `
Line int `json:"line"`
Type string `json:"type"`
Config interface{} `json:"config"`
// SkipRules will hold the rules to be skipped for the resource.
// Each iac provider should append the rules to be skipped for a resource,
// while extracting resource from the iac files
Expand All @@ -47,6 +48,9 @@ type AllResourceConfigs map[string][]ResourceConfig

// FindResourceByID Finds a given resource within the resource map and returns a reference to that resource
func (a AllResourceConfigs) FindResourceByID(resourceID string) (*ResourceConfig, error) {
if len(a) == 0 {
return nil, fmt.Errorf("AllResourceConfigs is nil or doesn't contain any resource type")
}
resTypeName := strings.Split(resourceID, ".")
if len(resTypeName) < 2 {
return nil, fmt.Errorf("resource ID has an invalid format %s", resourceID)
Expand All @@ -72,11 +76,38 @@ func (a AllResourceConfigs) FindResourceByID(resourceID string) (*ResourceConfig
return &resource, nil
}

// FindAllResourcesByID Finds all resources within the resource map
func (a AllResourceConfigs) FindAllResourcesByID(resourceID string) ([]*ResourceConfig, error) {
if len(a) == 0 {
return nil, fmt.Errorf("AllResourceConfigs is nil or doesn't contain any resource type")
}
resTypeName := strings.Split(resourceID, ".")
if len(resTypeName) < 2 {
return nil, fmt.Errorf("resource ID has an invalid format %s", resourceID)
}

resourceType := resTypeName[0]

resources := make([]*ResourceConfig, 0)
resourceTypeList := a[resourceType]
for i := range resourceTypeList {
if resourceTypeList[i].ID == resourceID {
resources = append(resources, &resourceTypeList[i])
}
}

return resources, nil
}

// GetResourceCount gives out the total number of resources present in a output.ResourceConfig object.
// Since the ResourceConfig mapping stores resources in lists which can be located resourceMapping[Type],
// `len(resourceMapping)` does not give the count of the resources but only gives out the total number of
// the type of resources inside the object.
func (a AllResourceConfigs) GetResourceCount() (count int) {
// handles nil map
if len(a) == 0 {
return 0
}
count = 0
for _, list := range a {
count = count + len(list)
Expand All @@ -86,6 +117,13 @@ func (a AllResourceConfigs) GetResourceCount() (count int) {

// UpdateResourceConfigs adds a resource of given type if it is not present in allResources
func (a AllResourceConfigs) UpdateResourceConfigs(resourceType string, resources []ResourceConfig) {
if _, ok := a[resourceType]; !ok {
if len(a) == 0 {
a = make(AllResourceConfigs)
}
a[resourceType] = resources
return
}
for _, res := range resources {
if !IsConfigPresent(a[resourceType], res) {
a[resourceType] = append(a[resourceType], res)
Expand Down

0 comments on commit dee8090

Please sign in to comment.