-
Notifications
You must be signed in to change notification settings - Fork 4
Containerfile scan #484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Containerfile scan #484
Changes from 6 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
76e5261
Add GitLab container scan command to detect dangerous Dockerfile patt…
frjcomp 610c169
Add GitHub container scan command
frjcomp d7be756
Implement recursive Dockerfile discovery for GitHub and GitLab
frjcomp 4c4cd8b
Replace hardcoded GitLab paths with recursive tree API discovery
frjcomp f2cb3a7
Add --public flag to GitHub container scan
frjcomp d76173c
refactor: remove dockerignore checks and optimize container scanning
frjcomp 2445fdf
stash
frjcomp 5f392d2
test: update gitlab container test to use artipacked command name
frjcomp 7295171
PR #484: Remove custom patterns and add GitHub container e2e tests
frjcomp c399d47
Address PR review comments: remove severity fields and extract shared…
frjcomp 69f61b4
Fix golangci-lint issues: replace deprecated github.String with githu…
frjcomp 3a216b4
refactor: nest artipacked container commands
frjcomp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this file!