Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/cmd/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
securefiles "github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/secureFiles"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/snippets"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/tf"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/users"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/variables"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/vuln"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -55,6 +56,7 @@ For SOCKS5 proxy:
glCmd.AddCommand(schedule.NewScheduleCmd())
glCmd.AddCommand(snippets.NewSnippetsRootCmd())
glCmd.AddCommand(tf.NewTFCmd())
glCmd.AddCommand(users.NewUsersRootCmd())

glCmd.PersistentFlags().StringVarP(&gitlabUrl, "gitlab", "g", "", "GitLab instance URL")
glCmd.PersistentFlags().StringVarP(&gitlabApiToken, "token", "t", "", "GitLab API Token")
Expand Down
22 changes: 22 additions & 0 deletions internal/cmd/gitlab/gitlab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/scanpublic"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/shodan"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/snippets"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/users"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/variables"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/vuln"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -39,6 +40,11 @@ func TestNewGitLabRootCmd(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, snippetsCmd)
assert.Equal(t, "snippets", snippetsCmd.Name())

usersCmd, _, err := cmd.Find([]string{"users"})
require.NoError(t, err)
require.NotNil(t, usersCmd)
assert.Equal(t, "users", usersCmd.Name())
}

func TestNewVulnCmd(t *testing.T) {
Expand Down Expand Up @@ -82,6 +88,18 @@ func TestNewRegisterCmd(t *testing.T) {
assert.NotNil(t, flags.Lookup("gitlab"), "'gitlab' flag should be registered")
}

func TestNewUsersRootCmd(t *testing.T) {
cmd := users.NewUsersRootCmd()

require.NotNil(t, cmd)
assert.Equal(t, "users", cmd.Use)
assert.NotEmpty(t, cmd.Short)

enumCmd, _, err := cmd.Find([]string{"enum"})
require.NoError(t, err)
assert.NotNil(t, enumCmd)
}

func TestNewShodanCmd(t *testing.T) {
cmd := shodan.NewShodanCmd()

Expand Down Expand Up @@ -126,6 +144,10 @@ func TestNewGitLabRootUnauthenticatedCmd(t *testing.T) {
publicScanCmd, _, err := cmd.Find([]string{"scan"})
require.NoError(t, err)
assert.NotNil(t, publicScanCmd)

usersCmd, _, err := cmd.Find([]string{"users"})
require.NoError(t, err)
assert.NotNil(t, usersCmd)
}

func TestNewScanPublicCmd(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/gitlab/gitlab_unauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/register"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/scanpublic"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/shodan"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/users"
"github.com/spf13/cobra"
)

Expand All @@ -18,6 +19,7 @@ func NewGitLabRootUnauthenticatedCmd() *cobra.Command {
glunaCmd.AddCommand(shodan.NewShodanCmd())
glunaCmd.AddCommand(register.NewRegisterCmd())
glunaCmd.AddCommand(scanpublic.NewScanPublicCmd())
glunaCmd.AddCommand(users.NewUsersRootCmd())

return glunaCmd
}
49 changes: 49 additions & 0 deletions internal/cmd/gitlab/users/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package users

import (
"github.com/CompassSecurity/pipeleek/pkg/config"
pkgusers "github.com/CompassSecurity/pipeleek/pkg/gitlab/users"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

func NewEnumCmd() *cobra.Command {
enumCmd := &cobra.Command{
Use: "enum",
Short: "Enumerate GitLab users",
Long: "Enumerate GitLab users visible via the GitLab users API.",
Example: `pipeleek gl users enum --gitlab https://gitlab.example.com --token glpat-xxxxxxxxxxx`,
Run: Enum,
}
enumCmd.Flags().StringP("gitlab", "g", "", "GitLab instance URL")
enumCmd.Flags().StringP("token", "t", "", "GitLab API Token")

return enumCmd
}

func Enum(cmd *cobra.Command, args []string) {
if err := config.AutoBindFlags(cmd, map[string]string{
"gitlab": "gitlab.url",
"token": "gitlab.token",
}); err != nil {
log.Fatal().Err(err).Msg("Failed to bind command flags to configuration keys")
}

if err := config.RequireConfigKeys("gitlab.url"); err != nil {
log.Fatal().Err(err).Msg("required configuration missing")
}

gitlabURL := config.GetString("gitlab.url")
gitlabAPIToken := config.GetString("gitlab.token")

if err := config.ValidateURL(gitlabURL, "GitLab URL"); err != nil {
log.Fatal().Err(err).Msg("Invalid GitLab URL")
}
if gitlabAPIToken != "" {
if err := config.ValidateToken(gitlabAPIToken, "GitLab API Token"); err != nil {
log.Fatal().Err(err).Msg("Invalid GitLab API Token")
}
}

pkgusers.RunEnum(gitlabURL, gitlabAPIToken)
}
79 changes: 79 additions & 0 deletions internal/cmd/gitlab/users/enum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package users

import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"

"github.com/CompassSecurity/pipeleek/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEnumCommand_WithToken(t *testing.T) {
t.Setenv("PIPELEEK_NO_CONFIG", "1")
config.ResetViper()
t.Cleanup(config.ResetViper)

var (
mu sync.Mutex
requests []*http.Request
)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
requests = append(requests, r.Clone(r.Context()))
mu.Unlock()

require.Equal(t, "/api/v4/users", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode([]map[string]any{})
}))
defer server.Close()

cmd := NewEnumCmd()
cmd.SetArgs([]string{"--gitlab", server.URL, "--token", "glpat-test"})

require.NoError(t, cmd.Execute())

mu.Lock()
defer mu.Unlock()
require.Len(t, requests, 1)
assert.Equal(t, "glpat-test", requests[0].Header.Get("PRIVATE-TOKEN"))
}

