diff --git a/runner/policy_artifacts.go b/runner/policy_artifacts.go index bfeb9d3..b57efc4 100644 --- a/runner/policy_artifacts.go +++ b/runner/policy_artifacts.go @@ -89,7 +89,7 @@ func (u *artifactUploader) storeEvaluation(ctx context.Context, evaluation *prot return nil, errors.New("the evaluation has no input data") } - bundle, err := tarDirectory(policyPath) + bundle, err := packageBundle(policyPath) if err != nil { return nil, fmt.Errorf("package policy bundle: %w", err) } @@ -170,6 +170,20 @@ func retryable(ctx context.Context, err error) bool { return true } +// packageBundle returns the policy bundle at policyPath for upload. A bundle archive, such as +// opa build's bundle.tar.gz used as a local policy, is sent as-is, since the API accepts a tar +// or gzipped tar; a directory, such as an extracted OCI policy, is archived. +func packageBundle(policyPath string) ([]byte, error) { + info, err := os.Stat(policyPath) + if err != nil { + return nil, err + } + if info.Mode().IsRegular() { + return os.ReadFile(policyPath) + } + return tarDirectory(policyPath) +} + // tarDirectory archives the regular files under dir. The API canonicalises the archive, // so file order, modes and times here do not affect the digest. func tarDirectory(dir string) ([]byte, error) { diff --git a/runner/policy_artifacts_test.go b/runner/policy_artifacts_test.go index 99bd534..a49f338 100644 --- a/runner/policy_artifacts_test.go +++ b/runner/policy_artifacts_test.go @@ -1,7 +1,9 @@ package runner import ( + "archive/tar" "bytes" + "compress/gzip" "context" "crypto/sha256" "encoding/hex" @@ -87,6 +89,25 @@ func writeBundle(t *testing.T, name string) string { return dir } +// writeBundleArchive writes a policy bundle as a gzipped tar, with the leading slashes opa +// build gives its entries. +func writeBundleArchive(t *testing.T, name string) string { + t.Helper() + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + tw := tar.NewWriter(zw) + module := []byte("package compliance_framework." + name + "\n\ntitle := \"" + name + "\"\n") + require.NoError(t, tw.WriteHeader(&tar.Header{Typeflag: tar.TypeReg, Name: "/policies/" + name + ".rego", Mode: 0o600, Size: int64(len(module))})) + _, err := tw.Write(module) + require.NoError(t, err) + require.NoError(t, tw.Close()) + require.NoError(t, zw.Close()) + + archive := filepath.Join(t.TempDir(), "bundle.tar.gz") + require.NoError(t, os.WriteFile(archive, buf.Bytes(), 0o644)) + return archive +} + func evidenceFor(title string, evaluation *proto.PolicyEvaluation) *proto.Evidence { return &proto.Evidence{UUID: "11111111-1111-1111-1111-111111111111", Title: title, PolicyEvaluation: evaluation} } @@ -133,6 +154,25 @@ func TestCreateEvidenceUploadsOncePerEvaluation(t *testing.T) { assert.Len(t, api.uploads, 3) } +func TestCreateEvidenceUploadsABundleArchiveAsIs(t *testing.T) { + archive := writeBundleArchive(t, "a") + api := &fakeAPI{} + helper := newTestHelper(t, api, archive) + + err := helper.CreateEvidence(context.Background(), []*proto.Evidence{ + evidenceFor("one", &proto.PolicyEvaluation{PolicyPath: archive, Input: []byte(`{}`)}), + }) + require.NoError(t, err) + + require.Len(t, api.evidence, 1) + refs, ok := api.evidence[0]["policy-artifacts"].(map[string]any) + require.True(t, ok, "evidence must carry policy-artifacts: %v", api.evidence[0]) + content, err := os.ReadFile(archive) + require.NoError(t, err) + sum := sha256.Sum256(content) + assert.Equal(t, "sha256:"+hex.EncodeToString(sum[:]), refs["bundle-digest"], "the archive must be uploaded unchanged, not wrapped in another tar") +} + func TestCreateEvidenceWithoutEvaluationIsUnchanged(t *testing.T) { api := &fakeAPI{} helper := newTestHelper(t, api)