Skip to content

Commit 441251e

Browse files
authored
refactor(misconf): replace custom Helm archive parsing with Helm SDK loaders (#10718)
1 parent 7d9d519 commit 441251e

143 files changed

Lines changed: 1746 additions & 6984 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

internal/testutil/gzip.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package testutil
22

33
import (
4+
"archive/tar"
45
"bytes"
56
"compress/gzip"
67
"io"
8+
"io/fs"
79
"os"
810
"testing"
911

@@ -75,3 +77,39 @@ func DecompressSparseGzip(t *testing.T, src, dst string) {
7577
}
7678
}
7779
}
80+
81+
// FSToTarGz creates an in-memory tar.gz archive from an fs.FS.
82+
// prefix is prepended to every file path inside the archive.
83+
func FSToTarGz(t *testing.T, fsys fs.FS, prefix string) []byte {
84+
t.Helper()
85+
var buf bytes.Buffer
86+
gw := gzip.NewWriter(&buf)
87+
tw := tar.NewWriter(gw)
88+
require.NoError(t, writeFSToTar(tw, fsys, prefix))
89+
require.NoError(t, tw.Close())
90+
require.NoError(t, gw.Close())
91+
return buf.Bytes()
92+
}
93+
94+
func writeFSToTar(tw *tar.Writer, fsys fs.FS, prefix string) error {
95+
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
96+
if err != nil || d.IsDir() {
97+
return err
98+
}
99+
data, err := fs.ReadFile(fsys, path)
100+
if err != nil {
101+
return err
102+
}
103+
name := path
104+
if prefix != "" {
105+
name = prefix + "/" + path
106+
}
107+
if err := tw.WriteHeader(&tar.Header{
108+
Name: name, Typeflag: tar.TypeReg, Size: int64(len(data)), Mode: 0o644,
109+
}); err != nil {
110+
return err
111+
}
112+
_, err = tw.Write(data)
113+
return err
114+
})
115+
}

pkg/iac/detection/peek.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func IsHelmChartArchive(path string, file io.Reader) bool {
4545
}
4646

4747
func IsArchive(path string) bool {
48-
return strings.HasSuffix(path, ".tar") || IsZip(path)
48+
return IsZip(path)
4949
}
5050

