@@ -44,6 +44,7 @@ type Source struct {
44
44
token string
45
45
url string
46
46
repos []string
47
+ groupIds []string
47
48
ignoreRepos []string
48
49
includeRepos []string
49
50
@@ -158,6 +159,7 @@ func (s *Source) Init(ctx context.Context, name string, jobId sources.JobID, sou
158
159
}
159
160
160
161
s .repos = conn .GetRepositories ()
162
+ s .groupIds = conn .GetGroupIds ()
161
163
s .ignoreRepos = conn .GetIgnoreRepos ()
162
164
s .includeRepos = conn .GetIncludeRepos ()
163
165
s .enumerateSharedProjects = ! conn .ExcludeProjectsSharedIntoGroups
@@ -266,14 +268,9 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, tar
266
268
return ctx .Err ()
267
269
},
268
270
}
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
- }
271
+
272
+ if err := s .listProjects (ctx , apiClient , ignoreRepo , reporter ); err != nil {
273
+ return err
277
274
}
278
275
279
276
} else {
@@ -287,6 +284,21 @@ func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, tar
287
284
return s .scanRepos (ctx , chunksChan )
288
285
}
289
286
287
+ func (s * Source ) listProjects (ctx context.Context ,
288
+ apiClient * gitlab.Client ,
289
+ ignoreProject func (string ) bool ,
290
+ visitor sources.UnitReporter ) error {
291
+ if len (s .groupIds ) > 0 {
292
+ return s .getAllProjectReposInGroups (ctx , apiClient , ignoreProject , visitor )
293
+ }
294
+
295
+ if feature .UseSimplifiedGitlabEnumeration .Load () {
296
+ return s .getAllProjectReposV2 (ctx , apiClient , ignoreProject , visitor )
297
+ }
298
+
299
+ return s .getAllProjectRepos (ctx , apiClient , ignoreProject , visitor )
300
+ }
301
+
290
302
func (s * Source ) scanTargets (ctx context.Context , client * gitlab.Client , targets []sources.ChunkingTarget , chunksChan chan * sources.Chunk ) error {
291
303
ctx = context .WithValues (ctx , "scan_type" , "targeted" )
292
304
for _ , tgt := range targets {
@@ -401,16 +413,9 @@ func (s *Source) Validate(ctx context.Context) []error {
401
413
},
402
414
}
403
415
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
- }
416
+ if err := s .listProjects (ctx , apiClient , ignoreProject , visitor ); err != nil {
417
+ errs = append (errs , err )
418
+ return errs
414
419
}
415
420
416
421
if len (repos ) == 0 {
@@ -478,7 +483,6 @@ func (s *Source) getAllProjectRepos(
478
483
reporter sources.UnitReporter ,
479
484
) error {
480
485
gitlabReposEnumerated .WithLabelValues (s .name ).Set (0 )
481
-
482
486
// Projects without repo will get user projects, groups projects, and subgroup projects.
483
487
user , _ , err := apiClient .Users .CurrentUser ()
484
488
if err != nil {
@@ -728,6 +732,118 @@ func (s *Source) getAllProjectReposV2(
728
732
return nil
729
733
}
730
734
735
+ // getAllProjectReposInGroups fetches all projects in a GitLab group and its subgroups.
736
+ // It uses the group projects API with include_subgroups=true parameter.
737
+ func (s * Source ) getAllProjectReposInGroups (
738
+ ctx context.Context ,
739
+ apiClient * gitlab.Client ,
740
+ ignoreRepo func (string ) bool ,
741
+ reporter sources.UnitReporter ,
742
+ ) error {
743
+ gitlabReposEnumerated .WithLabelValues (s .name ).Set (0 )
744
+ gitlabGroupsEnumerated .WithLabelValues (s .name ).Set (float64 (len (s .groupIds )))
745
+
746
+ processedProjects := make (map [string ]bool )
747
+
748
+ var projectsWithNamespace []string
749
+ const (
750
+ orderBy = "id"
751
+ paginationLimit = 100
752
+ )
753
+
754
+ listOpts := gitlab.ListOptions {PerPage : paginationLimit }
755
+ projectOpts := & gitlab.ListGroupProjectsOptions {
756
+ ListOptions : listOpts ,
757
+ OrderBy : gitlab .Ptr (orderBy ),
758
+ IncludeSubGroups : gitlab .Ptr (true ),
759
+ WithShared : gitlab .Ptr (true ),
760
+ }
761
+
762
+ // For non gitlab.com instances, you might want to adjust access levels
763
+ if s .url != gitlabBaseURL {
764
+ projectOpts .MinAccessLevel = gitlab .Ptr (gitlab .GuestPermissions )
765
+ }
766
+
767
+ ctx .Logger ().Info ("starting group projects enumeration" ,
768
+ "group_ids" , s .groupIds ,
769
+ "include_subgroups" , true ,
770
+ "list_options" , listOpts )
771
+
772
+ for _ , groupID := range s .groupIds {
773
+ groupCtx := context .WithValues (ctx , "group_id" , groupID )
774
+
775
+ projectOpts .Page = 0
776
+ groupCtx .Logger ().V (2 ).Info ("processing group" , "group_id" , groupID )
777
+
778
+ for {
779
+ projects , res , err := apiClient .Groups .ListGroupProjects (groupID , projectOpts )
780
+ if err != nil {
781
+ err = fmt .Errorf ("received error on listing projects for group %s: %w" , groupID , err )
782
+ if err := reporter .UnitErr (ctx , err ); err != nil {
783
+ return err
784
+ }
785
+ break
786
+ }
787
+
788
+ groupCtx .Logger ().V (3 ).Info ("listed group projects" , "count" , len (projects ))
789
+
790
+ for _ , proj := range projects {
791
+ projCtx := context .WithValues (ctx ,
792
+ "project_id" , proj .ID ,
793
+ "project_name" , proj .NameWithNamespace ,
794
+ "group_id" , groupID )
795
+
796
+ if processedProjects [proj .HTTPURLToRepo ] {
797
+ projCtx .Logger ().V (3 ).Info ("skipping project" , "reason" , "already processed" )
798
+ continue
799
+ }
800
+ processedProjects [proj .HTTPURLToRepo ] = true
801
+
802
+ // skip projects configured to be ignored.
803
+ if ignoreRepo (proj .PathWithNamespace ) {
804
+ projCtx .Logger ().V (3 ).Info ("skipping project" , "reason" , "ignored in config" )
805
+ continue
806
+ }
807
+
808
+ // report an error if we could not convert the project into a URL.
809
+ if _ , err := url .Parse (proj .HTTPURLToRepo ); err != nil {
810
+ projCtx .Logger ().V (3 ).Info ("skipping project" ,
811
+ "reason" , "URL parse failure" ,
812
+ "url" , proj .HTTPURLToRepo ,
813
+ "parse_error" , err )
814
+
815
+ err = fmt .Errorf ("could not parse url %q given by project: %w" , proj .HTTPURLToRepo , err )
816
+ if err := reporter .UnitErr (ctx , err ); err != nil {
817
+ return err
818
+ }
819
+ continue
820
+ }
821
+
822
+ // report the unit.
823
+ projCtx .Logger ().V (3 ).Info ("accepting project" )
824
+
825
+ unit := git.SourceUnit {Kind : git .UnitRepo , ID : proj .HTTPURLToRepo }
826
+ gitlabReposEnumerated .WithLabelValues (s .name ).Inc ()
827
+ projectsWithNamespace = append (projectsWithNamespace , proj .NameWithNamespace )
828
+
829
+ if err := reporter .UnitOk (ctx , unit ); err != nil {
830
+ return err
831
+ }
832
+ }
833
+
834
+ // handle pagination.
835
+ projectOpts .Page = res .NextPage
836
+ if res .NextPage == 0 {
837
+ break
838
+ }
839
+ }
840
+ }
841
+
842
+ ctx .Logger ().Info ("Enumerated GitLab group projects" , "count" , len (projectsWithNamespace ))
843
+
844
+ return nil
845
+ }
846
+
731
847
func (s * Source ) scanRepos (ctx context.Context , chunksChan chan * sources.Chunk ) error {
732
848
// If there is resume information available, limit this scan to only the repos that still need scanning.
733
849
reposToScan , progressIndexOffset := sources .FilterReposToResume (s .repos , s .GetProgress ().EncodedResumeInfo )
@@ -937,11 +1053,11 @@ func (s *Source) Enumerate(ctx context.Context, reporter sources.UnitReporter) e
937
1053
_ = reporter .UnitErr (ctx , fmt .Errorf ("could not compile include/exclude repo glob: %w" , err ))
938
1054
})
939
1055
940
- if feature .UseSimplifiedGitlabEnumeration .Load () {
941
- return s .getAllProjectReposV2 (ctx , apiClient , ignoreRepo , reporter )
942
- } else {
943
- return s .getAllProjectRepos (ctx , apiClient , ignoreRepo , reporter )
1056
+ if err := s .listProjects (ctx , apiClient , ignoreRepo , reporter ); err != nil {
1057
+ return err
944
1058
}
1059
+
1060
+ return nil
945
1061
}
946
1062
947
1063
// ChunkUnit downloads and reports chunks for the given GitLab repository unit.
0 commit comments