Skip to content

Commit

Permalink
feat: Added terraform command for parsing terraform show -json output
Browse files Browse the repository at this point in the history
  • Loading branch information
xntrik committed Nov 7, 2021
1 parent 974e93b commit 9391267
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 0 deletions.
6 changes: 6 additions & 0 deletions cmd/hcltm/hcltm.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func Run(args []string) int {
specCfg: cfg,
}, nil
},
"terraform": func() (cli.Command, error) {
return &TerraformCommand{
GlobalCmdOptions: globalCmdOptions,
specCfg: cfg,
}, nil
},
}

cli := &cli.CLI{
Expand Down
297 changes: 297 additions & 0 deletions cmd/hcltm/terraform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
package main

import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"strings"

"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclwrite"
tf "github.com/hashicorp/terraform-json"
"github.com/xntrik/hcltm/pkg/spec"
"github.com/xntrik/hcltm/pkg/terraform"
)

type TerraformCommand struct {
*GlobalCmdOptions
specCfg *spec.ThreatmodelSpecConfig
flagStdin bool
flagDefaultClassification string
flagAddToExisting string
flagTmName string
}

type TerraformJsonMode int

const (
UnknownMode TerraformJsonMode = iota
PlanMode
StateMode
)

func (c *TerraformCommand) Help() string {
helpText := `
Usage: hcltm terraform <files>
Parse output from 'terraform show -json' (as specified by <files>)
Options:
-config=<file>
Optional config file
-stdin
If set, will expect input to be piped in
-default-classification=<string>
If set, will assign the provided classification to output information_asset
-add-to-existing=<hcltm file>
If set, will add the generated information_assets into the provided file.
This will not overwrite the provided <hcltm file>
-tm-name=<string>
If -add-to-existing, this is used to specify a particular TM to target.
`
return strings.TrimSpace(helpText)
}

