package local import ( "context" "crypto/sha256" "encoding/json" "os" "path" "path/filepath" "strings" "sync" "github.com/google/wire" "github.com/opencontainers/go-digest" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/artifact" "github.com/aquasecurity/trivy/pkg/fanal/cache" "github.com/aquasecurity/trivy/pkg/fanal/handler" "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/fanal/walker" "github.com/aquasecurity/trivy/pkg/semaphore" ) var ( ArtifactSet = wire.NewSet( walker.NewFS, wire.Bind(new(Walker), new(*walker.FS)), NewArtifact, ) _ Walker = (*walker.FS)(nil) ) type Walker interface { Walk(root string, opt walker.Option, fn walker.WalkFunc) error } type Artifact struct { rootPath string cache cache.ArtifactCache walker Walker analyzer analyzer.AnalyzerGroup handlerManager handler.Manager artifactOption artifact.Option } func NewArtifact(rootPath string, c cache.ArtifactCache, w Walker, opt artifact.Option) (artifact.Artifact, error) { handlerManager, err := handler.NewManager(opt) if err != nil { return nil, xerrors.Errorf("handler initialize error: %w", err) } a, err := analyzer.NewAnalyzerGroup(opt.AnalyzerOptions()) if err != nil { return nil, xerrors.Errorf("analyzer group error: %w", err) } return Artifact{ rootPath: filepath.ToSlash(filepath.Clean(rootPath)), cache: c, walker: w, analyzer: a, handlerManager: handlerManager, artifactOption: opt, }, nil } func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) { var wg sync.WaitGroup result := analyzer.NewAnalysisResult() limit := semaphore.New(a.artifactOption.Parallel) opts := analyzer.AnalysisOptions{ Offline: a.artifactOption.Offline, FileChecksum: a.artifactOption.FileChecksum, } // Prepare filesystem for post analysis composite, err := a.analyzer.PostAnalyzerFS() if err != nil { return artifact.Reference{}, xerrors.Errorf("failed to prepare filesystem for post analysis: %w", err) } err = a.walker.Walk(a.rootPath, a.artifactOption.WalkerOption, func(filePath string, info os.FileInfo, opener analyzer.Opener) error { dir := a.rootPath // When the directory is the same as the filePath, a file was given // instead of a directory, rewrite the file path and directory in this case. if filePath == "." { dir, filePath = path.Split(a.rootPath) } if err := a.analyzer.AnalyzeFile(ctx, &wg, limit, result, dir, filePath, info, opener, nil, opts); err != nil { return xerrors.Errorf("analyze file (%s): %w", filePath, err) } // Skip post analysis if the file is not required analyzerTypes := a.analyzer.RequiredPostAnalyzers(filePath, info) if len(analyzerTypes) == 0 { return nil } // Build filesystem for post analysis if err := composite.CreateLink(analyzerTypes, dir, filePath, filepath.Join(dir, filePath)); err != nil { return xerrors.Errorf("failed to create link: %w", err) } return nil }) if err != nil { return artifact.Reference{}, xerrors.Errorf("walk filesystem: %w", err) } // Wait for all the goroutine to finish. wg.Wait() // Post-analysis if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil { return artifact.Reference{}, xerrors.Errorf("post analysis error: %w", err) } // Sort the analysis result for consistent results result.Sort() blobInfo := types.BlobInfo{ SchemaVersion: types.BlobJSONSchemaVersion, OS: result.OS, Repository: result.Repository, PackageInfos: result.PackageInfos, Applications: result.Applications, Misconfigurations: result.Misconfigurations, Secrets: result.Secrets, Licenses: result.Licenses, CustomResources: result.CustomResources, } if err = a.handlerManager.PostHandle(ctx, result, &blobInfo); err != nil { return artifact.Reference{}, xerrors.Errorf("failed to call hooks: %w", err) } cacheKey, err := a.calcCacheKey(blobInfo) if err != nil { return artifact.Reference{}, xerrors.Errorf("failed to calculate a cache key: %w", err) } if err = a.cache.PutBlob(cacheKey, blobInfo); err != nil { return artifact.Reference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err) } // get hostname var hostName string b, err := os.ReadFile(filepath.Join(a.rootPath, "etc", "hostname")) if err == nil && len(b) != 0 { hostName = strings.TrimSpace(string(b)) } else { // To slash for Windows hostName = filepath.ToSlash(a.rootPath) } return artifact.Reference{ Name: hostName, Type: artifact.TypeFilesystem, ID: cacheKey, // use a cache key as pseudo artifact ID BlobIDs: []string{cacheKey}, }, nil } func (a Artifact) Clean(reference artifact.Reference) error { return a.cache.DeleteBlobs(reference.BlobIDs) } func (a Artifact) calcCacheKey(blobInfo types.BlobInfo) (string, error) { // calculate hash of JSON and use it as pseudo artifactID and blobID h := sha256.New() if err := json.NewEncoder(h).Encode(blobInfo); err != nil { return "", xerrors.Errorf("json error: %w", err) } d := digest.NewDigest(digest.SHA256, h) cacheKey, err := cache.CalcKey(d.String(), a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption) if err != nil { return "", xerrors.Errorf("cache key: %w", err) } return cacheKey, nil }