diff --git a/readme.md b/readme.md index 2a5d41e..71bfc16 100644 --- a/readme.md +++ b/readme.md @@ -89,6 +89,8 @@ You can tweak `--threads`, `--max-artifact-size` and `--job-limit` to obtain a c `enum` command: Enumerate user permissions and accesss +`renovate` command: Enumerate self-hosted Renovate instances. More here on [Autodiscovery](https://blog.compass-security.com/2025/05/renovate-keeping-your-updates-secure/). + ### GitLab Proxy Support > **Note:** Proxying is currently supported only for GitLab commands. diff --git a/src/pipeleak/cmd/devops/api.go b/src/pipeleak/cmd/devops/api.go index a4e86c5..d82c8a1 100644 --- a/src/pipeleak/cmd/devops/api.go +++ b/src/pipeleak/cmd/devops/api.go @@ -231,7 +231,7 @@ func (a AzureDevOpsApiClient) ListBuildArtifacts(continuationToken string, organ if err != nil { log.Error().Err(err).Str("url", reqUrl).Str("organization", organization).Str("project", project).Msg("Failed to list build artifacts (network or client error)") } - + if res != nil && (res.StatusCode() == 404 || res.StatusCode() == 401) { log.Error().Int("status", res.StatusCode()).Str("organization", organization).Str("project", project).Str("url", reqUrl).Str("response", res.String()).Msg("Build artifacts list does not exist or you do not have access (HTTP error)") } diff --git a/src/pipeleak/cmd/gitlab/enum.go b/src/pipeleak/cmd/gitlab/enum.go index b841297..6ae15c6 100644 --- a/src/pipeleak/cmd/gitlab/enum.go +++ b/src/pipeleak/cmd/gitlab/enum.go @@ -139,7 +139,7 @@ func enumCurrentToken(client resty.Client, baseUrl string, pat string) { log.Error().Err(err).Str("url", u.String()).Msg("Failed fetching token details (network or client error)") return } - + if res != nil && res.StatusCode() != 200 { log.Error().Int("status", res.StatusCode()).Str("url", u.String()).Str("response", res.String()).Msg("Failed fetching token details (HTTP error)") return diff --git a/src/pipeleak/cmd/gitlab/gitlab.go b/src/pipeleak/cmd/gitlab/gitlab.go index 260e3c5..7b33131 100644 --- a/src/pipeleak/cmd/gitlab/gitlab.go +++ b/src/pipeleak/cmd/gitlab/gitlab.go @@ -1,6 +1,7 @@ package gitlab import ( + "github.com/CompassSecurity/pipeleak/cmd/gitlab/renovate" "github.com/CompassSecurity/pipeleak/cmd/gitlab/runners" "github.com/CompassSecurity/pipeleak/cmd/gitlab/scan" "github.com/CompassSecurity/pipeleak/cmd/gitlab/secureFiles" @@ -28,6 +29,7 @@ func NewGitLabRootCmd() *cobra.Command { glCmd.AddCommand(NewVariablesCmd()) glCmd.AddCommand(securefiles.NewSecureFilesCmd()) glCmd.AddCommand(NewEnumCmd()) + glCmd.AddCommand(renovate.NewRenovateCmd()) glCmd.PersistentFlags().StringVarP(&gitlabUrl, "gitlab", "g", "", "GitLab instance URL") err := glCmd.MarkPersistentFlagRequired("gitlab") diff --git a/src/pipeleak/cmd/gitlab/register.go b/src/pipeleak/cmd/gitlab/register.go index 8f1dbe1..c695064 100644 --- a/src/pipeleak/cmd/gitlab/register.go +++ b/src/pipeleak/cmd/gitlab/register.go @@ -1,10 +1,10 @@ package gitlab import ( + "github.com/CompassSecurity/pipeleak/cmd/gitlab/util" "github.com/CompassSecurity/pipeleak/helper" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/CompassSecurity/pipeleak/cmd/gitlab/util" ) var ( diff --git a/src/pipeleak/cmd/gitlab/renovate/renovate.go b/src/pipeleak/cmd/gitlab/renovate/renovate.go new file mode 100644 index 0000000..28a83bd --- /dev/null +++ b/src/pipeleak/cmd/gitlab/renovate/renovate.go @@ -0,0 +1,150 @@ +package renovate + +import ( + "bytes" + "strings" + + "github.com/CompassSecurity/pipeleak/cmd/gitlab/util" + "github.com/CompassSecurity/pipeleak/helper" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "gitlab.com/gitlab-org/api/client-go" + "gopkg.in/yaml.v3" +) + +var ( + gitlabApiToken string + gitlabUrl string + verbose bool + owned bool + member bool + projectSearchQuery string +) + +func NewRenovateCmd() *cobra.Command { + renovateCmd := &cobra.Command{ + Use: "renovate [no options!]", + Short: "Enumerate renovate runner projects", + Run: Enumerate, + } + + renovateCmd.PersistentFlags().StringVarP(&gitlabUrl, "gitlab", "g", "", "GitLab instance URL") + err := renovateCmd.MarkPersistentFlagRequired("gitlab") + if err != nil { + log.Fatal().Stack().Err(err).Msg("Unable to require gitlab flag") + } + + renovateCmd.PersistentFlags().StringVarP(&gitlabApiToken, "token", "t", "", "GitLab API Token") + err = renovateCmd.MarkPersistentFlagRequired("token") + if err != nil { + log.Error().Stack().Err(err).Msg("Unable to require token flag") + } + renovateCmd.MarkFlagsRequiredTogether("gitlab", "token") + + renovateCmd.PersistentFlags().BoolVarP(&owned, "owned", "o", false, "Scan user onwed projects only") + renovateCmd.PersistentFlags().BoolVarP(&member, "member", "m", false, "Scan projects the user is member of") + renovateCmd.Flags().StringVarP(&projectSearchQuery, "search", "s", "", "Query string for searching projects") + + renovateCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose logging") + + return renovateCmd +} + +func Enumerate(cmd *cobra.Command, args []string) { + helper.SetLogLevel(verbose) + git, err := util.GetGitlabClient(gitlabApiToken, gitlabUrl) + if err != nil { + log.Fatal().Stack().Err(err).Msg("failed creating gitlab client") + } + + fetchProjects(git) + + log.Info().Msg("Done, Bye Bye 🏳️‍🌈🔥") +} + +func fetchProjects(git *gitlab.Client) { + log.Info().Msg("Fetching projects") + + projectOpts := &gitlab.ListProjectsOptions{ + ListOptions: gitlab.ListOptions{ + PerPage: 100, + Page: 1, + }, + OrderBy: gitlab.Ptr("last_activity_at"), + Owned: gitlab.Ptr(owned), + Membership: gitlab.Ptr(member), + Search: gitlab.Ptr(projectSearchQuery), + } + + for { + projects, resp, err := git.Projects.ListProjects(projectOpts) + if err != nil { + log.Error().Stack().Err(err).Msg("Failed fetching projects") + break + } + + for _, project := range projects { + log.Debug().Str("url", project.WebURL).Msg("Fetch project jobs") + identifyRenovateBotJob(git, project) + } + + if resp.NextPage == 0 { + break + } + + projectOpts.Page = resp.NextPage + log.Info().Int("currentPage", projectOpts.Page).Msg("Fetched projects page") + } + + log.Info().Msg("Fetched all projects") +} + +func identifyRenovateBotJob(git *gitlab.Client, project *gitlab.Project) { + + lintOpts := &gitlab.ProjectLintOptions{ + IncludeJobs: gitlab.Ptr(true), + } + res, response, err := git.Validate.ProjectLint(project.ID, lintOpts) + + if response.StatusCode == 404 || response.StatusCode == 403 { + return // Project does not have a CI/CD configuration or is not accessible + } + + if err != nil { + log.Error().Stack().Err(err).Msg("Failed fetching project ci/cd yml") + return + } + + if strings.Contains(res.MergedYaml, "renovate/renovate") || strings.Contains(res.MergedYaml, "--autodiscover=true") { + log.Info().Str("project", project.Name).Str("url", project.WebURL).Msg("Found renovate bot job image") + yml, err := prettyPrintYAML(res.MergedYaml) + + if err != nil { + log.Error().Stack().Err(err).Msg("Failed pretty printing project ci/cd yml") + return + } + + // make windows compatible + log.Info().Msg("\n" + yml) + } +} + +func prettyPrintYAML(yamlStr string) (string, error) { + var node yaml.Node + + err := yaml.Unmarshal([]byte(yamlStr), &node) + if err != nil { + return "", err + } + + var buf bytes.Buffer + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + + err = encoder.Encode(&node) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/src/pipeleak/cmd/gitlab/variables.go b/src/pipeleak/cmd/gitlab/variables.go index c396ee3..93287b1 100644 --- a/src/pipeleak/cmd/gitlab/variables.go +++ b/src/pipeleak/cmd/gitlab/variables.go @@ -1,11 +1,11 @@ package gitlab import ( + "github.com/CompassSecurity/pipeleak/cmd/gitlab/util" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "gitlab.com/gitlab-org/api/client-go" - "github.com/CompassSecurity/pipeleak/cmd/gitlab/util" ) func NewVariablesCmd() *cobra.Command {