diff --git a/go/cmd/worker/main.go b/go/cmd/worker/main.go index 1edf4553542..2b9bbaf7ebb 100644 --- a/go/cmd/worker/main.go +++ b/go/cmd/worker/main.go @@ -83,7 +83,8 @@ func run() error { defer psClient.Close() stores := worker.Stores{ - SourceRepo: db.NewSourceRepositoryStore(dsClient), + SourceRepo: db.NewSourceRepositoryStore(dsClient), + RepoAllowList: db.NewRepoAllowListStore(dsClient), Vulnerability: db.NewVulnerabilityStore(db.VulnStoreConfig{ Client: dsClient, GCS: clients.NewGCSClient(gcsClient, vulnBucket), diff --git a/go/internal/database/datastore/models.go b/go/internal/database/datastore/models.go index 42b37c9e5a0..1cba1dfaafe 100644 --- a/go/internal/database/datastore/models.go +++ b/go/internal/database/datastore/models.go @@ -109,6 +109,17 @@ type AliasDenyListEntry struct { VulnID string `datastore:"bug_id"` } +// RepoAllowList holds repository URL or regex pattern and repo-based git enumeration flags. +type RepoAllowList struct { + Type string `datastore:"type"` // `url` or `regex` + Value string `datastore:"value"` // normalized URL or regex pattern + // The following corresponds to git analysis flags for gitter's affected-commits endpoint. + ConsiderAllBranches bool `datastore:"consider_all_branches"` + CherrypicksIntroduced bool `datastore:"cherrypicks_introduced"` + CherrypicksFixed bool `datastore:"cherrypicks_fixed"` + CherrypicksLimit bool `datastore:"cherrypicks_limit"` +} + type Severity struct { Type string `datastore:"type"` Score string `datastore:"score"` diff --git a/go/internal/database/datastore/repo_allowlist.go b/go/internal/database/datastore/repo_allowlist.go new file mode 100644 index 00000000000..9f278e98a6f --- /dev/null +++ b/go/internal/database/datastore/repo_allowlist.go @@ -0,0 +1,167 @@ +package datastore + +import ( + "context" + "fmt" + "log/slog" + "regexp" + "sync" + "time" + + "cloud.google.com/go/datastore" + "github.com/google/osv.dev/go/internal/models" + "github.com/google/osv.dev/go/logger" +) + +const defaultCacheTTL = 5 * time.Minute + +type regexCacheEntry struct { + pattern *regexp.Regexp + flags models.RepoAllowListFlags +} + +type allowListCache struct { + mu sync.RWMutex + urlCache map[string]models.RepoAllowListFlags + regexCache map[string]regexCacheEntry + lastFetched time.Time +} + +var cache = &allowListCache{ + urlCache: make(map[string]models.RepoAllowListFlags), + regexCache: make(map[string]regexCacheEntry), +} + +// For testing purposes +func resetCache() { + cache.mu.Lock() + defer cache.mu.Unlock() + cache.urlCache = make(map[string]models.RepoAllowListFlags) + cache.regexCache = make(map[string]regexCacheEntry) + cache.lastFetched = time.Time{} +} + +// RepoAllowListStore handles Datastore persistence and caching for the repository allowlist. +type RepoAllowListStore struct { + client *datastore.Client +} + +var _ models.RepoAllowListStore = (*RepoAllowListStore)(nil) + +// NewRepoAllowListStore returns a new RepoAllowListStore instance. +func NewRepoAllowListStore(client *datastore.Client) *RepoAllowListStore { + return &RepoAllowListStore{client: client} +} + +// GetFlags returns the combined feature flags for the given repoURL. +func (s *RepoAllowListStore) GetFlags(ctx context.Context, repoURL string) (models.RepoAllowListFlags, error) { + if repoURL == "" || s.client == nil { + return models.RepoAllowListFlags{}, nil + } + + normalized := normalizeRepo(repoURL) + if normalized == "" { + return models.RepoAllowListFlags{}, nil + } + + if err := s.loadCache(ctx); err != nil { + return models.RepoAllowListFlags{}, err + } + + cache.mu.RLock() + defer cache.mu.RUnlock() + + // 1. Exact URL match takes precedence over regex patterns + if flags, ok := cache.urlCache[normalized]; ok { + return flags, nil + } + + var res models.RepoAllowListFlags + + // 2. Regex pattern match (used when no exact URL match exists) + for _, re := range cache.regexCache { + // Try to match both the actual and normalized repoURL + if re.pattern.MatchString(repoURL) || re.pattern.MatchString(normalized) { + res.ConsiderAllBranches = res.ConsiderAllBranches || re.flags.ConsiderAllBranches + res.CherrypicksIntroduced = res.CherrypicksIntroduced || re.flags.CherrypicksIntroduced + res.CherrypicksFixed = res.CherrypicksFixed || re.flags.CherrypicksFixed + res.CherrypicksLimit = res.CherrypicksLimit || re.flags.CherrypicksLimit + } + } + + return res, nil +} + +// loadCache retrieves all allowlist entries from Datastore, caches the url and regexes. +func (s *RepoAllowListStore) loadCache(ctx context.Context) error { + // Fast path: check cache validity under read lock. + cache.mu.RLock() + if cache.urlCache != nil && cache.regexCache != nil && time.Since(cache.lastFetched) < defaultCacheTTL { + cache.mu.RUnlock() + return nil + } + cache.mu.RUnlock() + + cache.mu.Lock() + defer cache.mu.Unlock() + + // Re-check if cache is valid in case another goroutine refreshed it while we were waiting for write lock + if cache.urlCache != nil && cache.regexCache != nil && time.Since(cache.lastFetched) < defaultCacheTTL { + return nil + } + + var entries []RepoAllowList + query := datastore.NewQuery("RepoAllowList") + if _, err := s.client.GetAll(ctx, query, &entries); err != nil { + return fmt.Errorf("failed fetching RepoAllowList entities: %w", err) + } + + newURLCache := make(map[string]models.RepoAllowListFlags, len(entries)) + + for _, entry := range entries { + flags := models.RepoAllowListFlags{ + ConsiderAllBranches: entry.ConsiderAllBranches, + CherrypicksIntroduced: entry.CherrypicksIntroduced, + CherrypicksFixed: entry.CherrypicksFixed, + CherrypicksLimit: entry.CherrypicksLimit, + } + + switch entry.Type { + case "regex": + if entry.Value == "" { + continue + } + cached, ok := cache.regexCache[entry.Value] + var re *regexp.Regexp + if ok && cached.pattern != nil { + re = cached.pattern + } else { + var err error + re, err = regexp.Compile(entry.Value) + if err != nil { + logger.WarnContext(ctx, "Failed to compile RepoAllowList regex entry", slog.String("value", entry.Value), slog.Any("error", err)) + continue + } + } + cache.regexCache[entry.Value] = regexCacheEntry{ + pattern: re, + flags: flags, + } + + default: // URL exact match entries + if entry.Value == "" { + continue + } + newURLCache[entry.Value] = flags + normValue := normalizeRepo(entry.Value) + if normValue != "" { + newURLCache[normValue] = flags + } + } + } + + cache.urlCache = newURLCache + cache.lastFetched = time.Now() + + return nil +} diff --git a/go/internal/database/datastore/repo_allowlist_test.go b/go/internal/database/datastore/repo_allowlist_test.go new file mode 100644 index 00000000000..87bbb486d8a --- /dev/null +++ b/go/internal/database/datastore/repo_allowlist_test.go @@ -0,0 +1,213 @@ +package datastore + +import ( + "context" + "testing" + "time" + + "cloud.google.com/go/datastore" + "github.com/google/osv.dev/go/internal/models" + "github.com/google/osv.dev/go/testutils" +) + +func TestRepoAllowListStore_GetFlags(t *testing.T) { + resetCache() + ctx := context.Background() + dsClient := testutils.MustNewDatastoreClientForTesting(t) + store := NewRepoAllowListStore(dsClient) + + // Seed test data in Datastore with exact URLs and regexes for RepoAllowList + testEntries := []RepoAllowList{ + {Type: "url", Value: "github.com/google/osv.dev", ConsiderAllBranches: true, CherrypicksIntroduced: true, CherrypicksFixed: false, CherrypicksLimit: false}, + {Type: "url", Value: "github.com/foo/bar", ConsiderAllBranches: false, CherrypicksIntroduced: false, CherrypicksFixed: true, CherrypicksLimit: true}, + {Type: "url", Value: "github.com/all/flags", ConsiderAllBranches: true, CherrypicksIntroduced: true, CherrypicksFixed: true, CherrypicksLimit: true}, + {Type: "regex", Value: "github\\.com/org-glob/.*", ConsiderAllBranches: true, CherrypicksIntroduced: false, CherrypicksFixed: true, CherrypicksLimit: false}, + {Type: "regex", Value: "^https?://github\\.com/regex-intro/.*$", ConsiderAllBranches: false, CherrypicksIntroduced: true, CherrypicksFixed: false, CherrypicksLimit: true}, + {Type: "url", Value: "github.com/org-glob/override-repo", ConsiderAllBranches: false, CherrypicksIntroduced: true, CherrypicksFixed: false, CherrypicksLimit: false}, + {Type: "url", Value: "https://github.com/unnorm/repo.git", ConsiderAllBranches: false, CherrypicksIntroduced: false, CherrypicksFixed: false, CherrypicksLimit: true}, + } + + keys := []*datastore.Key{ + datastore.NameKey("RepoAllowList", "github.com/google/osv.dev", nil), + datastore.NameKey("RepoAllowList", "github.com/foo/bar", nil), + datastore.NameKey("RepoAllowList", "github.com/all/flags", nil), + datastore.NameKey("RepoAllowList", "github\\.com/org-glob/.*", nil), + datastore.NameKey("RepoAllowList", "^https?://github\\.com/regex-intro/.*$", nil), + datastore.NameKey("RepoAllowList", "github.com/org-glob/override-repo", nil), + datastore.NameKey("RepoAllowList", "https://github.com/unnorm/repo.git", nil), + } + + if _, err := dsClient.PutMulti(ctx, keys, testEntries); err != nil { + t.Fatalf("Failed to setup test data: %v", err) + } + + tests := []struct { + name string + repoURL string + wantFlags models.RepoAllowListFlags + }{ + { + name: "Empty repo URL", + repoURL: "", + wantFlags: models.RepoAllowListFlags{}, + }, + { + name: "Exact URL match (CAB and Intro enabled)", + repoURL: "https://github.com/google/osv.dev.git", + wantFlags: models.RepoAllowListFlags{ + ConsiderAllBranches: true, + CherrypicksIntroduced: true, + }, + }, + { + name: "Normalized lookup (Fixed and Limit enabled)", + repoURL: "https://github.com/foo/bar", + wantFlags: models.RepoAllowListFlags{ + CherrypicksFixed: true, + CherrypicksLimit: true, + }, + }, + { + name: "All flags enabled", + repoURL: "https://github.com/all/flags", + wantFlags: models.RepoAllowListFlags{ + ConsiderAllBranches: true, + CherrypicksIntroduced: true, + CherrypicksFixed: true, + CherrypicksLimit: true, + }, + }, + { + name: "Regex pattern match (CAB and Fixed enabled)", + repoURL: "https://github.com/org-glob/sub-repo.git", + wantFlags: models.RepoAllowListFlags{ + ConsiderAllBranches: true, + CherrypicksFixed: true, + }, + }, + { + name: "Exact URL match takes precedence over regex match", + repoURL: "https://github.com/org-glob/override-repo", + wantFlags: models.RepoAllowListFlags{ + CherrypicksIntroduced: true, + }, + }, + { + name: "Un-normalized Datastore URL entry matches normalized query", + repoURL: "https://github.com/unnorm/repo", + wantFlags: models.RepoAllowListFlags{ + CherrypicksLimit: true, + }, + }, + { + name: "Regex pattern match (Intro and Limit enabled)", + repoURL: "https://github.com/regex-intro/my-project", + wantFlags: models.RepoAllowListFlags{ + CherrypicksIntroduced: true, + CherrypicksLimit: true, + }, + }, + { + name: "Unlisted repo", + repoURL: "https://github.com/unlisted/repo", + wantFlags: models.RepoAllowListFlags{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFlags, err := store.GetFlags(ctx, tt.repoURL) + if err != nil { + t.Fatalf("GetFlags(%q) unexpected error: %v", tt.repoURL, err) + } + if gotFlags != tt.wantFlags { + t.Errorf("GetFlags(%q) = %+v, want %+v", tt.repoURL, gotFlags, tt.wantFlags) + } + }) + } +} + +func TestRepoAllowListStore_Caching(t *testing.T) { + resetCache() + ctx := context.Background() + dsClient := testutils.MustNewDatastoreClientForTesting(t) + store := NewRepoAllowListStore(dsClient) + + regexEntry := RepoAllowList{ + Type: "regex", + Value: "github\\.com/cached-org/.*", + ConsiderAllBranches: true, + CherrypicksIntroduced: true, + } + regexKey := datastore.NameKey("RepoAllowList", "github\\.com/cached-org/.*", nil) + + urlEntry := RepoAllowList{ + Type: "url", + Value: "github.com/cached-url/repo", + CherrypicksFixed: true, + CherrypicksLimit: true, + } + urlKey := datastore.NameKey("RepoAllowList", "github.com/cached-url/repo", nil) + + if _, err := dsClient.PutMulti(ctx, []*datastore.Key{regexKey, urlKey}, []RepoAllowList{regexEntry, urlEntry}); err != nil { + t.Fatalf("Failed setup: %v", err) + } + + // First match populates cache + flagsRegex, err := store.GetFlags(ctx, "https://github.com/cached-org/repo1") + if err != nil || !flagsRegex.ConsiderAllBranches || !flagsRegex.CherrypicksIntroduced { + t.Fatalf("Initial GetFlags failed for regex: got %+v, err %v", flagsRegex, err) + } + + flagsURL, err := store.GetFlags(ctx, "https://github.com/cached-url/repo") + if err != nil || !flagsURL.CherrypicksFixed || !flagsURL.CherrypicksLimit { + t.Fatalf("Initial GetFlags failed for url: got %+v, err %v", flagsURL, err) + } + + // Check that cache is populated + cache.mu.RLock() + if len(cache.regexCache) != 1 { + t.Errorf("expected 1 cached regex, got %d", len(cache.regexCache)) + } + if flags, ok := cache.urlCache["github.com/cached-url/repo"]; !ok || !flags.CherrypicksFixed || !flags.CherrypicksLimit { + t.Errorf("expected urlCache to contain github.com/cached-url/repo with CherrypicksFixed=true, CherrypicksLimit=true") + } + cache.mu.RUnlock() + + // Delete from Datastore to test that cache hit still succeeds before TTL expires + if err := dsClient.DeleteMulti(ctx, []*datastore.Key{regexKey, urlKey}); err != nil { + t.Fatalf("Failed deleting keys: %v", err) + } + + flagsCachedRegex, err := store.GetFlags(ctx, "https://github.com/cached-org/repo2") + if err != nil || !flagsCachedRegex.ConsiderAllBranches { + t.Errorf("Expected cache hit for regex to succeed even after DB deletion, got %+v, err %v", flagsCachedRegex, err) + } + + flagsCachedURL, err := store.GetFlags(ctx, "https://github.com/cached-url/repo") + if err != nil || !flagsCachedURL.CherrypicksFixed { + t.Errorf("Expected cache hit for url to succeed even after DB deletion, got %+v, err %v", flagsCachedURL, err) + } + + // Simulate TTL expiry by setting lastFetched into the past + cache.mu.Lock() + cache.lastFetched = time.Now().Add(-6 * time.Minute) + cache.mu.Unlock() + + // After TTL expiry, store should re-query DB and find no matches + flagsExpired, err := store.GetFlags(ctx, "https://github.com/cached-org/repo3") + if err != nil { + t.Fatalf("Unexpected error after cache expiry: %v", err) + } + if flagsExpired.ConsiderAllBranches || flagsExpired.CherrypicksIntroduced { + t.Errorf("Expected GetFlags for regex to return zero flags after TTL expiry and DB deletion, got %+v", flagsExpired) + } + + flagsExpiredURL, err := store.GetFlags(ctx, "https://github.com/cached-url/repo") + if err != nil { + t.Fatalf("Unexpected error after cache expiry for url: %v", err) + } + if flagsExpiredURL.CherrypicksFixed || flagsExpiredURL.CherrypicksLimit { + t.Errorf("Expected GetFlags for url to return zero flags after TTL expiry and DB deletion, got %+v", flagsExpiredURL) + } +} diff --git a/go/internal/models/repo_allowlist.go b/go/internal/models/repo_allowlist.go new file mode 100644 index 00000000000..01b33fc4bbc --- /dev/null +++ b/go/internal/models/repo_allowlist.go @@ -0,0 +1,18 @@ +// Package models contains the domain types for the OSV database. +package models + +import "context" + +// RepoAllowListFlags holds boolean feature flags for repository git analysis. +type RepoAllowListFlags struct { + ConsiderAllBranches bool + CherrypicksIntroduced bool + CherrypicksFixed bool + CherrypicksLimit bool +} + +// RepoAllowListStore is the repository allowlist store for repository configuration overrides. +type RepoAllowListStore interface { + // GetFlags returns the combined feature flags for the given repository URL. + GetFlags(ctx context.Context, repoURL string) (RepoAllowListFlags, error) +} diff --git a/go/internal/worker/affectedcommits.go b/go/internal/worker/affectedcommits.go index ff6c2af4fad..bd80aba184d 100644 --- a/go/internal/worker/affectedcommits.go +++ b/go/internal/worker/affectedcommits.go @@ -31,7 +31,8 @@ func (e *Engine) populateAffectedCommitsAndTags(ctx context.Context, vuln *osvsc if aRange.GetType() != osvschema.Range_GIT || repo == "" { continue } - resp, err := fetchAffectedCommits(ctx, e.GitterClient, e.GitterHost, aRange, sourceRepo.GitAnalysis, vuln.GetId()) + flags := e.getGitAnalysisFlags(ctx, sourceRepo.GitAnalysis, repo) + resp, err := fetchAffectedCommits(ctx, e.GitterClient, e.GitterHost, aRange, vuln.GetId(), flags) if err != nil { return models.AffectedCommitsResult{}, err } @@ -54,8 +55,8 @@ func (e *Engine) populateAffectedCommitsAndTags(ctx context.Context, vuln *osvsc }, nil } -func fetchAffectedCommits(ctx context.Context, client *http.Client, gitterHost string, aRange *osvschema.Range, gitAnalysis *models.GitAnalysisConfig, refID string) (*gitterpb.AffectedCommitsResponse, error) { - req, err := newAffectedCommitsRequest(aRange, gitAnalysis, refID) +func fetchAffectedCommits(ctx context.Context, client *http.Client, gitterHost string, aRange *osvschema.Range, refID string, flags models.RepoAllowListFlags) (*gitterpb.AffectedCommitsResponse, error) { + req, err := newAffectedCommitsRequest(aRange, flags, refID) if err != nil { return nil, fmt.Errorf("failed constructing gitter request: %w", err) } @@ -163,13 +164,13 @@ func applyAffectedCommitsAndTags(resp *gitterpb.AffectedCommitsResponse, affecte } } -func newAffectedCommitsRequest(affectedRange *osvschema.Range, gitAnalysis *models.GitAnalysisConfig, refID string) (*gitterpb.AffectedCommitsRequest, error) { +func newAffectedCommitsRequest(affectedRange *osvschema.Range, gitAnalysisFlags models.RepoAllowListFlags, refID string) (*gitterpb.AffectedCommitsRequest, error) { gitterReq := &gitterpb.AffectedCommitsRequest{ Url: affectedRange.GetRepo(), - ConsiderAllBranches: gitAnalysis.ConsiderAllBranches, - DetectCherrypicksIntroduced: gitAnalysis.DetectCherrypicks, - DetectCherrypicksFixed: gitAnalysis.DetectCherrypicks, - DetectCherrypicksLimit: gitAnalysis.DetectCherrypicks, + ConsiderAllBranches: gitAnalysisFlags.ConsiderAllBranches, + DetectCherrypicksIntroduced: gitAnalysisFlags.CherrypicksIntroduced, + DetectCherrypicksFixed: gitAnalysisFlags.CherrypicksFixed, + DetectCherrypicksLimit: gitAnalysisFlags.CherrypicksLimit, Events: make([]*gitterpb.Event, 0, len(affectedRange.GetEvents())), RefId: refID, } @@ -197,3 +198,29 @@ func newAffectedCommitsRequest(affectedRange *osvschema.Range, gitAnalysis *mode return gitterReq, nil } + +// getGitAnalysisFlags determines the gitter flags for a given repo by combining the source repo level and allowlist configs +func (e *Engine) getGitAnalysisFlags(ctx context.Context, sourceRepoGitConfig *models.GitAnalysisConfig, repo string) models.RepoAllowListFlags { + var flags models.RepoAllowListFlags + + if sourceRepoGitConfig != nil { + flags.ConsiderAllBranches = sourceRepoGitConfig.ConsiderAllBranches + flags.CherrypicksIntroduced = sourceRepoGitConfig.DetectCherrypicks + flags.CherrypicksFixed = sourceRepoGitConfig.DetectCherrypicks + flags.CherrypicksLimit = sourceRepoGitConfig.DetectCherrypicks + } + + if e.Stores.RepoAllowList != nil { + storeFlags, err := e.Stores.RepoAllowList.GetFlags(ctx, repo) + if err != nil { + logger.ErrorContext(ctx, "Failed to check RepoAllowList", slog.String("repo", repo), slog.Any("error", err)) + } else { + flags.ConsiderAllBranches = flags.ConsiderAllBranches || storeFlags.ConsiderAllBranches + flags.CherrypicksIntroduced = flags.CherrypicksIntroduced || storeFlags.CherrypicksIntroduced + flags.CherrypicksFixed = flags.CherrypicksFixed || storeFlags.CherrypicksFixed + flags.CherrypicksLimit = flags.CherrypicksLimit || storeFlags.CherrypicksLimit + } + } + + return flags +} diff --git a/go/internal/worker/affectedcommits_test.go b/go/internal/worker/affectedcommits_test.go index 34b4bd2c6fb..170e53893d2 100644 --- a/go/internal/worker/affectedcommits_test.go +++ b/go/internal/worker/affectedcommits_test.go @@ -86,3 +86,138 @@ func TestPopulateAffectedCommitsAndTags(t *testing.T) { t.Errorf("expected 3 events, got %d", len(aRange.GetEvents())) } } + +type mockRepoAllowListStore struct { + flags map[string]models.RepoAllowListFlags +} + +func (m *mockRepoAllowListStore) GetFlags(_ context.Context, repoURL string) (models.RepoAllowListFlags, error) { + if m.flags == nil { + return models.RepoAllowListFlags{}, nil + } + + return m.flags[repoURL], nil +} + +func TestGetGitAnalysisFlags(t *testing.T) { + ctx := context.Background() + mockStore := &mockRepoAllowListStore{ + flags: map[string]models.RepoAllowListFlags{ + "https://github.com/test-org/store-enabled-repo": { + ConsiderAllBranches: true, + CherrypicksFixed: true, + }, + "https://github.com/test-org/store-all-false-repo": {}, + }, + } + + engineWithAllowlist := &Engine{ + Stores: Stores{ + RepoAllowList: mockStore, + }, + } + engineWithoutAllowlist := &Engine{} + + sourceRepoAllFalse := &models.SourceRepository{ + GitAnalysis: &models.GitAnalysisConfig{ConsiderAllBranches: false, DetectCherrypicks: false}, + } + sourceRepoAllTrue := &models.SourceRepository{ + GitAnalysis: &models.GitAnalysisConfig{ConsiderAllBranches: true, DetectCherrypicks: true}, + } + + tests := []struct { + name string + engine *Engine + repo string + sourceRepo *models.SourceRepository + wantFlags models.RepoAllowListFlags + }{ + { + name: "Source repo true, store unlisted (all false) -> returns source repo flags", + engine: engineWithAllowlist, + repo: "https://github.com/unlisted/repo", + sourceRepo: sourceRepoAllTrue, + wantFlags: models.RepoAllowListFlags{ + ConsiderAllBranches: true, + CherrypicksIntroduced: true, + CherrypicksFixed: true, + CherrypicksLimit: true, + }, + }, + { + name: "Source repo false, store unlisted (all false) -> returns all false", + engine: engineWithAllowlist, + repo: "https://github.com/unlisted/repo", + sourceRepo: sourceRepoAllFalse, + wantFlags: models.RepoAllowListFlags{}, + }, + { + name: "Source repo false, store repo explicitly all false -> returns all false", + engine: engineWithAllowlist, + repo: "https://github.com/test-org/store-all-false-repo", + sourceRepo: sourceRepoAllFalse, + wantFlags: models.RepoAllowListFlags{}, + }, + { + name: "Source repo false, store repo has flags true -> returns store flags", + engine: engineWithAllowlist, + repo: "https://github.com/test-org/store-enabled-repo", + sourceRepo: sourceRepoAllFalse, + wantFlags: models.RepoAllowListFlags{ + ConsiderAllBranches: true, + CherrypicksFixed: true, + }, + }, + { + name: "Source repo true, store repo has different flags true -> returns merged flags", + engine: engineWithAllowlist, + repo: "https://github.com/test-org/store-enabled-repo", + sourceRepo: &models.SourceRepository{ + GitAnalysis: &models.GitAnalysisConfig{ConsiderAllBranches: false, DetectCherrypicks: true}, + }, + wantFlags: models.RepoAllowListFlags{ + ConsiderAllBranches: true, + CherrypicksIntroduced: true, + CherrypicksFixed: true, + CherrypicksLimit: true, + }, + }, + { + name: "Nil source repo, store repo has flags true -> returns store flags", + engine: engineWithAllowlist, + repo: "https://github.com/test-org/store-enabled-repo", + sourceRepo: nil, + wantFlags: models.RepoAllowListFlags{ + ConsiderAllBranches: true, + CherrypicksFixed: true, + }, + }, + { + name: "Nil source repo, store unlisted -> returns all false", + engine: engineWithAllowlist, + repo: "https://github.com/unlisted/repo", + sourceRepo: nil, + wantFlags: models.RepoAllowListFlags{}, + }, + { + name: "Nil allowlist store, source repo false -> returns all false", + engine: engineWithoutAllowlist, + repo: "https://github.com/test-org/store-enabled-repo", + sourceRepo: sourceRepoAllFalse, + wantFlags: models.RepoAllowListFlags{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gitAnalysis *models.GitAnalysisConfig + if tt.sourceRepo != nil { + gitAnalysis = tt.sourceRepo.GitAnalysis + } + got := tt.engine.getGitAnalysisFlags(ctx, gitAnalysis, tt.repo) + if got != tt.wantFlags { + t.Errorf("getGitAnalysisFlags(%q) = %+v, want %+v", tt.repo, got, tt.wantFlags) + } + }) + } +} diff --git a/go/internal/worker/worker.go b/go/internal/worker/worker.go index 03b15b2b155..8182deb7495 100644 --- a/go/internal/worker/worker.go +++ b/go/internal/worker/worker.go @@ -35,5 +35,6 @@ type Stores struct { Vulnerability models.VulnerabilityStore Relations models.RelationsStore ImportFindings models.ImportFindingsStore + RepoAllowList models.RepoAllowListStore PyPIPublisher clients.Publisher }