Repository: chainguard-dev/melange

Issue:
`melange completion` panics
```shellsession
$ melange completion 
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
chainguard.dev/melange/pkg/cli.New.completion.func2(0xc0001c3d00?, {0x31b9e80?, 0x4?, 0x1d9a73e?})
	chainguard.dev/melange/pkg/cli/completion.go:61 +0x133
github.com/spf13/cobra.(*Command).execute(0xc000588c08, {0x31b9e80, 0x0, 0x0})
	github.com/spf13/cobra@v1.8.1/command.go:989 +0xa91
github.com/spf13/cobra.(*Command).ExecuteC(0xc000588308)
	github.com/spf13/cobra@v1.8.1/command.go:1117 +0x3ff
github.com/spf13/cobra.(*Command).Execute(...)
	github.com/spf13/cobra@v1.8.1/command.go:1041
github.com/spf13/cobra.(*Command).ExecuteContext(0x20728c0?, {0x2072a80?, 0xc000414180?})
	github.com/spf13/cobra@v1.8.1/command.go:1034 +0x47
main.main()
	chainguard.dev/melange/main.go:30 +0x96
```

E.g. `melange completion bash` works though.


Discussion:
@krisharyan: /assign


Solution patch:
diff --git a/pkg/build/build.go b/pkg/build/build.go
index 33694a07..8f3a4d6e 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -18,6 +18,7 @@ import (
 	"archive/tar"
 	"compress/gzip"
 	"context"
+	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -313,6 +314,7 @@ func (b *Build) buildGuest(ctx context.Context, imgConfig apko_types.ImageConfig
 		b.ExtraPackages = append(b.ExtraPackages, []string{
 			"melange-microvm-init",
 			"gnutar",
+			"attr",
 		}...)
 	}
 
@@ -977,6 +979,34 @@ func (b *Build) BuildPackage(ctx context.Context) error {
 		}
 	}
 
+	// For each `setcap` entry in the package/sub-package, pull out the capability and data and set the xattr
+	// For example:
+	// setcap:
+	//   - path: /usr/bin/scary
+	//     add:
+	//       cap_sys_admin: "+ep"
+	caps, err := config.ParseCapabilities(b.Configuration.Package.SetCap)
+	if err != nil {
+		log.Warnf("failed to collect encoded capabilities for %v: %v", b.Configuration.Package.SetCap, err)
+	}
+
+	for path, cap := range caps {
+		enc := config.EncodeCapability(cap.Effective, cap.Permitted, cap.Inheritable)
+		fullPath := filepath.Join(melangeOutputDirName, pkg.Name, path)
+		if b.Runner.Name() == container.QemuName {
+			fullPath := filepath.Join(WorkDir, melangeOutputDirName, pkg.Name, path)
+			hex := fmt.Sprintf("0x%s", hex.EncodeToString(enc))
+			cmd := []string{"/bin/sh", "-c", fmt.Sprintf("setfattr -n security.capability -v %s %s", hex, fullPath)}
+			if err := b.Runner.Run(ctx, pr.config, map[string]string{}, cmd...); err != nil {
+				return fmt.Errorf("failed to set capabilities within VM on %s: %v\n", path, err)
+			}
+		} else {
+			if err := b.WorkspaceDirFS.SetXattr(fullPath, "security.capability", enc); err != nil {
+				log.Warnf("failed to set capabilities on %s: %v\n", path, err)
+			}
+		}
+	}
+
 	if err := b.retrieveWorkspace(ctx, b.WorkspaceDirFS); err != nil {
 		return fmt.Errorf("retrieving workspace: %w", err)
 	}
