Skip to content

Commit

Permalink
feat(misconf): support symlinks inside of Helm archives
Browse files Browse the repository at this point in the history
  • Loading branch information
nikpivkin committed May 3, 2024
1 parent 770b141 commit c4cdbca
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 32 deletions.
6 changes: 3 additions & 3 deletions pkg/iac/scanners/helm/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"helm.sh/helm/v3/pkg/releaseutil"

"github.com/aquasecurity/trivy/pkg/iac/debug"
detection2 "github.com/aquasecurity/trivy/pkg/iac/detection"
"github.com/aquasecurity/trivy/pkg/iac/detection"
"github.com/aquasecurity/trivy/pkg/iac/scanners/options"
)

Expand Down Expand Up @@ -133,7 +133,7 @@ func (p *Parser) ParseFS(ctx context.Context, target fs.FS, path string) error {
return nil
}

if detection2.IsArchive(path) {
if detection.IsArchive(path) {
tarFS, err := p.addTarToFS(path)
if errors.Is(err, errSkipFS) {
// an unpacked Chart already exists
Expand Down Expand Up @@ -320,5 +320,5 @@ func (p *Parser) required(path string, workingFS fs.FS) bool {
return false
}

return detection2.IsType(path, bytes.NewReader(content), detection2.FileTypeHelm)
return detection.IsType(path, bytes.NewReader(content), detection.FileTypeHelm)
}
110 changes: 81 additions & 29 deletions pkg/iac/scanners/helm/parser/parser_tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package parser

import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"

"github.com/liamg/memoryfs"
Expand All @@ -18,18 +18,18 @@ import (

var errSkipFS = errors.New("skip parse FS")

func (p *Parser) addTarToFS(path string) (fs.FS, error) {
func (p *Parser) addTarToFS(archivePath string) (fs.FS, error) {

Check failure on line 21 in pkg/iac/scanners/helm/parser/parser_tar.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

cyclomatic complexity 23 of func `(*Parser).addTarToFS` is high (> 20) (gocyclo)

Check failure on line 21 in pkg/iac/scanners/helm/parser/parser_tar.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

cyclomatic complexity 23 of func `(*Parser).addTarToFS` is high (> 20) (gocyclo)
tarFS := memoryfs.CloneFS(p.workingFS)

file, err := tarFS.Open(path)
file, err := tarFS.Open(archivePath)
if err != nil {
return nil, fmt.Errorf("failed to open tar: %w", err)
}
defer file.Close()

var tr *tar.Reader

if detection.IsZip(path) {
if detection.IsZip(archivePath) {
zipped, err := gzip.NewReader(file)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
Expand All @@ -41,6 +41,7 @@ func (p *Parser) addTarToFS(path string) (fs.FS, error) {
}

checkExistedChart := true
symlinks := make(map[string]string)

for {
header, err := tr.Next()
Expand All @@ -55,57 +56,108 @@ func (p *Parser) addTarToFS(path 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(filepath.Dir(path) + "/" + filepath.Dir(header.Name)); err == nil {
if _, err := tarFS.Stat(filepath.Dir(archivePath) + "/" + filepath.Dir(header.Name)); err == nil {
return nil, errSkipFS
}
checkExistedChart = false
}

// get the individual path and extract to the current directory
entryPath := header.Name
targetPath := path.Join(filepath.Dir(archivePath), filepath.Clean(header.Name))

switch header.Typeflag {
case tar.TypeDir:
if err := tarFS.MkdirAll(entryPath, os.FileMode(header.Mode)); err != nil && !errors.Is(err, fs.ErrExist) {
if err := tarFS.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil && !errors.Is(err, fs.ErrExist) {
return nil, err
}
case tar.TypeReg:
writePath := filepath.Dir(path) + "/" + entryPath
p.debug.Log("Unpacking tar entry %s", writePath)

_ = tarFS.MkdirAll(filepath.Dir(writePath), fs.ModePerm)

buf, err := copyChunked(tr, 1024)
if err != nil {
p.debug.Log("Unpacking tar entry %s", targetPath)
if err := copyFile(tarFS, tr, targetPath); err != nil {
return nil, err
}

p.debug.Log("writing file contents to %s", writePath)
if err := tarFS.WriteFile(writePath, buf.Bytes(), fs.ModePerm); err != nil {
return nil, fmt.Errorf("write file error: %w", err)
case tar.TypeSymlink:
if filepath.IsAbs(header.Linkname) {
p.debug.Log("Symlink %s is absolute, skipping", header.Linkname)
continue
}

symlinks[targetPath] = path.Join(filepath.Dir(targetPath), header.Linkname)

Check failure on line 84 in pkg/iac/scanners/helm/parser/parser_tar.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

G305: File traversal when extracting zip/tar archive (gosec)
default:
return nil, fmt.Errorf("header type %q is not supported", header.Typeflag)
}
}

if err := tarFS.Remove(path); err != nil {
return nil, fmt.Errorf("failed to remove tar from FS: %w", err)
for target, link := range symlinks {
fi, err := tarFS.Stat(link)
if err != nil {
p.debug.Log("stat error: %s", err)
continue
}
if fi.IsDir() {
if err := copyDir(tarFS, link, target); err != nil {
return nil, fmt.Errorf("copy dir error: %w", err)
}
continue
}

f, err := tarFS.Open(link)
if err != nil {
return nil, fmt.Errorf("open symlink error: %w", err)
}

if err := copyFile(tarFS, f, target); err != nil {
f.Close()

Check failure on line 109 in pkg/iac/scanners/helm/parser/parser_tar.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

G104: Errors unhandled. (gosec)
return nil, fmt.Errorf("copy file error: %w", err)
}
f.Close()

Check failure on line 112 in pkg/iac/scanners/helm/parser/parser_tar.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

G104: Errors unhandled. (gosec)
}

if err := tarFS.Remove(archivePath); err != nil {
return nil, fmt.Errorf("remove tar from FS error: %w", err)
}

return tarFS, nil
}

func copyChunked(src io.Reader, chunkSize int64) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
for {
if _, err := io.CopyN(buf, src, chunkSize); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("failed to copy: %w", err)
func copyFile(fsys *memoryfs.FS, src io.Reader, dst string) error {
if err := fsys.MkdirAll(filepath.Dir(dst), fs.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) {
return fmt.Errorf("mkdir error: %w", err)
}

b, err := io.ReadAll(src)
if err != nil {
return fmt.Errorf("read error: %w", err)
}

if err := fsys.WriteFile(dst, b, fs.ModePerm); err != nil {
return fmt.Errorf("write file error: %w", err)
}

return nil
}

func copyDir(fsys *memoryfs.FS, src string, dst string) error {

Check failure on line 139 in pkg/iac/scanners/helm/parser/parser_tar.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

paramTypeCombine: func(fsys *memoryfs.FS, src string, dst string) error could be replaced with func(fsys *memoryfs.FS, src, dst string) error (gocritic)

Check failure on line 139 in pkg/iac/scanners/helm/parser/parser_tar.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

paramTypeCombine: func(fsys *memoryfs.FS, src string, dst string) error could be replaced with func(fsys *memoryfs.FS, src, dst string) error (gocritic)
walkFn := func(filePath string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}

if entry.IsDir() {
return nil
}

dst := path.Join(dst, filePath[len(src):])

f, err := fsys.Open(filePath)
if err != nil {
return err
}

if err := copyFile(fsys, f, dst); err != nil {
return fmt.Errorf("copy file error: %w", err)
}
return nil
}

return buf, nil
return fs.WalkDir(fsys, src, walkFn)
}
22 changes: 22 additions & 0 deletions pkg/iac/scanners/helm/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,26 @@ func TestParseFS(t *testing.T) {
}
assert.Equal(t, expectedFiles, p.filepaths)
})

t.Run("archive with symlinks", func(t *testing.T) {
// mkdir -p chart && cd $_
// touch Chart.yaml
// mkdir -p dir && cp -p Chart.yaml dir/Chart.yaml
// mkdir -p sym-to-file && ln -s ../Chart.yaml sym-to-file/Chart.yaml
// ln -s dir sym-to-dir
// cd .. && tar -czvf chart.tar.gz chart && rm -rf chart
p, err := New(".")
require.NoError(t, err)

fsys := os.DirFS(filepath.Join("testdata", "archive-with-symlinks"))
require.NoError(t, p.ParseFS(context.TODO(), fsys, "chart.tar.gz"))

expectedFiles := []string{
"chart/Chart.yaml",
"chart/dir/Chart.yaml",
"chart/sym-to-dir/Chart.yaml",
"chart/sym-to-file/Chart.yaml",
}
assert.Equal(t, expectedFiles, p.filepaths)
})
}
Binary file not shown.

0 comments on commit c4cdbca

Please sign in to comment.