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 }