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/github/github.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package github

import (
"github.com/CompassSecurity/pipeleek/internal/cmd/github/renovate"
"github.com/CompassSecurity/pipeleek/internal/cmd/github/scan"
"github.com/spf13/cobra"
)
Expand All @@ -13,6 +14,7 @@ func NewGitHubRootCmd() *cobra.Command {
}

ghCmd.AddCommand(scan.NewScanCmd())
ghCmd.AddCommand(renovate.NewRenovateRootCmd())

return ghCmd
}
41 changes: 41 additions & 0 deletions internal/cmd/github/renovate/autodiscovery/autodiscovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package autodiscovery

import (
pkgrenovate "github.com/CompassSecurity/pipeleek/pkg/github/renovate/autodiscovery"
pkgscan "github.com/CompassSecurity/pipeleek/pkg/github/scan"
"github.com/spf13/cobra"
)

var (
autodiscoveryRepoName string
autodiscoveryUsername string
autodiscoveryAddWorkflow bool
)

func NewAutodiscoveryCmd() *cobra.Command {
autodiscoveryCmd := &cobra.Command{
Use: "autodiscovery",
Short: "Create a PoC for Renovate Autodiscovery misconfigurations exploitation",
Long: "Create a repository with a Renovate Bot configuration that will be picked up by an existing Renovate Bot user. The Renovate Bot will execute the malicious Gradle wrapper script during dependency updates, which you can customize in exploit.sh.",
Example: `
# Create a repository and invite the victim Renovate Bot user to it. Uses Gradle wrapper to execute arbitrary code during dependency updates.
pipeleek gh renovate autodiscovery --token ghp_xxxxx --github https://api.github.com --repo-name my-exploit-repo --username renovate-bot-user

# Create a repository with a GitHub Actions workflow for local testing (requires setting RENOVATE_TOKEN as repository secret)
pipeleek gh renovate autodiscovery --token ghp_xxxxx --github https://api.github.com --repo-name my-exploit-repo --add-renovate-workflow-for-debugging
`,
Run: func(cmd *cobra.Command, args []string) {
parent := cmd.Parent()
githubUrl, _ := parent.Flags().GetString("github")
githubApiToken, _ := parent.Flags().GetString("token")

client := pkgscan.SetupClient(githubApiToken, githubUrl)
pkgrenovate.RunGenerate(client, autodiscoveryRepoName, autodiscoveryUsername, autodiscoveryAddWorkflow)
},
}
autodiscoveryCmd.Flags().StringVarP(&autodiscoveryRepoName, "repo-name", "r", "", "The name for the created repository")
autodiscoveryCmd.Flags().StringVarP(&autodiscoveryUsername, "username", "u", "", "The username of the victim Renovate Bot user to invite")
autodiscoveryCmd.Flags().BoolVar(&autodiscoveryAddWorkflow, "add-renovate-workflow-for-debugging", false, "Creates a GitHub Actions workflow in the repo that runs Renovate Bot for local testing")

return autodiscoveryCmd
}
141 changes: 141 additions & 0 deletions internal/cmd/github/renovate/enum/enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package enum

