@@ -10,6 +10,7 @@ import (
10
10
11
11
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12
12
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
13
+ "github.com/trufflesecurity/trufflehog/v3/pkg/feature"
13
14
"github.com/trufflesecurity/trufflehog/v3/pkg/giturl"
14
15
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
15
16
"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
265
266
return ctx .Err ()
266
267
},
267
268
}
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
+ }
270
277
}
278
+
271
279
} else {
272
280
gitlabReposEnumerated .WithLabelValues (s .name ).Set (float64 (len (repos )))
273
281
}
@@ -392,9 +400,17 @@ func (s *Source) Validate(ctx context.Context) []error {
392
400
return nil
393
401
},
394
402
}
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
+ }
398
414
}
399
415
400
416
if len (repos ) == 0 {
@@ -453,9 +469,8 @@ func (s *Source) basicAuthSuccessful(apiClient *gitlab.Client) bool {
453
469
return false
454
470
}
455
471
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.
459
474
func (s * Source ) getAllProjectRepos (
460
475
ctx context.Context ,
461
476
apiClient * gitlab.Client ,
@@ -616,6 +631,106 @@ func (s *Source) getAllProjectRepos(
616
631
return nil
617
632
}
618
633
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
+
619
734
func (s * Source ) scanRepos (ctx context.Context , chunksChan chan * sources.Chunk ) error {
620
735
// If there is resume information available, limit this scan to only the repos that still need scanning.
621
736
reposToScan , progressIndexOffset := sources .FilterReposToResume (s .repos , s .GetProgress ().EncodedResumeInfo )
@@ -824,7 +939,12 @@ func (s *Source) Enumerate(ctx context.Context, reporter sources.UnitReporter) e
824
939
// TODO: Handle error returned from UnitErr.
825
940
_ = reporter .UnitErr (ctx , fmt .Errorf ("could not compile include/exclude repo glob: %w" , err ))
826
941
})
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
+ }
828
948
}
829
949
830
950
// ChunkUnit downloads and reports chunks for the given GitLab repository unit.
0 commit comments