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

feat(parser): added support to parse and scan terraform plans #4362

Merged
merged 7 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package Cx

import data.generic.terraform as terraform_lib
import data.generic.common as common_lib
import data.generic.terraform as terraform_lib

CxPolicy[result] {
resource := input.document[i].resource[res][name]
Expand All @@ -14,6 +14,7 @@ CxPolicy[result] {
"issueType": "MissingAttribute",
"keyExpectedValue": sprintf("%s[{{%s}}].tags is defined and not null", [res, name]),
"keyActualValue": sprintf("%s[{{%s}}].tags is undefined or null", [res, name]),
"searchLine": common_lib.build_search_line(["resource", res, name], []),
}
}

Expand All @@ -29,6 +30,7 @@ CxPolicy[result] {
"issueType": "MissingAttribute",
"keyExpectedValue": sprintf("%s[{{%s}}].tags has tags defined other than 'Name'", [res, name]),
"keyActualValue": sprintf("%s[{{%s}}].tags has no tags defined", [res, name]),
"searchLine": common_lib.build_search_line(["resource", res, name, "tags"], []),
}
}

Expand Down
11 changes: 11 additions & 0 deletions docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ KICS supports scanning OpenAPI 3.0 specs with `.json` and `.yaml` extension.

KICS supports scanning Terraform's HCL files with `.tf` extension and input variables using `terraform.tfvars` or files with `.auto.tfvars` extension that are in same directory of `.tf` files.

### Terraform Plan

KICS supports scanning terraform plans given in JSON. The `planned_values` will be extracted, built in a way KICS can understand, and scanned as a normal terraform file.

Results will point to the plan file.

To get terraform plan in JSON format simply run the command:
```
terraform show -json plan-sample.tfplan > plan-sample.tfplan.json
```

### Limitations

