github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/detection/detect.go (about)

     1  package detection
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"gopkg.in/yaml.v3"
    11  
    12  	"github.com/aquasecurity/defsec/pkg/types"
    13  	"github.com/aquasecurity/trivy-iac/pkg/scanners/azure/arm/parser/armjson"
    14  )
    15  
    16  type FileType string
    17  
    18  const (
    19  	FileTypeCloudFormation FileType = "cloudformation"
    20  	FileTypeTerraform      FileType = "terraform"
    21  	FileTypeTerraformPlan  FileType = "terraformplan"
    22  	FileTypeDockerfile     FileType = "dockerfile"
    23  	FileTypeKubernetes     FileType = "kubernetes"
    24  	FileTypeRbac           FileType = "rbac"
    25  	FileTypeYAML           FileType = "yaml"
    26  	FileTypeTOML           FileType = "toml"
    27  	FileTypeJSON           FileType = "json"
    28  	FileTypeHelm           FileType = "helm"
    29  	FileTypeAzureARM       FileType = "azure-arm"
    30  )
    31  
    32  var matchers = map[FileType]func(name string, r io.ReadSeeker) bool{}
    33  
    34  // nolint
    35  func init() {
    36  
    37  	matchers[FileTypeJSON] = func(name string, r io.ReadSeeker) bool {
    38  		ext := filepath.Ext(filepath.Base(name))
    39  		if !strings.EqualFold(ext, ".json") {
    40  			return false
    41  		}
    42  		if resetReader(r) == nil {
    43  			return true
    44  		}
    45  
    46  		var content interface{}
    47  		return json.NewDecoder(r).Decode(&content) == nil
    48  	}
    49  
    50  	matchers[FileTypeYAML] = func(name string, r io.ReadSeeker) bool {
    51  		ext := filepath.Ext(filepath.Base(name))
    52  		if !strings.EqualFold(ext, ".yaml") && !strings.EqualFold(ext, ".yml") {
    53  			return false
    54  		}
    55  		if resetReader(r) == nil {
    56  			return true
    57  		}
    58  
    59  		var content interface{}
    60  		return yaml.NewDecoder(r).Decode(&content) == nil
    61  	}
    62  
    63  	matchers[FileTypeHelm] = func(name string, r io.ReadSeeker) bool {
    64  		if IsHelmChartArchive(name, r) {
    65  			return true
    66  		}
    67  
    68  		return strings.HasSuffix(name, "hart.yaml")
    69  	}
    70  
    71  	matchers[FileTypeTOML] = func(name string, r io.ReadSeeker) bool {
    72  		ext := filepath.Ext(filepath.Base(name))
    73  		return strings.EqualFold(ext, ".toml")
    74  	}
    75  
    76  	matchers[FileTypeTerraform] = func(name string, _ io.ReadSeeker) bool {
    77  		return IsTerraformFile(name)
    78  	}
    79  
    80  	matchers[FileTypeTerraformPlan] = func(name string, r io.ReadSeeker) bool {
    81  		if IsType(name, r, FileTypeJSON) {
    82  			if resetReader(r) == nil {
    83  				return false
    84  			}
    85  
    86  			contents := make(map[string]interface{})
    87  			err := json.NewDecoder(r).Decode(&contents)
    88  			if err == nil {
    89  				if _, ok := contents["terraform_version"]; ok {
    90  					_, stillOk := contents["format_version"]
    91  					return stillOk
    92  				}
    93  			}
    94  		}
    95  		return false
    96  	}
    97  
    98  	matchers[FileTypeCloudFormation] = func(name string, r io.ReadSeeker) bool {
    99  		sniff := struct {
   100  			Resources map[string]map[string]interface{} `json:"Resources" yaml:"Resources"`
   101  		}{}
   102  
   103  		switch {
   104  		case IsType(name, r, FileTypeYAML):
   105  			if resetReader(r) == nil {
   106  				return false
   107  			}
   108  			if err := yaml.NewDecoder(r).Decode(&sniff); err != nil {
   109  				return false
   110  			}
   111  		case IsType(name, r, FileTypeJSON):
   112  			if resetReader(r) == nil {
   113  				return false
   114  			}
   115  			if err := json.NewDecoder(r).Decode(&sniff); err != nil {
   116  				return false
   117  			}
   118  		default:
   119  			return false
   120  		}
   121  
   122  		return sniff.Resources != nil
   123  	}
   124  
   125  	matchers[FileTypeAzureARM] = func(name string, r io.ReadSeeker) bool {
   126  
   127  		if resetReader(r) == nil {
   128  			return false
   129  		}
   130  
   131  		sniff := struct {
   132  			ContentType string                 `json:"contentType"`
   133  			Parameters  map[string]interface{} `json:"parameters"`
   134  			Resources   []interface{}          `json:"resources"`
   135  		}{}
   136  		metadata := types.NewUnmanagedMetadata()
   137  		if err := armjson.UnmarshalFromReader(r, &sniff, &metadata); err != nil {
   138  			return false
   139  		}
   140  
   141  		return (sniff.Parameters != nil && len(sniff.Parameters) > 0) ||
   142  			(sniff.Resources != nil && len(sniff.Resources) > 0)
   143  	}
   144  
   145  	matchers[FileTypeDockerfile] = func(name string, _ io.ReadSeeker) bool {
   146  		requiredFiles := []string{"Dockerfile", "Containerfile"}
   147  		for _, requiredFile := range requiredFiles {
   148  			base := filepath.Base(name)
   149  			ext := filepath.Ext(base)
   150  			if strings.TrimSuffix(base, ext) == requiredFile {
   151  				return true
   152  			}
   153  			if strings.EqualFold(ext, "."+requiredFile) {
   154  				return true
   155  			}
   156  		}
   157  		return false
   158  	}
   159  
   160  	matchers[FileTypeHelm] = func(name string, r io.ReadSeeker) bool {
   161  		helmFiles := []string{"Chart.yaml", ".helmignore", "values.schema.json", "NOTES.txt"}
   162  		for _, expected := range helmFiles {
   163  			if strings.HasSuffix(name, expected) {
   164  				return true
   165  			}
   166  		}
   167  		helmFileExtensions := []string{".yaml", ".tpl"}
   168  		ext := filepath.Ext(filepath.Base(name))
   169  		for _, expected := range helmFileExtensions {
   170  			if strings.EqualFold(ext, expected) {
   171  				return true
   172  			}
   173  		}
   174  		return IsHelmChartArchive(name, r)
   175  	}
   176  
   177  	matchers[FileTypeKubernetes] = func(name string, r io.ReadSeeker) bool {
   178  
   179  		if !IsType(name, r, FileTypeYAML) && !IsType(name, r, FileTypeJSON) {
   180  			return false
   181  		}
   182  		if resetReader(r) == nil {
   183  			return false
   184  		}
   185  
   186  		expectedProperties := []string{"apiVersion", "kind", "metadata"}
   187  
   188  		if IsType(name, r, FileTypeJSON) {
   189  			if resetReader(r) == nil {
   190  				return false
   191  			}
   192  
   193  			var result map[string]interface{}
   194  			if err := json.NewDecoder(r).Decode(&result); err != nil {
   195  				return false
   196  			}
   197  
   198  			for _, expected := range expectedProperties {
   199  				if _, ok := result[expected]; !ok {
   200  					return false
   201  				}
   202  			}
   203  			return true
   204  		}
   205  
   206  		// at this point, we need to inspect bytes
   207  		var buf bytes.Buffer
   208  		if _, err := io.Copy(&buf, r); err != nil {
   209  			return false
   210  		}
   211  		data := buf.Bytes()
   212  
   213  		marker := "\n---\n"
   214  		altMarker := "\r\n---\r\n"
   215  		if bytes.Contains(data, []byte(altMarker)) {
   216  			marker = altMarker
   217  		}
   218  
   219  		for _, partial := range strings.Split(string(data), marker) {
   220  			var result map[string]interface{}
   221  			if err := yaml.Unmarshal([]byte(partial), &result); err != nil {
   222  				continue
   223  			}
   224  			match := true
   225  			for _, expected := range expectedProperties {
   226  				if _, ok := result[expected]; !ok {
   227  					match = false
   228  					break
   229  				}
   230  			}
   231  			if match {
   232  				return true
   233  			}
   234  		}
   235  
   236  		return false
   237  	}
   238  }
   239  
   240  func IsTerraformFile(path string) bool {
   241  	for _, ext := range []string{".tf", ".tf.json", ".tfvars"} {
   242  		if strings.HasSuffix(path, ext) {
   243  			return true
   244  		}
   245  	}
   246  
   247  	return false
   248  }
   249  
   250  func IsType(name string, r io.ReadSeeker, t FileType) bool {
   251  	r = ensureSeeker(r)
   252  	f, ok := matchers[t]
   253  	if !ok {
   254  		return false
   255  	}
   256  	return f(name, r)
   257  }
   258  
   259  func GetTypes(name string, r io.ReadSeeker) []FileType {
   260  	var matched []FileType
   261  	r = ensureSeeker(r)
   262  	for check, f := range matchers {
   263  		if f(name, r) {
   264  			matched = append(matched, check)
   265  		}
   266  		resetReader(r)
   267  	}
   268  	return matched
   269  }
   270  
   271  func ensureSeeker(r io.Reader) io.ReadSeeker {
   272  	if r == nil {
   273  		return nil
   274  	}
   275  	if seeker, ok := r.(io.ReadSeeker); ok {
   276  		return seeker
   277  	}
   278  
   279  	var buf bytes.Buffer
   280  	if _, err := io.Copy(&buf, r); err == nil {
   281  		return bytes.NewReader(buf.Bytes())
   282  	}
   283  
   284  	return nil
   285  }
   286  
   287  func resetReader(r io.Reader) io.ReadSeeker {
   288  	if r == nil {
   289  		return nil
   290  	}
   291  	if seeker, ok := r.(io.ReadSeeker); ok {
   292  		_, _ = seeker.Seek(0, 0)
   293  		return seeker
   294  	}
   295  	return ensureSeeker(r)
   296  }