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  }