github.com/yogeshkumararora/slsa-github-generator@v1.10.1-0.20240520161934-11278bd5afb4/internal/builders/docker/pkg/builder_test.go (about)

     1  // Copyright 2022 SLSA Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package pkg
    16  
    17  import (
    18  	"errors"
    19  	"os"
    20  	"path/filepath"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	intoto "github.com/in-toto/in-toto-golang/in_toto"
    27  )
    28  
    29  func Test_CreateBuildDefinition(t *testing.T) {
    30  	config := &DockerBuildConfig{
    31  		SourceRepo:   "git+https://github.com/yogeshkumararora/slsa-github-generator@refs/heads/main",
    32  		SourceDigest: Digest{Alg: "sha1", Value: "cf5804b5c6f1a4b2a0b03401a487dfdfbe3a5f00"},
    33  		BuilderImage: DockerImage{
    34  			Name:   "bash",
    35  			Digest: Digest{Alg: "sha256", Value: "9e2ba52487d945504d250de186cb4fe2e3ba023ed2921dd6ac8b97ed43e76af9"},
    36  		},
    37  		BuildConfigPath: "internal/builders/docker/testdata/config.toml",
    38  	}
    39  
    40  	db := &DockerBuild{
    41  		config: config,
    42  		buildConfig: &BuildConfig{
    43  			Command:      []string{"cp", "internal/builders/docker/testdata/config.toml", "config.toml"},
    44  			ArtifactPath: "config.toml",
    45  		},
    46  	}
    47  
    48  	got := db.CreateBuildDefinition()
    49  
    50  	want, err := loadBuildDefinitionFromFile("../testdata/build-definition.json")
    51  	if err != nil {
    52  		t.Fatalf("%v", err)
    53  	}
    54  
    55  	if diff := cmp.Diff(got, want); diff != "" {
    56  		t.Errorf(diff)
    57  	}
    58  }
    59  
    60  func Test_GitClient_verifyOrFetchRepo(t *testing.T) {
    61  	config := &DockerBuildConfig{
    62  		// Use a small repo for test
    63  		SourceRepo: "git+https://github.com/project-oak/transparent-release",
    64  		// The digest value does not matter for the test
    65  		SourceDigest:    Digest{Alg: "sha1", Value: "does-not-matter"},
    66  		BuildConfigPath: "internal/builders/docker/testdata/config.toml",
    67  		ForceCheckout:   false,
    68  		// BuilderImage field is not relevant, so it is omitted
    69  	}
    70  	gc, err := newGitClient(config, 1)
    71  	if err != nil {
    72  		t.Fatalf("Could create GitClient: %v", err)
    73  	}
    74  
    75  	// We expect it to fail at verifyCommit
    76  	if got, want := gc.verifyOrFetchRepo(), errGitCommitMismatch; !errors.Is(got, want) {
    77  		t.Errorf("unexpected error: %v", cmp.Diff(got, want, cmpopts.EquateErrors()))
    78  	}
    79  }
    80  
    81  func Test_GitClient_fetchSourcesFromGitRepo(t *testing.T) {
    82  	// The call to fetchSourcesFromGitRepo will change directory. Here we store
    83  	// the current working directory, and change back to it when the test ends.
    84  	cwd, err := os.Getwd()
    85  	if err != nil {
    86  		t.Fatalf("couldn't get current working directory: %v", err)
    87  	}
    88  
    89  	config := &DockerBuildConfig{
    90  		// Use a small repo for test
    91  		SourceRepo: "git+https://github.com/project-oak/transparent-release",
    92  		// The digest value does not matter for the test
    93  		SourceDigest:    Digest{Alg: "sha1", Value: "does-no-matter"},
    94  		BuildConfigPath: "internal/builders/docker/testdata/config.toml",
    95  		ForceCheckout:   false,
    96  		// BuilderImage field is not relevant, so it is omitted
    97  	}
    98  	gc, err := newGitClient(config, 1)
    99  	if err != nil {
   100  		t.Fatalf("Could not create GitClient: %v", err)
   101  	}
   102  
   103  	// We expect the checkout to fail
   104  	if got, want := gc.fetchSourcesFromGitRepo(), errGitCheckout; !errors.Is(got, want) {
   105  		t.Errorf("unexpected error: %v", cmp.Diff(got, want, cmpopts.EquateErrors()))
   106  	}
   107  
   108  	// Cleanup
   109  	gc.cleanupAllFiles()
   110  	// Recover the original test state.
   111  	if err := os.Chdir(cwd); err != nil {
   112  		t.Errorf("couldn't change directory to %q: %v", cwd, err)
   113  	}
   114  }
   115  
   116  func Test_GitClient_sourceWithRef(t *testing.T) {
   117  	// This tests that specifying a source repository with a ref resolves
   118  	// to a valid GitClient source repository
   119  
   120  	config := &DockerBuildConfig{
   121  		// Use a small repo for test
   122  		SourceRepo: "git+https://github.com/project-oak/transparent-release@refs/heads/main",
   123  		// The digest value does not matter for the test
   124  		SourceDigest:    Digest{Alg: "sha1", Value: "does-no-matter"},
   125  		BuildConfigPath: "internal/builders/docker/testdata/config.toml",
   126  		ForceCheckout:   false,
   127  		// BuilderImage field is not relevant, so it is omitted
   128  	}
   129  	gc, err := newGitClient(config, 1)
   130  	if err != nil {
   131  		t.Fatalf("Could not create GitClient: %v", err)
   132  	}
   133  
   134  	// We expect that the sourceRef is set to refs/heads/main
   135  	if gc.sourceRef == nil || *gc.sourceRef != "refs/heads/main" {
   136  		t.Errorf("expected sourceRef to be refs/heads/main, got %s", *gc.sourceRef)
   137  	}
   138  }
   139  
   140  func Test_GitClient_invalidSourceRef(t *testing.T) {
   141  	// This tests that specifying a source repository with a ref resolves
   142  	// to a valid GitClient source repository
   143  
   144  	config := &DockerBuildConfig{
   145  		// Use a small repo for test
   146  		SourceRepo: "git+https://github.com/project-oak/transparent-release@refs/heads/main@invalid",
   147  		// The digest value does not matter for the test
   148  		SourceDigest:    Digest{Alg: "sha1", Value: "does-no-matter"},
   149  		BuildConfigPath: "internal/builders/docker/testdata/config.toml",
   150  		ForceCheckout:   false,
   151  		// BuilderImage field is not relevant, so it is omitted
   152  	}
   153  	_, err := newGitClient(config, 1)
   154  	if err == nil {
   155  		t.Fatalf("expected error creating GitClient")
   156  	}
   157  	if !strings.Contains(err.Error(), "invalid source repository format") {
   158  		t.Fatalf("expected invalid source ref error creating GitClient, got %s", err)
   159  	}
   160  }
   161  
   162  func Test_inspectArtifacts(t *testing.T) {
   163  	// Note: If the files in ../testdata/ change, this test must be updated.
   164  	pattern := "testdata/*"
   165  	out := t.TempDir()
   166  
   167  	wd, err := os.Getwd()
   168  	if err != nil {
   169  		t.Fatal(err)
   170  	}
   171  	// Execute this function from docker/ instead of pkg/ so that the testdata
   172  	// folder is in the current dir.
   173  	t.Cleanup(func() {
   174  		if err := os.Chdir(wd); err != nil {
   175  			t.Fatal(err)
   176  		}
   177  	})
   178  	if err := os.Chdir(filepath.Dir(wd)); err != nil {
   179  		t.Fatal(err)
   180  	}
   181  
   182  	got, err := inspectAndWriteArtifacts(pattern, out, filepath.Dir(wd))
   183  	if err != nil {
   184  		t.Fatalf("failed to inspect artifacts: %v", err)
   185  	}
   186  
   187  	s1 := intoto.Subject{
   188  		Name:   "build-definition.json",
   189  		Digest: map[string]string{"sha256": "ab5582bfb6128c534583e1fea92421158c9de5e72e86c78cf550a8adcbf12db5"},
   190  	}
   191  	s2 := intoto.Subject{
   192  		Name:   "config.toml",
   193  		Digest: map[string]string{"sha256": "975a0582b8c9607f3f20a6b8cfef01b25823e68c5c3658e6e1ccaaced2a3255d"},
   194  	}
   195  
   196  	s3 := intoto.Subject{
   197  		Name:   "slsa1-provenance.json",
   198  		Digest: map[string]string{"sha256": "8b43bccfe6704594dcfbd8824097c16f61b79b32ec5439f4704cdf0b4529958b"},
   199  	}
   200  
   201  	s4 := intoto.Subject{
   202  		Name:   "wildcard-config.toml",
   203  		Digest: map[string]string{"sha256": "d9b8670f1b9616db95b0dc84cbc68062c691ef31bb9240d82753de0739c59194"},
   204  	}
   205  
   206  	want := []intoto.Subject{s1, s2, s3, s4}
   207  
   208  	if diff := cmp.Diff(got, want); diff != "" {
   209  		t.Errorf(diff)
   210  	}
   211  }
   212  
   213  // When running in the checkout of the repository root, root == "".
   214  func Test_inspectArtifactsNoRoot(t *testing.T) {
   215  	// Note: If the files in ../testdata/ change, this test must be updated.
   216  	pattern := "testdata/*"
   217  	out := t.TempDir()
   218  
   219  	wd, err := os.Getwd()
   220  	if err != nil {
   221  		t.Fatal(err)
   222  	}
   223  	t.Cleanup(func() {
   224  		if err := os.Chdir(wd); err != nil {
   225  			t.Fatal(err)
   226  		}
   227  	})
   228  	if err := os.Chdir(".."); err != nil {
   229  		t.Fatal(err)
   230  	}
   231  
   232  	got, err := inspectAndWriteArtifacts(pattern, out, "")
   233  	if err != nil {
   234  		t.Fatalf("failed to inspect artifacts: %v", err)
   235  	}
   236  
   237  	s1 := intoto.Subject{
   238  		Name:   "build-definition.json",
   239  		Digest: map[string]string{"sha256": "ab5582bfb6128c534583e1fea92421158c9de5e72e86c78cf550a8adcbf12db5"},
   240  	}
   241  	s2 := intoto.Subject{
   242  		Name:   "config.toml",
   243  		Digest: map[string]string{"sha256": "975a0582b8c9607f3f20a6b8cfef01b25823e68c5c3658e6e1ccaaced2a3255d"},
   244  	}
   245  
   246  	s3 := intoto.Subject{
   247  		Name:   "slsa1-provenance.json",
   248  		Digest: map[string]string{"sha256": "8b43bccfe6704594dcfbd8824097c16f61b79b32ec5439f4704cdf0b4529958b"},
   249  	}
   250  
   251  	s4 := intoto.Subject{
   252  		Name:   "wildcard-config.toml",
   253  		Digest: map[string]string{"sha256": "d9b8670f1b9616db95b0dc84cbc68062c691ef31bb9240d82753de0739c59194"},
   254  	}
   255  
   256  	want := []intoto.Subject{s1, s2, s3, s4}
   257  
   258  	if diff := cmp.Diff(got, want); diff != "" {
   259  		t.Errorf(diff)
   260  	}
   261  }
   262  
   263  type testFetcher struct{}
   264  
   265  func (testFetcher) Fetch() (*RepoCheckoutInfo, error) {
   266  	return &RepoCheckoutInfo{}, nil
   267  }
   268  
   269  func Test_Builder_SetUpBuildState(t *testing.T) {
   270  	wd, err := os.Getwd()
   271  	if err != nil {
   272  		t.Fatal(err)
   273  	}
   274  	// Execute this function from docker/ instead of pkg/ so that the testdata config
   275  	// is in the current dir.
   276  	t.Cleanup(func() {
   277  		if err := os.Chdir(wd); err != nil {
   278  			t.Fatal(err)
   279  		}
   280  	})
   281  	if err := os.Chdir(filepath.Dir(wd)); err != nil {
   282  		t.Fatal(err)
   283  	}
   284  
   285  	config := DockerBuildConfig{
   286  		SourceRepo:   "git+https://github.com/project-oak/transparent-release",
   287  		SourceDigest: Digest{Alg: "sha1", Value: "cf5804b5c6f1a4b2a0b03401a487dfdfbe3a5f00"},
   288  		BuilderImage: DockerImage{
   289  			Name:   "bash",
   290  			Digest: Digest{Alg: "sha256", Value: "9e2ba52487d945504d250de186cb4fe2e3ba023ed2921dd6ac8b97ed43e76af9"},
   291  		},
   292  		BuildConfigPath: "testdata/config.toml",
   293  	}
   294  
   295  	f := testFetcher{}
   296  	b := Builder{
   297  		repoFetcher: f,
   298  		config:      config,
   299  	}
   300  
   301  	db, err := b.SetUpBuildState()
   302  	if err != nil {
   303  		t.Fatalf("couldn't set up build state: %v", err)
   304  	}
   305  	if db == nil {
   306  		t.Error("db is null")
   307  	}
   308  }
   309  
   310  func Test_ParseProvenance(t *testing.T) {
   311  	provenance := loadProvenance(t)
   312  	got := &provenance.Predicate.BuildDefinition
   313  
   314  	want, err := loadBuildDefinitionFromFile("../testdata/build-definition.json")
   315  	if err != nil {
   316  		t.Fatalf("%v", err)
   317  	}
   318  
   319  	if diff := cmp.Diff(got, want); diff != "" {
   320  		t.Errorf(diff)
   321  	}
   322  }
   323  
   324  func Test_ProvenanceStatementSLSA1_ToDockerBuildConfig(t *testing.T) {
   325  	provenance := loadProvenance(t)
   326  	got, err := provenance.ToDockerBuildConfig(true)
   327  	if err != nil {
   328  		t.Fatalf("%v", err)
   329  	}
   330  
   331  	want := &DockerBuildConfig{
   332  		SourceRepo: "git+https://github.com/yogeshkumararora/slsa-github-generator@refs/heads/main",
   333  		SourceDigest: Digest{
   334  			Alg:   "sha1",
   335  			Value: "cf5804b5c6f1a4b2a0b03401a487dfdfbe3a5f00",
   336  		},
   337  		BuilderImage: DockerImage{
   338  			Name: "bash",
   339  			Digest: Digest{
   340  				Alg:   "sha256",
   341  				Value: "9e2ba52487d945504d250de186cb4fe2e3ba023ed2921dd6ac8b97ed43e76af9",
   342  			},
   343  		},
   344  		BuildConfigPath: "internal/builders/docker/testdata/config.toml",
   345  		ForceCheckout:   true,
   346  	}
   347  	if diff := cmp.Diff(got, want); diff != "" {
   348  		t.Errorf(diff)
   349  	}
   350  }
   351  
   352  func loadProvenance(t *testing.T) ProvenanceStatementSLSA1 {
   353  	bytes, err := os.ReadFile("../testdata/slsa1-provenance.json")
   354  	if err != nil {
   355  		t.Fatalf("Reading the provenance file: %v", err)
   356  	}
   357  
   358  	provenance, err := ParseProvenance(bytes)
   359  	if err != nil {
   360  		t.Fatalf("Parsing the provenance file: %v", err)
   361  	}
   362  	return *provenance
   363  }