github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/helm/parser/parser.go (about) 1 package parser 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "io/fs" 10 "os" 11 "path/filepath" 12 "regexp" 13 "sort" 14 "strings" 15 16 "gopkg.in/yaml.v3" 17 18 "github.com/aquasecurity/defsec/pkg/debug" 19 "github.com/google/uuid" 20 "helm.sh/helm/v3/pkg/action" 21 "helm.sh/helm/v3/pkg/chart" 22 "helm.sh/helm/v3/pkg/chart/loader" 23 "helm.sh/helm/v3/pkg/release" 24 "helm.sh/helm/v3/pkg/releaseutil" 25 26 "github.com/aquasecurity/defsec/pkg/scanners/options" 27 "github.com/aquasecurity/trivy-iac/pkg/detection" 28 ) 29 30 var manifestNameRegex = regexp.MustCompile("# Source: [^/]+/(.+)") 31 32 type Parser struct { 33 helmClient *action.Install 34 rootPath string 35 ChartSource string 36 filepaths []string 37 debug debug.Logger 38 skipRequired bool 39 workingFS fs.FS 40 valuesFiles []string 41 values []string 42 fileValues []string 43 stringValues []string 44 apiVersions []string 45 } 46 47 type ChartFile struct { 48 TemplateFilePath string 49 ManifestContent string 50 } 51 52 func (p *Parser) SetDebugWriter(writer io.Writer) { 53 p.debug = debug.New(writer, "helm", "parser") 54 } 55 56 func (p *Parser) SetSkipRequiredCheck(b bool) { 57 p.skipRequired = b 58 } 59 60 func (p *Parser) SetValuesFile(s ...string) { 61 p.valuesFiles = s 62 } 63 64 func (p *Parser) SetValues(values ...string) { 65 p.values = values 66 } 67 68 func (p *Parser) SetFileValues(values ...string) { 69 p.fileValues = values 70 } 71 72 func (p *Parser) SetStringValues(values ...string) { 73 p.stringValues = values 74 } 75 76 func (p *Parser) SetAPIVersions(values ...string) { 77 p.apiVersions = values 78 } 79 80 func New(path string, options ...options.ParserOption) *Parser { 81 82 client := action.NewInstall(&action.Configuration{}) 83 client.DryRun = true // don't do anything 84 client.Replace = true // skip name check 85 client.ClientOnly = true // don't try to talk to a cluster 86 87 p := &Parser{ 88 helmClient: client, 89 ChartSource: path, 90 } 91 92 for _, option := range options { 93 option(p) 94 } 95 96 if p.apiVersions != nil { 97 p.helmClient.APIVersions = p.apiVersions 98 } 99 100 return p 101 } 102 103 func (p *Parser) ParseFS(ctx context.Context, target fs.FS, path string) error { 104 p.workingFS = target 105 106 if err := fs.WalkDir(p.workingFS, filepath.ToSlash(path), func(path string, entry fs.DirEntry, err error) error { 107 select { 108 case <-ctx.Done(): 109 return ctx.Err() 110 default: 111 } 112 if err != nil { 113 return err 114 } 115 if entry.IsDir() { 116 return nil 117 } 118 119 if !p.required(path, p.workingFS) { 120 return nil 121 } 122 123 if detection.IsArchive(path) { 124 tarFS, err := p.addTarToFS(path) 125 if errors.Is(err, errSkipFS) { 126 // an unpacked Chart already exists 127 return nil 128 } else if err != nil { 129 return fmt.Errorf("failed to add tar %q to FS: %w", path, err) 130 } 131 132 targetPath := filepath.Dir(path) 133 if targetPath == "" { 134 targetPath = "." 135 } 136 137 if err := p.ParseFS(ctx, tarFS, targetPath); err != nil { 138 return fmt.Errorf("parse tar FS error: %w", err) 139 } 140 return nil 141 } else { 142 return p.addPaths(path) 143 } 144 }); err != nil { 145 return fmt.Errorf("walk dir error: %w", err) 146 } 147 148 return nil 149 } 150 151 func (p *Parser) addPaths(paths ...string) error { 152 for _, path := range paths { 153 if _, err := fs.Stat(p.workingFS, path); err != nil { 154 return err 155 } 156 157 if strings.HasSuffix(path, "Chart.yaml") && p.rootPath == "" { 158 if err := p.extractChartName(path); err != nil { 159 return err 160 } 161 p.rootPath = filepath.Dir(path) 162 } 163 p.filepaths = append(p.filepaths, path) 164 } 165 return nil 166 } 167 168 func (p *Parser) extractChartName(chartPath string) error { 169 170 chart, err := p.workingFS.Open(chartPath) 171 if err != nil { 172 return err 173 } 174 defer func() { _ = chart.Close() }() 175 176 var chartContent map[string]interface{} 177 if err := yaml.NewDecoder(chart).Decode(&chartContent); err != nil { 178 // the chart likely has the name templated and so cannot be parsed as yaml - use a temporary name 179 if dir := filepath.Dir(chartPath); dir != "" && dir != "." { 180 p.helmClient.ReleaseName = dir 181 } else { 182 p.helmClient.ReleaseName = uuid.NewString() 183 } 184 return nil 185 } 186 187 if name, ok := chartContent["name"]; !ok { 188 return fmt.Errorf("could not extract the chart name from %s", chartPath) 189 } else { 190 p.helmClient.ReleaseName = fmt.Sprintf("%v", name) 191 } 192 return nil 193 } 194 195 func (p *Parser) RenderedChartFiles() ([]ChartFile, error) { 196 197 tempDir, err := os.MkdirTemp(os.TempDir(), "defsec") 198 if err != nil { 199 return nil, err 200 } 201 202 if err := p.writeBuildFiles(tempDir); err != nil { 203 return nil, err 204 } 205 206 workingChart, err := loadChart(tempDir) 207 if err != nil { 208 return nil, err 209 } 210 211 workingRelease, err := p.getRelease(workingChart) 212 if err != nil { 213 return nil, err 214 } 215 216 var manifests bytes.Buffer 217 _, _ = fmt.Fprintln(&manifests, strings.TrimSpace(workingRelease.Manifest)) 218 219 splitManifests := releaseutil.SplitManifests(manifests.String()) 220 manifestsKeys := make([]string, 0, len(splitManifests)) 221 for k := range splitManifests { 222 manifestsKeys = append(manifestsKeys, k) 223 } 224 return p.getRenderedManifests(manifestsKeys, splitManifests), nil 225 } 226 227 func (p *Parser) getRelease(chart *chart.Chart) (*release.Release, error) { 228 opts := &ValueOptions{ 229 ValueFiles: p.valuesFiles, 230 Values: p.values, 231 FileValues: p.fileValues, 232 StringValues: p.stringValues, 233 } 234 235 vals, err := opts.MergeValues() 236 if err != nil { 237 return nil, err 238 } 239 r, err := p.helmClient.RunWithContext(context.Background(), chart, vals) 240 if err != nil { 241 return nil, err 242 } 243 244 if r == nil { 245 return nil, fmt.Errorf("there is nothing in the release") 246 } 247 return r, nil 248 } 249 250 func loadChart(tempFs string) (*chart.Chart, error) { 251 loadedChart, err := loader.Load(tempFs) 252 if err != nil { 253 return nil, err 254 } 255 256 if req := loadedChart.Metadata.Dependencies; req != nil { 257 if err := action.CheckDependencies(loadedChart, req); err != nil { 258 return nil, err 259 } 260 } 261 262 return loadedChart, nil 263 } 264 265 func (*Parser) getRenderedManifests(manifestsKeys []string, splitManifests map[string]string) []ChartFile { 266 sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys)) 267 var manifestsToRender []ChartFile 268 for _, manifestKey := range manifestsKeys { 269 manifest := splitManifests[manifestKey] 270 submatch := manifestNameRegex.FindStringSubmatch(manifest) 271 if len(submatch) == 0 { 272 continue 273 } 274 manifestsToRender = append(manifestsToRender, ChartFile{ 275 TemplateFilePath: getManifestPath(manifest), 276 ManifestContent: manifest, 277 }) 278 } 279 return manifestsToRender 280 } 281 282 func getManifestPath(manifest string) string { 283 lines := strings.Split(manifest, "\n") 284 if len(lines) == 0 { 285 return "unknown.yaml" 286 } 287 manifestFilePathParts := strings.SplitN(strings.TrimPrefix(lines[0], "# Source: "), "/", 2) 288 if len(manifestFilePathParts) > 1 { 289 return manifestFilePathParts[1] 290 } 291 return manifestFilePathParts[0] 292 } 293 294 func (p *Parser) writeBuildFiles(tempFs string) error { 295 for _, path := range p.filepaths { 296 content, err := fs.ReadFile(p.workingFS, path) 297 if err != nil { 298 return err 299 } 300 workingPath := strings.TrimPrefix(path, p.rootPath) 301 workingPath = filepath.Join(tempFs, workingPath) 302 if err := os.MkdirAll(filepath.Dir(workingPath), os.ModePerm); err != nil { 303 return err 304 } 305 if err := os.WriteFile(workingPath, content, os.ModePerm); err != nil { 306 return err 307 } 308 } 309 return nil 310 } 311 312 func (p *Parser) required(path string, workingFS fs.FS) bool { 313 if p.skipRequired { 314 return true 315 } 316 content, err := fs.ReadFile(workingFS, path) 317 if err != nil { 318 return false 319 } 320 321 return detection.IsType(path, bytes.NewReader(content), detection.FileTypeHelm) 322 }