@@ -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
3029var manifestNameRegex = regexp .MustCompile ("# Source: [^/]+/(.+)" )
3130
3231type 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