Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions containers_github_com
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
2026-01-20T09:35:57Z info Log level set to info (default)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this file!

2026-01-20T09:35:57Z info Loaded config file file=/home/vscode/.config/pipeleek/pipeleek.yaml
2026-01-20T09:35:57Z info Loaded container scan patterns pattern_count=4
2026-01-20T09:35:57Z info Fetching repositories
2026-01-20T09:35:57Z fatal No search criteria specified. Use --owned, --member, --org, --repo, or --search
107 changes: 107 additions & 0 deletions internal/cmd/github/container/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package container

import (
"github.com/CompassSecurity/pipeleek/pkg/config"
pkgcontainer "github.com/CompassSecurity/pipeleek/pkg/github/container"
pkgscan "github.com/CompassSecurity/pipeleek/pkg/github/scan"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

var (
owned bool
member bool
public bool
projectSearchQuery string
page int
repository string
organization string
orderBy string
dangerousPatterns string
)

func NewContainerScanCmd() *cobra.Command {
containerCmd := &cobra.Command{
Use: "container",
Short: "Container image scanning commands",
Long: "Commands to scan for dangerous container image build patterns in GitHub repositories.",
}

containerCmd.AddCommand(NewScanCmd())

return containerCmd
}

func NewScanCmd() *cobra.Command {
scanCmd := &cobra.Command{
Use: "scan [no options!]",
Short: "Scan for dangerous container image build patterns",
Long: "Scan GitHub repositories for dangerous container image build patterns like COPY . /path",
Run: func(cmd *cobra.Command, args []string) {
if err := config.AutoBindFlags(cmd, map[string]string{
"github": "github.url",
"token": "github.token",
"owned": "github.container.scan.owned",
"member": "github.container.scan.member",
"public": "github.container.scan.public",
"repo": "github.container.scan.repo",
"organization": "github.container.scan.organization",
"search": "github.container.scan.search",
"page": "github.container.scan.page",
"order-by": "github.container.scan.order_by",
"dangerous-patterns": "github.container.scan.dangerous_patterns",
}); err != nil {
log.Fatal().Err(err).Msg("Failed to bind command flags to configuration keys")
}

githubUrl := config.GetString("github.url")
githubApiToken := config.GetString("github.token")

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

owned = config.GetBool("github.container.scan.owned")
member = config.GetBool("github.container.scan.member")
public = config.GetBool("github.container.scan.public")
repository = config.GetString("github.container.scan.repo")
organization = config.GetString("github.container.scan.organization")
projectSearchQuery = config.GetString("github.container.scan.search")
page = config.GetInt("github.container.scan.page")
orderBy = config.GetString("github.container.scan.order_by")

Scan(githubUrl, githubApiToken)
},
}

scanCmd.PersistentFlags().BoolVarP(&owned, "owned", "o", false, "Scan user owned repositories only")
scanCmd.PersistentFlags().BoolVarP(&member, "member", "m", false, "Scan repositories the user is member of")
scanCmd.PersistentFlags().BoolVar(&public, "public", false, "Scan public repositories only")
scanCmd.Flags().StringVarP(&repository, "repo", "r", "", "Repository to scan (if not set, all repositories will be scanned)")
scanCmd.Flags().StringVarP(&organization, "organization", "n", "", "Organization to scan")
scanCmd.Flags().StringVarP(&projectSearchQuery, "search", "s", "", "Query string for searching repositories")
scanCmd.Flags().IntVarP(&page, "page", "p", 1, "Page number to start fetching repositories from (default 1)")
scanCmd.Flags().StringVar(&orderBy, "order-by", "updated", "Order repositories by: stars, forks, updated")

return scanCmd
}

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

opts := pkgcontainer.ScanOptions{
GitHubUrl: githubUrl,
GitHubApiToken: githubApiToken,
Owned: owned,
Member: member,
Public: public,
ProjectSearchQuery: projectSearchQuery,
Page: page,
Repository: repository,
Organization: organization,
OrderBy: orderBy,
DangerousPatterns: dangerousPatterns,
}

pkgcontainer.RunScan(opts, client)
}
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/container"
"github.com/CompassSecurity/pipeleek/internal/cmd/github/renovate"
"github.com/CompassSecurity/pipeleek/internal/cmd/github/scan"
"github.com/spf13/cobra"
Expand All @@ -15,6 +16,7 @@ func NewGitHubRootCmd() *cobra.Command {

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

return ghCmd
}
101 changes: 101 additions & 0 deletions internal/cmd/gitlab/container/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package container

