mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
refactor(misconf): decouple input fs and track extracted files with fs references (#9281)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/liamg/memoryfs"
|
||||
"gopkg.in/yaml.v3"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
@@ -32,8 +33,7 @@ type Parser struct {
|
||||
helmClient *action.Install
|
||||
rootPath string
|
||||
ChartSource string
|
||||
filepaths []string
|
||||
workingFS fs.FS
|
||||
filepaths map[string]fs.FS
|
||||
valuesFiles []string
|
||||
values []string
|
||||
fileValues []string
|
||||
@@ -58,6 +58,7 @@ func New(src string, opts ...Option) (*Parser, error) {
|
||||
helmClient: client,
|
||||
ChartSource: src,
|
||||
logger: log.WithPrefix("helm parser"),
|
||||
filepaths: make(map[string]fs.FS),
|
||||
}
|
||||
|
||||
for _, option := range opts {
|
||||
@@ -81,9 +82,12 @@ func New(src string, opts ...Option) (*Parser, error) {
|
||||
}
|
||||
|
||||
func (p *Parser) ParseFS(ctx context.Context, fsys fs.FS, target string) error {
|
||||
p.workingFS = fsys
|
||||
return p.parseFS(ctx, fsys, target)
|
||||
}
|
||||
|
||||
if err := fs.WalkDir(p.workingFS, filepath.ToSlash(target), func(filePath string, entry fs.DirEntry, err error) error {
|
||||
func (p *Parser) parseFS(ctx context.Context, fsys fs.FS, target string) error {
|
||||
target = filepath.ToSlash(target)
|
||||
if err := fs.WalkDir(fsys, target, func(filePath string, entry fs.DirEntry, err error) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
@@ -96,31 +100,22 @@ func (p *Parser) ParseFS(ctx context.Context, fsys fs.FS, target string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := fs.Stat(p.workingFS, filePath); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if detection.IsArchive(filePath) && !isDependencyChartArchive(p.workingFS, filePath) {
|
||||
tarFS, err := p.addTarToFS(filePath)
|
||||
if errors.Is(err, errSkipFS) {
|
||||
if detection.IsArchive(filePath) && !isDependencyChartArchive(fsys, filePath) {
|
||||
memFS := memoryfs.New()
|
||||
if err := p.unpackArchive(fsys, memFS, filePath); errors.Is(err, errSkipFS) {
|
||||
// an unpacked Chart already exists
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to add tar %q to FS: %w", filePath, err)
|
||||
return fmt.Errorf("unpack archive %q: %w", filePath, err)
|
||||
}
|
||||
|
||||
targetPath := filepath.Dir(filePath)
|
||||
if targetPath == "" {
|
||||
targetPath = "."
|
||||
}
|
||||
|
||||
if err := p.ParseFS(ctx, tarFS, targetPath); err != nil {
|
||||
return fmt.Errorf("parse tar FS error: %w", err)
|
||||
if err := p.parseFS(ctx, memFS, "."); err != nil {
|
||||
return fmt.Errorf("parse archive FS error: %w", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return p.addPaths(filePath)
|
||||
}
|
||||
|
||||
return p.addPaths(fsys, filePath)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("walk dir error: %w", err)
|
||||
}
|
||||
@@ -138,26 +133,26 @@ func isDependencyChartArchive(fsys fs.FS, archivePath string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (p *Parser) addPaths(paths ...string) error {
|
||||
func (p *Parser) addPaths(fsys fs.FS, paths ...string) error {
|
||||
for _, filePath := range paths {
|
||||
if _, err := fs.Stat(p.workingFS, filePath); err != nil {
|
||||
if _, err := fs.Stat(fsys, filePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(filePath, "Chart.yaml") && p.rootPath == "" {
|
||||
if err := p.extractChartName(filePath); err != nil {
|
||||
if err := p.extractChartName(fsys, filePath); err != nil {
|
||||
return err
|
||||
}
|
||||
p.rootPath = filepath.Dir(filePath)
|
||||
}
|
||||
p.filepaths = append(p.filepaths, filePath)
|
||||
p.filepaths[filePath] = fsys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) extractChartName(chartPath string) error {
|
||||
func (p *Parser) extractChartName(fsys fs.FS, chartPath string) error {
|
||||
|
||||
chrt, err := p.workingFS.Open(chartPath)
|
||||
chrt, err := fsys.Open(chartPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -228,11 +223,10 @@ func (p *Parser) getRelease(chrt *chart.Chart) (*release.Release, error) {
|
||||
}
|
||||
|
||||
func (p *Parser) loadChart() (*chart.Chart, error) {
|
||||
|
||||
var files []*loader.BufferedFile
|
||||
|
||||
for _, filePath := range p.filepaths {
|
||||
b, err := fs.ReadFile(p.workingFS, filePath)
|
||||
for filePath, fsys := range p.filepaths {
|
||||
b, err := fs.ReadFile(fsys, filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -19,12 +19,10 @@ import (
|
||||
|
||||
var errSkipFS = errors.New("skip parse FS")
|
||||
|
||||
func (p *Parser) addTarToFS(archivePath string) (fs.FS, error) {
|
||||
tarFS := memoryfs.CloneFS(p.workingFS)
|
||||
|
||||
file, err := tarFS.Open(archivePath)
|
||||
func (p *Parser) unpackArchive(srcFS fs.FS, targetFS *memoryfs.FS, archivePath string) error {
|
||||
file, err := srcFS.Open(archivePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open tar: %w", err)
|
||||
return fmt.Errorf("failed to open tar: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@@ -33,7 +31,7 @@ func (p *Parser) addTarToFS(archivePath string) (fs.FS, error) {
|
||||
if detection.IsZip(archivePath) {
|
||||
zipped, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
return fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer zipped.Close()
|
||||
tr = tar.NewReader(zipped)
|
||||
@@ -50,7 +48,7 @@ func (p *Parser) addTarToFS(archivePath string) (fs.FS, error) {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get next entry: %w", err)
|
||||
return fmt.Errorf("failed to get next entry: %w", err)
|
||||
}
|
||||
|
||||
name := filepath.ToSlash(header.Name)
|
||||
@@ -59,28 +57,27 @@ func (p *Parser) addTarToFS(archivePath string) (fs.FS, error) {
|
||||
// Do not add archive files to FS if the chart already exists
|
||||
// This can happen when the source chart is located next to an archived chart (with the `helm package` command)
|
||||
// The first level folder in the archive is equal to the Chart name
|
||||
if _, err := tarFS.Stat(path.Dir(archivePath) + "/" + path.Dir(name)); err == nil {
|
||||
return nil, errSkipFS
|
||||
if _, err := fs.Stat(srcFS, path.Clean(path.Dir(archivePath)+"/"+path.Dir(name))); err == nil {
|
||||
return errSkipFS
|
||||
}
|
||||
checkExistedChart = false
|
||||
}
|
||||
|
||||
// get the individual path and extract to the current directory
|
||||
targetPath := path.Join(path.Dir(archivePath), path.Clean(name))
|
||||
|
||||
link := filepath.ToSlash(header.Linkname)
|
||||
targetPath := archiveEntryPath(archivePath, name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := tarFS.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return nil, err
|
||||
if err := targetFS.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
p.logger.Debug("Unpacking tar entry", log.FilePath(targetPath))
|
||||
if err := copyFile(tarFS, tr, targetPath); err != nil {
|
||||
return nil, err
|
||||
if err := copyFile(targetFS, tr, targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeSymlink:
|
||||
link := filepath.ToSlash(header.Linkname)
|
||||
if path.IsAbs(link) {
|
||||
p.logger.Debug("Symlink is absolute, skipping", log.String("link", link))
|
||||
continue
|
||||
@@ -88,21 +85,21 @@ func (p *Parser) addTarToFS(archivePath string) (fs.FS, error) {
|
||||
|
||||
symlinks[targetPath] = path.Join(path.Dir(targetPath), link) // nolint:gosec // virtual file system is used
|
||||
default:
|
||||
return nil, fmt.Errorf("header type %q is not supported", header.Typeflag)
|
||||
return fmt.Errorf("header type %q is not supported", header.Typeflag)
|
||||
}
|
||||
}
|
||||
|
||||
for target, link := range symlinks {
|
||||
if err := copySymlink(tarFS, link, target); err != nil {
|
||||
return nil, fmt.Errorf("copy symlink error: %w", err)
|
||||
if err := copySymlink(targetFS, link, target); err != nil {
|
||||
return fmt.Errorf("copy symlink error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tarFS.Remove(archivePath); err != nil {
|
||||
return nil, fmt.Errorf("remove tar from FS error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return tarFS, nil
|
||||
func archiveEntryPath(archivePath, name string) string {
|
||||
return path.Join(path.Dir(archivePath), path.Clean(name))
|
||||
}
|
||||
|
||||
func copySymlink(fsys *memoryfs.FS, src, dst string) error {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -19,7 +20,7 @@ func TestParseFS(t *testing.T) {
|
||||
"my-chart/Chart.yaml",
|
||||
"my-chart/templates/pod.yaml",
|
||||
}
|
||||
assert.Equal(t, expectedFiles, p.filepaths)
|
||||
assert.ElementsMatch(t, expectedFiles, lo.Keys(p.filepaths))
|
||||
})
|
||||
|
||||
t.Run("archive with symlinks", func(t *testing.T) {
|
||||
@@ -45,7 +46,7 @@ func TestParseFS(t *testing.T) {
|
||||
"chart/sym-to-dir/Chart.yaml",
|
||||
"chart/sym-to-file/Chart.yaml",
|
||||
}
|
||||
assert.Equal(t, expectedFiles, p.filepaths)
|
||||
assert.ElementsMatch(t, expectedFiles, lo.Keys(p.filepaths))
|
||||
})
|
||||
|
||||
t.Run("chart with multiple archived deps", func(t *testing.T) {
|
||||
@@ -60,7 +61,7 @@ func TestParseFS(t *testing.T) {
|
||||
"charts/common-2.26.0.tgz",
|
||||
"charts/opentelemetry-collector-0.108.0.tgz",
|
||||
}
|
||||
assert.Equal(t, expectedFiles, p.filepaths)
|
||||
assert.ElementsMatch(t, expectedFiles, lo.Keys(p.filepaths))
|
||||
})
|
||||
|
||||
t.Run("archives are not dependencies", func(t *testing.T) {
|
||||
@@ -75,6 +76,6 @@ func TestParseFS(t *testing.T) {
|
||||
"backup_charts/wordpress-operator/Chart.yaml",
|
||||
"backup_charts/mysql-operator/Chart.yaml",
|
||||
}
|
||||
assert.Subset(t, p.filepaths, expectedFiles)
|
||||
assert.Subset(t, lo.Keys(p.filepaths), expectedFiles)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user