github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/scanners/terraformplan/parser/parser.go (about)

     1  package parser
     2  
     3  import (
     4  	"crypto/md5" //#nosec
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/khulnasoft-lab/defsec/pkg/terraform"
    12  	"github.com/liamg/memoryfs"
    13  )
    14  
    15  type Parser struct {
    16  	debugWriter    io.Writer
    17  	stopOnHCLError bool
    18  }
    19  
    20  func New(options ...Option) *Parser {
    21  	parser := &Parser{}
    22  
    23  	for _, o := range options {
    24  		o(parser)
    25  	}
    26  	return parser
    27  }
    28  
    29  func (p *Parser) SetDebugWriter(writer io.Writer) {
    30  	p.debugWriter = writer
    31  }
    32  
    33  func (p *Parser) SetStopOnHCLError(b bool) {
    34  	p.stopOnHCLError = b
    35  }
    36  
    37  func (p *Parser) ParseFile(filepath string) (*PlanFile, error) {
    38  
    39  	if _, err := os.Stat(filepath); err != nil {
    40  		return nil, err
    41  	}
    42  
    43  	reader, err := os.Open(filepath)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	defer reader.Close()
    48  	return p.Parse(reader)
    49  }
    50  
    51  func (p *Parser) Parse(reader io.Reader) (*PlanFile, error) {
    52  
    53  	var planFile PlanFile
    54  
    55  	if err := json.NewDecoder(reader).Decode(&planFile); err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	return &planFile, nil
    60  
    61  }
    62  
    63  func (p *PlanFile) ToFS() (*memoryfs.FS, error) {
    64  
    65  	rootFS := memoryfs.New()
    66  
    67  	var fileResources []string
    68  
    69  	resources, err := getResources(p.PlannedValues.RootModule, p.ResourceChanges, p.Configuration)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	for _, r := range resources {
    75  		fileResources = append(fileResources, r.ToHCL())
    76  	}
    77  
    78  	fileContent := strings.Join(fileResources, "\n\n")
    79  	if err := rootFS.WriteFile("main.tf", []byte(fileContent), os.ModePerm); err != nil {
    80  		return nil, err
    81  	}
    82  	return rootFS, nil
    83  
    84  }
    85  
    86  func getResources(module Module, resourceChanges []ResourceChange, configuration Configuration) ([]terraform.PlanBlock, error) {
    87  	var resources []terraform.PlanBlock
    88  	for _, r := range module.Resources {
    89  		resourceName := r.Name
    90  		if strings.HasPrefix(r.Address, "module.") {
    91  			hashable := strings.TrimSuffix(strings.Split(r.Address, fmt.Sprintf(".%s.", r.Type))[0], ".data")
    92  			/* #nosec */
    93  			hash := fmt.Sprintf("%x", md5.Sum([]byte(hashable)))
    94  			resourceName = fmt.Sprintf("%s_%s", r.Name, hash)
    95  		}
    96  
    97  		res := terraform.NewPlanBlock(r.Mode, r.Type, resourceName)
    98  
    99  		changes := getValues(r.Address, resourceChanges)
   100  		// process the changes to get the after state
   101  		for k, v := range changes.After {
   102  			switch t := v.(type) {
   103  			case []interface{}:
   104  				if len(t) == 0 {
   105  					continue
   106  				}
   107  				val := t[0]
   108  				switch v := val.(type) {
   109  				// is it a HCL block?
   110  				case map[string]interface{}:
   111  					res.Blocks[k] = v
   112  				// just a normal attribute then
   113  				default:
   114  					res.Attributes[k] = v
   115  				}
   116  			default:
   117  				res.Attributes[k] = v
   118  			}
   119  		}
   120  
   121  		resourceConfig := getConfiguration(r.Address, configuration.RootModule)
   122  		if resourceConfig != nil {
   123  
   124  			for attr, val := range resourceConfig.Expressions {
   125  				if value, shouldReplace := unpackConfigurationValue(val, r); shouldReplace || !res.HasAttribute(attr) {
   126  					res.Attributes[attr] = value
   127  				}
   128  			}
   129  		}
   130  		resources = append(resources, *res)
   131  	}
   132  
   133  	for _, m := range module.ChildModules {
   134  		cr, err := getResources(m.Module, resourceChanges, configuration)
   135  		if err != nil {
   136  			return nil, err
   137  		}
   138  		resources = append(resources, cr...)
   139  	}
   140  
   141  	return resources, nil
   142  }
   143  
   144  func unpackConfigurationValue(val interface{}, r Resource) (interface{}, bool) {
   145  	switch t := val.(type) {
   146  	case map[string]interface{}:
   147  		for k, v := range t {
   148  			switch k {
   149  			case "references":
   150  				reference := v.([]interface{})[0].(string)
   151  				if strings.HasPrefix(r.Address, "module.") {
   152  					hashable := strings.TrimSuffix(strings.Split(r.Address, fmt.Sprintf(".%s.", r.Type))[0], ".data")
   153  					/* #nosec */
   154  					hash := fmt.Sprintf("%x", md5.Sum([]byte(hashable)))
   155  
   156  					parts := strings.Split(reference, ".")
   157  					var rejoin []string
   158  
   159  					name := parts[1]
   160  					remainder := parts[2:]
   161  					if parts[0] == "data" {
   162  						rejoin = append(rejoin, parts[:2]...)
   163  						name = parts[2]
   164  						remainder = parts[3:]
   165  					} else {
   166  						rejoin = append(rejoin, parts[:1]...)
   167  					}
   168  
   169  					rejoin = append(rejoin, fmt.Sprintf("%s_%s", name, hash))
   170  					rejoin = append(rejoin, remainder...)
   171  
   172  					reference = strings.Join(rejoin, ".")
   173  				}
   174  
   175  				shouldReplace := false
   176  				return terraform.PlanReference{Value: reference}, shouldReplace
   177  			case "constant_value":
   178  				return v, false
   179  			}
   180  		}
   181  	}
   182  
   183  	return nil, false
   184  }
   185  
   186  func getConfiguration(address string, configuration ConfigurationModule) *ConfigurationResource {
   187  
   188  	workingAddress := address
   189  	var moduleParts []string
   190  	for strings.HasPrefix(workingAddress, "module.") {
   191  		workingAddressParts := strings.Split(workingAddress, ".")
   192  		moduleParts = append(moduleParts, workingAddressParts[1])
   193  		workingAddress = strings.Join(workingAddressParts[2:], ".")
   194  	}
   195  
   196  	workingModule := configuration
   197  	for _, moduleName := range moduleParts {
   198  		if module, ok := workingModule.ModuleCalls[moduleName]; ok {
   199  			workingModule = module.Module
   200  		}
   201  	}
   202  
   203  	for _, resource := range workingModule.Resources {
   204  		if resource.Address == workingAddress {
   205  			return &resource
   206  		}
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func getValues(address string, resourceChange []ResourceChange) *ResourceChange {
   213  	for _, r := range resourceChange {
   214  		if r.Address == address {
   215  			return &r
   216  		}
   217  	}
   218  	return nil
   219  }