import (
"github.com/CompassSecurity/pipeleek/pkg/config"
pkgcontainer "github.com/CompassSecurity/pipeleek/pkg/gitlab/container"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
gitlab "gitlab.com/gitlab-org/api/client-go"
)

var (
owned bool
member bool
projectSearchQuery string
page int
repository string
namespace string
orderBy string
dangerousPatterns string
)

func NewContainerScanCmd() *cobra.Command {
containerCmd := &cobra.Command{
Use: "container",
Short: "Container image scanning commands",
Long: "Commands to scan for dangerous container image build patterns in GitLab projects.",
}

containerCmd.AddCommand(NewScanCmd())

return containerCmd
}

func NewScanCmd() *cobra.Command {
scanCmd := &cobra.Command{
Use: "scan [no options!]",
Short: "Scan for dangerous container image build patterns",
Long: "Scan GitLab projects for dangerous container image build patterns like COPY . /path",
Run: func(cmd *cobra.Command, args []string) {
if err := config.AutoBindFlags(cmd, map[string]string{
"gitlab": "gitlab.url",
"token": "gitlab.token",
"owned": "gitlab.container.scan.owned",
"member": "gitlab.container.scan.member",
"repo": "gitlab.container.scan.repo",
"namespace": "gitlab.container.scan.namespace",
"search": "gitlab.container.scan.search",
"page": "gitlab.container.scan.page",
"order-by": "gitlab.container.scan.order_by",
"dangerous-patterns": "gitlab.container.scan.dangerous_patterns",
}); err != nil {
log.Fatal().Err(err).Msg("Failed to bind command flags to configuration keys")
}

gitlabUrl := config.GetString("gitlab.url")
gitlabApiToken := config.GetString("gitlab.token")

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

owned = config.GetBool("gitlab.container.scan.owned")
member = config.GetBool("gitlab.container.scan.member")
repository = config.GetString("gitlab.container.scan.repo")
namespace = config.GetString("gitlab.container.scan.namespace")
projectSearchQuery = config.GetString("gitlab.container.scan.search")
page = config.GetInt("gitlab.container.scan.page")
orderBy = config.GetString("gitlab.container.scan.order_by")

Scan(gitlabUrl, gitlabApiToken)
},
}

scanCmd.PersistentFlags().BoolVarP(&owned, "owned", "o", false, "Scan user owned projects only")
scanCmd.PersistentFlags().BoolVarP(&member, "member", "m", false, "Scan projects the user is member of")
scanCmd.Flags().StringVarP(&repository, "repo", "r", "", "Repository to scan (if not set, all projects will be scanned)")
scanCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Namespace to scan")
scanCmd.Flags().StringVarP(&projectSearchQuery, "search", "s", "", "Query string for searching projects")
scanCmd.Flags().IntVarP(&page, "page", "p", 1, "Page number to start fetching projects from (default 1, fetch all pages)")
scanCmd.Flags().StringVar(&orderBy, "order-by", "last_activity_at", "Order projects by: id, name, path, created_at, updated_at, star_count, last_activity_at, or similarity")

return scanCmd
}

func Scan(gitlabUrl, gitlabApiToken string) {
opts := pkgcontainer.ScanOptions{
GitlabUrl: gitlabUrl,
GitlabApiToken: gitlabApiToken,
Owned: owned,
Member: member,
ProjectSearchQuery: projectSearchQuery,
Page: page,
Repository: repository,
Namespace: namespace,
OrderBy: orderBy,
DangerousPatterns: dangerousPatterns,
MinAccessLevel: int(gitlab.GuestPermissions),
}

pkgcontainer.RunScan(opts)
}
2 changes: 2 additions & 0 deletions internal/cmd/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gitlab

