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