import (
"github.com/CompassSecurity/pipeleek/pkg/config"
pkgrenovate "github.com/CompassSecurity/pipeleek/pkg/github/renovate/enum"
pkgscan "github.com/CompassSecurity/pipeleek/pkg/github/scan"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

var (
owned bool
member bool
searchQuery string
fast bool
dump bool
page int
repository string
organization string
orderBy string
extendRenovateConfigService string
)

func NewEnumCmd() *cobra.Command {
enumCmd := &cobra.Command{
Use: "enum [no options!]",
Short: "Enumerate Renovate configurations",
Long: "Enumerate GitHub repositories for Renovate bot configurations. Identifies repositories with Renovate workflows, config files, autodiscovery settings, and self-hosted configurations.",
Example: `
# Enumerate all owned repositories
pipeleek gh renovate enum --github https://api.github.com --token ghp_xxxxx --owned

# Enumerate specific organization
pipeleek gh renovate enum --github https://api.github.com --token ghp_xxxxx --org mycompany

# Enumerate with config file dump
pipeleek gh renovate enum --github https://api.github.com --token ghp_xxxxx --owned --dump

# Fast mode (skip config file detection)
pipeleek gh renovate enum --github https://api.github.com --token ghp_xxxxx --org myorg --fast

# Enumerate specific repository
pipeleek gh renovate enum --github https://api.github.com --token ghp_xxxxx --repo owner/repo
`,
PreRun: func(cmd *cobra.Command, args []string) {
// Bind parent flags to config
if err := config.BindCommandFlags(cmd.Parent(), "github.renovate", map[string]string{
"github": "github.url",
"token": "github.token",
}); err != nil {
log.Fatal().Err(err).Msg("Failed to bind parent flags")
}
},
Run: func(cmd *cobra.Command, args []string) {
if err := config.BindCommandFlags(cmd, "github.renovate.enum", nil); err != nil {
log.Fatal().Err(err).Msg("Failed to bind flags to config")
}

// Get github URL and token from config (supports all three methods)
githubUrl := config.GetString("github.url")
githubApiToken := config.GetString("github.token")

if githubUrl == "" {
log.Fatal().Msg("GitHub URL is required (use --github flag, config file, or PIPELEEK_GITHUB_URL env var)")
}
if githubApiToken == "" {
log.Fatal().Msg("GitHub token is required (use --token flag, config file, or PIPELEEK_GITHUB_TOKEN env var)")
}

// All flags can come from config, CLI flags, or env vars via Viper
if !cmd.Flags().Changed("owned") {
owned = config.GetBool("github.renovate.enum.owned")
}
if !cmd.Flags().Changed("member") {
member = config.GetBool("github.renovate.enum.member")
}
if !cmd.Flags().Changed("repo") {
repository = config.GetString("github.renovate.enum.repo")
}
if !cmd.Flags().Changed("org") {
organization = config.GetString("github.renovate.enum.org")
}
if !cmd.Flags().Changed("search") {
searchQuery = config.GetString("github.renovate.enum.search")
}
if !cmd.Flags().Changed("fast") {
fast = config.GetBool("github.renovate.enum.fast")
}
if !cmd.Flags().Changed("dump") {
dump = config.GetBool("github.renovate.enum.dump")
}
if !cmd.Flags().Changed("page") {
page = config.GetInt("github.renovate.enum.page")
}
if !cmd.Flags().Changed("order-by") {
orderBy = config.GetString("github.renovate.enum.order_by")
}
if !cmd.Flags().Changed("extend-renovate-config-service") {
extendRenovateConfigService = config.GetString("github.renovate.enum.extend_renovate_config_service")
}

Enumerate(githubUrl, githubApiToken)
},
}

enumCmd.PersistentFlags().BoolVarP(&owned, "owned", "o", false, "Scan user owned repositories only")
enumCmd.PersistentFlags().BoolVarP(&member, "member", "m", false, "Scan repositories the user is member of")
enumCmd.Flags().StringVarP(&repository, "repo", "r", "", "Repository to scan for Renovate configuration in format owner/repo (if not set, all repositories will be scanned)")
enumCmd.Flags().StringVar(&organization, "org", "", "Organization to scan")
enumCmd.Flags().StringVarP(&searchQuery, "search", "s", "", "Query string for searching repositories")
enumCmd.Flags().BoolVarP(&fast, "fast", "f", false, "Fast mode - skip renovate config file detection, only check workflow files for renovate bot job (default false)")
enumCmd.Flags().BoolVarP(&dump, "dump", "d", false, "Dump mode - save all config files to renovate-enum-out folder (default false)")
enumCmd.Flags().IntVarP(&page, "page", "p", 1, "Page number to start fetching repositories from (default 1, fetch all pages)")
enumCmd.Flags().StringVar(&orderBy, "order-by", "created", "Order repositories by: created, updated, pushed, or full_name")
enumCmd.Flags().StringVar(&extendRenovateConfigService, "extend-renovate-config-service", "", "Base URL of the resolver service e.g. http://localhost:3000 (docker run -ti -p 3000:3000 jfrcomp/renovate-config-resolver:latest). Renovate configs can be extended by shareable preset, resolving them makes enumeration more accurate.")

enumCmd.MarkFlagsMutuallyExclusive("owned", "member", "repo", "org", "search")

return enumCmd
}

func Enumerate(githubUrl, githubApiToken string) {
client := pkgscan.SetupClient(githubApiToken, githubUrl)

opts := pkgrenovate.EnumOptions{
GitHubURL: githubUrl,
GitHubToken: githubApiToken,
Owned: owned,
Member: member,
SearchQuery: searchQuery,
Fast: fast,
Dump: dump,
Page: page,
Repository: repository,
Organization: organization,
OrderBy: orderBy,
ExtendRenovateConfigService: extendRenovateConfigService,
}

pkgrenovate.RunEnumerate(client, opts)
}
39 changes: 39 additions & 0 deletions internal/cmd/github/renovate/privesc/privesc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package privesc

import (
pkgrenovate "github.com/CompassSecurity/pipeleek/pkg/github/renovate/privesc"
pkgscan "github.com/CompassSecurity/pipeleek/pkg/github/scan"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

var (
privescRenovateBranchesRegex string
privescRepoName string
)

func NewPrivescCmd() *cobra.Command {
privescCmd := &cobra.Command{
Use: "privesc",
Short: "Inject a malicious workflow job into the protected default branch abusing Renovate Bot's access",
Long: "Inject a job into the GitHub Actions workflow of the repository's default branch by adding a commit (race condition) to a Renovate Bot branch, which is then auto-merged into the main branch. Assumes the Renovate Bot has owner/admin access whereas you only have write access. See https://blog.compass-security.com/2025/05/renovate-keeping-your-updates-secure/",
Example: `pipeleek gh renovate privesc --token ghp_xxxxx --github https://api.github.com --repo-name owner/myproject --renovate-branches-regex 'renovate/.*'`,
Run: func(cmd *cobra.Command, args []string) {
parent := cmd.Parent()
githubUrl, _ := parent.Flags().GetString("github")
githubApiToken, _ := parent.Flags().GetString("token")

client := pkgscan.SetupClient(githubApiToken, githubUrl)
pkgrenovate.RunExploit(client, privescRepoName, privescRenovateBranchesRegex)
},
}
privescCmd.Flags().StringVarP(&privescRenovateBranchesRegex, "renovate-branches-regex", "b", "renovate/.*", "The branch name regex expression to match the Renovate Bot branch names (default: 'renovate/.*')")
privescCmd.Flags().StringVarP(&privescRepoName, "repo-name", "r", "", "The repository to target in format owner/repo")

err := privescCmd.MarkFlagRequired("repo-name")
if err != nil {
log.Fatal().Stack().Err(err).Msg("Unable to require repo-name flag")
}

return privescCmd
}
55 changes: 55 additions & 0 deletions internal/cmd/github/renovate/renovate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package renovate

import (
"github.com/CompassSecurity/pipeleek/internal/cmd/github/renovate/autodiscovery"
"github.com/CompassSecurity/pipeleek/internal/cmd/github/renovate/enum"
"github.com/CompassSecurity/pipeleek/internal/cmd/github/renovate/privesc"
"github.com/CompassSecurity/pipeleek/pkg/config"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

var (
githubApiToken string
githubUrl string
)

func NewRenovateRootCmd() *cobra.Command {
renovateCmd := &cobra.Command{
Use: "renovate",
Short: "Renovate related commands",
Long: "Commands to enumerate and exploit GitHub Renovate bot configurations.",
}

// Define PreRun to bind flags and validate configuration
renovateCmd.PreRun = func(cmd *cobra.Command, args []string) {
// Bind flags to config keys
if err := config.BindCommandFlags(cmd, "github.renovate", map[string]string{
"github": "github.url",
"token": "github.token",
}); err != nil {
log.Fatal().Err(err).Msg("Failed to bind flags to config")
}

// Get values from config (supports CLI flags, config file, and env vars)
githubUrl = config.GetString("github.url")
githubApiToken = config.GetString("github.token")

// Validate required values
if githubUrl == "" {
log.Fatal().Msg("GitHub URL is required (use --github flag, config file, or PIPELEEK_GITHUB_URL env var)")
}
if githubApiToken == "" {
log.Fatal().Msg("GitHub token is required (use --token flag, config file, or PIPELEEK_GITHUB_TOKEN env var)")
}
}

renovateCmd.PersistentFlags().StringVarP(&githubUrl, "github", "g", "https://api.github.com", "GitHub API base URL")
renovateCmd.PersistentFlags().StringVarP(&githubApiToken, "token", "t", "", "GitHub Personal Access Token")

renovateCmd.AddCommand(enum.NewEnumCmd())
renovateCmd.AddCommand(autodiscovery.NewAutodiscoveryCmd())
renovateCmd.AddCommand(privesc.NewPrivescCmd())

return renovateCmd
}
Loading