mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
feat(terraform): use .terraform cache for remote modules in plan scanning (#9277)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
@@ -31,7 +31,7 @@ type evaluator struct {
|
||||
ctx *tfcontext.Context
|
||||
blocks terraform.Blocks
|
||||
inputVars map[string]cty.Value
|
||||
moduleMetadata *modulesMetadata
|
||||
moduleMetadata *ModulesMetadata
|
||||
projectRootPath string // root of the current scan
|
||||
modulePath string
|
||||
moduleName string
|
||||
@@ -50,7 +50,7 @@ func newEvaluator(
|
||||
moduleName string,
|
||||
blocks terraform.Blocks,
|
||||
inputVars map[string]cty.Value,
|
||||
moduleMetadata *modulesMetadata,
|
||||
moduleMetadata *ModulesMetadata,
|
||||
workspace string,
|
||||
ignores ignore.Rules,
|
||||
logger *log.Logger,
|
||||
|
||||
@@ -6,19 +6,21 @@ import (
|
||||
"path"
|
||||
)
|
||||
|
||||
const manifestSnapshotFile = ".terraform/modules/modules.json"
|
||||
const ManifestSnapshotFile = ".terraform/modules/modules.json"
|
||||
|
||||
type modulesMetadata struct {
|
||||
Modules []struct {
|
||||
Key string `json:"Key"`
|
||||
Source string `json:"Source"`
|
||||
Version string `json:"Version"`
|
||||
Dir string `json:"Dir"`
|
||||
} `json:"Modules"`
|
||||
type ModulesMetadata struct {
|
||||
Modules []ModuleMetadata `json:"Modules"`
|
||||
}
|
||||
|
||||
func loadModuleMetadata(target fs.FS, fullPath string) (*modulesMetadata, string, error) {
|
||||
metadataPath := path.Join(fullPath, manifestSnapshotFile)
|
||||
type ModuleMetadata struct {
|
||||
Key string `json:"Key"`
|
||||
Source string `json:"Source"`
|
||||
Version string `json:"Version"`
|
||||
Dir string `json:"Dir"`
|
||||
}
|
||||
|
||||
func loadModuleMetadata(target fs.FS, fullPath string) (*ModulesMetadata, string, error) {
|
||||
metadataPath := path.Join(fullPath, ManifestSnapshotFile)
|
||||
|
||||
f, err := target.Open(metadataPath)
|
||||
if err != nil {
|
||||
@@ -26,7 +28,7 @@ func loadModuleMetadata(target fs.FS, fullPath string) (*modulesMetadata, string
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var metadata modulesMetadata
|
||||
var metadata ModulesMetadata
|
||||
if err := json.NewDecoder(f).Decode(&metadata); err != nil {
|
||||
return nil, metadataPath, err
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ func (s *Scanner) scan(ctx context.Context, reader io.Reader) (scan.Results, err
|
||||
|
||||
s.inner.AddParserOptions(
|
||||
tfparser.OptionsWithTfVars(snap.inputVariables),
|
||||
tfparser.OptionWithDownloads(false),
|
||||
)
|
||||
return s.inner.ScanFS(ctx, fsys, ".")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/liamg/memoryfs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
iox "github.com/aquasecurity/trivy/pkg/x/io"
|
||||
)
|
||||
|
||||
@@ -27,13 +29,7 @@ const (
|
||||
)
|
||||
|
||||
type (
|
||||
configSnapshotModuleRecord struct {
|
||||
Key string `json:"Key"`
|
||||
SourceAddr string `json:"Source,omitempty"`
|
||||
Dir string `json:"Dir"`
|
||||
}
|
||||
|
||||
configSnapshotModuleManifest []configSnapshotModuleRecord
|
||||
configSnapshotModuleManifest []parser.ModuleMetadata
|
||||
)
|
||||
|
||||
var errNoTerraformPlan = errors.New("no terraform plan file")
|
||||
@@ -79,13 +75,11 @@ func parseSnapshot(r io.Reader) (*snapshot, error) {
|
||||
inputVariables: make(map[string]cty.Value),
|
||||
}
|
||||
|
||||
var moduleManifest configSnapshotModuleManifest
|
||||
|
||||
for _, file := range zr.File {
|
||||
switch {
|
||||
case file.Name == configSnapshotManifestFile:
|
||||
var err error
|
||||
moduleManifest, err = readModuleManifest(file)
|
||||
snap.moduleManifest, err = readModuleManifest(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,12 +107,7 @@ func parseSnapshot(r io.Reader) (*snapshot, error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, record := range moduleManifest {
|
||||
// skip non-local modules
|
||||
if record.Dir != "." && !strings.HasPrefix(record.SourceAddr, ".") {
|
||||
delete(snap.modules, record.Key)
|
||||
continue
|
||||
}
|
||||
for _, record := range snap.moduleManifest {
|
||||
modSnap := snap.getOrCreateModuleSnapshot(record.Key)
|
||||
modSnap.dir = record.Dir
|
||||
}
|
||||
@@ -159,6 +148,7 @@ type (
|
||||
}
|
||||
|
||||
snapshot struct {
|
||||
moduleManifest configSnapshotModuleManifest
|
||||
modules map[string]*snapshotModule
|
||||
inputVariables map[string]cty.Value
|
||||
}
|
||||
@@ -202,6 +192,10 @@ func (s *snapshot) getOrCreateModuleSnapshot(key string) *snapshotModule {
|
||||
func (s *snapshot) toFS() (fs.FS, error) {
|
||||
fsys := memoryfs.New()
|
||||
|
||||
if err := s.writeManifest(fsys); err != nil {
|
||||
log.WithPrefix(log.PrefixMisconfiguration).Error("Failed to write manifest file", log.Err(err))
|
||||
}
|
||||
|
||||
for _, module := range s.modules {
|
||||
if err := fsys.MkdirAll(module.dir, fs.ModePerm); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
return nil, err
|
||||
@@ -218,3 +212,19 @@ func (s *snapshot) toFS() (fs.FS, error) {
|
||||
}
|
||||
return fsys, nil
|
||||
}
|
||||
|
||||
func (s *snapshot) writeManifest(fsys *memoryfs.FS) error {
|
||||
if err := fsys.MkdirAll(path.Dir(parser.ManifestSnapshotFile), fs.ModePerm); err != nil {
|
||||
return fmt.Errorf("create manifest directory: %w", err)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(parser.ModulesMetadata{Modules: s.moduleManifest})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal manifest snapshot: %w", err)
|
||||
}
|
||||
|
||||
if err := fsys.WriteFile(parser.ManifestSnapshotFile, b, fs.ModePerm); err != nil {
|
||||
return fmt.Errorf("write manifest snapshot: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -21,23 +20,46 @@ func TestReadSnapshot(t *testing.T) {
|
||||
expectedFiles []string
|
||||
}{
|
||||
{
|
||||
name: "just resource",
|
||||
dir: "just-resource",
|
||||
expectedFiles: []string{"main.tf", "terraform.tf"},
|
||||
name: "just resource",
|
||||
dir: "just-resource",
|
||||
expectedFiles: []string{
|
||||
"main.tf",
|
||||
"terraform.tf",
|
||||
".terraform/modules/modules.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with local module",
|
||||
dir: "with-local-module",
|
||||
expectedFiles: []string{"main.tf", "modules/ec2/main.tf", "terraform.tf"},
|
||||
name: "with local module",
|
||||
dir: "with-local-module",
|
||||
expectedFiles: []string{
|
||||
"main.tf",
|
||||
"terraform.tf",
|
||||
"modules/ec2/main.tf",
|
||||
".terraform/modules/modules.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with nested modules",
|
||||
dir: "nested-modules",
|
||||
expectedFiles: []string{
|
||||
"main.tf",
|
||||
"terraform.tf",
|
||||
"modules/s3/main.tf",
|
||||
"modules/s3/modules/logging/main.tf",
|
||||
".terraform/modules/modules.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with remote module",
|
||||
dir: "with-remote-module",
|
||||
expectedFiles: []string{
|
||||
"main.tf",
|
||||
"terraform.tf",
|
||||
".terraform/modules/modules.json",
|
||||
".terraform/modules/s3_bucket/main.tf",
|
||||
".terraform/modules/s3_bucket/outputs.tf",
|
||||
".terraform/modules/s3_bucket/variables.tf",
|
||||
".terraform/modules/s3_bucket/versions.tf",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -58,7 +80,7 @@ func TestReadSnapshot(t *testing.T) {
|
||||
files, err := getAllfiles(fsys)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.expectedFiles, files)
|
||||
assert.ElementsMatch(t, tt.expectedFiles, files)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,8 +102,6 @@ func getAllfiles(fsys fs.FS) ([]string, error) {
|
||||
if err := fs.WalkDir(fsys, ".", walkFn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Strings(files)
|
||||
return files, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user