podman
295 строк · 10.2 Кб
1package buildah2
3import (4"context"5"fmt"6"io"7"os"8"path/filepath"9"strings"10
11"github.com/containers/buildah/define"12"github.com/containers/buildah/internal/sbom"13"github.com/mattn/go-shellwords"14rspec "github.com/opencontainers/runtime-spec/specs-go"15"github.com/sirupsen/logrus"16"golang.org/x/exp/slices"17)
18
19func stringSliceReplaceAll(slice []string, replacements map[string]string, important []string) (built []string, replacedAnImportantValue bool) {20built = make([]string, 0, len(slice))21for i := range slice {22element := slice[i]23for from, to := range replacements {24previous := element25if element = strings.ReplaceAll(previous, from, to); element != previous {26if len(important) == 0 || slices.Contains(important, from) {27replacedAnImportantValue = true28}29}30}31built = append(built, element)32}33return built, replacedAnImportantValue34}
35
36// sbomScan iterates through the scanning configuration settings, generating
37// SBOM files and storing them either in the rootfs or in a local file path.
38func (b *Builder) sbomScan(ctx context.Context, options CommitOptions) (imageFiles, localFiles map[string]string, scansDir string, err error) {39// We'll use a temporary per-container directory for this one.40cdir, err := b.store.ContainerDirectory(b.ContainerID)41if err != nil {42return nil, nil, "", err43}44scansDir, err = os.MkdirTemp(cdir, "buildah-scan")45if err != nil {46return nil, nil, "", err47}48defer func() {49if err != nil {50if err := os.RemoveAll(scansDir); err != nil {51logrus.Warnf("removing temporary directory %q: %v", scansDir, err)52}53}54}()55
56// We may be producing sets of outputs using temporary containers, and57// there's no need to create more than one container for any one58// specific scanner image.59scanners := make(map[string]*Builder)60defer func() {61for _, scanner := range scanners {62scannerID := scanner.ContainerID63if err := scanner.Delete(); err != nil {64logrus.Warnf("removing temporary scanner container %q: %v", scannerID, err)65}66}67}()68
69// Just assume that every scanning method will be looking at the rootfs.70rootfs, err := b.Mount(b.MountLabel)71if err != nil {72return nil, nil, "", err73}74defer func(b *Builder) {75if err := b.Unmount(); err != nil {76logrus.Warnf("unmounting temporary scanner container %q: %v", b.ContainerID, err)77}78}(b)79
80// Iterate through all of the scanning strategies.81for _, scanSpec := range options.SBOMScanOptions {82// Pull the image and create a container we can run the scanner83// in, unless we've done that already for this scanner image.84scanBuilder, ok := scanners[scanSpec.Image]85if !ok {86builderOptions := BuilderOptions{87FromImage: scanSpec.Image,88ContainerSuffix: "scanner",89PullPolicy: scanSpec.PullPolicy,90BlobDirectory: options.BlobDirectory,91Logger: b.Logger,92SystemContext: options.SystemContext,93MountLabel: b.MountLabel,94ProcessLabel: b.ProcessLabel,95IDMappingOptions: &b.IDMappingOptions,96}97if scanBuilder, err = NewBuilder(ctx, b.store, builderOptions); err != nil {98return nil, nil, "", fmt.Errorf("creating temporary working container to run scanner: %w", err)99}100scanners[scanSpec.Image] = scanBuilder101}102// Now figure out which commands we need to run. First, try to103// parse a command ourselves, because syft's image (at least)104// doesn't include a shell. Build a slice of command slices.105var commands [][]string106for _, commandSpec := range scanSpec.Commands {107// Start by assuming it's shell -c $whatever.108parsedCommand := []string{"/bin/sh", "-c", commandSpec}109if shell := scanBuilder.Shell(); len(shell) != 0 {110parsedCommand = append(append([]string{}, shell...), commandSpec)111}112if !strings.ContainsAny(commandSpec, "<>|") { // An imperfect check for shell redirection being used.113// If we can parse it ourselves, though, prefer to use that result,114// in case the scanner image doesn't include a shell.115if parsed, err := shellwords.Parse(commandSpec); err == nil {116parsedCommand = parsed117}118}119commands = append(commands, parsedCommand)120}121// Set up a list of mounts for the rootfs and whichever context122// directories we're told were used.123const rootfsTargetDir = "/.rootfs"124const scansTargetDir = "/.scans"125const contextsTargetDirPrefix = "/.context"126runMounts := []rspec.Mount{127// Our temporary directory, read-write.128{129Type: define.TypeBind,130Source: scansDir,131Destination: scansTargetDir,132Options: []string{"rw", "z"},133},134// The rootfs, read-only.135{136Type: define.TypeBind,137Source: rootfs,138Destination: rootfsTargetDir,139Options: []string{"ro"},140},141}142// Each context directory, also read-only.143for i := range scanSpec.ContextDir {144contextMount := rspec.Mount{145Type: define.TypeBind,146Source: scanSpec.ContextDir[i],147Destination: fmt.Sprintf("%s%d", contextsTargetDirPrefix, i),148Options: []string{"ro"},149}150runMounts = append(runMounts, contextMount)151}152// Set up run options and mounts one time, and reuse it.153runOptions := RunOptions{154Logger: b.Logger,155Isolation: b.Isolation,156SystemContext: options.SystemContext,157Mounts: runMounts,158}159// We'll have to do some text substitutions so that we run the160// right commands, in the right order, pointing at the right161// mount points.162var resolvedCommands [][]string163var resultFiles []string164for _, command := range commands {165// Each command gets to produce its own file that we'll166// combine later if there's more than one of them.167contextDirScans := 0168for i := range scanSpec.ContextDir {169resultFile := filepath.Join(scansTargetDir, fmt.Sprintf("scan%d.json", len(resultFiles)))170// If the command mentions {CONTEXT}...171resolvedCommand, scansContext := stringSliceReplaceAll(command,172map[string]string{173"{CONTEXT}": fmt.Sprintf("%s%d", contextsTargetDirPrefix, i),174"{OUTPUT}": resultFile,175},176[]string{"{CONTEXT}"},177)178if !scansContext {179break180}181// ... resolve the path references and add it to the list of commands.182resolvedCommands = append(resolvedCommands, resolvedCommand)183resultFiles = append(resultFiles, resultFile)184contextDirScans++185}186if contextDirScans == 0 {187resultFile := filepath.Join(scansTargetDir, fmt.Sprintf("scan%d.json", len(resultFiles)))188// If the command didn't mention {CONTEXT}, but does mention {ROOTFS}...189resolvedCommand, scansRootfs := stringSliceReplaceAll(command,190map[string]string{191"{ROOTFS}": rootfsTargetDir,192"{OUTPUT}": resultFile,193},194[]string{"{ROOTFS}"},195)196// ... resolve the path references and add that to the list of commands.197if scansRootfs {198resolvedCommands = append(resolvedCommands, resolvedCommand)199resultFiles = append(resultFiles, resultFile)200}201}202}203// Run all of the commands, one after the other, producing one204// or more files named "scan%d.json" in our temporary directory.205for _, resolvedCommand := range resolvedCommands {206logrus.Debugf("Running scan command %q", resolvedCommand)207if err = scanBuilder.Run(resolvedCommand, runOptions); err != nil {208return nil, nil, "", fmt.Errorf("running scanning command %v: %w", resolvedCommand, err)209}210}211// Produce the combined output files that we need to create, if there are any.212var sbomResult, purlResult string213switch {214case scanSpec.ImageSBOMOutput != "":215sbomResult = filepath.Join(scansDir, filepath.Base(scanSpec.ImageSBOMOutput))216case scanSpec.SBOMOutput != "":217sbomResult = filepath.Join(scansDir, filepath.Base(scanSpec.SBOMOutput))218default:219sbomResult = filepath.Join(scansDir, "sbom-result")220}221switch {222case scanSpec.ImagePURLOutput != "":223purlResult = filepath.Join(scansDir, filepath.Base(scanSpec.ImagePURLOutput))224case scanSpec.PURLOutput != "":225purlResult = filepath.Join(scansDir, filepath.Base(scanSpec.PURLOutput))226default:227purlResult = filepath.Join(scansDir, "purl-result")228}229copyFile := func(destination, source string) error {230dst, err := os.Create(destination)231if err != nil {232return err233}234defer dst.Close()235src, err := os.Open(source)236if err != nil {237return err238}239defer src.Close()240if _, err = io.Copy(dst, src); err != nil {241return fmt.Errorf("copying %q to %q: %w", source, destination, err)242}243return nil244}245err = func() error {246for i := range resultFiles {247thisResultFile := filepath.Join(scansDir, filepath.Base(resultFiles[i]))248switch i {249case 0:250// Straight-up copy to create the first version of the final output.251if err = copyFile(sbomResult, thisResultFile); err != nil {252return err253}254// This shouldn't change any contents, but lets us generate the purl file.255err = sbom.Merge(scanSpec.MergeStrategy, thisResultFile, sbomResult, purlResult)256default:257// Hopefully we know how to merge information from the new one into the final output.258err = sbom.Merge(scanSpec.MergeStrategy, sbomResult, thisResultFile, purlResult)259}260}261return err262}()263if err != nil {264return nil, nil, "", err265}266// If these files are supposed to be written to the local filesystem, add267// their contents to the map of files we expect our caller to write.268if scanSpec.SBOMOutput != "" || scanSpec.PURLOutput != "" {269if localFiles == nil {270localFiles = make(map[string]string)271}272if scanSpec.SBOMOutput != "" {273localFiles[scanSpec.SBOMOutput] = sbomResult274}275if scanSpec.PURLOutput != "" {276localFiles[scanSpec.PURLOutput] = purlResult277}278}279// If these files are supposed to be written to the image, create a map of280// their contents so that we can either create a layer diff for them (or281// slipstream them into a squashed layer diff) later.282if scanSpec.ImageSBOMOutput != "" || scanSpec.ImagePURLOutput != "" {283if imageFiles == nil {284imageFiles = make(map[string]string)285}286if scanSpec.ImageSBOMOutput != "" {287imageFiles[scanSpec.ImageSBOMOutput] = sbomResult288}289if scanSpec.ImagePURLOutput != "" {290imageFiles[scanSpec.ImagePURLOutput] = purlResult291}292}293}294return imageFiles, localFiles, scansDir, nil295}
296