diff --git a/pkg/attestation/crafter/materials/oci_image_test.go b/pkg/attestation/crafter/materials/oci_image_test.go index dda38dd69..071ee568f 100644 --- a/pkg/attestation/crafter/materials/oci_image_test.go +++ b/pkg/attestation/crafter/materials/oci_image_test.go @@ -1,5 +1,5 @@ // -// Copyright 2025 The Chainloop Authors. +// Copyright 2025-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -74,7 +74,6 @@ func TestOCIImageCraft_Layout(t *testing.T) { testCases := []struct { name string layoutPath string - wantErr string wantDigest string wantName string wantTag string @@ -97,11 +96,6 @@ func TestOCIImageCraft_Layout(t *testing.T) { wantName: "oci-layout:v1.51.0", wantDigest: "sha256:a5303ef28a4bd9b6e06aa92c07831dd151ac64172695971226bdba4a11fc1b88", }, - { - name: "non-existent path", - layoutPath: "/non/existent/path", - wantErr: "UNAUTHORIZED", - }, } for _, tc := range testCases { @@ -115,11 +109,6 @@ func TestOCIImageCraft_Layout(t *testing.T) { require.NoError(t, err) got, err := craftedMaterial(crafter.Craft(context.TODO(), tc.layoutPath)) - if tc.wantErr != "" { - assert.ErrorContains(t, err, tc.wantErr) - return - } - require.NoError(t, err) assert.Equal(t, contractAPI.CraftingSchema_Material_CONTAINER_IMAGE.String(), got.MaterialType.String()) @@ -139,6 +128,33 @@ func TestOCIImageCraft_Layout(t *testing.T) { } } +// TestOCIImageCraft_UnresolvableRemoteReference covers the path where the input +// is neither an OCI layout directory nor a pullable image, so Craft resolves it +// as a remote reference. It runs against an in-memory registry on loopback +// instead of Docker Hub, so the failure is deterministic and needs no external +// network. +func TestOCIImageCraft_UnresolvableRemoteReference(t *testing.T) { + server := httptest.NewServer(registry.New(registry.Logger(log.New(io.Discard, "", 0)))) + defer server.Close() + + u, err := url.Parse(server.URL) + require.NoError(t, err) + + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_CONTAINER_IMAGE, + } + l := zerolog.Nop() + crafter, err := materials.NewOCIImageCrafter(schema, authn.DefaultKeychain, &l) + require.NoError(t, err) + + // The repository was never pushed, so the registry answers with a not-found + // error instead of serving a manifest. + _, err = craftedMaterial(crafter.Craft(context.TODO(), fmt.Sprintf("%s/does-not-exist:latest", u.Host))) + require.Error(t, err) + assert.ErrorContains(t, err, "NAME_UNKNOWN") +} + func TestOCIImageCraft_LayoutWithDigestSelector(t *testing.T) { testCases := []struct { name string diff --git a/pkg/policies/engine/rego/httpsend_test.go b/pkg/policies/engine/rego/httpsend_test.go index 172a24efd..3565e03b7 100644 --- a/pkg/policies/engine/rego/httpsend_test.go +++ b/pkg/policies/engine/rego/httpsend_test.go @@ -160,6 +160,33 @@ violations contains resp.error.message if { assert.NotContains(t, result.Violations[0].Violation, "is not allowed") } +// TestRego_AllowedHostIsReachable proves that a host on the allow-list is +// actually reached by http.send, using a loopback httptest server instead of a +// live external host. PublicTargetsOnly is not set, so netguard does not block +// the loopback target (see evalContext). +func TestRego_AllowedHostIsReachable(t *testing.T) { + var hits int + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + hits++ + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + serverURL, err := url.Parse(srv.URL) + require.NoError(t, err) + + policy := &engine.Policy{ + Name: "allowed-host", + Source: fmt.Appendf(nil, httpSendPolicyTemplate, fmt.Sprintf(`{"method": "GET", "url": %q}`, srv.URL)), + } + + // OPA matches the allowed host without its port. + r := NewEngine(engine.WithAllowedHostnames(serverURL.Hostname())) + _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) + require.NoError(t, err) + assert.Equal(t, 1, hits) +} + func TestRego_HTTPSendPublicTargetsOnly(t *testing.T) { // httptest listens on the loopback interface, so it stands in for any // destination that is not publicly routable. diff --git a/pkg/policies/engine/rego/rego_test.go b/pkg/policies/engine/rego/rego_test.go index 9c0a787eb..c9ff8ebee 100644 --- a/pkg/policies/engine/rego/rego_test.go +++ b/pkg/policies/engine/rego/rego_test.go @@ -325,44 +325,19 @@ func TestRego_WithRestrictiveMode(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "eval_builtin_error: http.send: disallowed host: github.com") }) +} - t.Run("allowed network requests from defaults", func(t *testing.T) { - regoContent, err := os.ReadFile("testfiles/restricted_mode_networking_allowed_host.rego") - require.NoError(t, err) - - r := NewEngine() - policy := &engine.Policy{ - Name: "policy", - Source: regoContent, - } - - _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) - assert.NoError(t, err) - }) - - t.Run("allowed network requests from defaults plus custom domains", func(t *testing.T) { - defaultHosts, err := os.ReadFile("testfiles/restricted_mode_networking_allowed_host.rego") - require.NoError(t, err) - customHosts, err := os.ReadFile("testfiles/restrictive_mode_networking.rego") - require.NoError(t, err) - - r := NewEngine(engine.WithAllowedHostnames("github.com")) - policy := &engine.Policy{ - Name: "policy", - Source: defaultHosts, - } - - _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) - assert.NoError(t, err) - - policy = &engine.Policy{ - Name: "policy", - Source: customHosts, - } - - _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) - assert.NoError(t, err) - }) +// TestRego_AllowedHostnames asserts the host allow-list membership without any +// network: that the defaults are present and that WithAllowedHostnames appends +// to them rather than replacing them. Whether an allowed host is actually +// reachable is covered hermetically by TestRego_AllowedHostIsReachable. +func TestRego_AllowedHostnames(t *testing.T) { + base := NewEngine().Capabilities().AllowNet + assert.Contains(t, base, "www.chainloop.dev") + assert.Contains(t, base, "www.cisa.gov") + + withCustom := NewEngine(engine.WithAllowedHostnames("github.com")).Capabilities().AllowNet + assert.Subset(t, withCustom, []string{"www.chainloop.dev", "www.cisa.gov", "github.com"}) } func TestRego_WithPermissiveMode(t *testing.T) { diff --git a/pkg/policies/engine/rego/testfiles/restricted_mode_networking_allowed_host.rego b/pkg/policies/engine/rego/testfiles/restricted_mode_networking_allowed_host.rego deleted file mode 100644 index 14d18748e..000000000 --- a/pkg/policies/engine/rego/testfiles/restricted_mode_networking_allowed_host.rego +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import rego.v1 - -################################ -# Common section do NOT change # -################################ - -result := { - "skipped": skipped, - "violations": violations, - "skip_reason": skip_reason, -} - -default skip_reason := "" - -skip_reason := m if { - not valid_input - m := "invalid input" -} - -default skipped := true - -skipped := false if valid_input - -######################################## -# EO Common section, custom code below # -######################################## - -# Validates if the input is valid and can be understood by this policy -valid_input := true - -violations contains msg if { - kev := http.send({"method": "GET", "url": "https://www.chainloop.dev", "cache": true}).body - msg := "" -} \ No newline at end of file