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  }