github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/artifact/local/fs.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/json"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/opencontainers/go-digest"
    14  	"golang.org/x/xerrors"
    15  
    16  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    17  	"github.com/devseccon/trivy/pkg/fanal/artifact"
    18  	"github.com/devseccon/trivy/pkg/fanal/cache"
    19  	"github.com/devseccon/trivy/pkg/fanal/handler"
    20  	"github.com/devseccon/trivy/pkg/fanal/types"
    21  	"github.com/devseccon/trivy/pkg/fanal/walker"
    22  	"github.com/devseccon/trivy/pkg/log"
    23  	"github.com/devseccon/trivy/pkg/semaphore"
    24  )
    25  
    26  type Artifact struct {
    27  	rootPath       string
    28  	cache          cache.ArtifactCache
    29  	walker         walker.FS
    30  	analyzer       analyzer.AnalyzerGroup
    31  	handlerManager handler.Manager
    32  
    33  	artifactOption artifact.Option
    34  }
    35  
    36  func NewArtifact(rootPath string, c cache.ArtifactCache, opt artifact.Option) (artifact.Artifact, error) {
    37  	handlerManager, err := handler.NewManager(opt)
    38  	if err != nil {
    39  		return nil, xerrors.Errorf("handler initialize error: %w", err)
    40  	}
    41  
    42  	a, err := analyzer.NewAnalyzerGroup(opt.AnalyzerOptions())
    43  	if err != nil {
    44  		return nil, xerrors.Errorf("analyzer group error: %w", err)
    45  	}
    46  
    47  	return Artifact{
    48  		rootPath: filepath.ToSlash(filepath.Clean(rootPath)),
    49  		cache:    c,
    50  		walker: walker.NewFS(buildPathsToSkip(rootPath, opt.SkipFiles), buildPathsToSkip(rootPath, opt.SkipDirs),
    51  			opt.Parallel, opt.WalkOption.ErrorCallback),
    52  		analyzer:       a,
    53  		handlerManager: handlerManager,
    54  
    55  		artifactOption: opt,
    56  	}, nil
    57  }
    58  
    59  // buildPathsToSkip builds correct patch for skipDirs and skipFiles
    60  func buildPathsToSkip(base string, paths []string) []string {
    61  	var relativePaths []string
    62  	absBase, err := filepath.Abs(base)
    63  	if err != nil {
    64  		log.Logger.Warnf("Failed to get an absolute path of %s: %s", base, err)
    65  		return nil
    66  	}
    67  	for _, path := range paths {
    68  		// Supports three types of flag specification.
    69  		// All of them are converted into the relative path from the root directory.
    70  		// 1. Relative skip dirs/files from the root directory
    71  		//     The specified dirs and files will be used as is.
    72  		//       e.g. $ trivy fs --skip-dirs bar ./foo
    73  		//     The skip dir from the root directory will be `bar/`.
    74  		// 2. Relative skip dirs/files from the working directory
    75  		//     The specified dirs and files wll be converted to the relative path from the root directory.
    76  		//       e.g. $ trivy fs --skip-dirs ./foo/bar ./foo
    77  		//     The skip dir will be converted to `bar/`.
    78  		// 3. Absolute skip dirs/files
    79  		//     The specified dirs and files wll be converted to the relative path from the root directory.
    80  		//       e.g. $ trivy fs --skip-dirs /bar/foo/baz ./foo
    81  		//     When the working directory is
    82  		//       3.1 /bar: the skip dir will be converted to `baz/`.
    83  		//       3.2 /hoge : the skip dir will be converted to `../../bar/foo/baz/`.
    84  
    85  		absSkipPath, err := filepath.Abs(path)
    86  		if err != nil {
    87  			log.Logger.Warnf("Failed to get an absolute path of %s: %s", base, err)
    88  			continue
    89  		}
    90  		rel, err := filepath.Rel(absBase, absSkipPath)
    91  		if err != nil {
    92  			log.Logger.Warnf("Failed to get a relative path from %s to %s: %s", base, path, err)
    93  			continue
    94  		}
    95  
    96  		var relPath string
    97  		switch {
    98  		case !filepath.IsAbs(path) && strings.HasPrefix(rel, ".."):
    99  			// #1: Use the path as is
   100  			relPath = path
   101  		case !filepath.IsAbs(path) && !strings.HasPrefix(rel, ".."):
   102  			// #2: Use the relative path from the root directory
   103  			relPath = rel
   104  		case filepath.IsAbs(path):
   105  			// #3: Use the relative path from the root directory
   106  			relPath = rel
   107  		}
   108  		relPath = filepath.ToSlash(relPath)
   109  		relativePaths = append(relativePaths, relPath)
   110  	}
   111  	return relativePaths
   112  }
   113  
   114  func (a Artifact) Inspect(ctx context.Context) (types.ArtifactReference, error) {
   115  	var wg sync.WaitGroup
   116  	result := analyzer.NewAnalysisResult()
   117  	limit := semaphore.New(a.artifactOption.Parallel)
   118  	opts := analyzer.AnalysisOptions{
   119  		Offline:      a.artifactOption.Offline,
   120  		FileChecksum: a.artifactOption.FileChecksum,
   121  	}
   122  
   123  	// Prepare filesystem for post analysis
   124  	composite, err := a.analyzer.PostAnalyzerFS()
   125  	if err != nil {
   126  		return types.ArtifactReference{}, xerrors.Errorf("failed to prepare filesystem for post analysis: %w", err)
   127  	}
   128  
   129  	err = a.walker.Walk(a.rootPath, func(filePath string, info os.FileInfo, opener analyzer.Opener) error {
   130  		dir := a.rootPath
   131  
   132  		// When the directory is the same as the filePath, a file was given
   133  		// instead of a directory, rewrite the file path and directory in this case.
   134  		if filePath == "." {
   135  			dir, filePath = path.Split(a.rootPath)
   136  		}
   137  
   138  		if err := a.analyzer.AnalyzeFile(ctx, &wg, limit, result, dir, filePath, info, opener, nil, opts); err != nil {
   139  			return xerrors.Errorf("analyze file (%s): %w", filePath, err)
   140  		}
   141  
   142  		// Skip post analysis if the file is not required
   143  		analyzerTypes := a.analyzer.RequiredPostAnalyzers(filePath, info)
   144  		if len(analyzerTypes) == 0 {
   145  			return nil
   146  		}
   147  
   148  		// Build filesystem for post analysis
   149  		if err := composite.CreateLink(analyzerTypes, dir, filePath, filepath.Join(dir, filePath)); err != nil {
   150  			return xerrors.Errorf("failed to create link: %w", err)
   151  		}
   152  
   153  		return nil
   154  	})
   155  	if err != nil {
   156  		return types.ArtifactReference{}, xerrors.Errorf("walk filesystem: %w", err)
   157  	}
   158  
   159  	// Wait for all the goroutine to finish.
   160  	wg.Wait()
   161  
   162  	// Post-analysis
   163  	if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil {
   164  		return types.ArtifactReference{}, xerrors.Errorf("post analysis error: %w", err)
   165  	}
   166  
   167  	// Sort the analysis result for consistent results
   168  	result.Sort()
   169  
   170  	blobInfo := types.BlobInfo{
   171  		SchemaVersion:     types.BlobJSONSchemaVersion,
   172  		OS:                result.OS,
   173  		Repository:        result.Repository,
   174  		PackageInfos:      result.PackageInfos,
   175  		Applications:      result.Applications,
   176  		Misconfigurations: result.Misconfigurations,
   177  		Secrets:           result.Secrets,
   178  		Licenses:          result.Licenses,
   179  		CustomResources:   result.CustomResources,
   180  	}
   181  
   182  	if err = a.handlerManager.PostHandle(ctx, result, &blobInfo); err != nil {
   183  		return types.ArtifactReference{}, xerrors.Errorf("failed to call hooks: %w", err)
   184  	}
   185  
   186  	cacheKey, err := a.calcCacheKey(blobInfo)
   187  	if err != nil {
   188  		return types.ArtifactReference{}, xerrors.Errorf("failed to calculate a cache key: %w", err)
   189  	}
   190  
   191  	if err = a.cache.PutBlob(cacheKey, blobInfo); err != nil {
   192  		return types.ArtifactReference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err)
   193  	}
   194  
   195  	// get hostname
   196  	var hostName string
   197  	b, err := os.ReadFile(filepath.Join(a.rootPath, "etc", "hostname"))
   198  	if err == nil && len(b) != 0 {
   199  		hostName = strings.TrimSpace(string(b))
   200  	} else {
   201  		// To slash for Windows
   202  		hostName = filepath.ToSlash(a.rootPath)
   203  	}
   204  
   205  	return types.ArtifactReference{
   206  		Name:    hostName,
   207  		Type:    types.ArtifactFilesystem,
   208  		ID:      cacheKey, // use a cache key as pseudo artifact ID
   209  		BlobIDs: []string{cacheKey},
   210  	}, nil
   211  }
   212  
   213  func (a Artifact) Clean(reference types.ArtifactReference) error {
   214  	return a.cache.DeleteBlobs(reference.BlobIDs)
   215  }
   216  
   217  func (a Artifact) calcCacheKey(blobInfo types.BlobInfo) (string, error) {
   218  	// calculate hash of JSON and use it as pseudo artifactID and blobID
   219  	h := sha256.New()
   220  	if err := json.NewEncoder(h).Encode(blobInfo); err != nil {
   221  		return "", xerrors.Errorf("json error: %w", err)
   222  	}
   223  
   224  	d := digest.NewDigest(digest.SHA256, h)
   225  	cacheKey, err := cache.CalcKey(d.String(), a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption)
   226  	if err != nil {
   227  		return "", xerrors.Errorf("cache key: %w", err)
   228  	}
   229  
   230  	return cacheKey, nil
   231  }