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