github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/analyzer/imgconf/apk/apk.go (about) 1 package apk 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "net/http" 10 builtinos "os" 11 "sort" 12 "strings" 13 "time" 14 15 v1 "github.com/google/go-containerregistry/pkg/v1" 16 "golang.org/x/xerrors" 17 18 "github.com/devseccon/trivy/pkg/fanal/analyzer" 19 "github.com/devseccon/trivy/pkg/fanal/types" 20 ) 21 22 const ( 23 envApkIndexArchiveURL = "FANAL_APK_INDEX_ARCHIVE_URL" 24 analyzerVersion = 1 25 ) 26 27 var defaultApkIndexArchiveURL = "https://raw.githubusercontent." + 28 "com/knqyf263/apkIndex-archive/master/alpine/v%s/main/x86_64/history.json" 29 30 func init() { 31 analyzer.RegisterConfigAnalyzer(analyzer.TypeApkCommand, newAlpineCmdAnalyzer) 32 } 33 34 type alpineCmdAnalyzer struct { 35 apkIndexArchiveURL string 36 } 37 38 func newAlpineCmdAnalyzer(_ analyzer.ConfigAnalyzerOptions) (analyzer.ConfigAnalyzer, error) { 39 apkIndexArchiveURL := defaultApkIndexArchiveURL 40 if builtinos.Getenv(envApkIndexArchiveURL) != "" { 41 apkIndexArchiveURL = builtinos.Getenv(envApkIndexArchiveURL) 42 } 43 return alpineCmdAnalyzer{apkIndexArchiveURL: apkIndexArchiveURL}, nil 44 } 45 46 type apkIndex struct { 47 Package map[string]archive 48 Provide provide 49 } 50 51 type archive struct { 52 Origin string 53 Versions version 54 Dependencies []string 55 Provides []string 56 } 57 58 type provide struct { 59 SO map[string]pkg // package which provides the shared object 60 Package map[string]pkg // package which provides the package 61 } 62 63 type pkg struct { 64 Package string 65 Versions version 66 } 67 68 type version map[string]int 69 70 func (a alpineCmdAnalyzer) Analyze(_ context.Context, input analyzer.ConfigAnalysisInput) (*analyzer.ConfigAnalysisResult, error) { 71 if input.Config == nil { 72 return nil, nil 73 } 74 var apkIndexArchive *apkIndex 75 var err error 76 if apkIndexArchive, err = a.fetchApkIndexArchive(input.OS); err != nil { 77 log.Println(err) 78 return nil, xerrors.Errorf("failed to fetch apk index archive: %w", err) 79 } 80 81 pkgs := a.parseConfig(apkIndexArchive, input.Config) 82 if len(pkgs) == 0 { 83 return nil, nil 84 } 85 86 return &analyzer.ConfigAnalysisResult{ 87 HistoryPackages: pkgs, 88 }, nil 89 } 90 func (a alpineCmdAnalyzer) fetchApkIndexArchive(targetOS types.OS) (*apkIndex, error) { 91 // 3.9.3 => 3.9 92 osVer := targetOS.Name 93 if strings.Count(osVer, ".") > 1 { 94 osVer = osVer[:strings.LastIndex(osVer, ".")] 95 } 96 97 url := fmt.Sprintf(a.apkIndexArchiveURL, osVer) 98 var reader io.Reader 99 if strings.HasPrefix(url, "file://") { 100 var err error 101 reader, err = builtinos.Open(strings.TrimPrefix(url, "file://")) 102 if err != nil { 103 return nil, xerrors.Errorf("failed to read APKINDEX archive file: %w", err) 104 } 105 } else { 106 // nolint 107 resp, err := http.Get(url) 108 if err != nil { 109 return nil, xerrors.Errorf("failed to fetch APKINDEX archive: %w", err) 110 } 111 defer resp.Body.Close() 112 reader = resp.Body 113 } 114 apkIndexArchive := &apkIndex{} 115 if err := json.NewDecoder(reader).Decode(apkIndexArchive); err != nil { 116 return nil, xerrors.Errorf("failed to decode APKINDEX JSON: %w", err) 117 } 118 119 return apkIndexArchive, nil 120 } 121 122 func (a alpineCmdAnalyzer) parseConfig(apkIndexArchive *apkIndex, config *v1.ConfigFile) (packages []types.Package) { 123 envs := make(map[string]string) 124 for _, env := range config.Config.Env { 125 index := strings.Index(env, "=") 126 envs["$"+env[:index]] = env[index+1:] 127 } 128 129 uniqPkgs := make(map[string]types.Package) 130 for _, history := range config.History { 131 pkgs := a.parseCommand(history.CreatedBy, envs) 132 pkgs = a.resolveDependencies(apkIndexArchive, pkgs) 133 results := a.guessVersion(apkIndexArchive, pkgs, history.Created.Time) 134 for _, result := range results { 135 uniqPkgs[result.Name] = result 136 } 137 } 138 for _, pkg := range uniqPkgs { 139 packages = append(packages, pkg) 140 } 141 142 return packages 143 } 144 145 func (a alpineCmdAnalyzer) parseCommand(command string, envs map[string]string) (pkgs []string) { 146 if strings.Contains(command, "#(nop)") { 147 return nil 148 } 149 150 command = strings.TrimPrefix(command, "/bin/sh -c") 151 var commands []string 152 for _, cmd := range strings.Split(command, "&&") { 153 for _, c := range strings.Split(cmd, ";") { 154 commands = append(commands, strings.TrimSpace(c)) 155 } 156 } 157 for _, cmd := range commands { 158 if !strings.HasPrefix(cmd, "apk") { 159 continue 160 } 161 162 var add bool 163 for _, field := range strings.Fields(cmd) { 164 switch { 165 case strings.HasPrefix(field, "-") || strings.HasPrefix(field, "."): 166 continue 167 case field == "add": 168 add = true 169 case add: 170 if strings.HasPrefix(field, "$") { 171 pkgs = append(pkgs, strings.Fields(envs[field])...) 172 continue 173 } 174 pkgs = append(pkgs, field) 175 } 176 } 177 } 178 return pkgs 179 } 180 func (a alpineCmdAnalyzer) resolveDependencies(apkIndexArchive *apkIndex, originalPkgs []string) (pkgs []string) { 181 uniqPkgs := make(map[string]struct{}) 182 for _, pkgName := range originalPkgs { 183 if _, ok := uniqPkgs[pkgName]; ok { 184 continue 185 } 186 187 seenPkgs := make(map[string]struct{}) 188 for _, p := range a.resolveDependency(apkIndexArchive, pkgName, seenPkgs) { 189 uniqPkgs[p] = struct{}{} 190 } 191 } 192 for pkg := range uniqPkgs { 193 pkgs = append(pkgs, pkg) 194 } 195 return pkgs 196 } 197 198 func (a alpineCmdAnalyzer) resolveDependency(apkIndexArchive *apkIndex, pkgName string, 199 seenPkgs map[string]struct{}) (pkgNames []string) { 200 pkg, ok := apkIndexArchive.Package[pkgName] 201 if !ok { 202 return nil 203 } 204 if _, ok = seenPkgs[pkgName]; ok { 205 return nil 206 } 207 seenPkgs[pkgName] = struct{}{} 208 209 pkgNames = append(pkgNames, pkgName) 210 for _, dependency := range pkg.Dependencies { 211 // sqlite-libs=3.26.0-r3 => sqlite-libs 212 dependency, _, _ = strings.Cut(dependency, "=") 213 214 if strings.HasPrefix(dependency, "so:") { 215 soProvidePkg := apkIndexArchive.Provide.SO[dependency[3:]].Package 216 pkgNames = append(pkgNames, a.resolveDependency(apkIndexArchive, soProvidePkg, seenPkgs)...) 217 continue 218 } else if strings.HasPrefix(dependency, "pc:") || strings.HasPrefix(dependency, "cmd:") { 219 continue 220 } 221 pkgProvidePkg, ok := apkIndexArchive.Provide.Package[dependency] 222 if ok { 223 pkgNames = append(pkgNames, a.resolveDependency(apkIndexArchive, pkgProvidePkg.Package, seenPkgs)...) 224 continue 225 } 226 pkgNames = append(pkgNames, a.resolveDependency(apkIndexArchive, dependency, seenPkgs)...) 227 } 228 return pkgNames 229 } 230 231 type historyVersion struct { 232 Version string 233 BuiltAt int 234 } 235 236 func (a alpineCmdAnalyzer) guessVersion(apkIndexArchive *apkIndex, originalPkgs []string, 237 createdAt time.Time) (pkgs []types.Package) { 238 for _, pkg := range originalPkgs { 239 archive, ok := apkIndexArchive.Package[pkg] 240 if !ok { 241 continue 242 } 243 244 var historyVersions []historyVersion 245 for version, builtAt := range archive.Versions { 246 historyVersions = append(historyVersions, historyVersion{ 247 Version: version, 248 BuiltAt: builtAt, 249 }) 250 } 251 sort.Slice(historyVersions, func(i, j int) bool { 252 return historyVersions[i].BuiltAt < historyVersions[j].BuiltAt 253 }) 254 255 createdUnix := int(createdAt.Unix()) 256 var candidateVersion string 257 for _, historyVersion := range historyVersions { 258 if historyVersion.BuiltAt <= createdUnix { 259 candidateVersion = historyVersion.Version 260 } else if createdUnix < historyVersion.BuiltAt { 261 break 262 } 263 } 264 if candidateVersion == "" { 265 continue 266 } 267 268 pkgs = append(pkgs, types.Package{ 269 Name: pkg, 270 Version: candidateVersion, 271 }) 272 273 // Add origin package name 274 if archive.Origin != "" && archive.Origin != pkg { 275 pkgs = append(pkgs, types.Package{ 276 Name: archive.Origin, 277 Version: candidateVersion, 278 }) 279 } 280 } 281 return pkgs 282 } 283 284 func (a alpineCmdAnalyzer) Required(targetOS types.OS) bool { 285 return targetOS.Family == types.Alpine 286 } 287 288 func (a alpineCmdAnalyzer) Type() analyzer.Type { 289 return analyzer.TypeApkCommand 290 } 291 292 func (a alpineCmdAnalyzer) Version() int { 293 return analyzerVersion 294 }