func TestEnumCommand_WithoutToken(t *testing.T) {
t.Setenv("PIPELEEK_NO_CONFIG", "1")
config.ResetViper()
t.Cleanup(config.ResetViper)

var (
mu sync.Mutex
requests []*http.Request
)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
requests = append(requests, r.Clone(r.Context()))
mu.Unlock()

require.Equal(t, "/api/v4/users", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode([]map[string]any{})
}))
defer server.Close()

cmd := NewEnumCmd()
cmd.SetArgs([]string{"--gitlab", server.URL})

require.NoError(t, cmd.Execute())

mu.Lock()
defer mu.Unlock()
require.Len(t, requests, 1)
assert.Empty(t, requests[0].Header.Get("PRIVATE-TOKEN"))
}
15 changes: 15 additions & 0 deletions internal/cmd/gitlab/users/users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package users

import "github.com/spf13/cobra"

func NewUsersRootCmd() *cobra.Command {
usersCmd := &cobra.Command{
Use: "users",
Short: "GitLab user related commands",
Long: "Commands to enumerate GitLab users.",
}

usersCmd.AddCommand(NewEnumCmd())

return usersCmd
}
69 changes: 69 additions & 0 deletions pkg/gitlab/users/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package users

import (
"github.com/CompassSecurity/pipeleek/pkg/gitlab/util"
"github.com/rs/zerolog/log"
gitlab "gitlab.com/gitlab-org/api/client-go"
)

func RunEnum(gitlabURL, token string) {
git, err := util.GetGitlabClient(token, gitlabURL)
if err != nil {
log.Fatal().Stack().Err(err).Msg("Failed creating gitlab client")
}

log.Info().Msg("Enumerating GitLab users")

totalUsers := 0
page := int64(1)
for page != -1 {
users, nextPage, err := listUsers(git, page)
if err != nil {
log.Fatal().Stack().Err(err).Msg("Failed listing GitLab users")
}

for _, user := range users {
if user == nil {
continue
}

totalUsers++
log.Info().
Int64("id", user.ID).
Str("username", user.Username).
Str("name", user.Name).
Str("publicEmail", user.PublicEmail).
Str("profile", user.WebURL).
Str("state", user.State).
Bool("bot", user.Bot).
Bool("admin", user.IsAdmin).
Bool("external", user.External).
Bool("privateProfile", user.PrivateProfile).
Msg("GitLab user")
log.Debug().Interface("full_user", user).Msg("Full User details")
}

page = nextPage
}

log.Info().Int("users", totalUsers).Msg("GitLab user enumeration complete")
}

func listUsers(git *gitlab.Client, page int64) ([]*gitlab.User, int64, error) {
users, resp, err := git.Users.ListUsers(&gitlab.ListUsersOptions{
ListOptions: gitlab.ListOptions{
PerPage: 100,
Page: page,
},
})
if err != nil {
return nil, -1, err
}

nextPage := int64(-1)
if resp != nil && resp.NextPage > 0 {
nextPage = resp.NextPage
}

return users, nextPage, nil
}
53 changes: 53 additions & 0 deletions tests/e2e/gitlab/unauth/users/enum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//go:build e2e

package e2e

import (
"encoding/json"
"net/http"
"testing"
"time"

"github.com/CompassSecurity/pipeleek/tests/e2e/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGitLabUnauthenticatedUsersEnum(t *testing.T) {
server, getRequests, cleanup := testutil.StartMockServerWithRecording(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

if r.URL.Path != "/api/v4/users" {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
return
}

w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode([]map[string]any{{
"id": 7,
"username": "public-user",
"name": "Public User",
"web_url": "http://" + r.Host + "/public-user",
"state": "active",
}})
})
defer cleanup()

stdout, stderr, exitErr := testutil.RunCLI(t, []string{
"gluna", "users", "enum",
"--gitlab", server.URL,
}, nil, 15*time.Second)

require.NoError(t, exitErr)

requests := getRequests()
require.Len(t, requests, 1)
assert.Equal(t, "/api/v4/users", requests[0].Path)
assert.Empty(t, requests[0].Headers.Get("PRIVATE-TOKEN"))
assert.Contains(t, stdout, "GitLab user")
assert.Contains(t, stdout, "public-user")

t.Logf("STDOUT:\n%s", stdout)
t.Logf("STDERR:\n%s", stderr)
}
Loading