Skip to content

Commit 51edb3d

Browse files
authored
gl users enum and gluna users enum (#629)
* Add GitLab users enum commands
1 parent d2d775c commit 51edb3d

11 files changed

Lines changed: 1577 additions & 18 deletions

File tree

.github/copilot-instructions.md

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -191,40 +191,56 @@ make serve-docs # Installs dependencies if needed, generates and serves docs
191191

192192
### Configuration Loading Pattern (MANDATORY)
193193

194-
**ALWAYS use `config.AutoBindFlags` for configuration loading in ALL commands:**
194+
**Use `config.NewCommandSetup` as the canonical command configuration pattern.**
195195

196196
```go
197197
func CommandRun(cmd *cobra.Command, args []string) {
198-
// 1. Bind flags to config keys
199-
if err := config.AutoBindFlags(cmd, map[string]string{
200-
"platform-flag": "platform.url",
201-
"token": "platform.token",
202-
"threads": "common.threads",
203-
}); err != nil {
204-
log.Fatal().Err(err).Msg("Failed to bind command flags to configuration keys")
205-
}
206-
207-
// 2. Validate required keys
208-
if err := config.RequireConfigKeys("platform.url", "platform.token"); err != nil {
209-
log.Fatal().Err(err).Msg("required configuration missing")
210-
}
211-
212-
// 3. Get values from unified config (supports flags/env/file)
198+
config.NewCommandSetup(cmd).
199+
WithFlagBindings(map[string]string{
200+
"url": "platform.url",
201+
"token": "platform.token",
202+
"threads": "common.threads",
203+
}).
204+
RequireKeys("platform.url", "platform.token").
205+
AddValidator(func() error { return config.ValidateURL(config.GetString("platform.url"), "Platform URL") }).
206+
AddValidator(func() error { return config.ValidateToken(config.GetString("platform.token"), "Platform API Token") }).
207+
MustBind()
208+
209+
// Read values from unified config (supports flags/env/file)
213210
url := config.GetString("platform.url")
214211
token := config.GetString("platform.token")
215212
threads := config.GetInt("common.threads")
216213
}
217214
```
218215

216+
**Migration policy (mandatory):**
217+
- When modifying an existing command, migrate that touched command to `config.NewCommandSetup`.
218+
- Do not leave mixed setup styles in the same command implementation.
219+
- `config.AutoBindFlags` is still an internal building block, but command code should use `NewCommandSetup`.
220+
219221
**Key naming convention:**
220222
- Platform settings: `<platform>.<key>` (e.g., `github.url`, `gitlab.token`)
221223
- Subcommand settings: `<platform>.<subcommand>.<key>` (e.g., `github.renovate.enum.owned`)
222224
- Common settings: `common.<key>` (e.g., `common.threads`)
223225

226+
### Command Coverage Policy (MANDATORY)
227+
228+
- When asked to update "all commands" for a platform, enumerate command files recursively under `internal/cmd/<platform>/` before editing.
229+
- Apply the requested change to scan and non-scan child commands, including nested children.
230+
- Do not stop after updating only `scan` commands.
231+
- Include grouped or nested command trees when applicable (for example, runners/list, runners/exploit, cicd/yaml, users/enum).
232+
233+
**Checklist for broad command updates:**
234+
1. Discover command tree under `internal/cmd/<platform>/` recursively.
235+
2. Identify every command Run entrypoint impacted by the request.
236+
3. Apply updates across all applicable child commands (scan + non-scan + nested).
237+
4. Verify no command directory in scope was skipped.
238+
5. Confirm touched commands follow `config.NewCommandSetup` migration policy.
239+
224240
**DO NOT:**
225241
- Read flags directly with `cmd.Flags().GetString()` - always use config system
226-
- Use `config.BindCommandFlags` - it's deprecated in favor of `AutoBindFlags`
227-
- Skip `RequireConfigKeys` validation for required flags
242+
- Use `config.BindCommandFlags` - it's deprecated
243+
- Skip required key validation for mandatory config values
228244

229245
### Package Organization
230246

internal/cmd/gitlab/gitlab.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
securefiles "github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/secureFiles"
1313
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/snippets"
1414
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/tf"
15+
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/users"
1516
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/variables"
1617
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/vuln"
1718
"github.com/spf13/cobra"
@@ -55,6 +56,7 @@ For SOCKS5 proxy:
5556
glCmd.AddCommand(schedule.NewScheduleCmd())
5657
glCmd.AddCommand(snippets.NewSnippetsRootCmd())
5758
glCmd.AddCommand(tf.NewTFCmd())
59+
glCmd.AddCommand(users.NewUsersRootCmd())
5860

5961
glCmd.PersistentFlags().StringVarP(&gitlabUrl, "url", "u", "", "GitLab instance URL")
6062
glCmd.PersistentFlags().StringVarP(&gitlabApiToken, "token", "t", "", "GitLab API Token")

internal/cmd/gitlab/gitlab_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/scanpublic"
99
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/shodan"
1010
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/snippets"
11+
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/users"
1112
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/variables"
1213
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/vuln"
1314
"github.com/stretchr/testify/assert"
@@ -39,6 +40,11 @@ func TestNewGitLabRootCmd(t *testing.T) {
3940
require.NoError(t, err)
4041
require.NotNil(t, snippetsCmd)
4142
assert.Equal(t, "snippets", snippetsCmd.Name())
43+
44+
usersCmd, _, err := cmd.Find([]string{"users"})
45+
require.NoError(t, err)
46+
require.NotNil(t, usersCmd)
47+
assert.Equal(t, "users", usersCmd.Name())
4248
}
4349

4450
func TestNewVulnCmd(t *testing.T) {
@@ -82,6 +88,32 @@ func TestNewRegisterCmd(t *testing.T) {
8288
assert.NotNil(t, flags.Lookup("url"), "'url' flag should be registered")
8389
}
8490

91+
func TestNewUsersRootCmd(t *testing.T) {
92+
cmd := users.NewUsersRootCmd()
93+
94+
require.NotNil(t, cmd)
95+
assert.Equal(t, "users", cmd.Use)
96+
assert.NotEmpty(t, cmd.Short)
97+
98+
enumCmd, _, err := cmd.Find([]string{"enum"})
99+
require.NoError(t, err)
100+
assert.NotNil(t, enumCmd)
101+
assert.NotNil(t, enumCmd.Flags().Lookup("token"))
102+
}
103+
104+
func TestNewUnauthenticatedUsersRootCmd(t *testing.T) {
105+
cmd := users.NewUnauthenticatedUsersRootCmd()
106+
107+
require.NotNil(t, cmd)
108+
assert.Equal(t, "users", cmd.Use)
109+
assert.NotEmpty(t, cmd.Short)
110+
111+
enumCmd, _, err := cmd.Find([]string{"enum"})
112+
require.NoError(t, err)
113+
assert.NotNil(t, enumCmd)
114+
assert.Nil(t, enumCmd.Flags().Lookup("token"))
115+
}
116+
85117
func TestNewShodanCmd(t *testing.T) {
86118
cmd := shodan.NewShodanCmd()
87119

@@ -126,6 +158,10 @@ func TestNewGitLabRootUnauthenticatedCmd(t *testing.T) {
126158
publicScanCmd, _, err := cmd.Find([]string{"scan"})
127159
require.NoError(t, err)
128160
assert.NotNil(t, publicScanCmd)
161+
162+
usersCmd, _, err := cmd.Find([]string{"users"})
163+
require.NoError(t, err)
164+
assert.NotNil(t, usersCmd)
129165
}
130166

131167
func TestNewScanPublicCmd(t *testing.T) {

internal/cmd/gitlab/gitlab_unauth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/register"
55
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/scanpublic"
66
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/shodan"
7+
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/users"
78
"github.com/spf13/cobra"
89
)
910

@@ -18,6 +19,7 @@ func NewGitLabRootUnauthenticatedCmd() *cobra.Command {
1819
glunaCmd.AddCommand(shodan.NewShodanCmd())
1920
glunaCmd.AddCommand(register.NewRegisterCmd())
2021
glunaCmd.AddCommand(scanpublic.NewScanPublicCmd())
22+
glunaCmd.AddCommand(users.NewUnauthenticatedUsersRootCmd())
2123

2224
return glunaCmd
2325
}

internal/cmd/gitlab/users/enum.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package users
2+
3+
import (
4+
"strings"
5+
6+
"github.com/CompassSecurity/pipeleek/pkg/config"
7+
pkgusers "github.com/CompassSecurity/pipeleek/pkg/gitlab/users"
8+
"github.com/rs/zerolog/log"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var flagBindings = map[string]string{
13+
"url": "gitlab.url",
14+
"token": "gitlab.token",
15+
}
16+
17+
func NewEnumCmd() *cobra.Command {
18+
return newEnumCmd(true)
19+
}
20+
21+
func NewUnauthenticatedEnumCmd() *cobra.Command {
22+
return newEnumCmd(false)
23+
}
24+
25+
func newEnumCmd(includeTokenFlag bool) *cobra.Command {
26+
enumCmd := &cobra.Command{
27+
Use: "enum",
28+
Short: "Enumerate GitLab users",
29+
Long: "Enumerate GitLab users visible via the GitLab users API.",
30+
Example: unauthenticatedEnumExample(includeTokenFlag),
31+
Run: Enum,
32+
}
33+
enumCmd.Flags().StringP("url", "u", "", "GitLab instance URL")
34+
if includeTokenFlag {
35+
enumCmd.Flags().StringP("token", "t", "", "GitLab API Token")
36+
}
37+
38+
return enumCmd
39+
}
40+
41+
func unauthenticatedEnumExample(includeTokenFlag bool) string {
42+
if includeTokenFlag {
43+
return `pipeleek gl users enum --url https://gitlab.example.com --token glpat-xxxxxxxxxxx`
44+
}
45+
46+
return `pipeleek gluna users enum --url https://gitlab.example.com`
47+
}
48+
49+
func Enum(cmd *cobra.Command, args []string) {
50+
config.NewCommandSetup(cmd).
51+
WithFlagBindings(flagBindings).
52+
RequireKeys("gitlab.url").
53+
AddValidator(func() error { return config.ValidateURL(config.GetString("gitlab.url"), "GitLab URL") }).
54+
MustBind()
55+
56+
gitlabURL := config.GetString("gitlab.url")
57+
gitlabAPIToken := config.GetString("gitlab.token")
58+
59+
// gluna commands are intentionally unauthenticated for users enum.
60+
if isSubcommandOf(cmd, "gluna") {
61+
if strings.TrimSpace(gitlabAPIToken) != "" {
62+
log.Warn().Msg("Ignoring provided GitLab API token for gluna users enum; command runs unauthenticated")
63+
}
64+
gitlabAPIToken = ""
65+
}
66+
67+
if gitlabAPIToken != "" {
68+
if err := config.ValidateToken(gitlabAPIToken, "GitLab API Token"); err != nil {
69+
log.Fatal().Err(err).Msg("Invalid GitLab API Token")
70+
}
71+
}
72+
73+
pkgusers.RunEnum(gitlabURL, gitlabAPIToken)
74+
}
75+
76+
func isSubcommandOf(cmd *cobra.Command, rootName string) bool {
77+
for current := cmd; current != nil; current = current.Parent() {
78+
if current.Name() == rootName {
79+
return true
80+
}
81+
}
82+
return false
83+
}

0 commit comments

Comments
 (0)