Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,18 @@ func main() {

sandbox.InitNetwork(ctx)

// Confirm the configured sandbox images are accessible before we start
// consuming messages. This catches a misconfigured or unavailable image at
// startup instead of letting messages be acked errantly later on. When
// noPull is set the worker uses images that are already present locally, so
// the remote registry check is skipped.
if !cfg.imageSpec.noPull {
if err := worker.CheckSandboxImagesAccessible(ctx, cfg.imageSpec.tag); err != nil {
slog.ErrorContext(ctx, "Sandbox image preflight check failed", "error", err)
os.Exit(1)
}
}

// If configured, start a webserver so that Go's pprof can be accessed for
// debugging and profiling.
if os.Getenv("OSSF_MALWARE_ANALYSIS_ENABLE_PROFILER") != "" {
Expand Down
10 changes: 8 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.26.3
require (
cloud.google.com/go/pubsub v1.50.2
github.com/blendle/zapdriver v1.3.1
github.com/google/go-containerregistry v0.21.6
github.com/gopacket/gopacket v1.6.0
github.com/ossf/package-feeds v0.0.0-20240903033607-939890176fa6
github.com/package-url/packageurl-go v0.1.6
Expand Down Expand Up @@ -57,6 +58,8 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v29.4.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
Expand All @@ -78,11 +81,13 @@ require (
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
Expand All @@ -104,4 +109,5 @@ require (
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
gotest.tools/v3 v3.5.2 // indirect
)
22 changes: 16 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU=
github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
Expand Down Expand Up @@ -115,6 +119,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM=
github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo=
github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo=
github.com/google/go-replayers/grpcreplay v1.3.0/go.mod h1:v6NgKtkijC0d3e3RW8il6Sy5sqRVUwoQa4mHOGEy8DI=
github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk=
Expand Down Expand Up @@ -150,8 +156,12 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/ossf/package-feeds v0.0.0-20240903033607-939890176fa6 h1:Yb9zEPd7FX8INY7PGCb7VYS6bpawSLYDgmfadScUi68=
github.com/ossf/package-feeds v0.0.0-20240903033607-939890176fa6/go.mod h1:IufKOa9FZbng8zOYAmzzosmV1mxp+Wzth7wGQugSvt8=
github.com/package-url/packageurl-go v0.1.6 h1:YO3p6u1XmCUliivUg/qWphaY8vI6hxSnnPv7Bfg3m5M=
Expand All @@ -166,16 +176,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
Expand Down Expand Up @@ -257,7 +266,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
Expand Down Expand Up @@ -300,3 +308,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
65 changes: 65 additions & 0 deletions internal/worker/preflight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package worker

import (
"context"
"errors"
"fmt"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)

// SandboxImageURLs returns the fully qualified references (image:tag) of every
// sandbox container image the worker needs in order to run analysis. If tag is
// empty, "latest" is used to match the default applied by the sandbox package.
func SandboxImageURLs(tag string) []string {
if tag == "" {
tag = "latest"
}
return []string{
fmt.Sprintf("%s:%s", defaultStaticAnalysisImage, tag),
fmt.Sprintf("%s:%s", defaultDynamicAnalysisImage, tag),
}
}

// ImageChecker reports whether a single image reference is accessible. It
// returns a non-nil error if the image cannot be resolved. It exists so that
// CheckSandboxImagesAvailable can be unit tested without making real network
// calls.
type ImageChecker func(ctx context.Context, image string) error

// remoteImageChecker confirms that image is accessible in its registry by
// performing a HEAD/GET against the manifest using go-containerregistry's
// remote.Get. It does not pull the image.
func remoteImageChecker(ctx context.Context, image string) error {
ref, err := name.ParseReference(image)
if err != nil {
return fmt.Errorf("parsing image reference: %w", err)
}
if _, err := remote.Get(ref, remote.WithContext(ctx), remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil {
return err
}
return nil
}

// CheckSandboxImagesAvailable verifies that every image in images is accessible
// using check. It returns an error naming each image that could not be
// accessed, or nil if they are all available.
func CheckSandboxImagesAvailable(ctx context.Context, images []string, check ImageChecker) error {
var errs []error
for _, image := range images {
if err := check(ctx, image); err != nil {
errs = append(errs, fmt.Errorf("sandbox image %q is not accessible: %w", image, err))
}
}
return errors.Join(errs...)
}

// CheckSandboxImagesAccessible confirms that all sandbox images configured for
// the given tag can be reached in their registry before the worker starts
// consuming messages. It fails loudly so a misconfigured or unavailable image
// is caught at startup rather than silently acking messages later on.
func CheckSandboxImagesAccessible(ctx context.Context, tag string) error {
return CheckSandboxImagesAvailable(ctx, SandboxImageURLs(tag), remoteImageChecker)
}
96 changes: 96 additions & 0 deletions internal/worker/preflight_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package worker

import (
"context"
"errors"
"strings"
"testing"
)

func TestSandboxImageURLs(t *testing.T) {
tests := []struct {
name string
tag string
want []string
}{
{
name: "explicit tag",
tag: "v1.2.3",
want: []string{
"gcr.io/ossf-malware-analysis/static-analysis:v1.2.3",
"gcr.io/ossf-malware-analysis/dynamic-analysis:v1.2.3",
},
},
{
name: "empty tag defaults to latest",
tag: "",
want: []string{
"gcr.io/ossf-malware-analysis/static-analysis:latest",
"gcr.io/ossf-malware-analysis/dynamic-analysis:latest",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := SandboxImageURLs(tt.tag)
if len(got) != len(tt.want) {
t.Fatalf("SandboxImageURLs(%q) = %v; want %v", tt.tag, got, tt.want)
}
for i := range tt.want {
if got[i] != tt.want[i] {
t.Errorf("SandboxImageURLs(%q)[%d] = %q; want %q", tt.tag, i, got[i], tt.want[i])
}
}
})
}
}

func TestCheckSandboxImagesAvailable_AllPresent(t *testing.T) {
images := []string{"registry/a:latest", "registry/b:latest"}
check := func(ctx context.Context, image string) error { return nil }

if err := CheckSandboxImagesAvailable(context.Background(), images, check); err != nil {
t.Errorf("CheckSandboxImagesAvailable() = %v; want nil", err)
}
}

func TestCheckSandboxImagesAvailable_OneMissing(t *testing.T) {
const missing = "registry/missing:latest"
images := []string{"registry/present:latest", missing}

check := func(ctx context.Context, image string) error {
if image == missing {
return errors.New("manifest unknown")
}
return nil
}

err := CheckSandboxImagesAvailable(context.Background(), images, check)
if err == nil {
t.Fatal("CheckSandboxImagesAvailable() = nil; want error naming the missing image")
}
if !strings.Contains(err.Error(), missing) {
t.Errorf("error %q does not name the missing image %q", err.Error(), missing)
}
if strings.Contains(err.Error(), "registry/present:latest") {
t.Errorf("error %q should not name the available image", err.Error())
}
}

func TestCheckSandboxImagesAvailable_MultipleMissing(t *testing.T) {
images := []string{"registry/a:latest", "registry/b:latest"}
check := func(ctx context.Context, image string) error {
return errors.New("manifest unknown")
}

err := CheckSandboxImagesAvailable(context.Background(), images, check)
if err == nil {
t.Fatal("CheckSandboxImagesAvailable() = nil; want error")
}
for _, image := range images {
if !strings.Contains(err.Error(), image) {
t.Errorf("error %q does not name image %q", err.Error(), image)
}
}
}