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 }