Skip to content

Commit 21a0fcc

Browse files
Merge pull request #1256 from Checkmarx/feature/AST-108029-gitignore-filter
Added --use-gitignore flag (AST-108029)
2 parents 1060626 + 4369ad0 commit 21a0fcc

File tree

5 files changed

+335
-0
lines changed

5 files changed

+335
-0
lines changed

internal/commands/scan.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,7 @@ func scanCreateSubCommand(
870870

871871
// reading sbom-only flag
872872
createScanCmd.PersistentFlags().Bool(commonParams.SbomFlag, false, "Scan only the specified SBOM file (supported formats xml or json)")
873+
createScanCmd.PersistentFlags().Bool(commonParams.GitIgnoreFileFilterFlag, false, commonParams.GitIgnoreFileFilterUsage)
873874

874875
return createScanCmd
875876
}
@@ -1747,6 +1748,7 @@ func getUploadURLFromSource(cmd *cobra.Command, uploadsWrapper wrappers.UploadsW
17471748

17481749
scaResolverParams, scaResolver := getScaResolverFlags(cmd)
17491750
isSbom, _ := cmd.PersistentFlags().GetBool(commonParams.SbomFlag)
1751+
isGitIgnoreFilter, _ := cmd.Flags().GetBool(commonParams.GitIgnoreFileFilterFlag)
17501752
var directoryPath string
17511753
if isSbom {
17521754
sbomFile, _ := cmd.Flags().GetString(commonParams.SourcesFlag)
@@ -1763,6 +1765,29 @@ func getUploadURLFromSource(cmd *cobra.Command, uploadsWrapper wrappers.UploadsW
17631765
}
17641766
} else {
17651767
zipFilePath, directoryPath, err = definePathForZipFileOrDirectory(cmd)
1768+
if isGitIgnoreFilter {
1769+
gitIgnoreFilter, err := getGitignorePatterns(directoryPath, zipFilePath)
1770+
if err != nil {
1771+
return "", "", err
1772+
}
1773+
1774+
if len(gitIgnoreFilter) > 0 {
1775+
if sourceDirFilter == "" {
1776+
sourceDirFilter = gitIgnoreFilter[0]
1777+
for i := 1; i < len(gitIgnoreFilter); i++ {
1778+
if !strings.Contains(sourceDirFilter, gitIgnoreFilter[i]) {
1779+
sourceDirFilter += "," + gitIgnoreFilter[i]
1780+
}
1781+
}
1782+
} else {
1783+
for _, pattern := range gitIgnoreFilter {
1784+
if !strings.Contains(sourceDirFilter, pattern) {
1785+
sourceDirFilter += "," + pattern
1786+
}
1787+
}
1788+
}
1789+
}
1790+
}
17661791
}
17671792

17681793
if zipFilePath != "" && scaResolverPath != "" {
@@ -3249,3 +3274,97 @@ func isValidJSONOrXML(path string) (bool, error) {
32493274

32503275
return true, nil
32513276
}
3277+
3278+
func getGitignorePatterns(directoryPath, zipFilePath string) ([]string, error) {
3279+
var data []byte
3280+
var err error
3281+
if directoryPath != "" {
3282+
gitignorePath := filepath.Join(directoryPath, ".gitignore")
3283+
if _, err := os.Stat(gitignorePath); os.IsNotExist(err) {
3284+
return nil, fmt.Errorf(".gitignore file not found in directory: %s", directoryPath)
3285+
}
3286+
data, err = os.ReadFile(gitignorePath)
3287+
if err != nil {
3288+
return nil, err
3289+
}
3290+
}
3291+
if zipFilePath != "" {
3292+
data, err = readGitIgnoreFromZip(zipFilePath)
3293+
if err != nil {
3294+
return nil, err
3295+
}
3296+
}
3297+
lines := strings.Split(string(data), "\n")
3298+
var patterns []string
3299+
for _, line := range lines {
3300+
line = strings.TrimSpace(line)
3301+
3302+
// This condition skips lines that are empty, comments.
3303+
// Excluding the lines that contain negotiation characters like !, which are used to negate patterns
3304+
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "!") {
3305+
continue
3306+
}
3307+
// Convert build/** to build for path.Match() supported patterns
3308+
if strings.HasSuffix(line, "/**") {
3309+
line = strings.TrimSuffix(line, "/**")
3310+
}
3311+
// Convert **/temp/ to temp for path.Match() supported patterns
3312+
if strings.HasPrefix(line, "**/") {
3313+
line = strings.TrimPrefix(line, "**/")
3314+
}
3315+
// Convert temp/ to temp for path.Match() supported patterns
3316+
if strings.HasSuffix(line, "/") {
3317+
line = strings.TrimSuffix(line, "/")
3318+
}
3319+
3320+
// Convert LoginController[!0-3].java to LoginController[^0-3].java for path.Match() supported patterns
3321+
if strings.Contains(line, "!") {
3322+
line = strings.ReplaceAll(line, "!", "^")
3323+
}
3324+
patterns = append(patterns, "!"+line)
3325+
}
3326+
return patterns, nil
3327+
}
3328+
3329+
func readGitIgnoreFromZip(zipPath string) ([]byte, error) {
3330+
r, err := zip.OpenReader(zipPath)
3331+
if err != nil {
3332+
return []byte(""), fmt.Errorf("failed to open zip: %s", zipPath)
3333+
}
3334+
defer r.Close()
3335+
3336+
rootFolder := ""
3337+
if len(r.File) > 0 {
3338+
parts := strings.Split(r.File[0].Name, "/")
3339+
if len(parts) > 1 {
3340+
rootFolder = parts[0]
3341+
}
3342+
}
3343+
expectedGitignorePath := rootFolder + "/.gitignore"
3344+
3345+
for _, f := range r.File {
3346+
if f.Name != expectedGitignorePath {
3347+
continue
3348+
}
3349+
rc, err := f.Open()
3350+
if err != nil {
3351+
return []byte(""), fmt.Errorf("failed to open .gitignore inside zip: %w", err)
3352+
}
3353+
3354+
// Read file content
3355+
data, err := io.ReadAll(rc)
3356+
if err != nil {
3357+
err := rc.Close()
3358+
if err != nil {
3359+
return nil, err
3360+
}
3361+
return []byte(""), fmt.Errorf("failed to read .gitignore content inside zip : %w", err)
3362+
}
3363+
// Close with error handling
3364+
if err := rc.Close(); err != nil {
3365+
logger.PrintfIfVerbose("Error closing .gitignore reader: %v", err)
3366+
}
3367+
return data, nil
3368+
}
3369+
return []byte(""), fmt.Errorf(".gitignore not found in zip: %s", zipPath)
3370+
}

internal/commands/scan_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"log"
1111
"os"
12+
"path/filepath"
1213
"reflect"
1314
"strings"
1415
"testing"
@@ -2544,3 +2545,190 @@ func Test_CreateScanWithSbomFlag(t *testing.T) {
25442545

25452546
assert.ErrorContains(t, err, "Failed creating a scan: Input in bad format: failed to read file:")
25462547
}
2548+
2549+
func TestGetGitignorePatterns_DirPath_GitIgnore_NotFound(t *testing.T) {
2550+
dir := t.TempDir()
2551+
_, err := getGitignorePatterns(dir, "")
2552+
assert.ErrorContains(t, err, ".gitignore file not found in directory")
2553+
}
2554+
2555+
func TestGetGitignorePatterns_DirPath_GitIgnore_PermissionDenied(t *testing.T) {
2556+
dir := t.TempDir()
2557+
err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(""), 0000)
2558+
if err != nil {
2559+
t.Fatalf("Failed to write .gitignore: %v", err)
2560+
}
2561+
_, err = getGitignorePatterns(dir, "")
2562+
assert.ErrorContains(t, err, "permission denied")
2563+
}
2564+
2565+
func TestGetGitignorePatterns_DirPath_GitIgnore_EmptyPatternList(t *testing.T) {
2566+
dir := t.TempDir()
2567+
err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(""), 0644)
2568+
if err != nil {
2569+
t.Fatalf("Failed to write .gitignore: %v", err)
2570+
}
2571+
gitIgnoreFilter, err := getGitignorePatterns(dir, "")
2572+
if err != nil {
2573+
t.Fatalf("Error in fetching pattern from .gitignore file: %v", err)
2574+
}
2575+
assert.Assert(t, len(gitIgnoreFilter) == 0, "Expected no patterns from empty .gitignore file")
2576+
}
2577+
2578+
func TestGetGitignorePatterns_DirPath_GitIgnore_PatternList(t *testing.T) {
2579+
dir := t.TempDir()
2580+
err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(`src
2581+
src/
2582+
**/vullib
2583+
**/admin/
2584+
vulnerability/**
2585+
application-jira.yml
2586+
*.yml
2587+
LoginController[0-1].java
2588+
LoginController[!0-3].java
2589+
LoginController[01].java
2590+
LoginController[!456].java
2591+
?pplication-jira.yml
2592+
a*cation-jira.yml`), 0644)
2593+
if err != nil {
2594+
t.Fatalf("Failed to write .gitignore: %v", err)
2595+
}
2596+
gitIgnoreFilter, err := getGitignorePatterns(dir, "")
2597+
if err != nil {
2598+
t.Fatalf("Error in fetching pattern from .gitignore file: %v", err)
2599+
}
2600+
assert.Assert(t, len(gitIgnoreFilter) > 0, "Expected patterns from .gitignore file")
2601+
}
2602+
2603+
func TestGetGitignorePatterns_ZipPath_GitIgnore_FailedToOpenZipFIle(t *testing.T) {
2604+
dir := t.TempDir()
2605+
zipPath := filepath.Join(dir, "example.zip")
2606+
2607+
// Create the zip file
2608+
zipFile, err := os.Create(zipPath)
2609+
if err != nil {
2610+
t.Fatalf("Failed to create zip file: %v", err)
2611+
}
2612+
defer func(zipFile *os.File) {
2613+
err := zipFile.Close()
2614+
if err != nil {
2615+
t.Fatalf("Failed to close zip file: %v", err)
2616+
}
2617+
}(zipFile)
2618+
_, err = getGitignorePatterns("", zipPath)
2619+
assert.ErrorContains(t, err, "failed to open zip")
2620+
}
2621+
2622+
func TestGetGitignorePatterns_ZipPath_GitIgnore_NotFound(t *testing.T) {
2623+
dir := t.TempDir()
2624+
zipPath := filepath.Join(dir, "example.zip")
2625+
2626+
// Create the zip file
2627+
zipFile, err := os.Create(zipPath)
2628+
if err != nil {
2629+
t.Fatalf("Failed to create zip file: %v", err)
2630+
}
2631+
defer func(zipFile *os.File) {
2632+
err := zipFile.Close()
2633+
if err != nil {
2634+
t.Fatalf("Failed to close zip file: %v", err)
2635+
}
2636+
}(zipFile)
2637+
2638+
// Create a zip writer
2639+
zipWriter := zip.NewWriter(zipFile)
2640+
err = zipWriter.Close()
2641+
if err != nil {
2642+
return
2643+
}
2644+
2645+
_, err = getGitignorePatterns("", zipPath)
2646+
assert.ErrorContains(t, err, ".gitignore not found in zip")
2647+
}
2648+
2649+
func TestGetGitignorePatterns_ZipPath_GitIgnore_EmptyPatternList(t *testing.T) {
2650+
dir := t.TempDir()
2651+
zipPath := filepath.Join(dir, "example.zip")
2652+
2653+
// Create the zip file
2654+
zipFile, err := os.Create(zipPath)
2655+
if err != nil {
2656+
t.Fatalf("Failed to create zip file: %v", err)
2657+
}
2658+
defer func(zipFile *os.File) {
2659+
err := zipFile.Close()
2660+
if err != nil {
2661+
t.Fatalf("Failed to close zip file: %v", err)
2662+
}
2663+
}(zipFile)
2664+
2665+
// Create a zip writer
2666+
zipWriter := zip.NewWriter(zipFile)
2667+
2668+
// Add a file to the zip archive
2669+
fileInZip, err := zipWriter.Create("example" + "/.gitignore")
2670+
if err != nil {
2671+
t.Fatalf("Failed to add file to zip: %v", err)
2672+
}
2673+
2674+
_, err = fileInZip.Write([]byte(""))
2675+
if err != nil {
2676+
t.Fatalf("Failed to write data to zip: %v", err)
2677+
}
2678+
err = zipWriter.Close()
2679+
if err != nil {
2680+
return
2681+
}
2682+
2683+
gitIgnoreFilter, _ := getGitignorePatterns("", zipPath)
2684+
assert.Assert(t, len(gitIgnoreFilter) == 0, "Expected no patterns from empty .gitignore file")
2685+
}
2686+
2687+
func TestGetGitignorePatterns_ZipPath_GitIgnore_PatternList(t *testing.T) {
2688+
dir := t.TempDir()
2689+
zipPath := filepath.Join(dir, "example.zip")
2690+
2691+
// Create the zip file
2692+
zipFile, err := os.Create(zipPath)
2693+
if err != nil {
2694+
t.Fatalf("Failed to create zip file: %v", err)
2695+
}
2696+
defer func(zipFile *os.File) {
2697+
err := zipFile.Close()
2698+
if err != nil {
2699+
t.Fatalf("Failed to close zip file: %v", err)
2700+
}
2701+
}(zipFile)
2702+
2703+
// Create a zip writer
2704+
zipWriter := zip.NewWriter(zipFile)
2705+
2706+
// Add a file to the zip archive
2707+
fileInZip, err := zipWriter.Create("example" + "/.gitignore")
2708+
if err != nil {
2709+
t.Fatalf("Failed to add file to zip: %v", err)
2710+
}
2711+
_, err = fileInZip.Write([]byte(`src
2712+
src/
2713+
**/vullib
2714+
**/admin/
2715+
vulnerability/**
2716+
application-jira.yml
2717+
*.yml
2718+
LoginController[0-1].java
2719+
LoginController[!0-3].java
2720+
LoginController[01].java
2721+
LoginController[!456].java
2722+
?pplication-jira.yml
2723+
a*cation-jira.yml`))
2724+
if err != nil {
2725+
t.Fatalf("Failed to write data to zip: %v", err)
2726+
}
2727+
err = zipWriter.Close()
2728+
if err != nil {
2729+
return
2730+
}
2731+
2732+
gitIgnoreFilter, _ := getGitignorePatterns("", zipPath)
2733+
assert.Assert(t, len(gitIgnoreFilter) > 0, "Expected patterns from .gitignore file")
2734+
}

