Skip to content

Commit

Permalink
Add support for following symlinks (gitleaks#1010)
Browse files Browse the repository at this point in the history
* Add support for following symlinks

* Update detect/detect.go

Co-authored-by: Zachary Rice <zricezrice@gmail.com>

* Update cmd/detect.go

Co-authored-by: Zachary Rice <zricezrice@gmail.com>
  • Loading branch information
RickyGrassmuck and zricethezav authored Oct 13, 2022
1 parent e15ab0d commit 6d801ed
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 11 deletions.
7 changes: 7 additions & 0 deletions cmd/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ func init() {
rootCmd.AddCommand(detectCmd)
detectCmd.Flags().String("log-opts", "", "git log options")
detectCmd.Flags().Bool("no-git", false, "treat git repo as a regular directory and scan those files, --log-opts has no effect on the scan when --no-git is set")
detectCmd.Flags().Bool("follow-symlinks", false, "Scan files that are symlinks to other files")

}

var detectCmd = &cobra.Command{
Expand Down Expand Up @@ -89,6 +91,11 @@ func runDetect(cmd *cobra.Command, args []string) {
}
}

// set follow symlinks flag
if detector.FollowSymlinks, err = cmd.Flags().GetBool("follow-symlinks"); err != nil {
log.Fatal().Err(err).Msg("")
}

// set exit code
exitCode, err := cmd.Flags().GetInt("exit-code")
if err != nil {
Expand Down
43 changes: 38 additions & 5 deletions detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -52,6 +53,9 @@ type Detector struct {
// files larger than this will be skipped
MaxTargetMegaBytes int

// followSymlinks is a flag to enable scanning symlink files
FollowSymlinks bool

// commitMap is used to keep track of commits that have been scanned.
// This is only used for logging purposes and git scans.
commitMap map[string]bool
Expand Down Expand Up @@ -85,7 +89,8 @@ type Fragment struct {
Raw string

// FilePath is the path to the file if applicable
FilePath string
FilePath string
SymlinkFile string

// CommitSHA is the SHA of the commit if applicable
CommitSHA string
Expand Down Expand Up @@ -194,6 +199,7 @@ func (d *Detector) detectRule(fragment Fragment, rule config.Rule) []report.Find
finding := report.Finding{
Description: rule.Description,
File: fragment.FilePath,
SymlinkFile: fragment.SymlinkFile,
RuleID: rule.RuleID,
Match: fmt.Sprintf("file detected: %s", fragment.FilePath),
Tags: rule.Tags,
Expand Down Expand Up @@ -241,6 +247,7 @@ func (d *Detector) detectRule(fragment Fragment, rule config.Rule) []report.Find
finding := report.Finding{
Description: rule.Description,
File: fragment.FilePath,
SymlinkFile: fragment.SymlinkFile,
RuleID: rule.RuleID,
StartLine: loc.startLine,
EndLine: loc.endLine,
Expand Down Expand Up @@ -384,11 +391,16 @@ func (d *Detector) DetectGit(source string, logOpts string, gitScanType GitScanT
return d.findings, nil
}

type scanTarget struct {
Path string
Symlink string
}

// DetectFiles accepts a path to a source directory or file and begins a scan of the
// file or directory.
func (d *Detector) DetectFiles(source string) ([]report.Finding, error) {
s := semgroup.NewGroup(context.Background(), 4)
paths := make(chan string)
paths := make(chan scanTarget)
s.Go(func() error {
defer close(paths)
return filepath.Walk(source,
Expand All @@ -403,15 +415,33 @@ func (d *Detector) DetectFiles(source string) ([]report.Finding, error) {
return nil
}
if fInfo.Mode().IsRegular() {
paths <- path
paths <- scanTarget{
Path: path,
Symlink: "",
}
}
if fInfo.Mode().Type() == fs.ModeSymlink && d.FollowSymlinks {
realPath, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
realPathFileInfo, _ := os.Stat(realPath)
if realPathFileInfo.IsDir() {
log.Debug().Msgf("found symlinked directory: %s -> %s [skipping]", path, realPath)
return nil
}
paths <- scanTarget{
Path: realPath,
Symlink: path,
}
}
return nil
})
})
for pa := range paths {
p := pa
s.Go(func() error {
b, err := os.ReadFile(p)
b, err := os.ReadFile(p.Path)
if err != nil {
return err
}
Expand All @@ -426,7 +456,10 @@ func (d *Detector) DetectFiles(source string) ([]report.Finding, error) {

fragment := Fragment{
Raw: string(b),
FilePath: p,
FilePath: p.Path,
}
if p.Symlink != "" {
fragment.SymlinkFile = p.Symlink
}
for _, finding := range d.Detect(fragment) {
// need to add 1 since line counting starts at 1
Expand Down
57 changes: 57 additions & 0 deletions detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ func TestFromFiles(t *testing.T) {
Secret: "AKIALALEMEL33243OLIA",
Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
File: "../testdata/repos/nogit/main.go",
SymlinkFile: "",
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
Entropy: 3.0841837,
Expand Down Expand Up @@ -537,6 +538,7 @@ func TestFromFiles(t *testing.T) {
}
cfg, _ := vc.Translate()
detector := NewDetector(cfg)
detector.FollowSymlinks = true
findings, err := detector.DetectFiles(tt.source)
if err != nil {
t.Error(err)
Expand All @@ -546,6 +548,61 @@ func TestFromFiles(t *testing.T) {
}
}

func TestDetectWithSymlinks(t *testing.T) {
tests := []struct {
cfgName string
source string
expectedFindings []report.Finding
}{
{
source: filepath.Join(repoBasePath, "symlinks/file_symlink"),
cfgName: "simple",
expectedFindings: []report.Finding{
{
Description: "Asymmetric Private Key",
StartLine: 1,
EndLine: 1,
StartColumn: 1,
EndColumn: 35,
Match: "-----BEGIN OPENSSH PRIVATE KEY-----",
Secret: "-----BEGIN OPENSSH PRIVATE KEY-----",
Line: "-----BEGIN OPENSSH PRIVATE KEY-----",
File: "../testdata/repos/symlinks/source_file/id_ed25519",
SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519",
RuleID: "apkey",
Tags: []string{"key", "AsymmetricPrivateKey"},
Entropy: 3.587164,
Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1",
},
},
},
}

for _, tt := range tests {
viper.AddConfigPath(configPath)
viper.SetConfigName("simple")
viper.SetConfigType("toml")
err := viper.ReadInConfig()
if err != nil {
t.Error(err)
}

var vc config.ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
t.Error(err)
}
cfg, _ := vc.Translate()
detector := NewDetector(cfg)
detector.FollowSymlinks = true
findings, err := detector.DetectFiles(tt.source)
if err != nil {
t.Error(err)
}
assert.ElementsMatch(t, tt.expectedFindings, findings)
}
}

func moveDotGit(from, to string) error {
repoDirs, err := os.ReadDir("../testdata/repos")
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions report/csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func writeCsv(f []Finding, w io.WriteCloser) error {
err := cw.Write([]string{"RuleID",
"Commit",
"File",
"SymlinkFile",
"Secret",
"Match",
"StartLine",
Expand All @@ -35,6 +36,7 @@ func writeCsv(f []Finding, w io.WriteCloser) error {
err = cw.Write([]string{f.RuleID,
f.Commit,
f.File,
f.SymlinkFile,
f.Secret,
f.Match,
strconv.Itoa(f.StartLine),
Expand Down
1 change: 1 addition & 0 deletions report/csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestWriteCSV(t *testing.T) {
EndColumn: 2,
Message: "opps",
File: "auth.py",
SymlinkFile: "",
Commit: "0000000000000000",
Author: "John Doe",
Email: "johndoe@gmail.com",
Expand Down
6 changes: 3 additions & 3 deletions report/finding.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ type Finding struct {
Secret string

// File is the name of the file containing the finding
File string

Commit string
File string
SymlinkFile string
Commit string

// Entropy is the shannon entropy of Value
Entropy float32
Expand Down
1 change: 1 addition & 0 deletions report/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestWriteJSON(t *testing.T) {
EndColumn: 2,
Message: "opps",
File: "auth.py",
SymlinkFile: "",
Commit: "0000000000000000",
Author: "John Doe",
Email: "johndoe@gmail.com",
Expand Down
6 changes: 5 additions & 1 deletion report/sarif.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,15 @@ func getResults(findings []Finding) []Results {
}

func getLocation(f Finding) []Locations {
uri := f.File
if f.SymlinkFile != "" {
uri = f.SymlinkFile
}
return []Locations{
{
PhysicalLocation: PhysicalLocation{
ArtifactLocation: ArtifactLocation{
URI: f.File,
URI: uri,
},
Region: Region{
StartLine: f.StartLine,
Expand Down
4 changes: 2 additions & 2 deletions testdata/expected/report/csv_simple.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
RuleID,Commit,File,Secret,Match,StartLine,EndLine,StartColumn,EndColumn,Author,Message,Date,Email,Fingerprint
test-rule,0000000000000000,auth.py,a secret,line containing secret,1,2,1,2,John Doe,opps,10-19-2003,johndoe@gmail.com,fingerprint
RuleID,Commit,File,SymlinkFile,Secret,Match,StartLine,EndLine,StartColumn,EndColumn,Author,Message,Date,Email,Fingerprint
test-rule,0000000000000000,auth.py,,a secret,line containing secret,1,2,1,2,John Doe,opps,10-19-2003,johndoe@gmail.com,fingerprint
1 change: 1 addition & 0 deletions testdata/expected/report/json_simple.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Match": "line containing secret",
"Secret": "a secret",
"File": "auth.py",
"SymlinkFile": "",
"Commit": "0000000000000000",
"Entropy": 0,
"Author": "John Doe",
Expand Down
1 change: 1 addition & 0 deletions testdata/repos/symlinks/file_symlink/symlinked_id_ed25519
7 changes: 7 additions & 0 deletions testdata/repos/symlinks/source_file/id_ed25519
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XAAAAJDiKO3C4ijt
wgAAAAtzc2gtZWQyNTUxOQAAACA8YWKYztuuvxUIMomc3zv0OdXCT57Cc2cRYu3TMbX9XA
AAAECzmj8DGxg5YHtBK4AmBttMXDQHsPAaCyYHQjJ4YujRBTxhYpjO266/FQgyiZzfO/Q5
1cJPnsJzZxFi7dMxtf1cAAAADHJvb3RAZGV2aG9zdAE=
-----END OPENSSH PRIVATE KEY-----

0 comments on commit 6d801ed

Please sign in to comment.