Skip to content

Commit f5e655e

Browse files
author
Owen Rumney
authored
feat: adding helm support (fanal#534)
* feat: adding helm support - adding tests for helm analyzer - add test for non helm tarball - adding in-memory filesystem for helm - handle multiple charts at a time - check the size is smaller than arbitrary size of 200MB if a tarball
1 parent df47d1b commit f5e655e

36 files changed

Lines changed: 2146 additions & 320 deletions

analyzer/analyzer.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,9 @@ func (ag AnalyzerGroup) AnalyzeFile(ctx context.Context, wg *sync.WaitGroup, lim
319319
log.Logger.Debugf("Analysis error: %s", err)
320320
return
321321
}
322-
result.Merge(ret)
322+
if ret != nil {
323+
result.Merge(ret)
324+
}
323325
}(a, rc)
324326
}
325327

analyzer/config/config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"sort"
66
"strings"
77

8+
"github.com/aquasecurity/fanal/analyzer/config/helm"
89
"golang.org/x/xerrors"
910

1011
"github.com/aquasecurity/fanal/analyzer"
@@ -35,7 +36,7 @@ func (o *ScannerOption) Sort() {
3536
}
3637

3738
func RegisterConfigAnalyzers(filePatterns []string) error {
38-
var dockerRegexp, jsonRegexp, yamlRegexp *regexp.Regexp
39+
var dockerRegexp, jsonRegexp, yamlRegexp, helmRegexp *regexp.Regexp
3940
for _, p := range filePatterns {
4041
// e.g. "dockerfile:my_dockerfile_*"
4142
s := strings.SplitN(p, separator, 2)
@@ -55,6 +56,8 @@ func RegisterConfigAnalyzers(filePatterns []string) error {
5556
jsonRegexp = r
5657
case types.YAML:
5758
yamlRegexp = r
59+
case types.Helm:
60+
helmRegexp = r
5861
default:
5962
return xerrors.Errorf("unknown file type: %s, pattern: %s", fileType, pattern)
6063
}
@@ -64,6 +67,7 @@ func RegisterConfigAnalyzers(filePatterns []string) error {
6467
analyzer.RegisterAnalyzer(terraform.NewConfigAnalyzer())
6568
analyzer.RegisterAnalyzer(json.NewConfigAnalyzer(jsonRegexp))
6669
analyzer.RegisterAnalyzer(yaml.NewConfigAnalyzer(yamlRegexp))
70+
analyzer.RegisterAnalyzer(helm.NewConfigAnalyzer(helmRegexp))
6771

6872
return nil
6973
}

analyzer/config/helm/helm.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package helm
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"context"
7+
"errors"
8+
"io"
9+
"os"
10+
"path/filepath"
11+
"regexp"
12+
"strings"
13+
14+
"github.com/aquasecurity/fanal/analyzer"
15+
"github.com/aquasecurity/fanal/types"
16+
dio "github.com/aquasecurity/go-dep-parser/pkg/io"
17+
"golang.org/x/xerrors"
18+
)
19+
20+
const version = 1
21+
22+
const maxTarSize = 209_715_200 // 200MB
23+
24+
type ConfigAnalyzer struct {
25+
filePattern *regexp.Regexp
26+
}
27+
28+
func NewConfigAnalyzer(filePattern *regexp.Regexp) ConfigAnalyzer {
29+
return ConfigAnalyzer{
30+
filePattern: filePattern,
31+
}
32+
}
33+
34+
func (a ConfigAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
35+
if isArchive(input.FilePath) {
36+
if !isHelmChart(input.FilePath, input.Content) {
37+
return nil, nil
38+
}
39+
// reset the content
40+
_, err := input.Content.Seek(0, 0)
41+
if err != nil {
42+
return nil, err
43+
}
44+
}
45+
b, err := io.ReadAll(input.Content)
46+
if err != nil {
47+
return nil, xerrors.Errorf("failed to read %s: %w", input.FilePath, err)
48+
}
49+
50+
return &analyzer.AnalysisResult{
51+
Files: map[types.HandlerType][]types.File{
52+
// it will be passed to misconfig post handler
53+
types.MisconfPostHandler: {
54+
{
55+
Type: types.Helm,
56+
Path: input.FilePath,
57+
Content: b,
58+
},
59+
},
60+
},
61+
}, nil
62+
}
63+
64+
func (a ConfigAnalyzer) Required(filePath string, info os.FileInfo) bool {
65+
if a.filePattern != nil && a.filePattern.MatchString(filePath) {
66+
return true
67+
}
68+
69+
if info.Size() > maxTarSize {
70+
// tarball is too big to be Helm chart - move on
71+
return false
72+
}
73+
74+
ext := filepath.Ext(filePath)
75+
for _, acceptable := range []string{".tpl", ".json", ".yaml", ".tar", ".tgz", ".tar.gz"} {
76+
if strings.EqualFold(ext, acceptable) {
77+
return true
78+
}
79+
}
80+
81+
name := filepath.Base(filePath)
82+
for _, acceptable := range []string{"Chart.yaml", ".helmignore"} {
83+
if strings.EqualFold(name, acceptable) {
84+
return true
85+
}
86+
}
87+
88+
return false
89+
}
90+
91+
func (ConfigAnalyzer) Type() analyzer.Type {
92+
return analyzer.TypeHelm
93+
}
94+
95+
func (ConfigAnalyzer) Version() int {
96+
return version
97+
}
98+
99+
func isHelmChart(path string, file dio.ReadSeekerAt) bool {
100+
101+
var err error
102+
var fr io.Reader = file
103+
104+
if isGzip(path) {
105+
if fr, err = gzip.NewReader(file); err != nil {
106+
return false
107+
}
108+
}
109+
tr := tar.NewReader(fr)
110+
111+
for {
112+
header, err := tr.Next()
113+
if err != nil {
114+
if errors.Is(err, io.EOF) {
115+
break
116+
}
117+
return false
118+
}
119+
120+
if header.Typeflag == tar.TypeReg && strings.HasSuffix(header.Name, "Chart.yaml") {
121+
return true
122+
}
123+
}
124+
return false
125+
}
126+
127+
func isArchive(path string) bool {
128+
if strings.HasSuffix(path, ".tar") || isGzip(path) {
129+
return true
130+
}
131+
return false
132+
}
133+
134+
func isGzip(path string) bool {
135+
if strings.HasSuffix(path, ".tgz") ||
136+
strings.HasSuffix(path, ".tar.gz") {
137+
return true
138+
}
139+
return false
140+
}

0 commit comments

Comments
 (0)