Skip to content
Open
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
109 changes: 109 additions & 0 deletions internal/cmd/github/container/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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: "Artipacked auditing commands",
Long: "Commands to audit for artipacked misconfiguration in container builds: when Dockerfiles copy secrets during build and leave them in published images.",
}

containerCmd.AddCommand(NewArtipackedCmd())

return containerCmd
}

func NewArtipackedCmd() *cobra.Command {
artipackedCmd := &cobra.Command{
Use: "artipacked [no options!]",
Short: "Audit for artipacked misconfiguration (secrets in container images)",
Long: "Scan GitHub repositories for artipacked misconfiguration: dangerous container build patterns that leak secrets like COPY . /path without .dockerignore",
Run: func(cmd *cobra.Command, args []string) {
if err := config.AutoBindFlags(cmd, map[string]string{
"github": "github.url",
"token": "github.token",
"owned": "github.container.artipacked.owned",
"member": "github.container.artipacked.member",
"public": "github.container.artipacked.public",
"repo": "github.container.artipacked.repo",
"organization": "github.container.artipacked.organization",
"search": "github.container.artipacked.search",
"page": "github.container.artipacked.page",
"order-by": "github.container.artipacked.order_by",
"dangerous-patterns": "github.container.artipacked.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.artipacked.owned")
member = config.GetBool("github.container.artipacked.member")
public = config.GetBool("github.container.artipacked.public")
repository = config.GetString("github.container.artipacked.repo")
organization = config.GetString("github.container.artipacked.organization")
projectSearchQuery = config.GetString("github.container.artipacked.search")
page = config.GetInt("github.container.artipacked.page")
orderBy = config.GetString("github.container.artipacked.order_by")

Scan(githubUrl, githubApiToken)
},
}

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

return artipackedCmd
}

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: "Artipacked auditing commands",
Long: "Commands to audit for artipacked misconfiguration in container builds: when Dockerfiles copy secrets during build and leave them in published images.",
}

containerCmd.AddCommand(NewArtipackedCmd())

return containerCmd
}

func NewArtipackedCmd() *cobra.Command {
artipackedCmd := &cobra.Command{
Use: "artipacked [no options!]",
Short: "Audit for artipacked misconfiguration (secrets in container images)",
Long: "Scan GitLab projects for artipacked misconfiguration: dangerous container build patterns that leak secrets like COPY . /path without .dockerignore",
Run: func(cmd *cobra.Command, args []string) {
if err := config.AutoBindFlags(cmd, map[string]string{
"gitlab": "gitlab.url",
"token": "gitlab.token",
"owned": "gitlab.container.artipacked.owned",
"member": "gitlab.container.artipacked.member",
"repo": "gitlab.container.artipacked.repo",
"namespace": "gitlab.container.artipacked.namespace",
"search": "gitlab.container.artipacked.search",
"page": "gitlab.container.artipacked.page",
"order-by": "gitlab.container.artipacked.order_by",
"dangerous-patterns": "gitlab.container.artipacked.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.artipacked.owned")
member = config.GetBool("gitlab.container.artipacked.member")
repository = config.GetString("gitlab.container.artipacked.repo")
namespace = config.GetString("gitlab.container.artipacked.namespace")
projectSearchQuery = config.GetString("gitlab.container.artipacked.search")
page = config.GetInt("gitlab.container.artipacked.page")
orderBy = config.GetString("gitlab.container.artipacked.order_by")

Scan(gitlabUrl, gitlabApiToken)
},
}

artipackedCmd.PersistentFlags().BoolVarP(&owned, "owned", "o", false, "Scan user owned projects only")
artipackedCmd.PersistentFlags().BoolVarP(&member, "member", "m", false, "Scan projects the user is member of")
artipackedCmd.Flags().StringVarP(&repository, "repo", "r", "", "Repository to scan (if not set, all projects will be scanned)")
artipackedCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Namespace to scan")
artipackedCmd.Flags().StringVarP(&projectSearchQuery, "search", "s", "", "Query string for searching projects")
artipackedCmd.Flags().IntVarP(&page, "page", "p", 1, "Page number to start fetching projects from (default 1, fetch all pages)")
artipackedCmd.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 artipackedCmd
}

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
31 changes: 31 additions & 0 deletions pkg/container/patterns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package container

import (
"regexp"
)

// 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*)?$`),
Description: "Copies entire working directory to root - exposes all files including secrets",
},
{
Name: "copy_all_anywhere",
Pattern: regexp.MustCompile(`(?i)^COPY\s+(\./?|\*|\.\/\*|\.\*)(\s+|$)`),
Description: "Copies entire working directory into container - may expose sensitive files",
},
{
Name: "add_all_to_root",
Pattern: regexp.MustCompile(`(?i)^ADD\s+\./?(\s+/\s*)?$`),
Description: "Adds entire working directory to root - exposes all files including secrets",
},
{
Name: "add_all_anywhere",
Pattern: regexp.MustCompile(`(?i)^ADD\s+(\./?|\*|\.\/\*|\.\*)(\s+|$)`),
Description: "Adds entire working directory into container - may expose sensitive files",
},
}
}
110 changes: 110 additions & 0 deletions pkg/container/scanner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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
}

// PatternMatch represents a matched pattern with details
type PatternMatch struct {
PatternName string
MatchedLine string
}

// ScanDockerfileForPatterns scans Dockerfile content and returns all pattern matches
func ScanDockerfileForPatterns(content string, patterns []Pattern) []PatternMatch {
var matches []PatternMatch
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, PatternMatch{
PatternName: pattern.Name,
MatchedLine: strings.TrimSpace(line),
})
break // Only match once per pattern
}
}
}

return matches
}

// ScanDockerfileContent checks a Dockerfile's content against patterns and returns matched lines
// Deprecated: Use ScanDockerfileForPatterns instead
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