Skip to content

Commit 7907482

Browse files
Simplified Gitlab Enumeration (#4283)
* simplified gitlab enumeration * few more enhancements * configurable * added feature flag * resolved cody's comment
1 parent 20bd4d8 commit 7907482

File tree

3 files changed

+138
-14
lines changed

3 files changed

+138
-14
lines changed

main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,9 @@ func run(state overseer.State) {
450450
// OSS Default APK handling on
451451
feature.EnableAPKHandler.Store(true)
452452

453+
// OSS Default simplified gitlab enumeration
454+
feature.UseSimplifiedGitlabEnumeration.Store(true)
455+
453456
conf := &config.Config{}
454457
if *configFilename != "" {
455458
var err error

pkg/feature/feature.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package feature
33
import "sync/atomic"
44

55
var (
6-
ForceSkipBinaries atomic.Bool
7-
ForceSkipArchives atomic.Bool
8-
SkipAdditionalRefs atomic.Bool
9-
EnableAPKHandler atomic.Bool
10-
UserAgentSuffix AtomicString
6+
ForceSkipBinaries atomic.Bool
7+
ForceSkipArchives atomic.Bool
8+
SkipAdditionalRefs atomic.Bool
9+
EnableAPKHandler atomic.Bool
10+
UserAgentSuffix AtomicString
11+
UseSimplifiedGitlabEnumeration atomic.Bool
1112
)
1213

1314
type AtomicString struct {

pkg/sources/gitlab/gitlab.go

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
1212
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
1314
"github.com/trufflesecurity/trufflehog/v3/pkg/giturl"
1415
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
1516
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
@@ -265,9 +266,16 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, tar
265266
return ctx.Err()
266267
},
267268
}
268-
if err := s.getAllProjectRepos(ctx, apiClient, ignoreRepo, reporter); err != nil {
269-
return err
269+
if feature.UseSimplifiedGitlabEnumeration.Load() {
270+
if err := s.getAllProjectReposV2(ctx, apiClient, ignoreRepo, reporter); err != nil {
271+
return err
272+
}
273+
} else {
274+
if err := s.getAllProjectRepos(ctx, apiClient, ignoreRepo, reporter); err != nil {
275+
return err
276+
}
270277
}
278+
271279
} else {
272280
gitlabReposEnumerated.WithLabelValues(s.name).Set(float64(len(repos)))
273281
}
@@ -392,9 +400,17 @@ func (s *Source) Validate(ctx context.Context) []error {
392400
return nil
393401
},
394402
}
395-
if err := s.getAllProjectRepos(ctx, apiClient, ignoreProject, visitor); err != nil {
396-
errs = append(errs, err)
397-
return errs
403+
404+
if feature.UseSimplifiedGitlabEnumeration.Load() {
405+
if err := s.getAllProjectReposV2(ctx, apiClient, ignoreProject, visitor); err != nil {
406+
errs = append(errs, err)
407+
return errs
408+
}
409+
} else {
410+
if err := s.getAllProjectRepos(ctx, apiClient, ignoreProject, visitor); err != nil {
411+
errs = append(errs, err)
412+
return errs
413+
}
398414
}
399415

400416
if len(repos) == 0 {
@@ -453,9 +469,8 @@ func (s *Source) basicAuthSuccessful(apiClient *gitlab.Client) bool {
453469
return false
454470
}
455471

456-
// getAllProjectRepos enumerates all GitLab projects using the provided API
457-
// client. The reporter is used to report the valid repository found for
458-
// projects that are not ignored.
472+
// getAllProjectRepos enumerates all GitLab projects using the provided API client.
473+
// The reporter is used to report the valid repository found for projects that are not ignored.
459474
func (s *Source) getAllProjectRepos(
460475
ctx context.Context,
461476
apiClient *gitlab.Client,
@@ -616,6 +631,106 @@ func (s *Source) getAllProjectRepos(
616631
return nil
617632
}
618633

634+
// getAllProjectReposV2 uses simplified logic to enumerate through all projects using list-all-projects API.
635+
// The reporter is used to report the valid repository found for projects that are not ignored.
636+
func (s *Source) getAllProjectReposV2(
637+
ctx context.Context,
638+
apiClient *gitlab.Client,
639+
ignoreRepo func(string) bool,
640+
reporter sources.UnitReporter,
641+
) error {
642+
gitlabReposEnumerated.WithLabelValues(s.name).Set(0)
643+
644+
// record the projectsWithNamespace for logging.
645+
var projectsWithNamespace []string
646+
647+
const (
648+
orderBy = "id" // TODO: use keyset pagination (https://docs.gitlab.com/ee/api/rest/index.html#keyset-based-pagination)
649+
paginationLimit = 100 // default is 20, max is 100.
650+
)
651+
652+
listOpts := gitlab.ListOptions{PerPage: paginationLimit}
653+
projectQueryOptions := &gitlab.ListProjectsOptions{
654+
OrderBy: gitlab.Ptr(orderBy),
655+
ListOptions: listOpts,
656+
Membership: gitlab.Ptr(true),
657+
}
658+
659+
// for non gitlab.com instances, include all available projects (public + membership).
660+
if s.url != gitlabBaseURL {
661+
projectQueryOptions.Membership = gitlab.Ptr(false)
662+
}
663+
664+
ctx.Logger().Info("starting projects enumeration",
665+
"list_options", listOpts,
666+
"all_available", *projectQueryOptions.Membership)
667+
668+
// paginate through all projects until no more pages remain.
669+
for {
670+
projects, res, err := apiClient.Projects.ListProjects(projectQueryOptions)
671+
if err != nil {
672+
err = fmt.Errorf("received error on listing projects: %w", err)
673+
if err := reporter.UnitErr(ctx, err); err != nil {
674+
return err
675+
}
676+
677+
break
678+
}
679+
680+
ctx.Logger().V(3).Info("listed projects", "count", len(projects))
681+
682+
// process each project
683+
for _, proj := range projects {
684+
projCtx := context.WithValues(ctx,
685+
"project_id", proj.ID,
686+
"project_name", proj.NameWithNamespace)
687+
688+
// skip projects configured to be ignored.
689+
if ignoreRepo(proj.PathWithNamespace) {
690+
projCtx.Logger().V(3).Info("skipping project", "reason", "ignored in config")
691+
692+
continue
693+
}
694+
695+
// report an error if we could not convert the project into a URL.
696+
if _, err := url.Parse(proj.HTTPURLToRepo); err != nil {
697+
projCtx.Logger().V(3).Info("skipping project",
698+
"reason", "URL parse failure",
699+
"url", proj.HTTPURLToRepo,
700+
"parse_error", err)
701+
702+
err = fmt.Errorf("could not parse url %q given by project: %w", proj.HTTPURLToRepo, err)
703+
if err := reporter.UnitErr(ctx, err); err != nil {
704+
return err
705+
}
706+
707+
continue
708+
}
709+
710+
// report the unit.
711+
projCtx.Logger().V(3).Info("accepting project")
712+
713+
unit := git.SourceUnit{Kind: git.UnitRepo, ID: proj.HTTPURLToRepo}
714+
gitlabReposEnumerated.WithLabelValues(s.name).Inc()
715+
projectsWithNamespace = append(projectsWithNamespace, proj.NameWithNamespace)
716+
717+
if err := reporter.UnitOk(ctx, unit); err != nil {
718+
return err
719+
}
720+
}
721+
722+
// handle pagination.
723+
projectQueryOptions.Page = res.NextPage
724+
if res.NextPage == 0 {
725+
break
726+
}
727+
}
728+
729+
ctx.Logger().Info("Enumerated GitLab projects", "count", len(projectsWithNamespace))
730+
731+
return nil
732+
}
733+
619734
func (s *Source) scanRepos(ctx context.Context, chunksChan chan *sources.Chunk) error {
620735
// If there is resume information available, limit this scan to only the repos that still need scanning.
621736
reposToScan, progressIndexOffset := sources.FilterReposToResume(s.repos, s.GetProgress().EncodedResumeInfo)
@@ -824,7 +939,12 @@ func (s *Source) Enumerate(ctx context.Context, reporter sources.UnitReporter) e
824939
// TODO: Handle error returned from UnitErr.
825940
_ = reporter.UnitErr(ctx, fmt.Errorf("could not compile include/exclude repo glob: %w", err))
826941
})
827-
return s.getAllProjectRepos(ctx, apiClient, ignoreRepo, reporter)
942+
943+
if feature.UseSimplifiedGitlabEnumeration.Load() {
944+
return s.getAllProjectReposV2(ctx, apiClient, ignoreRepo, reporter)
945+
} else {
946+
return s.getAllProjectRepos(ctx, apiClient, ignoreRepo, reporter)
947+
}
828948
}
829949

830950
// ChunkUnit downloads and reports chunks for the given GitLab repository unit.

0 commit comments

Comments
 (0)