internal/params/flags.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ const (
169169
LogFileUsage = "Saves logs to the specified file path only"
170170
LogFileConsoleFlag = "log-file-console"
171171
LogFileConsoleUsage = "Saves logs to the specified file path as well as to the console"
172+
GitIgnoreFileFilterFlag = "use-gitignore"
173+
GitIgnoreFileFilterUsage = "Exclude files and directories from the scan based on the patterns defined in the directory's .gitignore file"
172174
// INDIVIDUAL FILTER FLAGS
173175
SastFilterFlag = "sast-filter"
174176
SastFilterUsage = "SAST filter"
13 KB
Binary file not shown.

test/integration/scan_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2544,3 +2544,29 @@ func TestCreateScan_SbomScanForNotExistingFile(t *testing.T) {
25442544
assert.ErrorContains(t, err, "Failed creating a scan: Input in bad format: failed to read file:")
25452545

25462546
}
2547+
2548+
func TestCreateScanFilterGitIgnoreFile_GitIgnoreNotFound(t *testing.T) {
2549+
args := []string{
2550+
"scan", "create",
2551+
flag(params.ProjectName), getProjectNameForScanTests(),
2552+
flag(params.BranchFlag), "dummy_branch",
2553+
flag(params.SourcesFlag), "data/insecure.zip",
2554+
flag(params.GitIgnoreFileFilterFlag),
2555+
}
2556+
2557+
err, _ := executeCommand(t, args...)
2558+
assert.ErrorContains(t, err, ".gitignore not found in zip")
2559+
}
2560+
2561+
func TestCreateScanFilterGitIgnoreFile_GitIgnoreExist(t *testing.T) {
2562+
args := []string{
2563+
"scan", "create",
2564+
flag(params.ProjectName), getProjectNameForScanTests(),
2565+
flag(params.BranchFlag), "dummy_branch",
2566+
flag(params.SourcesFlag), "data/sources-gitignore.zip",
2567+
flag(params.GitIgnoreFileFilterFlag),
2568+
}
2569+
2570+
err, _ := executeCommand(t, args...)
2571+
assert.NilError(t, err, "Scan creation with gitignore filter should pass without error")
2572+
}

0 commit comments

Comments
 (0)