@@ -1221,6 +1251,7 @@ func (b *Build) buildWorkspaceConfig(ctx context.Context) *container.Config {
 		cfg.CPUModel = b.Configuration.Package.Resources.CPUModel
 		cfg.Memory = b.Configuration.Package.Resources.Memory
 		cfg.Disk = b.Configuration.Package.Resources.Disk
+		cfg.MicroVM = b.Configuration.Package.Resources.MicroVM
 	}
 	if b.Configuration.Capabilities.Add != nil {
 		cfg.Capabilities.Add = b.Configuration.Capabilities.Add
@@ -1360,6 +1391,17 @@ func sourceDateEpoch(defaultTime time.Time) (time.Time, error) {
 	return time.Unix(sec, 0).UTC(), nil
 }
 
+// xattrIgnoreList contains a mapping of xattr names used by various
+// security features which leak their state into packages.  We need to
+// ignore these xattrs because they require special permissions to be
+// set when the underlying security features are in use.
+var xattrIgnoreList = map[string]bool{
+	"com.apple.provenance":          true,
+	"security.csm":                  true,
+	"security.selinux":              true,
+	"com.docker.grpcfuse.ownership": true,
+}
+
 // Record on-disk xattrs and mode bits set during package builds in order to apply them in the new in-memory filesystem
 // This will allow in-memory and bind mount runners to persist xattrs correctly
 func storeXattrs(dir string) (map[string]map[string][]byte, map[string]fs.FileMode, error) {
@@ -1404,6 +1446,10 @@ func storeXattrs(dir string) (map[string]map[string][]byte, map[string]fs.FileMo
 		attrs := stringsFromByteSlice(buf[:read])
 		result := make(map[string][]byte)
 		for _, attr := range attrs {
+			if _, ok := xattrIgnoreList[attr]; ok {
+				continue
+			}
+
 			s, err := unix.Getxattr(path, attr, nil)
 			if err != nil {
 				continue
diff --git a/pkg/build/pipelines/ruby/clean.yaml b/pkg/build/pipelines/ruby/clean.yaml
index c4ab7b80..3bbb252e 100644
--- a/pkg/build/pipelines/ruby/clean.yaml
+++ b/pkg/build/pipelines/ruby/clean.yaml
@@ -15,5 +15,7 @@ pipeline:
       INSTALL_DIR=${{targets.contextdir}}/$(ruby -e 'puts Gem.default_dir')
       rm -rf ${INSTALL_DIR}/build_info \
              ${INSTALL_DIR}/cache
-      find ${INSTALL_DIR} -name 'gem_make.out' -exec rm {} \;
-      find ${INSTALL_DIR} -name 'mkmf.log' -exec rm {} \;
+      if [ -d "${INSTALL_DIR}" ]; then
+        find "${INSTALL_DIR}" -name 'gem_make.out' -exec rm {} \;
+        find "${INSTALL_DIR}" -name 'mkmf.log' -exec rm {} \;
+      fi
diff --git a/pkg/cli/completion.go b/pkg/cli/completion.go
index 597338cf..42b46d93 100644
--- a/pkg/cli/completion.go
+++ b/pkg/cli/completion.go
@@ -16,7 +16,7 @@ package cli
 
 import (
 	"os"
-
+	"log"
 	"github.com/spf13/cobra"
 )
 
@@ -58,27 +58,34 @@ $ melange completion fish > ~/.config/fish/completions/melange.fish
 		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
 		Args:                  cobra.MaximumNArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
+			if len(args) == 0 {
+				// Print an error message and exit if no argument is provided
+					log.Fatal("A shell type (bash, zsh, fish, powershell) is required.")
+			}
+		
 			switch args[0] {
 			case "bash":
 				err := cmd.Root().GenBashCompletion(os.Stdout)
 				if err != nil {
-					return
+					log.Fatalf("Error generating Bash completion script: %v", err)
 				}
 			case "zsh":
 				err := cmd.Root().GenZshCompletion(os.Stdout)
 				if err != nil {
-					return
+					log.Fatalf("Error generating Zsh completion script: %v", err)
 				}
 			case "fish":
 				err := cmd.Root().GenFishCompletion(os.Stdout, true)
 				if err != nil {
-					return
+					log.Fatalf("Error generating fish completion script: %v", err)
 				}
 			case "powershell":
 				err := cmd.Root().GenPowerShellCompletion(os.Stdout)
 				if err != nil {
-					return
+					log.Fatalf("Error generating PowerShell completion script: %v", err)
 				}
+			default:
+				log.Fatalf("A shell type (bash, zsh, fish, powershell) is required.")
 			}
 		},
 	}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index da193a0e..f4ae63f1 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -17,6 +17,7 @@ package config
 import (
 	"bytes"
 	"context"
+	"encoding/binary"
 	"errors"
 	"fmt"
 	"io/fs"
@@ -125,6 +126,8 @@ type Package struct {
 	// The CPE field values to be used for matching against NVD vulnerability
 	// records, if known.
 	CPE CPE `json:"cpe,omitempty" yaml:"cpe,omitempty"`
+	// Capabilities to set after the pipeline completes.
+	SetCap []Capability `json:"setcap,omitempty" yaml:"setcap,omitempty"`
 
 	// Optional: The amount of time to allow this build to take before timing out.
 	Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
@@ -155,6 +158,15 @@ type CPE struct {
 	Other     string `json:"other,omitempty" yaml:"other,omitempty"`
 }
 
+// Capability stores paths and an associated map of capabilities and justification to include in a package.
+// These capabilities will be set after pipelines run to avoid permissions issues with `setcap`.
+// Empty justifications will result in an error.
+type Capability struct {
+	Path   string            `json:"path,omitempty" yaml:"path,omitempty"`
+	Add    map[string]string `json:"add,omitempty" yaml:"add,omitempty"`
+	Reason string            `json:"reason,omitempty" yaml:"reason,omitempty"`
+}
+
 func (cpe CPE) IsZero() bool {
 	return cpe == CPE{}
 }
@@ -164,6 +176,7 @@ type Resources struct {
 	CPUModel string `json:"cpumodel,omitempty" yaml:"cpumodel,omitempty"`
 	Memory   string `json:"memory,omitempty" yaml:"memory,omitempty"`
 	Disk     string `json:"disk,omitempty" yaml:"disk,omitempty"`
+	MicroVM  bool   `json:"microvm,omitempty" yaml:"microvm,omitempty"`
 }
 
 // CPEString returns the CPE string for the package, suitable for matching
@@ -694,6 +707,8 @@ type Subpackage struct {
 	Checks Checks `json:"checks,omitempty" yaml:"checks,omitempty"`
 	// Test section for the subpackage.
 	Test *Test `json:"test,omitempty" yaml:"test,omitempty"`
+	// Capabilities to set after the pipeline completes.
+	SetCap []Capability `json:"setcap,omitempty" yaml:"setcap,omitempty"`
 }
 
 type Input struct {
@@ -1277,6 +1292,7 @@ func replacePackage(r *strings.Replacer, commit string, in Package) Package {
 		CPE:                in.CPE,
 		Timeout:            in.Timeout,
 		Resources:          in.Resources,
+		SetCap:             in.SetCap,
 	}
 }
 
@@ -1630,6 +1646,9 @@ func (cfg Configuration) validate(ctx context.Context) error {
 	if err := validatePipelines(ctx, cfg.Pipeline); err != nil {
 		return ErrInvalidConfiguration{Problem: err}
 	}
+	if err := validateCapabilities(cfg.Package.SetCap); err != nil {
+		return ErrInvalidConfiguration{Problem: err}
+	}
 
 	saw := map[string]int{cfg.Package.Name: -1}
 	for i, sp := range cfg.Subpackages {
@@ -1656,6 +1675,9 @@ func (cfg Configuration) validate(ctx context.Context) error {
 		if err := validatePipelines(ctx, sp.Pipeline); err != nil {
 			return ErrInvalidConfiguration{Problem: err}
 		}
+		if err := validateCapabilities(sp.SetCap); err != nil {
+			return ErrInvalidConfiguration{Problem: err}
+		}
 	}
 
 	if err := validateCPE(cfg.Package.CPE); err != nil {
@@ -1795,3 +1817,126 @@ func (dep *Dependencies) Summarize(ctx context.Context) {
 		}
 	}
 }
+
+// validCapabilities contains a list of _in-use_ capabilities and their respective bits from existing package specs.
+// https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h#L106-L422
+var validCapabilities = map[string]uint32{
+	"cap_net_bind_service": 10,
+	"cap_net_admin":        12,
+	"cap_net_raw":          13,
+	"cap_ipc_lock":         14,
+	"cap_sys_admin":        21,
+}
+
+func getCapabilityValue(attr string) uint32 {
+	if value, ok := validCapabilities[attr]; ok {
+		return 1 << value
+	}
+	return 0
+}
+
+func validateCapabilities(setcap []Capability) error {
+	var errs []error
+
+	for _, cap := range setcap {
+		for add := range cap.Add {
+			// Allow for multiple capabilities per addition
+			// e.g., cap_net_raw,cap_net_admin,cap_net_bind_service+eip
+			for p := range strings.SplitSeq(add, ",") {
+				if _, ok := validCapabilities[p]; !ok {
+					errs = append(errs, fmt.Errorf("invalid capability %q for path %q", p, cap.Path))
+				}
+			}
+		}
+		if cap.Reason == "" {
+			errs = append(errs, fmt.Errorf("unjustified reason for capability %q", cap.Add))
+		}
+	}
+
+	if len(errs) == 0 {
+		return nil
+	}
+
+	return errors.Join(errs...)
+}
+
+type capabilityData struct {
+	Effective   uint32
+	Permitted   uint32
+	Inheritable uint32
+}
+
+// ParseCapabilities processes all capabilities for a given path.
+func ParseCapabilities(caps []Capability) (map[string]capabilityData, error) {
+	pathCapabilities := map[string]capabilityData{}
+
+	for _, c := range caps {
+		for attrs, data := range c.Add {
+			for attr := range strings.SplitSeq(attrs, ",") {
+				capValues := getCapabilityValue(attr)
+				effective, permitted, inheritable := parseCapability(data)
+
+				caps, ok := pathCapabilities[c.Path]
+				if !ok {
+					caps = struct {
+						Effective   uint32
+						Permitted   uint32
+						Inheritable uint32
+					}{}
+				}
+
+				if effective {
+					caps.Effective |= capValues
+				}
+				if permitted {
+					caps.Permitted |= capValues
+				}
+				if inheritable {
+					caps.Inheritable |= capValues
+				}
+
+				pathCapabilities[c.Path] = caps
+			}
+		}
+	}
+
+	return pathCapabilities, nil
+}
+
+// parseCapability determines which bits are set for a given capability.
+func parseCapability(capFlag string) (effective, permitted, inheritable bool) {
+	for _, c := range capFlag {
+		switch c {
+		case 'e':
+			effective = true
+		case 'p':
+			permitted = true
+		case 'i':
+			inheritable = true
+		}
+	}
+	return
+}
+
+// EncodeCapability returns the byte slice necessary to set the final capability xattr.
+func EncodeCapability(effectiveBits, permittedBits, inheritableBits uint32) []byte {
+	revision := uint32(0x03000000)
+
+	var flags uint32 = 0
+	if effectiveBits != 0 {
+		flags = 0x01
+	}
+	magic := revision | flags
+
+	data := make([]byte, 24)
+
+	binary.LittleEndian.PutUint32(data[0:4], magic)
+	binary.LittleEndian.PutUint32(data[4:8], permittedBits)
+	binary.LittleEndian.PutUint32(data[8:12], inheritableBits)
+
+	binary.LittleEndian.PutUint32(data[12:16], 0)
+	binary.LittleEndian.PutUint32(data[16:20], 0)
+	binary.LittleEndian.PutUint32(data[20:24], 0)
+
+	return data
+}
diff --git a/pkg/container/config.go b/pkg/container/config.go
index a8986ad4..5db0a9d3 100644
--- a/pkg/container/config.go
+++ b/pkg/container/config.go
@@ -60,4 +60,5 @@ type Config struct {
 	SSHHostKey            string
 	Disk                  string
 	Timeout               time.Duration
+	MicroVM               bool
 }
diff --git a/pkg/container/qemu_runner.go b/pkg/container/qemu_runner.go
index 49f9c23a..225b36da 100644
--- a/pkg/container/qemu_runner.go
+++ b/pkg/container/qemu_runner.go
@@ -477,19 +477,39 @@ func createMicroVM(ctx context.Context, cfg *Config) error {
 
 	baseargs := []string{}
 	microvm := false
-	// load microvm profile and bios, shave some milliseconds from boot
-	// using this will make a complete boot->initrd (with working network) In ~700ms
-	// instead of ~900ms.
-	for _, p := range []string{
-		"/usr/share/qemu/bios-microvm.bin",
-		"/usr/share/seabios/bios-microvm.bin",
-	} {
-		if _, err := os.Stat(p); err == nil && cfg.Arch.ToAPK() != "aarch64" {
-			// only enable pcie for network, enable RTC for kernel, disable i8254PIT, i8259PIC and serial port
-			baseargs = append(baseargs, "-machine", "microvm,rtc=on,pcie=on,pit=off,pic=off,isa-serial=off")
-			baseargs = append(baseargs, "-bios", p)
-			microvm = true
-			break
+	// by default, cfg.MicroVM will be false
+	// override the MicroVM config value with the environment variable if present
+	// and said variable is parseable as a bool
+	if env, ok := os.LookupEnv("QEMU_USE_MICROVM"); ok {
+		if val, err := strconv.ParseBool(env); err == nil {
+			cfg.MicroVM = val
+		}
+	}
+
+	kernelConsole := "console=hvc0"
+	serialArgs := []string{
+		"-device", "virtio-serial-pci,id=virtio-serial0",
+		"-chardev", "stdio,id=charconsole0",
+		"-device", "virtconsole,chardev=charconsole0,id=console0",
+	}
+	if cfg.MicroVM {
+		// load microvm profile and bios, shave some milliseconds from boot
+		// using this will make a complete boot->initrd (with working network) In ~700ms
+		// instead of ~900ms.
+		for _, p := range []string{
+			"/usr/share/qemu/bios-microvm.bin",
+			"/usr/share/seabios/bios-microvm.bin",
+		} {
+			if _, err := os.Stat(p); err == nil && cfg.Arch.ToAPK() != "aarch64" {
+				// only enable pcie for network, enable RTC for kernel, disable i8254PIT, i8259PIC and serial port
+				baseargs = append(baseargs, "-machine", "microvm,rtc=on,pcie=on,pit=off,pic=off,isa-serial=on")
+				baseargs = append(baseargs, "-bios", p)
+				// microvm in qemu any version tested will not send hvc0/virtconsole to stdout
+				kernelConsole = "console=ttyS0"
+				serialArgs = []string{"-serial", "stdio"}
+				microvm = true
+				break
+			}
 		}
 	}
 
@@ -561,15 +581,15 @@ func createMicroVM(ctx context.Context, cfg *Config) error {
 		}
 	}
 
-	baseargs = append(baseargs, "-daemonize")
 	// ensure we disable unneeded devices, this is less needed if we use microvm machines
 	// but still useful otherwise
 	baseargs = append(baseargs, "-display", "none")
 	baseargs = append(baseargs, "-no-reboot")
 	baseargs = append(baseargs, "-no-user-config")
+	baseargs = append(baseargs, "-nographic")
 	baseargs = append(baseargs, "-nodefaults")
 	baseargs = append(baseargs, "-parallel", "none")
-	baseargs = append(baseargs, "-serial", "none")
+	baseargs = append(baseargs, serialArgs...)
 	baseargs = append(baseargs, "-vga", "none")
 	// use -netdev + -device instead of -nic, as this is better supported by microvm machine type
 	baseargs = append(baseargs, "-netdev", "user,id=id1,hostfwd=tcp:"+cfg.SSHAddress+"-:22")
@@ -579,7 +599,7 @@ func createMicroVM(ctx context.Context, cfg *Config) error {
 	// panic=-1 ensures that if the init fails, we immediately exit the machine
 	// Add default SSH keys to the VM
 	sshkey := base64.StdEncoding.EncodeToString(pubKey)
-	baseargs = append(baseargs, "-append", "quiet nomodeset panic=-1 sshkey="+sshkey)
+	baseargs = append(baseargs, "-append", kernelConsole+" debug loglevel=7 nomodeset panic=-1 sshkey="+sshkey)
 	// we will *not* mount workspace using qemu, this will use 9pfs which is network-based, and will
 	// kill all performances (lots of small files)
 	// instead we will copy back the finished workspace artifacts when done.
@@ -611,41 +631,89 @@ func createMicroVM(ctx context.Context, cfg *Config) error {
 	qemuCmd := exec.CommandContext(ctx, fmt.Sprintf("qemu-system-%s", cfg.Arch.ToAPK()), baseargs...)
 	clog.FromContext(ctx).Debugf("qemu: executing - %s", strings.Join(qemuCmd.Args, " "))
 
-	output, err := qemuCmd.CombinedOutput()
-	if err != nil {
-		// ensure cleanup of resources
+	outRead, outWrite := io.Pipe()
+	errRead, errWrite := io.Pipe()
+
+	qemuCmd.Stdout = outWrite
+	qemuCmd.Stderr = errWrite
+
+	if err := qemuCmd.Start(); err != nil {
 		defer os.Remove(cfg.ImgRef)
 		defer os.Remove(cfg.Disk)
-
-		clog.FromContext(ctx).Errorf("qemu: failed to run qemu command: %v - %s", err, string(output))
-		return err
+		return fmt.Errorf("qemu: failed to start qemu command: %w", err)
 	}
 
-	// 5 minutes timeout
-	retries := 6000
-	try := 0
-	for try <= retries {
-		if err := ctx.Err(); err != nil {
-			return fmt.Errorf("checking SSH server: %w", err)
+	logCtx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	go func() {
+		defer outRead.Close()
+		scanner := bufio.NewScanner(outRead)
+		for scanner.Scan() && logCtx.Err() == nil {
+			line := scanner.Text()
+			log.Debugf("qemu: %s", line)
+		}
+		if err := scanner.Err(); err != nil {
+			log.Warnf("qemu stdout scanner error: %v", err)
 		}
+	}()
 
-		try++
-		time.Sleep(time.Millisecond * 500)
+	go func() {
+		defer errRead.Close()
+		scanner := bufio.NewScanner(errRead)
+		for scanner.Scan() && logCtx.Err() == nil {
+			line := scanner.Text()
+			log.Warnf("qemu: %s", line)
+		}
+		if err := scanner.Err(); err != nil {
+			log.Warnf("qemu stderr scanner error: %v", err)
+		}
+	}()
 
-		clog.FromContext(ctx).Debugf("qemu: waiting for ssh to come up, try %d of %d", try, retries)
-		// Attempt to connect to the address
-		err = checkSSHServer(cfg.SSHAddress)
-		if err == nil {
-			break
-		} else {
-			clog.FromContext(ctx).Debug(err.Error())
+	qemuExit := make(chan error, 1)
+	go func() {
+		err := qemuCmd.Wait()
+		outWrite.Close()
+		errWrite.Close()
+		qemuExit <- err
+	}()
+
+	started := make(chan struct{})
+
+	go func() {
+		// one-hour timeout with a 500ms sleep
+		retries := 7200
+		try := 0
+		for try < retries {
+			if logCtx.Err() != nil {
+				return
+			}
+
+			try++
+			time.Sleep(time.Millisecond * 500)
+
+			log.Debugf("qemu: waiting for ssh to come up, try %d of %d", try, retries)
+			err = checkSSHServer(cfg.SSHAddress)
+			if err == nil {
+				close(started)
+				return
+			} else {
+				log.Debug(err.Error())
+			}
 		}
-	}
-	if try >= retries {
-		// ensure cleanup of resources
+	}()
+
+	select {
+	case <-started:
+		log.Info("qemu: VM started successfully, SSH server is up")
+	case err := <-qemuExit:
+		defer os.Remove(cfg.ImgRef)
+		defer os.Remove(cfg.Disk)
+		return fmt.Errorf("qemu: VM exited unexpectedly: %v", err)
+	case <-ctx.Done():
 		defer os.Remove(cfg.ImgRef)
 		defer os.Remove(cfg.Disk)
-		return fmt.Errorf("qemu: could not start VM, timeout reached")
+		return fmt.Errorf("qemu: context canceled while waiting for VM to start")
 	}
 
 	err = getHostKey(ctx, cfg)



Test patch:
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index f629bb7d..2e3f5442 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -1,8 +1,11 @@
 package config
 
 import (
+	"bytes"
+	"encoding/binary"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/chainguard-dev/clog/slogtest"
@@ -531,3 +534,392 @@ func TestGetScheduleMessage(t *testing.T) {
 		}
 	}
 }
+
+func TestSetCap(t *testing.T) {
+	tests := []struct {
+		setcap []Capability
+		err    bool
+	}{
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_net_bind_service": "+eip"},
+					Reason: "Needed for package foo because xyz",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_net_raw": "+eip"},
+					Reason: "Needed for package baz because xyz",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_net_raw,cap_net_admin,cap_net_bind_service": "+ep"},
+					Reason: "Valid combination of three capabilities on a single line",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path: "/bar",
+					Add: map[string]string{
+						"cap_net_raw":          "+ep",
+						"cap_net_admin":        "+ep",
+						"cap_net_bind_service": "+ep",
+					},
+					Reason: "Valid combination of three capabilities on separate lines",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path: "/foo",
+					Add: map[string]string{
+						"cap_net_raw": "+ep",
+					},
+					Reason: "First package in a multi-package, multi-capability capability addition.",
+				},
+				{
+					Path: "/bar",
+					Add: map[string]string{
+						"cap_net_admin":        "+ep",
+						"cap_net_bind_service": "+ep",
+					},
+					Reason: "Second package in a multi-package, multi-capability capability addition.",
+				},
+				{
+					Path: "/baz",
+					Add: map[string]string{
+						"cap_net_raw,cap_net_admin,cap_net_bind_service": "+eip",
+					},
+					Reason: "Third package in a multi-package, multi-capability capability addition.",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path: "/foo",
+					Add: map[string]string{
+						"cap_net_raw": "+ep",
+					},
+					Reason: "First package in a multi-package, multi-capability capability addition.",
+				},
+				{
+					Path: "/bar",
+					Add: map[string]string{
+						"cap_setfcap":          "+ep",
+						"cap_net_bind_service": "+ep",
+					},
+					Reason: "Tying to sneak an invalid capability into multiple paths.",
+				},
+				{
+					Path: "/baz",
+					Add: map[string]string{
+						"cap_net_raw,cap_net_admin,cap_net_bind_service": "+eip",
+					},
+					Reason: "Third package in a multi-package, multi-capability capability addition.",
+				},
+			},
+			true,
+		},
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_sys_admin": "+ep"},
+					Reason: "Needed for package baz",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_ipc_lock": "+ep"},
+					Reason: "Needed for package baz",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_net_admin": "+ep"},
+					Reason: "Needed for package baz",
+				},
+			},
+			false,
+		},
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_net_admin": "+ep"},
+					Reason: "",
+				},
+			},
+			true,
+		},
+		{
+			[]Capability{
+				{
+					Path:   "/bar",
+					Add:    map[string]string{"cap_setfcap": "+ep"},
+					Reason: "I want to arbitrarily set capabilities",
+				},
+			},
+			true,
+		},
+	}
+
+	for _, test := range tests {
+		err := validateCapabilities(test.setcap)
+		if (err != nil) != test.err {
+			t.Errorf("validateCapabilities(%v) returned error %v, expected error: %v", test.setcap, err, test.err)
+		}
+	}
+}
+
+// Mock resources to test setcap capabilities
+type mockFS struct {
+	xattrs map[string]map[string][]byte
+}
+
+func newMockFS() *mockFS {
+	return &mockFS{
+		xattrs: make(map[string]map[string][]byte),
+	}
+}
+
+func (fs *mockFS) SetXattr(path, attr string, value []byte) error {
+	if _, ok := fs.xattrs[path]; !ok {
+		fs.xattrs[path] = make(map[string][]byte)
+	}
+	fs.xattrs[path][attr] = value
+	return nil
+}
+
+func (fs *mockFS) GetXattr(path, attr string) ([]byte, error) {
+	if attrs, ok := fs.xattrs[path]; ok {
+		if value, ok := attrs[attr]; ok {
+			return value, nil
+		}
+	}
+	return nil, os.ErrNotExist
+}
+
+func TestSetCapability(t *testing.T) {
+	type Config struct {
+		Package struct {
+			SetCap []Capability
+		}
+	}
+
+	type Builder struct {
+		Configuration  Config
+		WorkspaceDirFS *mockFS
+	}
+
+	testCases := []struct {
+		name          string
+		caps          []Capability
+		expectedAttrs map[string]map[string][]byte
+	}{
+		{
+			name: "Basic capability +ep",
+			caps: []Capability{
+				{
+					Path: "/usr/bin/fping",
+					Add: map[string]string{
+						"cap_net_raw": "+ep",
+					},
+					Reason: "foo",
+				},
+			},
+			expectedAttrs: map[string]map[string][]byte{
+				"/usr/bin/fping": {
+					"security.capability": nil,
+				},
+			},
+		},
+		{
+			name: "Multiple capabilities",
+			caps: []Capability{
+				{
+					Path: "/usr/bin/traceroute",
+					Add: map[string]string{
+						"cap_net_raw":   "+ep",
+						"cap_net_admin": "+eip",
+					},
+					Reason: "foo",
+				},
+			},
+			expectedAttrs: map[string]map[string][]byte{
+				"/usr/bin/traceroute": {
+					"security.capability": nil,
+				},
+			},
+		},
+		{
+			name: "Multiple paths",
+			caps: []Capability{
+				{
+					Path: "/bin/ping",
+					Add: map[string]string{
+						"cap_net_raw": "+ep",
+					},
+					Reason: "foo",
+				},
+				{
+					Path: "/usr/bin/traceroute",
+					Add: map[string]string{
+						"cap_net_admin": "+eip",
+					},
+					Reason: "foo",
+				},
+			},
+			expectedAttrs: map[string]map[string][]byte{
+				"/bin/ping": {
+					"security.capability": nil,
+				},
+				"/usr/bin/traceroute": {
+					"security.capability": nil,
+				},
+			},
+		},
+		{
+			name: "Single-line capabilities with same flags",
+			caps: []Capability{
+				{
+					Path: "/bin/custom-tool",
+					Add: map[string]string{
+						"cap_net_raw,cap_net_admin": "+ep",
+					},
+					Reason: "foo",
+				},
+			},
+			expectedAttrs: map[string]map[string][]byte{
+				"/bin/custom-tool": {
+					"security.capability": nil,
+				},
+			},
+		},
+		{
+			name: "Multiple comma-separated capabilities with different flags",
+			caps: []Capability{
+				{
+					Path: "/usr/bin/privileged-tool",
+					Add: map[string]string{
+						"cap_net_raw,cap_net_admin,cap_net_bind_service": "+eip",
+						"cap_sys_admin": "+p",
+					},
+					Reason: "foo",
+				},
+			},
+			expectedAttrs: map[string]map[string][]byte{
+				"/usr/bin/privileged-tool": {
+					"security.capability": nil,
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			b := &Builder{
+				WorkspaceDirFS: newMockFS(),
+			}
+			b.Configuration.Package.SetCap = tc.caps
+
+			caps, err := ParseCapabilities(b.Configuration.Package.SetCap)
+			if err != nil {
+				t.Fatalf("Failed to collect capabilities: %v", err)
+			}
+
+			expectedAttrs := make(map[string][]byte)
+			for path, c := range caps {
+				encoded := EncodeCapability(c.Effective, c.Permitted, c.Inheritable)
+				expectedAttrs[path] = encoded
+
+				if err := b.WorkspaceDirFS.SetXattr(path, "security.capability", encoded); err != nil {
+					t.Fatalf("failed to set xattr for %s: %v", path, err)
+				}
+			}
+
+			for path, expected := range expectedAttrs {
+				data, err := b.WorkspaceDirFS.GetXattr(path, "security.capability")
+				if err != nil {
+					t.Errorf("Failed to get xattr %s: %v", path, err)
+					continue
+				}
+
+				if !bytes.Equal(data, expected) {
+					t.Errorf("Mismatched xattr for %s:\ngot:  %x\nwant: %x", path, data, expected)
+				}
+
+				if len(data) < 24 {
+					t.Errorf("Capability data too short for %s: got %d bytes", path, len(data))
+					continue
+				}
+
+				magic := binary.LittleEndian.Uint32(data[0:4])
+				revision := magic & 0xFF000000
+				flags := magic & 0x000000FF
+
+				if revision != 0x03000000 {
+					t.Errorf("Invalid revision: %x", revision)
+				}
+
+				permitted := binary.LittleEndian.Uint32(data[4:8])
+				inheritable := binary.LittleEndian.Uint32(data[8:12])
+				rootid := binary.LittleEndian.Uint32(data[20:24])
+
+				if rootid != 0 {
+					t.Errorf("Unexpected rootid: %d", rootid)
+				}
+
+				effective := flags & 0x01
+
+				for _, capEntry := range tc.caps {
+					if capEntry.Path != path {
+						continue
+					}
+					for attr, flag := range capEntry.Add {
+						for _, a := range strings.Split(attr, ",") {
+							val := getCapabilityValue(a)
+							e, p, i := parseCapability(flag)
+
+							if e && effective != 1 {
+								t.Errorf("Expected effective bit set for %s", path)
+							}
+							if p && (permitted&val != val) {
+								t.Errorf("Expected permitted cap %s in %s", a, path)
+							}
+							if i && (inheritable&val != val) {
+								t.Errorf("Expected inheritable cap %s in %s", a, path)
+							}
+						}
+					}
+				}
+			}
+		})
+	}
+}