From 6fa3849c10dc4e0942ebd6b6629c49671bf0e008 Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Mon, 1 Sep 2025 13:42:41 +0400 Subject: [PATCH] test: add HTTP basic authentication to git test server (#9407) --- internal/gittest/server.go | 57 ++++-- pkg/fanal/artifact/repo/git_test.go | 178 +++++++++++++++++- .../resolvers/cache_integration_test.go | 4 +- pkg/plugin/manager_unix_test.go | 4 +- 4 files changed, 219 insertions(+), 24 deletions(-) diff --git a/internal/gittest/server.go b/internal/gittest/server.go index 0a0012b297..2d02c4a220 100644 --- a/internal/gittest/server.go +++ b/internal/gittest/server.go @@ -15,6 +15,7 @@ import ( "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/sosedoff/gitkit" "github.com/stretchr/testify/require" @@ -27,7 +28,31 @@ var signature = &object.Signature{ When: time.Now(), } -func NewServer(t *testing.T, repo, dir string) *httptest.Server { +// Options contains configuration options for git server authentication +type Options struct { + Username string + Password string +} + +// setupGitServer creates and starts a git server with the given bare repository directory +func setupGitServer(t *testing.T, bareDir string, opts Options) *httptest.Server { + hasAuth := opts.Username != "" && opts.Password != "" + service := gitkit.New(gitkit.Config{ + Dir: bareDir, + Auth: hasAuth, + }) + if hasAuth { + service.AuthFunc = func(cred gitkit.Credential, _ *gitkit.Request) (bool, error) { + return cred.Username == opts.Username && cred.Password == opts.Password, nil + } + } + err := service.Setup() + require.NoError(t, err) + + return httptest.NewServer(service) +} + +func NewServer(t *testing.T, repo, dir string, opts Options) *httptest.Server { wtDir := t.TempDir() // git init @@ -53,16 +78,10 @@ func NewServer(t *testing.T, repo, dir string) *httptest.Server { _, err = git.PlainClone(gitDir, true, &git.CloneOptions{URL: wtDir}) require.NoError(t, err) - // Set up a git server - service := gitkit.New(gitkit.Config{Dir: bareDir}) - err = service.Setup() - require.NoError(t, err) - - return httptest.NewServer(service) + return setupGitServer(t, bareDir, opts) } -// NewServerWithRepository creates a git server with an existing repository -func NewServerWithRepository(t *testing.T, repo, dir string) *httptest.Server { +func NewServerWithRepository(t *testing.T, repo, dir string, opts Options) *httptest.Server { // Create a bare repository bareDir := t.TempDir() gitDir := filepath.Join(bareDir, repo+".git") @@ -85,17 +104,12 @@ func NewServerWithRepository(t *testing.T, repo, dir string) *httptest.Server { require.NoError(t, err) } - // Set up a git server - service := gitkit.New(gitkit.Config{Dir: bareDir}) - err = service.Setup() - require.NoError(t, err) - - return httptest.NewServer(service) + return setupGitServer(t, bareDir, opts) } // NewTestServer creates a git server with the local copy of "github.com/aquasecurity/trivy-test-repo". // If the test repository doesn't exist, it suggests running 'mage test:unit'. -func NewTestServer(t *testing.T) *httptest.Server { +func NewTestServer(t *testing.T, opts Options) *httptest.Server { _, filePath, _, _ := runtime.Caller(0) dir := filepath.Join(filepath.Dir(filePath), "testdata", "test-repo") @@ -103,14 +117,21 @@ func NewTestServer(t *testing.T) *httptest.Server { require.Fail(t, "test-repo not found. Please run 'mage test:unit' to set up the test fixtures") } - return NewServerWithRepository(t, "test-repo", dir) + return NewServerWithRepository(t, "test-repo", dir, opts) } -func Clone(t *testing.T, ts *httptest.Server, repo, worktree string) *git.Repository { +func Clone(t *testing.T, ts *httptest.Server, repo, worktree string, opts Options) *git.Repository { cloneOptions := git.CloneOptions{ URL: ts.URL + "/" + repo + ".git", } + if opts.Username != "" && opts.Password != "" { + cloneOptions.Auth = &http.BasicAuth{ + Username: opts.Username, + Password: opts.Password, + } + } + r, err := git.PlainClone(worktree, false, &cloneOptions) require.NoError(t, err) diff --git a/pkg/fanal/artifact/repo/git_test.go b/pkg/fanal/artifact/repo/git_test.go index 44cdedc693..d17f705d97 100644 --- a/pkg/fanal/artifact/repo/git_test.go +++ b/pkg/fanal/artifact/repo/git_test.go @@ -3,10 +3,12 @@ package repo import ( + "net/url" "os" "path/filepath" "testing" + "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,7 +24,7 @@ import ( ) func TestNewArtifact(t *testing.T) { - ts := gittest.NewTestServer(t) + ts := gittest.NewTestServer(t, gittest.Options{}) defer ts.Close() type args struct { @@ -164,7 +166,7 @@ func TestNewArtifact(t *testing.T) { } func TestArtifact_Inspect(t *testing.T) { - ts := gittest.NewTestServer(t) + ts := gittest.NewTestServer(t, gittest.Options{}) defer ts.Close() tests := []struct { @@ -330,6 +332,178 @@ func TestArtifact_Inspect(t *testing.T) { } } +// setupAuthTestServer creates a test server with authentication and returns parsed URL with /test-repo.git path +func setupAuthTestServer(t *testing.T, username, password string) *url.URL { + t.Helper() + ts := gittest.NewTestServer(t, gittest.Options{ + Username: username, + Password: password, + }) + t.Cleanup(ts.Close) + + tsURL, err := url.Parse(ts.URL) + require.NoError(t, err) + tsURL.Path = "/test-repo.git" + + return tsURL +} + +// testInspectArtifact is a helper function to inspect an artifact and assert the results +func testInspectArtifact(t *testing.T, target, wantRepoURL, wantErr string) { + t.Helper() + art, cleanup, err := NewArtifact(target, cache.NewMemoryCache(), walker.NewFS(), artifact.Option{}) + t.Cleanup(cleanup) + + if wantErr != "" { + require.ErrorContains(t, err, wantErr) + return + } + require.NoError(t, err) + + // Verify Inspect works + ref, err := art.Inspect(t.Context()) + require.NoError(t, err) + + // Verify the RepoURL + assert.Equal(t, wantRepoURL, ref.RepoMetadata.RepoURL) + + // Verify we have blob IDs (indicating successful scan) + assert.NotEmpty(t, ref.BlobIDs) +} + +func TestArtifact_InspectWithAuth(t *testing.T) { + const ( + testUsername = "testuser" + testPassword = "testpass" + ) + + // Test with environment variable authentication (GITHUB_TOKEN, GITLAB_TOKEN) + t.Run("environment variable authentication", func(t *testing.T) { + const testGitUsername = "fanal-aquasecurity-scan" // This is the username used by Trivy + + // Setup test server with authentication + tsURL := setupAuthTestServer(t, testGitUsername, testPassword) + + tests := []struct { + name string + target string + envVars map[string]string + wantErr string + wantRepoURL string + }{ + { + name: "success with GITHUB_TOKEN", + target: tsURL.String(), + envVars: map[string]string{ + "GITHUB_TOKEN": testPassword, + }, + wantRepoURL: tsURL.String(), + }, + { + name: "success with GITLAB_TOKEN", + target: tsURL.String(), + envVars: map[string]string{ + "GITLAB_TOKEN": testPassword, + }, + wantRepoURL: tsURL.String(), + }, + { + name: "failure without token", + target: tsURL.String(), + wantErr: "authentication required", + }, + { + name: "failure with wrong token", + target: tsURL.String(), + envVars: map[string]string{ + "GITHUB_TOKEN": "wrongpassword", + }, + wantErr: "authentication required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set test environment variables + for key, value := range tt.envVars { + t.Setenv(key, value) + } + + // Test using helper function + testInspectArtifact(t, tt.target, tt.wantRepoURL, tt.wantErr) + }) + } + }) + + // Test with URL-embedded authentication + t.Run("URL embedded authentication", func(t *testing.T) { + // Setup test server with authentication + tsURL := setupAuthTestServer(t, testUsername, testPassword) + + // Helper function to generate target URL with credentials + makeTarget := func(username, password string) string { + u := *tsURL // Copy the URL + if username != "" && password != "" { + u.User = url.UserPassword(username, password) + } + return u.String() + } + + tests := []struct { + name string + target string + wantRepoURL string + wantErr string + }{ + { + name: "success with embedded credentials", + target: makeTarget(testUsername, testPassword), + wantRepoURL: makeTarget(testUsername, testPassword), // TODO: username/password should be stripped + }, + { + name: "failure with wrong password", + target: makeTarget(testUsername, "wrongpass"), + wantErr: "authentication required", + }, + { + name: "failure without credentials", + target: makeTarget("", ""), + wantErr: "authentication required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test using helper function + testInspectArtifact(t, tt.target, tt.wantRepoURL, tt.wantErr) + }) + } + }) + + // Test cloning with embedded credentials and then scanning the local directory + t.Run("clone with credentials then scan local", func(t *testing.T) { + // Setup test server with authentication + tsURL := setupAuthTestServer(t, testUsername, testPassword) + + // Add credentials to URL + tsURL.User = url.UserPassword(testUsername, testPassword) + targetWithCreds := tsURL.String() + + // Clone the repository with URL-embedded credentials + cloneDir := filepath.Join(t.TempDir(), "cloned-repo") + + // Use go-git directly to clone with URL-embedded credentials + _, err := git.PlainClone(cloneDir, false, &git.CloneOptions{ + URL: targetWithCreds, + }) + require.NoError(t, err) + + // Scan and verify the local cloned directory + // TODO: The credentials in the URL should be stripped in the RepoURL + testInspectArtifact(t, cloneDir, targetWithCreds, "") + }) +} + func Test_newURL(t *testing.T) { type args struct { rawurl string diff --git a/pkg/iac/scanners/terraform/parser/resolvers/cache_integration_test.go b/pkg/iac/scanners/terraform/parser/resolvers/cache_integration_test.go index b6c4badfcd..19db038c9a 100644 --- a/pkg/iac/scanners/terraform/parser/resolvers/cache_integration_test.go +++ b/pkg/iac/scanners/terraform/parser/resolvers/cache_integration_test.go @@ -51,7 +51,7 @@ func buildGitSource(repoURL string) string { return "git::" + repoURL } func TestResolveModuleFromCache(t *testing.T) { repo := "terraform-aws-s3-bucket" - gs := gittest.NewServer(t, repo, "testdata/terraform-aws-s3-bucket") + gs := gittest.NewServer(t, repo, "testdata/terraform-aws-s3-bucket", gittest.Options{}) defer gs.Close() repoURL := gs.URL + "/" + repo + ".git" @@ -141,7 +141,7 @@ func TestResolveModuleFromCache(t *testing.T) { func TestResolveModuleFromCacheWithDifferentSubdir(t *testing.T) { repo := "terraform-aws-s3-bucket" - gs := gittest.NewServer(t, repo, "testdata/terraform-aws-s3-bucket") + gs := gittest.NewServer(t, repo, "testdata/terraform-aws-s3-bucket", gittest.Options{}) defer gs.Close() repoURL := gs.URL + "/" + repo + ".git" diff --git a/pkg/plugin/manager_unix_test.go b/pkg/plugin/manager_unix_test.go index 2299407249..98a8baa01a 100644 --- a/pkg/plugin/manager_unix_test.go +++ b/pkg/plugin/manager_unix_test.go @@ -27,10 +27,10 @@ import ( ) func setupGitRepository(t *testing.T, repo, dir string) *httptest.Server { - gs := gittest.NewServer(t, repo, dir) + gs := gittest.NewServer(t, repo, dir, gittest.Options{}) worktree := t.TempDir() - r := gittest.Clone(t, gs, repo, worktree) + r := gittest.Clone(t, gs, repo, worktree, gittest.Options{}) // git tag gittest.SetTag(t, r, "v0.2.0")