github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/internal/builds/builds.go (about)

     1  package builds
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"path/filepath"
     8  	"slices"
     9  
    10  	goapk "github.com/chainguard-dev/go-apk/pkg/apk"
    11  	"github.com/wolfi-dev/wolfictl/pkg/apk"
    12  	"github.com/wolfi-dev/wolfictl/pkg/scan"
    13  )
    14  
    15  // Find walks the given filesystem and returns a map of build groups, where each
    16  // group is keyed by the origin package name, and contains the origin package
    17  // and its subpackages. Find expects the filesystem to be laid out just like
    18  // Melange outputs built APKs, starting with a "packages" directory containing
    19  // subdirectories for each architecture, and APK files within those
    20  // subdirectories.
    21  func Find(fsys fs.FS, architectures []string) (map[string]BuildGroup, error) {
    22  	buildsByOrigin := make(map[string]BuildGroup)
    23  
    24  	err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
    25  		if err != nil {
    26  			return err
    27  		}
    28  
    29  		if path == "." {
    30  			return nil
    31  		}
    32  
    33  		if d.IsDir() {
    34  			if !slices.Contains(architectures, d.Name()) {
    35  				// This is not an arch directory we care about
    36  				return fs.SkipDir
    37  			}
    38  
    39  			return nil
    40  		}
    41  
    42  		// This is a file
    43  
    44  		if filepath.Ext(d.Name()) != ".apk" {
    45  			// Not an APK file
    46  			return nil
    47  		}
    48  
    49  		// This is an APK file
    50  
    51  		f, err := fsys.Open(path)
    52  		if err != nil {
    53  			return fmt.Errorf("failed to open file %q: %w", path, err)
    54  		}
    55  		pkginfo, err := apk.PKGINFOFromAPK(f)
    56  		if err != nil {
    57  			return fmt.Errorf("failed to parse APK file %q: %w", path, err)
    58  		}
    59  		fileinfo, err := f.Stat()
    60  		if err != nil {
    61  			return fmt.Errorf("failed to stat file %q: %w", path, err)
    62  		}
    63  		_ = f.Close() // done with the file!
    64  
    65  		// Add to the build group
    66  
    67  		built := newBuiltPackage(fileinfo, pkginfo, path)
    68  		k := built.buildGroupKey()
    69  
    70  		if _, ok := buildsByOrigin[k]; !ok {
    71  			buildsByOrigin[k] = BuildGroup{
    72  				Fsys: fsys,
    73  			}
    74  		}
    75  		bg := buildsByOrigin[k]
    76  
    77  		if built.PkgInfo.Name == built.PkgInfo.Origin {
    78  			// This is a top-level package
    79  			bg.Origin = built
    80  		} else {
    81  			// This is a subpackage
    82  			bg.Subpackages = append(bg.Subpackages, built)
    83  		}
    84  
    85  		buildsByOrigin[k] = bg
    86  
    87  		return nil
    88  	})
    89  	if err != nil {
    90  		return nil, fmt.Errorf("failed to walk packages dir: %w", err)
    91  	}
    92  
    93  	return buildsByOrigin, nil
    94  }
    95  
    96  // Package describes a built (e.g. by Melange) APK package file that resides on
    97  // a filesystem.
    98  type Package struct {
    99  	// FsysPath is the path to the package file on the filesystem.
   100  	FsysPath string
   101  
   102  	// FileInfo is the file info of the package file.
   103  	FileInfo fs.FileInfo
   104  
   105  	// PkgInfo is the parsed package information (found in an APK's PKGINFO file).
   106  	PkgInfo *goapk.Package
   107  }
   108  
   109  func (p Package) buildGroupKey() string {
   110  	// Construct a key for use in build group maps, using the origin name, (full)
   111  	// version string, and architecture.
   112  	return fmt.Sprintf(
   113  		"%s-%s-%s",
   114  		p.PkgInfo.Origin,
   115  		p.PkgInfo.Version,
   116  		p.PkgInfo.Arch,
   117  	)
   118  }
   119  
   120  func newBuiltPackage(fi fs.FileInfo, p *goapk.Package, fsysPath string) Package {
   121  	return Package{
   122  		FsysPath: fsysPath,
   123  		FileInfo: fi,
   124  		PkgInfo:  p,
   125  	}
   126  }
   127  
   128  // BuildGroup describes a set of Packages that were produced as a result of a
   129  // Melange build of a package definition, which includes the origin package and
   130  // 0-n subpackages as well.
   131  type BuildGroup struct {
   132  	// Fsys is the filesystem where the build group was found.
   133  	Fsys fs.FS
   134  
   135  	Origin      Package
   136  	Subpackages []Package
   137  }
   138  
   139  // Scan uses the provided scan.Scanner to scan the APKs in the build group for
   140  // vulnerabilities. It returns a slice of scan results, one for each APK in the
   141  // build group. The first slice member is the result for the origin APK, and the
   142  // rest are for the subpackages.
   143  func (bg BuildGroup) Scan(ctx context.Context, scanner *scan.Scanner, distroID string) ([]scan.Result, error) {
   144  	fsys := bg.Fsys
   145  
   146  	targets := append([]Package{bg.Origin}, bg.Subpackages...)
   147  
   148  	var results []scan.Result
   149  
   150  	for _, target := range targets {
   151  		// TODO: Run these scans in parallel to restore friendship with Jon.
   152  
   153  		apkfile, err := fsys.Open(target.FsysPath)
   154  		if err != nil {
   155  			return nil, fmt.Errorf("failed to open APK %q: %w", target.FsysPath, err)
   156  		}
   157  
   158  		result, err := scanner.ScanAPK(ctx, apkfile, distroID)
   159  		if err != nil {
   160  			return nil, fmt.Errorf("failed to scan APK %q: %w", target.FsysPath, err)
   161  		}
   162  
   163  		results = append(results, *result)
   164  	}
   165  
   166  	return results, nil
   167  }