Although KICS support variables and interpolations, KICS does not support functions and enviroment variables. In case of variables used as function parameters, it will parse as wrapped expression, so the following function call:
Expand Down
4 changes: 2 additions & 2 deletions e2e/fixtures/E2E_CLI_032_RESULT.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
"files": [
{
"file_name": "fixtures/samples/terraform.tf",
"similarity_id": "406b71d9fd0edb656a4735df30dde77c5f8a6c4ec3caa3442f986a92832c653b",
"similarity_id": "fa5d403b0ee5702d19523b159dfe830584ac506471647bbfb94d101d258cb3c8",
"line": 10,
"issue_type": "MissingAttribute",
"search_key": "aws_redshift_cluster[{{default1}}]",
Expand All @@ -266,7 +266,7 @@
},
{
"file_name": "fixtures/samples/terraform.tf",
"similarity_id": "b44463ffd0f5c1eadc04ce6649982da68658349ad880daef470250661d3d1512",
"similarity_id": "ff26328ed857afb92e2be8b946b4dd28fb0e5125fae679653e0117e5b9359554",
"line": 1,
"issue_type": "MissingAttribute",
"search_key": "aws_redshift_cluster[{{default}}]",
Expand Down
2 changes: 1 addition & 1 deletion e2e/fixtures/E2E_CLI_033_RESULT.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
"files": [
{
"file_name": "fixtures/samples/terraform-single.tf",
"similarity_id": "406b71d9fd0edb656a4735df30dde77c5f8a6c4ec3caa3442f986a92832c653b",
"similarity_id": "ff26328ed857afb92e2be8b946b4dd28fb0e5125fae679653e0117e5b9359554",
"line": 1,
"issue_type": "MissingAttribute",
"search_key": "aws_redshift_cluster[{{default1}}]",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ func init() { //nolint
Args: []cmdArgs{

[]string{"scan", "--include-queries", "e38a8e0a-b88b-4902-b3fe-b0fcb17d5c10",
"--exclude-results", "406b71d9fd0edb656a4735df30dde77c5f8a6c4ec3caa3442f986a92832c653b",
"--exclude-results", "ff26328ed857afb92e2be8b946b4dd28fb0e5125fae679653e0117e5b9359554",
"-q", "../assets/queries", "-p", "fixtures/samples/terraform-single.tf"},

[]string{"scan", "--include-queries", "e38a8e0a-b88b-4902-b3fe-b0fcb17d5c10",
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/hashicorp/go-getter v1.5.9
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/hcl/v2 v2.10.1
github.com/hashicorp/terraform-json v0.13.0
github.com/johnfercher/maroto v0.33.0
github.com/mailru/easyjson v0.7.7
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
Expand Down
12 changes: 9 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,9 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
Expand All @@ -842,6 +843,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY=
github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk=
github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c/go.mod h1:fHzc09UnyJyqyW+bFuq864eh+wC7dj65aXmXLRe5to0=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
Expand Down Expand Up @@ -1036,8 +1039,9 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4=
github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
Expand All @@ -1059,8 +1063,9 @@ github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7p
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/buildkit v0.8.1/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ=
github.com/moby/buildkit v0.9.1 h1:6noq8jvkaRs3OQGTIDf5U5Etkgq6zsPmQHGWi5yhh88=
github.com/moby/buildkit v0.9.1/go.mod h1:oVZKk3TMm0MlDx7XxnlF0wKmcpyrzOs9GEp0VXKWFPk=
Expand Down Expand Up @@ -1280,6 +1285,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE=
github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+yDFh9SZXUTvspXTjbFXgZGP/UvhU1S65A4A=
Expand Down
12 changes: 12 additions & 0 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ var (
k8sRegexKind = regexp.MustCompile("(\\s*\"kind\":)|(\\s*kind:)")
k8sRegexMetadata = regexp.MustCompile("(\\s*\"metadata\":)|(\\s*metadata:)")
ansibleVaultRegex = regexp.MustCompile(`^\s*\$ANSIBLE_VAULT.*`)
tfPlanRegexPV = regexp.MustCompile("\\s*\"planned_values\":")
tfPlanRegexRC = regexp.MustCompile("\\s*\"resource_changes\":")
tfPlanRegexConf = regexp.MustCompile("\\s*\"configuration\":")
tfPlanRegexTV = regexp.MustCompile("\\s*\"terraform_version\":")
blueprintsRegexKind = regexp.MustCompile("(\\s*\"kind\":)|(\\s*kind:)")
blueprintsRegexProperties = regexp.MustCompile("(\\s*\"properties\":)|(\\s*properties:)")
)
Expand Down Expand Up @@ -148,6 +152,14 @@ var types = map[string]regexSlice{
armRegexResources,
},
},
"terraform": {
[]*regexp.Regexp{
tfPlanRegexConf,
tfPlanRegexPV,
tfPlanRegexRC,
tfPlanRegexTV,
},
},
"blueprintsartifacts": {
[]*regexp.Regexp{
blueprintsRegexKind,
Expand Down
9 changes: 9 additions & 0 deletions pkg/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ func TestAnalyzer_Analyze(t *testing.T) {
wantExclude: []string{filepath.FromSlash("../../test/fixtures/type-test01/template01/metadata.json")},
wantErr: false,
},
{
name: "analyze_test_tfplan",
paths: []string{
filepath.FromSlash("../../test/fixtures/tfplan"),
},
wantTypes: []string{"terraform"},
wantExclude: []string{},
wantErr: false,
},
}

for _, tt := range tests {
Expand Down
13 changes: 10 additions & 3 deletions pkg/parser/json/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ func (p *Parser) Parse(_ string, fileContent []byte) ([]model.Document, error) {
}

jLine := initializeJSONLine(fileContent)
dd := jLine.setLineInfo(r)
kicsJSON := jLine.setLineInfo(r)

return []model.Document{dd}, nil
// Try to parse JSON as Terraform plan
kicsPlan, err := parseTFPlan(kicsJSON)
if err != nil {
// JSON is not a tf plan
return []model.Document{kicsJSON}, nil
}

return []model.Document{kicsPlan}, nil
}

// SupportedExtensions returns extensions supported by this parser, which is json extension
Expand All @@ -43,7 +50,7 @@ func (p *Parser) GetKind() model.FileKind {

// SupportedTypes returns types supported by this parser, which are cloudFormation
func (p *Parser) SupportedTypes() []string {
return []string{"CloudFormation", "OpenAPI", "AzureResourceManager"}
return []string{"CloudFormation", "OpenAPI", "AzureResourceManager", "Terraform"}
}

// GetCommentToken return an empty string, since JSON does not have comment token
Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/json/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestParser_SupportedExtensions(t *testing.T) {
// TestParser_SupportedExtensions tests the functions [SupportedTypes()] and all the methods called by them
func TestParser_SupportedTypes(t *testing.T) {
p := &Parser{}
require.Equal(t, []string{"CloudFormation", "OpenAPI", "AzureResourceManager"}, p.SupportedTypes())
require.Equal(t, []string{"CloudFormation", "OpenAPI", "AzureResourceManager", "Terraform"}, p.SupportedTypes())
}

// TestParser_Parse tests the functions [Parse()] and all the methods called by them
Expand Down
75 changes: 75 additions & 0 deletions pkg/parser/json/tfplan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package json

import (
"encoding/json"

"github.com/Checkmarx/kics/pkg/model"
hcl_plan "github.com/hashicorp/terraform-json"
)

// KicsPlan is an auxiliary structure for parsing tfplans as a KICS Document
type KicsPlan struct {
Resource map[string]KicsPlanResource `json:"resource"`
}

// KicsPlanResource is an auxiliary structure for parsing tfplans as a KICS Document
type KicsPlanResource map[string]KicsPlanNamedResource

// KicsPlanNamedResource is an auxiliary structure for parsing tfplans as a KICS Document
type KicsPlanNamedResource map[string]interface{}

// parseTFPlan unmarshals Document as a plan so it can be rebuilt with only
// the required information
func parseTFPlan(doc model.Document) (model.Document, error) {
var plan *hcl_plan.Plan
b, err := json.Marshal(doc)
if err != nil {
return model.Document{}, err
}
// Unmarshal our Document as a plan so we are able retrieve planned_values
// in a easier way
err = json.Unmarshal(b, &plan)
if err != nil {
// Consider as regular JSON and not tfplan
return model.Document{}, err
}

parsedPlan := readPlan(plan)
return parsedPlan, nil
}

// readPlan will get the information needed and parse it in a way KICS understands it
func readPlan(plan *hcl_plan.Plan) model.Document {
modRes := readModule(plan.PlannedValues.RootModule.Resources)

doc := model.Document{}

kp := KicsPlan{
Resource: modRes,
}
tmpDocBytes, err := json.Marshal(kp)
if err != nil {
return model.Document{}
}
err = json.Unmarshal(tmpDocBytes, &doc)
if err != nil {
return model.Document{}
}

return doc
}

// readModule will iterate over all planned_value getting the information required
func readModule(resources []*hcl_plan.StateResource) map[string]KicsPlanResource {
convRes := make(map[string]KicsPlanResource)
// initialize all the types interfaces
for _, resource := range resources {
convNamedRes := make(map[string]KicsPlanNamedResource)
convRes[resource.Type] = convNamedRes
}
// fill in all the types interfaces
for _, resource := range resources {
convRes[resource.Type][resource.Name] = resource.AttributeValues
}
return convRes
}
88 changes: 88 additions & 0 deletions pkg/parser/json/tfplan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package json

import (
"testing"

"github.com/Checkmarx/kics/pkg/model"
"github.com/stretchr/testify/require"
)

func TestJson_parseTFPlan(t *testing.T) {
type args struct {
doc model.Document
}

tests := []struct {
name string
args args
want model.Document
wantErr bool
}{
{
name: "test - parse as tfplan",
args: args{
doc: model.Document{
"format_version": "0.2",
"terraform_version": "1.0.5",
"variables": map[string]interface{}{},
"planned_values": map[string]interface{}{
"root_module": map[string]interface{}{
"resources": []map[string]interface{}{
{
"address": "fakewebservices_database.prod_db",
"mode": "managed",
"type": "fakewebservices_database",
"name": "prod_db",
"provider_name": "registry.terraform.io/hashicorp/fakewebservices",
"schema_version": 0,
"values": map[string]interface{}{
"name": "Production DB",
"size": 256,
},
"sensitive_values": map[string]interface{}{},
},
},
},
},
"resource_changes": []map[string]interface{}{},
"configuration": map[string]interface{}{},
},
},
want: model.Document{
"resource": map[string]interface{}{
"fakewebservices_database": map[string]interface{}{
"prod_db": map[string]interface{}{
"name": "Production DB",
"size": (float64)(256),
},
},
},
},
wantErr: false,
},
{
name: "test - should not parse tfplan",
args: args{
doc: model.Document{
"resource": map[string]interface{}{
"name": "martin",
},
},
},
want: model.Document{},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseTFPlan(tt.args.doc)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.want, got)
})
}
}
Loading