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  }