github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/scanners/helm/parser/parser.go (about) 1 package parser 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/fs" 9 "os" 10 "path/filepath" 11 "regexp" 12 "sort" 13 "strings" 14 15 "github.com/google/uuid" 16 17 "github.com/khulnasoft-lab/defsec/pkg/debug" 18 19 "gopkg.in/yaml.v3" 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/khulnasoft-lab/defsec/pkg/detection" 27 "github.com/khulnasoft-lab/defsec/pkg/scanners/options" 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 err != nil { 126 return err 127 } 128 129 targetPath := filepath.Dir(path) 130 if targetPath == "" { 131 targetPath = "." 132 } 133 134 if err := p.ParseFS(ctx, tarFS, targetPath); err != nil { 135 return err 136 } 137 return nil 138 } 139 140 return p.addPaths(path) 141 }); err != nil { 142 return err 143 } 144 145 return nil 146 } 147 148 func (p *Parser) addPaths(paths ...string) error { 149 for _, path := range paths { 150 if _, err := fs.Stat(p.workingFS, path); err != nil { 151 return err 152 } 153 154 if strings.HasSuffix(path, "Chart.yaml") && p.rootPath == "" { 155 if err := p.extractChartName(path); err != nil { 156 return err 157 } 158 p.rootPath = filepath.Dir(path) 159 } 160 p.filepaths = append(p.filepaths, path) 161 } 162 return nil 163 } 164 165 func (p *Parser) extractChartName(chartPath string) error { 166 167 chart, err := p.workingFS.Open(chartPath) 168 if err != nil { 169 return err 170 } 171 defer func() { _ = chart.Close() }() 172 173 var chartContent map[string]interface{} 174 if err := yaml.NewDecoder(chart).Decode(&chartContent); err != nil { 175 // the chart likely has the name templated and so cannot be parsed as yaml - use a temporary name 176 if dir := filepath.Dir(chartPath); dir != "" && dir != "." { 177 p.helmClient.ReleaseName = dir 178 } else { 179 p.helmClient.ReleaseName = uuid.NewString() 180 } 181 return nil 182 } 183 184 if name, ok := chartContent["name"]; !ok { 185 return fmt.Errorf("could not extract the chart name from %s", chartPath) 186 } else { 187 p.helmClient.ReleaseName = fmt.Sprintf("%v", name) 188 } 189 return nil 190 } 191 192 func (p *Parser) RenderedChartFiles() ([]ChartFile, error) { 193 194 tempDir, err := os.MkdirTemp(os.TempDir(), "defsec") 195 if err != nil { 196 return nil, err 197 } 198 199 if err := p.writeBuildFiles(tempDir); err != nil { 200 return nil, err 201 } 202 203 workingChart, err := loadChart(tempDir) 204 if err != nil { 205 return nil, err 206 } 207 208 workingRelease, err := p.getRelease(workingChart) 209 if err != nil { 210 return nil, err 211 } 212 213 var manifests bytes.Buffer 214 _, _ = fmt.Fprintln(&manifests, strings.TrimSpace(workingRelease.Manifest)) 215 216 splitManifests := releaseutil.SplitManifests(manifests.String()) 217 manifestsKeys := make([]string, 0, len(splitManifests)) 218 for k := range splitManifests { 219 manifestsKeys = append(manifestsKeys, k) 220 } 221 return p.getRenderedManifests(manifestsKeys, splitManifests), nil 222 } 223 224 func (p *Parser) getRelease(chart *chart.Chart) (*release.Release, error) { 225 opts := &ValueOptions{ 226 ValueFiles: p.valuesFiles, 227 Values: p.values, 228 FileValues: p.fileValues, 229 StringValues: p.stringValues, 230 } 231 232 vals, err := opts.MergeValues() 233 if err != nil { 234 return nil, err 235 } 236 r, err := p.helmClient.RunWithContext(context.Background(), chart, vals) 237 if err != nil { 238 return nil, err 239 } 240 241 if r == nil { 242 return nil, fmt.Errorf("there is nothing in the release") 243 } 244 return r, nil 245 } 246 247 func loadChart(tempFs string) (*chart.Chart, error) { 248 loadedChart, err := loader.Load(tempFs) 249 if err != nil { 250 return nil, err 251 } 252 253 if req := loadedChart.Metadata.Dependencies; req != nil { 254 if err := action.CheckDependencies(loadedChart, req); err != nil { 255 return nil, err 256 } 257 } 258 259 return loadedChart, nil 260 } 261 262 func (*Parser) getRenderedManifests(manifestsKeys []string, splitManifests map[string]string) []ChartFile { 263 sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys)) 264 var manifestsToRender []ChartFile 265 for _, manifestKey := range manifestsKeys { 266 manifest := splitManifests[manifestKey] 267 submatch := manifestNameRegex.FindStringSubmatch(manifest) 268 if len(submatch) == 0 { 269 continue 270 } 271 manifestsToRender = append(manifestsToRender, ChartFile{ 272 TemplateFilePath: getManifestPath(manifest), 273 ManifestContent: manifest, 274 }) 275 } 276 return manifestsToRender 277 } 278 279 func getManifestPath(manifest string) string { 280 lines := strings.Split(manifest, "\n") 281 if len(lines) == 0 { 282 return "unknown.yaml" 283 } 284 manifestFilePathParts := strings.SplitN(strings.TrimPrefix(lines[0], "# Source: "), "/", 2) 285 if len(manifestFilePathParts) > 1 { 286 return manifestFilePathParts[1] 287 } 288 return manifestFilePathParts[0] 289 } 290 291 func (p *Parser) writeBuildFiles(tempFs string) error { 292 for _, path := range p.filepaths { 293 content, err := fs.ReadFile(p.workingFS, path) 294 if err != nil { 295 return err 296 } 297 workingPath := strings.TrimPrefix(path, p.rootPath) 298 workingPath = filepath.Join(tempFs, workingPath) 299 if err := os.MkdirAll(filepath.Dir(workingPath), os.ModePerm); err != nil { 300 return err 301 } 302 if err := os.WriteFile(workingPath, content, os.ModePerm); err != nil { 303 return err 304 } 305 } 306 return nil 307 } 308 309 func (p *Parser) required(path string, workingFS fs.FS) bool { 310 if p.skipRequired { 311 return true 312 } 313 content, err := fs.ReadFile(workingFS, path) 314 if err != nil { 315 return false 316 } 317 318 return detection.IsType(path, bytes.NewReader(content), detection.FileTypeHelm) 319 }