| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | package buildah | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 	"path/filepath" | 
					
						
							| 
									
										
										
										
											2025-01-23 22:27:47 +08:00
										 |  |  | 	"slices" | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 	"strings" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/containers/buildah/define" | 
					
						
							|  |  |  | 	"github.com/containers/buildah/internal/sbom" | 
					
						
							|  |  |  | 	"github.com/mattn/go-shellwords" | 
					
						
							|  |  |  | 	rspec "github.com/opencontainers/runtime-spec/specs-go" | 
					
						
							|  |  |  | 	"github.com/sirupsen/logrus" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func stringSliceReplaceAll(slice []string, replacements map[string]string, important []string) (built []string, replacedAnImportantValue bool) { | 
					
						
							|  |  |  | 	built = make([]string, 0, len(slice)) | 
					
						
							|  |  |  | 	for i := range slice { | 
					
						
							|  |  |  | 		element := slice[i] | 
					
						
							|  |  |  | 		for from, to := range replacements { | 
					
						
							|  |  |  | 			previous := element | 
					
						
							|  |  |  | 			if element = strings.ReplaceAll(previous, from, to); element != previous { | 
					
						
							| 
									
										
										
										
											2024-01-31 23:03:20 +08:00
										 |  |  | 				if len(important) == 0 || slices.Contains(important, from) { | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 					replacedAnImportantValue = true | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		built = append(built, element) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return built, replacedAnImportantValue | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // sbomScan iterates through the scanning configuration settings, generating
 | 
					
						
							|  |  |  | // SBOM files and storing them either in the rootfs or in a local file path.
 | 
					
						
							|  |  |  | func (b *Builder) sbomScan(ctx context.Context, options CommitOptions) (imageFiles, localFiles map[string]string, scansDir string, err error) { | 
					
						
							|  |  |  | 	// We'll use a temporary per-container directory for this one.
 | 
					
						
							|  |  |  | 	cdir, err := b.store.ContainerDirectory(b.ContainerID) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, nil, "", err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	scansDir, err = os.MkdirTemp(cdir, "buildah-scan") | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, nil, "", err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	defer func() { | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			if err := os.RemoveAll(scansDir); err != nil { | 
					
						
							|  |  |  | 				logrus.Warnf("removing temporary directory %q: %v", scansDir, err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}() | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 	scansSubdir := filepath.Join(scansDir, "scans") | 
					
						
							|  |  |  | 	if err = os.Mkdir(scansSubdir, 0o700); err != nil { | 
					
						
							|  |  |  | 		return nil, nil, "", err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if err = os.Chmod(scansSubdir, 0o777); err != nil { | 
					
						
							|  |  |  | 		return nil, nil, "", err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// We may be producing sets of outputs using temporary containers, and
 | 
					
						
							|  |  |  | 	// there's no need to create more than one container for any one
 | 
					
						
							|  |  |  | 	// specific scanner image.
 | 
					
						
							|  |  |  | 	scanners := make(map[string]*Builder) | 
					
						
							|  |  |  | 	defer func() { | 
					
						
							|  |  |  | 		for _, scanner := range scanners { | 
					
						
							|  |  |  | 			scannerID := scanner.ContainerID | 
					
						
							|  |  |  | 			if err := scanner.Delete(); err != nil { | 
					
						
							|  |  |  | 				logrus.Warnf("removing temporary scanner container %q: %v", scannerID, err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Just assume that every scanning method will be looking at the rootfs.
 | 
					
						
							|  |  |  | 	rootfs, err := b.Mount(b.MountLabel) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, nil, "", err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	defer func(b *Builder) { | 
					
						
							|  |  |  | 		if err := b.Unmount(); err != nil { | 
					
						
							|  |  |  | 			logrus.Warnf("unmounting temporary scanner container %q: %v", b.ContainerID, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}(b) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Iterate through all of the scanning strategies.
 | 
					
						
							|  |  |  | 	for _, scanSpec := range options.SBOMScanOptions { | 
					
						
							|  |  |  | 		// Pull the image and create a container we can run the scanner
 | 
					
						
							|  |  |  | 		// in, unless we've done that already for this scanner image.
 | 
					
						
							|  |  |  | 		scanBuilder, ok := scanners[scanSpec.Image] | 
					
						
							|  |  |  | 		if !ok { | 
					
						
							|  |  |  | 			builderOptions := BuilderOptions{ | 
					
						
							|  |  |  | 				FromImage:        scanSpec.Image, | 
					
						
							|  |  |  | 				ContainerSuffix:  "scanner", | 
					
						
							|  |  |  | 				PullPolicy:       scanSpec.PullPolicy, | 
					
						
							|  |  |  | 				BlobDirectory:    options.BlobDirectory, | 
					
						
							|  |  |  | 				Logger:           b.Logger, | 
					
						
							|  |  |  | 				SystemContext:    options.SystemContext, | 
					
						
							|  |  |  | 				MountLabel:       b.MountLabel, | 
					
						
							|  |  |  | 				ProcessLabel:     b.ProcessLabel, | 
					
						
							|  |  |  | 				IDMappingOptions: &b.IDMappingOptions, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if scanBuilder, err = NewBuilder(ctx, b.store, builderOptions); err != nil { | 
					
						
							|  |  |  | 				return nil, nil, "", fmt.Errorf("creating temporary working container to run scanner: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			scanners[scanSpec.Image] = scanBuilder | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// Now figure out which commands we need to run.  First, try to
 | 
					
						
							|  |  |  | 		// parse a command ourselves, because syft's image (at least)
 | 
					
						
							|  |  |  | 		// doesn't include a shell.  Build a slice of command slices.
 | 
					
						
							|  |  |  | 		var commands [][]string | 
					
						
							|  |  |  | 		for _, commandSpec := range scanSpec.Commands { | 
					
						
							|  |  |  | 			// Start by assuming it's shell -c $whatever.
 | 
					
						
							|  |  |  | 			parsedCommand := []string{"/bin/sh", "-c", commandSpec} | 
					
						
							|  |  |  | 			if shell := scanBuilder.Shell(); len(shell) != 0 { | 
					
						
							| 
									
										
										
										
											2025-04-08 03:27:12 +08:00
										 |  |  | 				parsedCommand = append(slices.Clone(shell), commandSpec) | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 			if !strings.ContainsAny(commandSpec, "<>|") { // An imperfect check for shell redirection being used.
 | 
					
						
							|  |  |  | 				// If we can parse it ourselves, though, prefer to use that result,
 | 
					
						
							|  |  |  | 				// in case the scanner image doesn't include a shell.
 | 
					
						
							|  |  |  | 				if parsed, err := shellwords.Parse(commandSpec); err == nil { | 
					
						
							|  |  |  | 					parsedCommand = parsed | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			commands = append(commands, parsedCommand) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// Set up a list of mounts for the rootfs and whichever context
 | 
					
						
							|  |  |  | 		// directories we're told were used.
 | 
					
						
							|  |  |  | 		const rootfsTargetDir = "/.rootfs" | 
					
						
							|  |  |  | 		const scansTargetDir = "/.scans" | 
					
						
							|  |  |  | 		const contextsTargetDirPrefix = "/.context" | 
					
						
							|  |  |  | 		runMounts := []rspec.Mount{ | 
					
						
							|  |  |  | 			// Our temporary directory, read-write.
 | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				Type:        define.TypeBind, | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 				Source:      scansSubdir, | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 				Destination: scansTargetDir, | 
					
						
							|  |  |  | 				Options:     []string{"rw", "z"}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			// The rootfs, read-only.
 | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				Type:        define.TypeBind, | 
					
						
							|  |  |  | 				Source:      rootfs, | 
					
						
							|  |  |  | 				Destination: rootfsTargetDir, | 
					
						
							|  |  |  | 				Options:     []string{"ro"}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// Each context directory, also read-only.
 | 
					
						
							|  |  |  | 		for i := range scanSpec.ContextDir { | 
					
						
							|  |  |  | 			contextMount := rspec.Mount{ | 
					
						
							|  |  |  | 				Type:        define.TypeBind, | 
					
						
							|  |  |  | 				Source:      scanSpec.ContextDir[i], | 
					
						
							|  |  |  | 				Destination: fmt.Sprintf("%s%d", contextsTargetDirPrefix, i), | 
					
						
							|  |  |  | 				Options:     []string{"ro"}, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			runMounts = append(runMounts, contextMount) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// Set up run options and mounts one time, and reuse it.
 | 
					
						
							|  |  |  | 		runOptions := RunOptions{ | 
					
						
							|  |  |  | 			Logger:        b.Logger, | 
					
						
							|  |  |  | 			Isolation:     b.Isolation, | 
					
						
							|  |  |  | 			SystemContext: options.SystemContext, | 
					
						
							|  |  |  | 			Mounts:        runMounts, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// We'll have to do some text substitutions so that we run the
 | 
					
						
							|  |  |  | 		// right commands, in the right order, pointing at the right
 | 
					
						
							|  |  |  | 		// mount points.
 | 
					
						
							|  |  |  | 		var resolvedCommands [][]string | 
					
						
							|  |  |  | 		var resultFiles []string | 
					
						
							|  |  |  | 		for _, command := range commands { | 
					
						
							|  |  |  | 			// Each command gets to produce its own file that we'll
 | 
					
						
							|  |  |  | 			// combine later if there's more than one of them.
 | 
					
						
							|  |  |  | 			contextDirScans := 0 | 
					
						
							|  |  |  | 			for i := range scanSpec.ContextDir { | 
					
						
							|  |  |  | 				resultFile := filepath.Join(scansTargetDir, fmt.Sprintf("scan%d.json", len(resultFiles))) | 
					
						
							|  |  |  | 				// If the command mentions {CONTEXT}...
 | 
					
						
							|  |  |  | 				resolvedCommand, scansContext := stringSliceReplaceAll(command, | 
					
						
							|  |  |  | 					map[string]string{ | 
					
						
							|  |  |  | 						"{CONTEXT}": fmt.Sprintf("%s%d", contextsTargetDirPrefix, i), | 
					
						
							|  |  |  | 						"{OUTPUT}":  resultFile, | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 					[]string{"{CONTEXT}"}, | 
					
						
							|  |  |  | 				) | 
					
						
							|  |  |  | 				if !scansContext { | 
					
						
							|  |  |  | 					break | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				// ... resolve the path references and add it to the list of commands.
 | 
					
						
							|  |  |  | 				resolvedCommands = append(resolvedCommands, resolvedCommand) | 
					
						
							|  |  |  | 				resultFiles = append(resultFiles, resultFile) | 
					
						
							|  |  |  | 				contextDirScans++ | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if contextDirScans == 0 { | 
					
						
							|  |  |  | 				resultFile := filepath.Join(scansTargetDir, fmt.Sprintf("scan%d.json", len(resultFiles))) | 
					
						
							|  |  |  | 				// If the command didn't mention {CONTEXT}, but does mention {ROOTFS}...
 | 
					
						
							|  |  |  | 				resolvedCommand, scansRootfs := stringSliceReplaceAll(command, | 
					
						
							|  |  |  | 					map[string]string{ | 
					
						
							|  |  |  | 						"{ROOTFS}": rootfsTargetDir, | 
					
						
							|  |  |  | 						"{OUTPUT}": resultFile, | 
					
						
							|  |  |  | 					}, | 
					
						
							|  |  |  | 					[]string{"{ROOTFS}"}, | 
					
						
							|  |  |  | 				) | 
					
						
							|  |  |  | 				// ... resolve the path references and add that to the list of commands.
 | 
					
						
							|  |  |  | 				if scansRootfs { | 
					
						
							|  |  |  | 					resolvedCommands = append(resolvedCommands, resolvedCommand) | 
					
						
							|  |  |  | 					resultFiles = append(resultFiles, resultFile) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// Run all of the commands, one after the other, producing one
 | 
					
						
							|  |  |  | 		// or more files named "scan%d.json" in our temporary directory.
 | 
					
						
							|  |  |  | 		for _, resolvedCommand := range resolvedCommands { | 
					
						
							|  |  |  | 			logrus.Debugf("Running scan command %q", resolvedCommand) | 
					
						
							|  |  |  | 			if err = scanBuilder.Run(resolvedCommand, runOptions); err != nil { | 
					
						
							|  |  |  | 				return nil, nil, "", fmt.Errorf("running scanning command %v: %w", resolvedCommand, err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// Produce the combined output files that we need to create, if there are any.
 | 
					
						
							|  |  |  | 		var sbomResult, purlResult string | 
					
						
							|  |  |  | 		switch { | 
					
						
							|  |  |  | 		case scanSpec.ImageSBOMOutput != "": | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 			sbomResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.ImageSBOMOutput)) | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 		case scanSpec.SBOMOutput != "": | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 			sbomResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.SBOMOutput)) | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 		default: | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 			sbomResult = filepath.Join(scansSubdir, "sbom-result") | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		switch { | 
					
						
							|  |  |  | 		case scanSpec.ImagePURLOutput != "": | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 			purlResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.ImagePURLOutput)) | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 		case scanSpec.PURLOutput != "": | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 			purlResult = filepath.Join(scansSubdir, filepath.Base(scanSpec.PURLOutput)) | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 		default: | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 			purlResult = filepath.Join(scansSubdir, "purl-result") | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		copyFile := func(destination, source string) error { | 
					
						
							|  |  |  | 			dst, err := os.Create(destination) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			defer dst.Close() | 
					
						
							|  |  |  | 			src, err := os.Open(source) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			defer src.Close() | 
					
						
							|  |  |  | 			if _, err = io.Copy(dst, src); err != nil { | 
					
						
							|  |  |  | 				return fmt.Errorf("copying %q to %q: %w", source, destination, err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		err = func() error { | 
					
						
							|  |  |  | 			for i := range resultFiles { | 
					
						
							| 
									
										
										
										
											2025-06-11 01:22:20 +08:00
										 |  |  | 				thisResultFile := filepath.Join(scansSubdir, filepath.Base(resultFiles[i])) | 
					
						
							| 
									
										
										
										
											2023-12-05 03:05:38 +08:00
										 |  |  | 				switch i { | 
					
						
							|  |  |  | 				case 0: | 
					
						
							|  |  |  | 					// Straight-up copy to create the first version of the final output.
 | 
					
						
							|  |  |  | 					if err = copyFile(sbomResult, thisResultFile); err != nil { | 
					
						
							|  |  |  | 						return err | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					// This shouldn't change any contents, but lets us generate the purl file.
 | 
					
						
							|  |  |  | 					err = sbom.Merge(scanSpec.MergeStrategy, thisResultFile, sbomResult, purlResult) | 
					
						
							|  |  |  | 				default: | 
					
						
							|  |  |  | 					// Hopefully we know how to merge information from the new one into the final output.
 | 
					
						
							|  |  |  | 					err = sbom.Merge(scanSpec.MergeStrategy, sbomResult, thisResultFile, purlResult) | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		}() | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, nil, "", err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// If these files are supposed to be written to the local filesystem, add
 | 
					
						
							|  |  |  | 		// their contents to the map of files we expect our caller to write.
 | 
					
						
							|  |  |  | 		if scanSpec.SBOMOutput != "" || scanSpec.PURLOutput != "" { | 
					
						
							|  |  |  | 			if localFiles == nil { | 
					
						
							|  |  |  | 				localFiles = make(map[string]string) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if scanSpec.SBOMOutput != "" { | 
					
						
							|  |  |  | 				localFiles[scanSpec.SBOMOutput] = sbomResult | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if scanSpec.PURLOutput != "" { | 
					
						
							|  |  |  | 				localFiles[scanSpec.PURLOutput] = purlResult | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		// If these files are supposed to be written to the image, create a map of
 | 
					
						
							|  |  |  | 		// their contents so that we can either create a layer diff for them (or
 | 
					
						
							|  |  |  | 		// slipstream them into a squashed layer diff) later.
 | 
					
						
							|  |  |  | 		if scanSpec.ImageSBOMOutput != "" || scanSpec.ImagePURLOutput != "" { | 
					
						
							|  |  |  | 			if imageFiles == nil { | 
					
						
							|  |  |  | 				imageFiles = make(map[string]string) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if scanSpec.ImageSBOMOutput != "" { | 
					
						
							|  |  |  | 				imageFiles[scanSpec.ImageSBOMOutput] = sbomResult | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if scanSpec.ImagePURLOutput != "" { | 
					
						
							|  |  |  | 				imageFiles[scanSpec.ImagePURLOutput] = purlResult | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return imageFiles, localFiles, scansDir, nil | 
					
						
							|  |  |  | } |