Skip to content

Commit faed25b

Browse files
authored
Analyze command (fanal#12)
* Extract commands * Analyze commands * Add comment * Resolve dependency
1 parent 856dd3a commit faed25b

File tree

9 files changed

+76679
-22
lines changed

9 files changed

+76679
-22
lines changed

analyzer/analyzer.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import (
1515
)
1616

1717
var (
18-
osAnalyzers []OSAnalyzer
19-
pkgAnalyzers []PkgAnalyzer
20-
libAnalyzers []LibraryAnalyzer
18+
osAnalyzers []OSAnalyzer
19+
pkgAnalyzers []PkgAnalyzer
20+
libAnalyzers []LibraryAnalyzer
21+
commandAnalyzers []CommandAnalyzer
2122

2223
// ErrUnknownOS occurs when unknown OS is analyzed.
2324
ErrUnknownOS = xerrors.New("Unknown OS")
@@ -35,6 +36,11 @@ type PkgAnalyzer interface {
3536
RequiredFiles() []string
3637
}
3738

39+
type CommandAnalyzer interface {
40+
Analyze(OS, extractor.FileMap) ([]Package, error)
41+
RequiredFiles() []string
42+
}
43+
3844
type FilePath string
3945

4046
type LibraryAnalyzer interface {
@@ -74,6 +80,10 @@ func RegisterPkgAnalyzer(analyzer PkgAnalyzer) {
7480
pkgAnalyzers = append(pkgAnalyzers, analyzer)
7581
}
7682

83+
func RegisterCommandAnalyzer(analyzer CommandAnalyzer) {
84+
commandAnalyzers = append(commandAnalyzers, analyzer)
85+
}
86+
7787
func RegisterLibraryAnalyzer(analyzer LibraryAnalyzer) {
7888
libAnalyzers = append(libAnalyzers, analyzer)
7989
}
@@ -92,7 +102,7 @@ func RequiredFilenames() []string {
92102
return filenames
93103
}
94104

95-
func Analyze(ctx context.Context, imageName string, opts ...types.DockerOption) (filesMap extractor.FileMap, err error) {
105+
func Analyze(ctx context.Context, imageName string, opts ...types.DockerOption) (fileMap extractor.FileMap, err error) {
96106
// default docker option
97107
opt := types.DockerOption{
98108
Timeout: 600 * time.Second,
@@ -105,27 +115,27 @@ func Analyze(ctx context.Context, imageName string, opts ...types.DockerOption)
105115
r, err := e.SaveLocalImage(ctx, imageName)
106116
if err != nil {
107117
// when no docker daemon is installed or no image exists in the local machine
108-
filesMap, err = e.Extract(ctx, imageName, RequiredFilenames())
118+
fileMap, err = e.Extract(ctx, imageName, RequiredFilenames())
109119
if err != nil {
110120
return nil, xerrors.Errorf("failed to extract files: %w", err)
111121
}
112-
return filesMap, nil
122+
return fileMap, nil
113123
}
114124

115-
filesMap, err = e.ExtractFromFile(ctx, r, RequiredFilenames())
125+
fileMap, err = e.ExtractFromFile(ctx, r, RequiredFilenames())
116126
if err != nil {
117127
return nil, xerrors.Errorf("failed to extract files from saved tar: %w", err)
118128
}
119-
return filesMap, nil
129+
return fileMap, nil
120130
}
121131

122-
func AnalyzeFromFile(ctx context.Context, r io.ReadCloser) (filesMap extractor.FileMap, err error) {
132+
func AnalyzeFromFile(ctx context.Context, r io.ReadCloser) (fileMap extractor.FileMap, err error) {
123133
e := docker.NewDockerExtractor(types.DockerOption{})
124-
filesMap, err = e.ExtractFromFile(ctx, r, RequiredFilenames())
134+
fileMap, err = e.ExtractFromFile(ctx, r, RequiredFilenames())
125135
if err != nil {
126136
return nil, xerrors.Errorf("failed to extract files from tar: %w", err)
127137
}
128-
return filesMap, nil
138+
return fileMap, nil
129139
}
130140

131141
func GetOS(filesMap extractor.FileMap) (OS, error) {
@@ -151,6 +161,17 @@ func GetPackages(filesMap extractor.FileMap) ([]Package, error) {
151161
return nil, ErrUnknownOS
152162
}
153163

164+
func GetPackagesFromCommands(targetOS OS, filesMap extractor.FileMap) ([]Package, error) {
165+
for _, analyzer := range commandAnalyzers {
166+
pkgs, err := analyzer.Analyze(targetOS, filesMap)
167+
if err != nil {
168+
continue
169+
}
170+
return pkgs, nil
171+
}
172+
return nil, nil
173+
}
174+
154175
func CheckPackage(pkg *Package) bool {
155176
return pkg.Name != "" && pkg.Version != ""
156177
}

analyzer/command/apk/apk.go

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

0 commit comments

Comments
 (0)