5151
func IsZip(path string) bool {

pkg/iac/scanners/helm/parser/parser.go

Lines changed: 99 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,18 @@ import (
1919
"helm.sh/helm/v4/pkg/chart/loader/archive"
2020
chartv2 "helm.sh/helm/v4/pkg/chart/v2"
2121
loaderv2 "helm.sh/helm/v4/pkg/chart/v2/loader"
22+
chartutilv2 "helm.sh/helm/v4/pkg/chart/v2/util"
2223
"helm.sh/helm/v4/pkg/release"
2324
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
2425

25-
"github.com/aquasecurity/trivy/pkg/iac/detection"
2626
"github.com/aquasecurity/trivy/pkg/log"
27-
"github.com/aquasecurity/trivy/pkg/mapfs"
2827
)
2928

3029
var manifestNameRegex = regexp.MustCompile("# Source: [^/]+/(.+)")
3130

3231
type Parser struct {
3332
logger *log.Logger
3433
helmClient *action.Install
35-
rootPath string
36-
ChartSource string
37-
filepaths map[string]fs.FS
3834
valuesFiles []string
3935
values []string
4036
fileValues []string
@@ -43,22 +39,20 @@ type Parser struct {
4339
kubeVersion string
4440
}
4541

46-
type ChartFile struct {
47-
TemplateFilePath string
48-
ManifestContent string
42+
// Manifest is a rendered Helm template — the output of helm template for a single Kubernetes resource.
43+
type Manifest struct {
44+
Path string
45+
Content string
4946
}
5047

51-
func New(src string, opts ...Option) (*Parser, error) {
52-
48+
func New(opts ...Option) (*Parser, error) {
5349
client := action.NewInstall(&action.Configuration{})
5450
client.DryRunStrategy = action.DryRunClient // to avoid the client making calls to the server
5551
client.Replace = true // skip name check
5652

5753
p := &Parser{
58-
helmClient: client,
59-
ChartSource: src,
60-
logger: log.WithPrefix("helm parser"),
61-
filepaths: make(map[string]fs.FS),
54+
helmClient: client,
55+
logger: log.WithPrefix("helm parser"),
6256
}
6357

6458
for _, option := range opts {
@@ -81,11 +75,11 @@ func New(src string, opts ...Option) (*Parser, error) {
8175
return p, nil
8276
}
8377

84-
func (p *Parser) ParseFS(ctx context.Context, fsys fs.FS, target string) error {
85-
return p.parseFS(ctx, fsys, target)
86-
}
78+
// ParseFS renders a Helm chart from a directory rooted at target within fsys.
79+
func (p *Parser) ParseFS(ctx context.Context, fsys fs.FS, target string) ([]Manifest, error) {
80+
var rootPath string
81+
filepaths := make(map[string]fs.FS)
8782

88-
func (p *Parser) parseFS(ctx context.Context, fsys fs.FS, target string) error {
8983
target = filepath.ToSlash(target)
9084
if err := fs.WalkDir(fsys, target, func(filePath string, entry fs.DirEntry, err error) error {
9185
select {
@@ -100,102 +94,123 @@ func (p *Parser) parseFS(ctx context.Context, fsys fs.FS, target string) error {
10094
return nil
10195
}
10296

103-
if detection.IsArchive(filePath) && !isDependencyChartArchive(fsys, filePath) {
104-
memFS := mapfs.New()
105-
if err := p.unpackArchive(fsys, memFS, filePath); errors.Is(err, errSkipFS) {
106-
// an unpacked Chart already exists
107-
return nil
108-
} else if err != nil {
109-
return fmt.Errorf("unpack archive %q: %w", filePath, err)
97+
if strings.HasSuffix(filePath, chartutilv2.ChartfileName) && rootPath == "" {
98+
data, err := fs.ReadFile(fsys, filePath)
99+
if err != nil {
100+
return err
110101
}
111-
112-
if err := p.parseFS(ctx, memFS, "."); err != nil {
113-
return fmt.Errorf("parse archive FS error: %w", err)
102+
dir := path.Dir(filePath)
103+
if dir == "." {
104+
dir = ""
114105
}
115-
return nil
106+
p.applyChartName(data, dir)
107+
rootPath = path.Dir(filePath)
116108
}
117-
118-
return p.addPaths(fsys, filePath)
109+
filepaths[filePath] = fsys
110+
return nil
119111
}); err != nil {
120-
return fmt.Errorf("walk dir error: %w", err)
112+
return nil, fmt.Errorf("walk dir error: %w", err)
121113
}
122114

123-
return nil
124-
}
125-
126-
func isDependencyChartArchive(fsys fs.FS, archivePath string) bool {
127-
parent := path.Dir(archivePath)
128-
if path.Base(parent) != "charts" {
129-
return false
115+
var files []*archive.BufferedFile
116+
for filePath, fsys := range filepaths {
117+
b, err := fs.ReadFile(fsys, filePath)
118+
if err != nil {
119+
return nil, err
120+
}
121+
filePath = strings.TrimPrefix(filePath, rootPath+"/")
122+
filePath = filepath.ToSlash(filePath)
123+
files = append(files, &archive.BufferedFile{
124+
Name: filePath,
125+
Data: b,
126+
})
130127
}
131128

132-
_, err := fs.Stat(fsys, path.Join(parent, "..", "Chart.yaml"))
133-
return err == nil
129+
return p.render(ctx, files)
134130
}
135131

136-
func (p *Parser) addPaths(fsys fs.FS, paths ...string) error {
137-
for _, filePath := range paths {
138-
if _, err := fs.Stat(fsys, filePath); err != nil {
139-
return err
140-
}
132+
// ParseArchive renders a Helm chart from a gzip-compressed tar archive within fsys.
133+
func (p *Parser) ParseArchive(ctx context.Context, fsys fs.FS, archivePath string) ([]Manifest, error) {
134+
f, err := fsys.Open(archivePath)
135+
if err != nil {
136+
return nil, fmt.Errorf("open archive: %w", err)
137+
}
138+
defer f.Close()
141139

142-
if strings.HasSuffix(filePath, "Chart.yaml") && p.rootPath == "" {
143-
if err := p.extractChartName(fsys, filePath); err != nil {
144-
return err
145-
}
146-
p.rootPath = filepath.Dir(filePath)
140+
files, err := archive.LoadArchiveFiles(f)
141+
if err != nil {
142+
return nil, fmt.Errorf("load archive files: %w", err)
143+
}
144+
145+
for _, file := range files {
146+
if file.Name == chartutilv2.ChartfileName {
147+
p.applyChartName(file.Data, "")
148+
break
147149
}
148-
p.filepaths[filePath] = fsys
149150
}
150-
return nil
151+
152+
return p.render(ctx, files)
151153
}
152154

153-
func (p *Parser) extractChartName(fsys fs.FS, chartPath string) error {
154-
chrt, err := fsys.Open(chartPath)
155-
if err != nil {
156-
return err
155+
func (p *Parser) applyChartName(data []byte, fallback string) {
156+
if name, err := parseChartName(data); err == nil {
157+
p.helmClient.ReleaseName = name
158+
return
157159
}
158-
defer func() { _ = chrt.Close() }()
159-
160-
var chartContent map[string]any
161-
if err := yaml.NewDecoder(chrt).Decode(&chartContent); err != nil {
162-
// the chart likely has the name templated and so cannot be parsed as yaml - use a temporary name
163-
if dir := filepath.Dir(chartPath); dir != "" && dir != "." {
164-
p.helmClient.ReleaseName = dir
165-
} else {
166-
p.helmClient.ReleaseName = uuid.NewString()
167-
}
168-
return nil
160+
if fallback != "" {
161+
p.helmClient.ReleaseName = fallback
162+
} else {
163+
p.helmClient.ReleaseName = uuid.NewString()
169164
}
165+
}
170166

171-
name, ok := chartContent["name"]
172-
if !ok {
173-
return fmt.Errorf("could not extract the chart name from %s", chartPath)
167+
func parseChartName(data []byte) (string, error) {
168+
var meta struct {
169+
Name string `yaml:"name"`
174170
}
175-
p.helmClient.ReleaseName = fmt.Sprintf("%v", name)
176-
return nil
171+
if err := yaml.Unmarshal(data, &meta); err != nil {
172+
return "", err
173+
}
174+
if meta.Name == "" {
175+
return "", errors.New("could not extract the chart name from Chart.yaml")
176+
}
177+
return meta.Name, nil
177178
}
178179

179-
func (p *Parser) RenderedChartFiles() ([]ChartFile, error) {
180-
chrt, err := p.loadChart()
180+
func (p *Parser) render(ctx context.Context, files []*archive.BufferedFile) ([]Manifest, error) {
181+
chrt, err := p.loadChart(files)
181182
if err != nil {
182183
return nil, err
183184
}
184185

185-
acc, err := p.getRelease(chrt)
186+
acc, err := p.getRelease(ctx, chrt)
186187
if err != nil {
187188
return nil, err
188189
}
189190

190-
splitManifests := releaseutil.SplitManifests(strings.TrimSpace(acc.Manifest()))
191-
manifestsKeys := make([]string, 0, len(splitManifests))
192-
for k := range splitManifests {
193-
manifestsKeys = append(manifestsKeys, k)
191+
manifests := releaseutil.SplitManifests(strings.TrimSpace(acc.Manifest()))
192+
keys := make([]string, 0, len(manifests))
193+
for k := range manifests {
194+
keys = append(keys, k)
194195
}
195-
return p.getRenderedManifests(manifestsKeys, splitManifests), nil
196+
sort.Sort(releaseutil.BySplitManifestsOrder(keys))
197+
198+
var result []Manifest
199+
for _, key := range keys {
200+
manifest := manifests[key]
201+
submatch := manifestNameRegex.FindStringSubmatch(manifest)
202+
if submatch == nil {
203+
continue
204+
}
205+
result = append(result, Manifest{
206+
Path: submatch[1],
207+
Content: manifest,
208+
})
209+
}
210+
return result, nil
196211
}
197212

198-
func (p *Parser) getRelease(chrt *chartv2.Chart) (release.Accessor, error) {
213+
func (p *Parser) getRelease(ctx context.Context, chrt *chartv2.Chart) (release.Accessor, error) {
199214
opts := &ValueOptions{
200215
ValueFiles: p.valuesFiles,
201216
Values: p.values,
@@ -208,7 +223,7 @@ func (p *Parser) getRelease(chrt *chartv2.Chart) (release.Accessor, error) {
208223
return nil, err
209224
}
210225

211-
rel, err := p.helmClient.RunWithContext(context.Background(), chrt, vals)
226+
rel, err := p.helmClient.RunWithContext(ctx, chrt, vals)
212227
if err != nil {
213228
return nil, err
214229
}
@@ -224,23 +239,7 @@ func (p *Parser) getRelease(chrt *chartv2.Chart) (release.Accessor, error) {
224239
return acc, nil
225240
}
226241

227-
func (p *Parser) loadChart() (*chartv2.Chart, error) {
228-
var files []*archive.BufferedFile
229-
230-
for filePath, fsys := range p.filepaths {
231-
b, err := fs.ReadFile(fsys, filePath)
232-
if err != nil {
233-
return nil, err
234-
}
235-
236-
filePath = strings.TrimPrefix(filePath, p.rootPath+"/")
237-
filePath = filepath.ToSlash(filePath)
238-
files = append(files, &archive.BufferedFile{
239-
Name: filePath,
240-
Data: b,
241-
})
242-
}
243-
242+
func (p *Parser) loadChart(files []*archive.BufferedFile) (*chartv2.Chart, error) {
244243
chrt, err := loaderv2.LoadFiles(files)
245244
if err != nil {
246245
return nil, err
@@ -259,32 +258,3 @@ func (p *Parser) loadChart() (*chartv2.Chart, error) {
259258

260259
return chrt, nil
261260
}
262-
263-
func (*Parser) getRenderedManifests(manifestsKeys []string, splitManifests map[string]string) []ChartFile {
264-
sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))
265-
var manifestsToRender []ChartFile
266-
for _, manifestKey := range manifestsKeys {
267-
manifest := splitManifests[manifestKey]
268-
submatch := manifestNameRegex.FindStringSubmatch(manifest)
269-
if len(submatch) == 0 {
270-
continue
271-
}
272-
manifestsToRender = append(manifestsToRender, ChartFile{
273-
TemplateFilePath: getManifestPath(manifest),
274-
ManifestContent: manifest,
275-
})
276-
}
277-
return manifestsToRender
278-
}
279-
280-
func getManifestPath(manifest string) string {
281-
lines := strings.Split(manifest, "\n")
282-
if len(lines) == 0 {
283-
return "unknown.yaml"
284-
}
285-
manifestFilePathParts := strings.SplitN(strings.TrimPrefix(lines[0], "# Source: "), "/", 2)
286-
if len(manifestFilePathParts) > 1 {
287-
return manifestFilePathParts[1]
288-
}
289-
return manifestFilePathParts[0]
290-
}

0 commit comments

Comments
 (0)