func (c *TerraformCommand) Run(args []string) int {
flagSet := c.GetFlagset("terraform")
flagSet.BoolVar(&c.flagStdin, "stdin", false, "If set, will expect input to be piped in")
flagSet.StringVar(&c.flagDefaultClassification, "default-classification", "", "If set, will provide a default information_classification for all assets")
flagSet.StringVar(&c.flagAddToExisting, "add-to-existing", "", "If set, will add assets to this threat model")
flagSet.StringVar(&c.flagTmName, "tm-name", "", "If set, and using add-to-existing, targets a specific threat model")
flagSet.Parse(args)

if c.flagConfig != "" {
err := c.specCfg.LoadSpecConfigFile(c.flagConfig)

if err != nil {
fmt.Printf("Error: %s\n", err)
return 1
}
}

tmParser := spec.NewThreatmodelParser(c.specCfg)
var tm spec.Threatmodel

if c.flagAddToExisting != "" {
err := tmParser.ParseFile(c.flagAddToExisting, false)
if err != nil {
fmt.Printf("Error parsing provided <hcltm file>: %s\n", err)
return 1
}
if len(tmParser.GetWrapped().Threatmodels) == 0 {
fmt.Printf("Need at least 1 threat model\n")
return 1
}
if len(tmParser.GetWrapped().Threatmodels) > 1 {
foundExisting := false
errMsg := "This <hcltm file> contains multiple models, select one with the -tm-name=<string> flag\n\nmodels:\n"
for _, individualTm := range tmParser.GetWrapped().Threatmodels {
if c.flagTmName == individualTm.Name {
tm = individualTm
foundExisting = true
break
}
errMsg += fmt.Sprintf("%s\n", individualTm.Name)
}

if !foundExisting {
fmt.Printf(errMsg)
return 1
}
} else {
tm = tmParser.GetWrapped().Threatmodels[0]
}

}

var in []byte
var mode TerraformJsonMode = UnknownMode

if c.flagStdin {
// Try and parse STDIN
reader := bufio.NewReader(os.Stdin)
var output []rune
for {
input, _, err := reader.ReadRune()
if err != nil && err == io.EOF {
break
}
output = append(output, input)
}

in = []byte(string(output))
} else {

if len(flagSet.Args()) == 0 {
fmt.Printf("Please provide <files> or -stdin\n")
return 1
} else {

var err error
in, err = ioutil.ReadFile(flagSet.Args()[0])
if err != nil {
fmt.Printf("Error reading file: %s\n", err)
return 1
}
}
}

p := tf.Plan{}
s := tf.State{}

err := p.UnmarshalJSON(in)
if err != nil {
fmt.Printf("Error unmarshalling JSON: %s\n", err)
return 1
}

err = p.Validate()
if err != nil {
fmt.Printf("Error validating JSON: %s\n", err)
return 1
}

if p.PlannedValues != nil {
mode = PlanMode
} else {
err := s.UnmarshalJSON(in)
if err != nil {
fmt.Printf("Error unmarshalling JSON: %s\n", err)
return 1
}

err = s.Validate()
if err != nil {
fmt.Printf("Error validating JSON: %s\n", err)
return 1
}

if s.Values != nil {
mode = StateMode
}
}

tfc := terraform.NewCollection()

switch mode {
case PlanMode:
for _, r := range p.PlannedValues.RootModule.Resources {
provName := strings.Split(r.Type, "_")
if len(provName) > 1 {
if prov, exists := tfc[provName[0]]; exists {
if res, ok := prov.Resources[r.Type]; ok {
tmAsset := spec.InformationAsset{
Name: fmt.Sprintf("%s %s", r.Type, r.Name),
Source: "terraform plan",
}

if c.flagDefaultClassification != "" {
tmAsset.InformationClassification = c.flagDefaultClassification
}

for _, attr := range res.Attributes {
if attrVal, attrExists := r.AttributeValues[attr]; attrExists && attrVal != nil {
if len(tmAsset.Description) > 0 {
tmAsset.Description = fmt.Sprintf("%s, %s: %s", tmAsset.Description, attr, attrVal)
} else {
tmAsset.Description = fmt.Sprintf("%s: %s", attr, attrVal)
}
}
}

if c.flagAddToExisting != "" {
tm.InformationAssets = append(tm.InformationAssets, &tmAsset)

} else {
err = c.out(&tmAsset, os.Stdout)
if err != nil {
fmt.Printf("Error writing out: %s\n", err)
return 1
}
}
}
}
}
}

case StateMode:
fmt.Printf("State Mode\n")
fmt.Printf("%d\n", len(s.Values.RootModule.Resources))
for _, r := range s.Values.RootModule.Resources {
provName := strings.Split(r.Type, "_")
if len(provName) > 1 {
if prov, exists := tfc[provName[0]]; exists {
if res, ok := prov.Resources[r.Type]; ok {
tmAsset := spec.InformationAsset{
Name: fmt.Sprintf("%s %s", r.Type, r.Name),
Source: "terraform state",
}

if c.flagDefaultClassification != "" {
tmAsset.InformationClassification = c.flagDefaultClassification
}

for _, attr := range res.Attributes {
if attrVal, attrExists := r.AttributeValues[attr]; attrExists && attrVal != nil {
if len(tmAsset.Description) > 0 {
tmAsset.Description = fmt.Sprintf("%s, %s: %s", tmAsset.Description, attr, attrVal)
} else {
tmAsset.Description = fmt.Sprintf("%s: %s", attr, attrVal)
}
}
}

err = c.out(&tmAsset, os.Stdout)
if err != nil {
fmt.Printf("Error writing out: %s\n", err)
return 1
}

}
}
}
}

case UnknownMode:
fmt.Printf("Unknown mode\n")
default:
fmt.Printf("Unknown mode\n")
}

if c.flagAddToExisting != "" {
newTm := spec.NewThreatmodelParser(c.specCfg)
err = newTm.AddTMAndWrite(tm, os.Stdout, false)
if err != nil {
fmt.Printf("Error writing out: %s\n", err)
return 1
}

}

return 0
}

func (c *TerraformCommand) out(asset *spec.InformationAsset, out *os.File) error {
w := bufio.NewWriter(out)
defer w.Flush()
hclOut := hclwrite.NewEmptyFile()
block := gohcl.EncodeAsBlock(asset, "information_asset")
hclOut.Body().AppendBlock(block)
_, err := w.Write(hclOut.Bytes())
if err != nil {
return fmt.Errorf("Error writing out: %s\n", err)
}
return nil
}

func (c *TerraformCommand) Synopsis() string {
return "Parse output from 'terraform show -json'"
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.10.1
github.com/hashicorp/terraform-json v0.13.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
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/hcl/v2 v2.10.1 h1:h4Xx4fsrRE26ohAk/1iGF/JBqRQbyUqu5Lvj60U54ys=
github.com/hashicorp/hcl/v2 v2.10.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
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/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
Expand Down Expand Up @@ -188,6 +192,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk=
github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
Expand Down
1 change: 1 addition & 0 deletions pkg/spec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type InformationAsset struct {
Name string `hcl:"name,label"`
Description string `hcl:"description,optional"`
InformationClassification string `hcl:"information_classification,optional"`
Source string `hcl:"source,optional"`
}

type Threat struct {
Expand Down
Loading

0 comments on commit 9391267

Please sign in to comment.