import (
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/cicd"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/container"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/enum"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/renovate"
"github.com/CompassSecurity/pipeleek/internal/cmd/gitlab/runners"
Expand Down Expand Up @@ -45,6 +46,7 @@ For SOCKS5 proxy:
glCmd.AddCommand(securefiles.NewSecureFilesCmd())
glCmd.AddCommand(enum.NewEnumCmd())
glCmd.AddCommand(renovate.NewRenovateRootCmd())
glCmd.AddCommand(container.NewContainerScanCmd())
glCmd.AddCommand(cicd.NewCiCdCmd())
glCmd.AddCommand(schedule.NewScheduleCmd())

Expand Down
60 changes: 60 additions & 0 deletions pkg/container/patterns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package container

import (
"regexp"
"strings"
)

// DefaultPatterns returns the default dangerous patterns to detect in Dockerfiles
func DefaultPatterns() []Pattern {
return []Pattern{
{
Name: "copy_all_to_root",
Pattern: regexp.MustCompile(`(?i)^COPY\s+\./?(\s+/\s*)?$`),
Severity: "high",
Description: "Copies entire working directory to root - exposes all files including secrets",
},
{
Name: "copy_all_anywhere",
Pattern: regexp.MustCompile(`(?i)^COPY\s+(\./?|\*|\.\/\*|\.\*)(\s+|$)`),
Severity: "high",
Description: "Copies entire working directory into container - may expose sensitive files",
},
{
Name: "add_all_to_root",
Pattern: regexp.MustCompile(`(?i)^ADD\s+\./?(\s+/\s*)?$`),
Severity: "high",
Description: "Adds entire working directory to root - exposes all files including secrets",
},
{
Name: "add_all_anywhere",
Pattern: regexp.MustCompile(`(?i)^ADD\s+(\./?|\*|\.\/\*|\.\*)(\s+|$)`),
Severity: "high",
Description: "Adds entire working directory into container - may expose sensitive files",
},
}
}

// ParseCustomPatterns parses a comma-separated string of patterns into a slice of Pattern objects
// The patterns are treated as regex strings
func ParseCustomPatterns(patternsStr string) []Pattern {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove custom patterns

if strings.TrimSpace(patternsStr) == "" {
return []Pattern{}
}

patterns := []Pattern{}
for _, p := range strings.Split(patternsStr, ",") {
p = strings.TrimSpace(p)
if p != "" {
if regex, err := regexp.Compile(p); err == nil {
patterns = append(patterns, Pattern{
Name: p,
Pattern: regex,
Severity: "medium",
Description: "Custom dangerous pattern",
})
}
}
}
return patterns
}
75 changes: 75 additions & 0 deletions pkg/container/scanner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package container

import (
"regexp"
"strings"
)

// IsMultistage checks if Dockerfile content uses multistage builds by counting FROM statements
func IsMultistage(content string) bool {
lines := strings.Split(content, "\n")

fromCount := 0
fromPattern := regexp.MustCompile(`(?i)^\s*FROM\s+`)

for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
// Skip empty lines and comments
if trimmedLine == "" || strings.HasPrefix(trimmedLine, "#") {
continue
}

if fromPattern.MatchString(line) {
fromCount++
if fromCount > 1 {
return true
}
}
}

return false
}

// ScanDockerfileContent checks a Dockerfile's content against patterns and returns matched lines
func ScanDockerfileContent(content string, patterns []Pattern) []string {
var matches []string
lines := strings.Split(content, "\n")

// Check against all patterns
for _, pattern := range patterns {
// Search through lines to find a match
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
// Skip empty lines and comments
if trimmedLine == "" || strings.HasPrefix(trimmedLine, "#") {
continue
}

if pattern.Pattern.MatchString(line) {
matches = append(matches, strings.TrimSpace(line))
break
}
}
}

return matches
}

// ScanDockerfileForPattern checks if a Dockerfile matches a specific pattern
func ScanDockerfileForPattern(content string, pattern Pattern) bool {
lines := strings.Split(content, "\n")

for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
// Skip empty lines and comments
if trimmedLine == "" || strings.HasPrefix(trimmedLine, "#") {
continue
}

if pattern.Pattern.MatchString(line) {
return true
}
}

return false
}
Loading
Loading