diff --git a/.github/workflows/gobuild.yml b/.github/workflows/gobuild.yml
index 2779c9f..6c99de5 100644
--- a/.github/workflows/gobuild.yml
+++ b/.github/workflows/gobuild.yml
@@ -13,7 +13,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
- go-version: 1.18
+ go-version: 1.21
- name: Build executables
run: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9ecc54e..e9bb3af 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,7 +20,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
- go-version: 1.18
+ go-version: 1.21
- name: Run GoReleaser and release executables
uses: goreleaser/goreleaser-action@v2
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4007b40..6fc8537 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,7 +12,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
- go-version: 1.18
+ go-version: 1.21
- name: Test
run: |
diff --git a/.golangci.yaml b/.golangci.yaml
index 7cdf1ea..1b2753b 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,14 +1,11 @@
linters:
enable:
- - gofumpt
- gofmt
- gosimple
- - godot
- godox
- dupl
- funlen
- gocritic
- goprintffuncname
- - ifshort
presets:
- unused
diff --git a/Dockerfile b/Dockerfile
index e55295e..e5cde6b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.17-alpine
+FROM golang:1.21-alpine
WORKDIR /src/
COPY . /src/
diff --git a/README.md b/README.md
index 83d4357..82440f8 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Pillager is designed to provide a simple means of leveraging Go's strong concurr
directories for sensitive information in files. Pillager does this by standing on the shoulders
of [a few giants](#shoulders-of-giants). Once pillager finds files that match the specified pattern, the file is scanned
using a series of concurrent workers that each take a line of the file from the job queue and hunt for sensitive pattern
-matches. The available pattern filters can be defined in a rules.toml file or you can use the default ruleset.
+matches. The available pattern filters can be defined in a pillager.toml file or you can use the default ruleset.
## Installation
@@ -31,7 +31,7 @@ matches. The available pattern filters can be defined in a rules.toml file or yo
If you have Go setup on your system, you can install Pillager with `go get`
```shell script
-go get github.com/brittonhayes/pillager
+go install github.com/brittonhayes/pillager@latest
```
### Scoop (Windows)
@@ -79,29 +79,56 @@ Pillager provides a terminal user interface built with [bubbletea](https://githu
### Gitleaks Rules
Pillager provides full support for Gitleaks[^2] rules. This can either be passed
-in with a rules.toml[^1] file, or you can use the default ruleset by leaving the rules flag blank.
+in with a rules[^1] section in your pillager.toml file, or you can use the default ruleset by leaving the config flag blank.
[^1]: [Gitleaks Rules Reference](https://github.com/zricethezav/gitleaks/blob/57f9bc83d169bea363f2990a4de334b54efc3d7d/config/gitleaks.toml)
```toml
-# rules.toml
-title = "pillager rules"
+# pillager.toml
+# Basic configuration
+verbose = false
+path = "."
+workers = 4
+redact = true
+reporter = "json-pretty"
+
+# Rules for secret detection
+[[rules]]
+description = "AWS Access Key"
+id = "aws-access-key"
+regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
+tags = ["aws", "credentials"]
[[rules]]
-id = "gitlab-pat"
-description = "GitLab Personal Access Token"
-regex = '''glpat-[0-9a-zA-Z\-\_]{20}'''
+description = "AWS Secret Key"
+id = "aws-secret-key"
+regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
+tags = ["aws", "credentials"]
[[rules]]
-id = "aws-access-token"
-description = "AWS"
-regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
+description = "GitHub Token"
+id = "github-token"
+regex = '''ghp_[0-9a-zA-Z]{36}'''
+tags = ["github", "token"]
-# Cryptographic keys
[[rules]]
-id = "PKCS8-PK"
-description = "PKCS8 private key"
-regex = '''-----BEGIN PRIVATE KEY-----'''
+description = "Private Key"
+id = "private-key"
+regex = '''-----BEGIN (?:RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY( BLOCK)?-----'''
+tags = ["key", "private"]
+
+# Allowlist configuration
+[allowlist]
+paths = [
+ ".*/_test\\.go$",
+ ".*/testdata/.*",
+ ".*\\.md$",
+ ".*/vendor/.*"
+]
+regexes = [
+ "EXAMPLE_KEY",
+ "DUMMY_SECRET"
+]
```
### Built-in Output Formats
@@ -132,10 +159,10 @@ pillager hunt ./example -f json | jq
pillager hunt . -f yaml
```
-#### TOML
+#### JSON Pretty
```shell
-pillager hunt . -f toml
+pillager hunt . -f json-pretty
```
#### HTML
@@ -171,14 +198,14 @@ pillager hunt . --template "{{ range .}}Secret: {{.Secret}}{{end}}"
#### Custom Go Template from File
```shell
-pillager hunt . -t "$(cat pkg/templates/simple.tmpl)"
+pillager hunt . -t "$(cat internal/templates/simple.tmpl)"
```
### Custom Templates
-Pillager allows you to use powerful `go text/template` to customize the output format. Here are a few template examples.
+Pillager allows you to use powerful `go text/template` and [sprig](https://masterminds.github.io/sprig/) functions to customize the output format. Here are a few template examples.
#### Basic
@@ -203,16 +230,11 @@ Pillager allows you to use powerful `go text/template` to customize the output f
```
-> More template examples can be found in the [templates](./pkg/templates) directory.
+> More template examples can be found in the [templates](./internal/templates) directory.
## Documentation
-:books: [View the docs](pkg/hunter)
-
-GoDoc documentation is available on [pkg.go.dev for pillager](https://pkg.go.dev/github.com/brittonhayes/pillager) but
-it is also available for all packages in the repository in markdown format. Just open the folder of any package, and
-you'll see the GoDocs rendered in beautiful Github-flavored markdown thanks to the
-awesome [gomarkdoc](https://github.com/princjef/gomarkdoc) tool.
+GoDoc documentation is available on [pkg.go.dev for pillager](https://pkg.go.dev/github.com/brittonhayes/pillager).
## Development
@@ -237,7 +259,7 @@ If you've seen a CLI written in Go before, there's a pretty high chance it was b
library enough. It empowers developers to make consistent, dynamic, and self-documenting command line tools with ease.
Some examples include `kubectl`, `hugo`, and Github's `gh` CLI.
-#### [Gitleaks](https://github.com/zricethezav/gitleaks)
+#### [Gitleaks](https://github.com/gitleaks/gitleaks)
**What is Gitleaks?**
@@ -248,9 +270,9 @@ it's worth your time to check it out.
**Why is Gitleaks relevant to Pillager?**
-[^2]: [Gitleaks](https://github.com/zricethezav/gitleaks)
+[^2]: [Gitleaks](https://github.com/gitleaks/gitleaks)
-Pillager implements the powerful [rules](https://github.com/zricethezav/gitleaks#rules-summary) functionality of
+Pillager implements the powerful [rules](https://github.com/gitleaks/gitleaks#rules-summary) functionality of
Gitleaks while taking a different approach to presenting and handling the secrets found. While I have provided a
baseline set of default rules, Pillager becomes much more powerful if you allow users to create rules for their own
use-cases.
diff --git a/Taskfile.yml b/Taskfile.yml
index d60a1d9..24f010f 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -17,8 +17,8 @@ tasks:
cmds:
- rm bin/pillager
- go clean -cache
- - rm -rf pkg/hunter/testdata
- - mkdir pkg/hunter/testdata
+ - rm -rf pkg/pillager/testdata
+ - mkdir pkg/pillager/testdata
lint:
desc: runs golint
diff --git a/_examples/hunter/main.go b/_examples/hunter/main.go
deleted file mode 100644
index f65c999..0000000
--- a/_examples/hunter/main.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package main
-
-import (
- "os"
-
- "github.com/brittonhayes/pillager/pkg/format"
- "github.com/brittonhayes/pillager/pkg/hunter"
- "github.com/rs/zerolog"
- "github.com/rs/zerolog/log"
-)
-
-func main() {
- // Create a new hunter config
- h, err := hunter.New(
- hunter.WithScanPath("."),
- hunter.WithWorkers(2),
- hunter.WithFormat(format.Simple{}),
- hunter.WithLogLevel(zerolog.DebugLevel.String()),
- )
- if err != nil {
- log.Fatal().Err(err).Send()
- }
-
- // Start hunting
- results, err := h.Hunt()
- if err != nil {
- log.Fatal().Err(err).Send()
- }
-
- // Report results
- if err = h.Report(os.Stdout, results); err != nil {
- log.Fatal().Err(err).Send()
- }
-}
diff --git a/_examples/scanner/main.go b/_examples/scanner/main.go
new file mode 100644
index 0000000..d7f489a
--- /dev/null
+++ b/_examples/scanner/main.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "fmt"
+ "runtime"
+
+ "github.com/brittonhayes/pillager"
+ "github.com/brittonhayes/pillager/pkg/scanner"
+)
+
+func main() {
+ // Set scanner options
+ opts := pillager.Options{
+ Path: ".",
+ Workers: runtime.NumCPU(),
+ Reporter: "json",
+ Redact: true,
+ }
+
+ // Create a new gitleaks scanner
+ s, _ := scanner.NewGitleaksScanner(opts)
+
+ // Scan the current directory
+ results, _ := s.Scan(opts.Path)
+
+ // Report results
+ fmt.Println(results)
+}
diff --git a/cmd/pillager/main.go b/cmd/pillager/main.go
index e54566f..86b6841 100644
--- a/cmd/pillager/main.go
+++ b/cmd/pillager/main.go
@@ -1,10 +1,19 @@
-// Package pillager is the entrypoint to the Pillager CLI
package main
import (
- pillager "github.com/brittonhayes/pillager/internal/commands"
+ "os"
+
+ "github.com/brittonhayes/pillager/internal/commands"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
)
+func init() {
+ // Setup default logging
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+ zerolog.SetGlobalLevel(zerolog.InfoLevel)
+}
+
func main() {
- pillager.Execute()
+ commands.Execute()
}
diff --git a/doc.go b/doc.go
deleted file mode 100644
index 23d5449..0000000
--- a/doc.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-Copyright © 2020 Britton Hayes
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-*/
-
-// Package pillager is a tool for hunting through filesystems for sensitive information.
-//
-// Installation
-//
-// Go
-//
-// go get github.com/brittonhayes/pillager
-//
-// Windows
-//
-// scoop bucket add pillager https://github.com/brittonhayes/pillager-scoop.git
-// scoop install pillager
-//
-// OSX/Linux
-//
-// brew tap brittonhayes/homebrew-pillager
-// brew install pillager
-//
-//go:generate golangci-lint run ./...
-//go:generate gomarkdoc ./pkg/hunter/...
-//go:generate gomarkdoc ./pkg/rules/...
-//go:generate gomarkdoc ./pkg/format/...
-package pillager
diff --git a/go.mod b/go.mod
index 94f7744..54a0227 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/brittonhayes/pillager
-go 1.18
+go 1.21
require (
github.com/BurntSushi/toml v1.1.0
diff --git a/internal/commands/hunt.go b/internal/commands/hunt.go
index 7f29ac9..7d93576 100644
--- a/internal/commands/hunt.go
+++ b/internal/commands/hunt.go
@@ -4,14 +4,16 @@
package commands
import (
+ "fmt"
"os"
"runtime"
- "github.com/brittonhayes/pillager/pkg/format"
- "github.com/brittonhayes/pillager/pkg/hunter"
- "github.com/brittonhayes/pillager/pkg/rules"
+ "github.com/brittonhayes/pillager"
+ "github.com/brittonhayes/pillager/pkg/scanner"
"github.com/brittonhayes/pillager/pkg/tui/model"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
@@ -19,11 +21,11 @@ var (
verbose bool
redact bool
level string
- rulesConfig string
reporter string
templ string
workers int
interactive bool
+ config string
)
// huntCmd represents the hunt command.
@@ -56,46 +58,77 @@ var huntCmd = &cobra.Command{
Custom Go Template Format from Template File:
pillager hunt ./example --template "$(cat pkg/templates/simple.tmpl)"
`,
- Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
- // Read gitleaks config from file
- // or fallback to default
- gitleaksConfig := rules.NewLoader(
- rules.WithFile(rulesConfig),
- ).Load()
+ if level != "" {
+ lvl, err := zerolog.ParseLevel(level)
+ if err != nil {
+ return fmt.Errorf("invalid log level: %w", err)
+ }
+ zerolog.SetGlobalLevel(lvl)
+ }
- h, err := hunter.New(
- hunter.WithGitleaksConfig(gitleaksConfig),
- hunter.WithScanPath(args[0]),
- hunter.WithWorkers(workers),
- hunter.WithVerbose(verbose),
- hunter.WithTemplate(templ),
- hunter.WithRedact(redact),
- hunter.WithFormat(format.StringToReporter(reporter)),
- hunter.WithLogLevel(level),
- )
+ configLoader := scanner.NewConfigLoader()
+ opts, err := configLoader.LoadConfig(config)
if err != nil {
- return err
+ if config != "" {
+ return fmt.Errorf("failed to load config: %w", err)
+ }
+ opts = &pillager.Options{
+ Workers: runtime.NumCPU(),
+ Verbose: false,
+ Template: "",
+ Redact: false,
+ Reporter: "json",
+ }
+ }
+
+ // Get path from args if provided, otherwise use config path
+ scanPath := ""
+ if len(args) > 0 {
+ scanPath = args[0]
+ }
+
+ // Merge command line flags with config file
+ flagOpts := &pillager.Options{
+ Path: scanPath,
+ Redact: redact,
+ Verbose: verbose,
+ Workers: workers,
+ Reporter: reporter,
+ Template: templ,
+ }
+ configLoader.MergeWithFlags(opts, flagOpts)
+
+ // Check if path is provided either via args or config
+ if opts.Path == "" {
+ return fmt.Errorf("scan path must be provided either as an argument or in the config file")
+ }
+
+ s, err := scanner.NewGitleaksScanner(*opts)
+ if err != nil {
+ return fmt.Errorf("failed to create scanner: %w", err)
}
if interactive {
- return runInteractive(h)
+ return runInteractive(s)
}
- results, err := h.Hunt()
+ results, err := s.Scan()
if err != nil {
return err
}
- if err = h.Report(os.Stdout, results); err != nil {
- return err
+ if len(results) == 0 {
+ fmt.Println("[]")
+ log.Debug().Msg("no secrets or sensitive information were found at the target directory")
+ return nil
}
- return nil
+ return s.Reporter().Report(os.Stdout, results)
},
}
-func runInteractive(h *hunter.Hunter) error {
+func runInteractive(h scanner.Scanner) error {
m := model.NewModel(h)
p := tea.NewProgram(m, tea.WithAltScreen())
return p.Start()
@@ -106,9 +139,9 @@ func init() {
huntCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "run in interactive mode")
huntCmd.Flags().IntVarP(&workers, "workers", "w", runtime.NumCPU(), "number of concurrent workers")
huntCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "enable scanner verbose output")
- huntCmd.Flags().StringVarP(&level, "log-level", "l", "error", "set logging level")
- huntCmd.Flags().StringVarP(&rulesConfig, "rules", "r", "", "path to gitleaks rules.toml config")
- huntCmd.Flags().StringVarP(&reporter, "format", "f", "json", "set secret reporter (json, yaml)")
+ huntCmd.Flags().StringVarP(&level, "log-level", "l", "info", "set logging level")
+ huntCmd.Flags().StringVarP(&config, "config", "c", "", "path to pillager config file")
+ huntCmd.Flags().StringVarP(&reporter, "format", "f", "json-pretty", "set secret reporter format (json, yaml, html, html-table, table, markdown)")
huntCmd.Flags().BoolVar(&redact, "redact", false, "redact secret from results")
huntCmd.Flags().StringVarP(&templ, "template", "t", "", "set go text/template string for output format")
}
diff --git a/internal/templates/html-table.tmpl b/internal/templates/html-table.tmpl
new file mode 100644
index 0000000..e6b37cd
--- /dev/null
+++ b/internal/templates/html-table.tmpl
@@ -0,0 +1,175 @@
+
+
+
+
+
+ Pillager - Scan Results
+
+
+
+
+
+
+
+
+
+ Pillager
+
+
+ Results of your latest hunt from {{ now.Format "2006-01-02 15:04 MST" }}
+
+
+ {{/* Group results by file using dict/values */}}
+ {{ $grouped := dict }}
+ {{ range . }}
+ {{ $findings := index $grouped .File | default list }}
+ {{ $grouped = set $grouped .File (append $findings .) }}
+ {{ end }}
+
+ {{ range $file, $findings := $grouped }}
+
+
+ {{ $file }}
+ {{ len $findings }} findings
+
+
+
+
+
+ Line |
+ Secret |
+
+
+
+ {{ range $findings }}
+
+ {{.StartLine}} |
+ {{.Secret}} |
+
+ {{ end }}
+
+
+
+
+ {{ end }}
+
+
+
+
+
diff --git a/internal/templates/html.tmpl b/internal/templates/html.tmpl
new file mode 100644
index 0000000..103fcd1
--- /dev/null
+++ b/internal/templates/html.tmpl
@@ -0,0 +1,192 @@
+
+
+
+
+
+ Pillager - Scan Results
+
+
+
+
+
+
+
+
+
+ Pillager
+
+
+ Results of your latest hunt from {{ now.Format "2006-01-02 15:04 MST" }}
+
+
+
+ {{ range . }}
+
+
{{.File}}
+
{{.Secret}}
+
+ View JSON
+ Line: {{.StartLine}}
+
+
+
+ {{end}}
+
+
+
+
+
+
+
diff --git a/internal/templates/markdown.tmpl b/internal/templates/markdown.tmpl
new file mode 100644
index 0000000..21000a9
--- /dev/null
+++ b/internal/templates/markdown.tmpl
@@ -0,0 +1,25 @@
+# Pillager
+Results of your latest hunt from {{ now.Format "2006-01-02 15:04 MST" }}
+
+{{ len . }} Findings
+
+{{ range . -}}
+
+Finding in `{{ .File }}` (Line {{ .StartLine }})
+
+- **Location**: Line {{ .StartLine }} to {{ .EndLine }}
+{{- if .RuleID }}
+- **Rule Matched**: {{ .RuleID }}
+{{- end }}
+{{- if .Description }}
+- **Description**: {{ .Description }}
+{{- end }}
+- **Secret**: {{ .Secret }}
+- **Match Context**: `{{ .Match }}`
+{{- if .StartColumn }}
+- **Position**: Column {{ .StartColumn }}
+{{- end }}
+
+
+
+{{end}}
diff --git a/pkg/templates/simple.tmpl b/internal/templates/simple.tmpl
similarity index 100%
rename from pkg/templates/simple.tmpl
rename to internal/templates/simple.tmpl
diff --git a/pkg/templates/table.tmpl b/internal/templates/table.tmpl
similarity index 64%
rename from pkg/templates/table.tmpl
rename to internal/templates/table.tmpl
index 1a65f34..1f9d6ac 100644
--- a/pkg/templates/table.tmpl
+++ b/internal/templates/table.tmpl
@@ -1,3 +1,6 @@
+# Pillager
+Results of your latest hunt from {{ now.Format "2006-01-02 15:04 MST" }}
+
| File | Line | Secret |
| --------| ---------| -------- |
{{ range . -}}
diff --git a/internal/templates/templates.go b/internal/templates/templates.go
new file mode 100644
index 0000000..5d7d3c6
--- /dev/null
+++ b/internal/templates/templates.go
@@ -0,0 +1,33 @@
+// Package templates contains a compilation of go templates for rendering secret findings.
+package templates
+
+import (
+ _ "embed"
+)
+
+var (
+ //go:embed simple.tmpl
+ Simple string
+
+ //go:embed html.tmpl
+ HTML string
+
+ //go:embed markdown.tmpl
+ Markdown string
+
+ //go:embed table.tmpl
+ Table string
+
+ //go:embed html-table.tmpl
+ HTMLTable string
+)
+
+// DefaultTemplate is the base template used to format a Finding into the
+// custom output format.
+const DefaultTemplate = `{{ with . -}}
+{{ range . -}}
+Line: {{ quote .StartLine}}
+File: {{ quote .File }}
+Secret: {{ quote .Secret }}
+---
+{{ end -}}{{- end}}`
diff --git a/pillager.go b/pillager.go
new file mode 100644
index 0000000..01129cc
--- /dev/null
+++ b/pillager.go
@@ -0,0 +1,96 @@
+/*
+Copyright © 2020 Britton Hayes
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+// Package pillager is a tool for hunting through filesystems for sensitive information.
+//
+// # Installation
+//
+// Go
+//
+// go install github.com/brittonhayes/pillager@latest
+//
+// Windows
+//
+// scoop bucket add pillager https://github.com/brittonhayes/pillager-scoop.git
+// scoop install pillager
+//
+// OSX/Linux
+//
+// brew tap brittonhayes/homebrew-pillager
+// brew install pillager
+//
+//go:generate golangci-lint run ./...
+package pillager
+
+// Finding contains information about strings that
+// have been captured by a tree-sitter query.
+type Finding struct {
+ Description string
+ StartLine int
+ EndLine int
+ StartColumn int
+ EndColumn int
+
+ Match string
+
+ // Secret contains the full content of what is matched in
+ // the tree-sitter query.
+ Secret string
+
+ // File is the name of the file containing the finding
+ File string
+
+ // Entropy is the shannon entropy of Value
+ Entropy float32
+
+ // Rule is the name of the rule that was matched
+ RuleID string
+}
+
+// Options holds configuration for scanners
+type Options struct {
+ Path string `toml:"path"`
+ Template string `toml:"template"`
+ Workers int `toml:"workers"`
+ Verbose bool `toml:"verbose"`
+ Redact bool `toml:"redact"`
+ Reporter string `toml:"reporter"`
+ Rules []Rule `toml:"rules"`
+ Allowlist Allowlist `toml:"allowlist"`
+}
+
+// Rule represents a scanning rule
+type Rule struct {
+ ID string `toml:"id"`
+ Description string `toml:"description"`
+ Path string `toml:"path"`
+ Regex string `toml:"regex"`
+ Keywords []string `toml:"keywords"`
+ Tags []string `toml:"tags"`
+ Allowlist Allowlist
+}
+
+// Allowlist represents paths and patterns to ignore
+type Allowlist struct {
+ Paths []string `toml:"paths"`
+ Regexes []string `toml:"regexes"`
+}
diff --git a/pillager.toml b/pillager.toml
new file mode 100644
index 0000000..ff1853c
--- /dev/null
+++ b/pillager.toml
@@ -0,0 +1,44 @@
+# Basic configuration
+verbose = false
+path = "."
+workers = 4
+redact = true
+reporter = "json-pretty"
+
+# Rules for secret detection
+[[rules]]
+description = "AWS Access Key"
+id = "aws-access-key"
+regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
+tags = ["aws", "credentials"]
+
+[[rules]]
+description = "AWS Secret Key"
+id = "aws-secret-key"
+regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
+tags = ["aws", "credentials"]
+
+[[rules]]
+description = "GitHub Token"
+id = "github-token"
+regex = '''ghp_[0-9a-zA-Z]{36}'''
+tags = ["github", "token"]
+
+[[rules]]
+description = "Private Key"
+id = "private-key"
+regex = '''-----BEGIN (?:RSA|OPENSSH|DSA|EC|PGP) PRIVATE KEY( BLOCK)?-----'''
+tags = ["key", "private"]
+
+# Allowlist configuration
+[allowlist]
+paths = [
+ ".*/_test\\.go$",
+ ".*/testdata/.*",
+ ".*\\.md$",
+ ".*/vendor/.*"
+]
+regexes = [
+ "EXAMPLE_KEY",
+ "DUMMY_SECRET"
+]
\ No newline at end of file
diff --git a/pkg/format/README.md b/pkg/format/README.md
deleted file mode 100755
index 15c33cc..0000000
--- a/pkg/format/README.md
+++ /dev/null
@@ -1,172 +0,0 @@
-
-
-# format
-
-```go
-import "github.com/brittonhayes/pillager/pkg/format"
-```
-
-Package format contains the renderer and available output formats
-
-## Index
-
-- [type Custom](<#type-custom>)
- - [func (c Custom) Report(w io.Writer, findings []report.Finding) error](<#func-custom-report>)
- - [func (c *Custom) WithTemplate(t string)](<#func-custom-withtemplate>)
-- [type HTML](<#type-html>)
- - [func (h HTML) Report(w io.Writer, findings []report.Finding) error](<#func-html-report>)
-- [type HTMLTable](<#type-htmltable>)
- - [func (h HTMLTable) Report(w io.Writer, findings []report.Finding) error](<#func-htmltable-report>)
-- [type JSON](<#type-json>)
- - [func (j JSON) Report(w io.Writer, findings []report.Finding) error](<#func-json-report>)
-- [type Markdown](<#type-markdown>)
- - [func (m Markdown) Report(w io.Writer, findings []report.Finding) error](<#func-markdown-report>)
-- [type Reporter](<#type-reporter>)
- - [func StringToReporter(s string) Reporter](<#func-stringtoreporter>)
-- [type Simple](<#type-simple>)
- - [func (s Simple) Report(w io.Writer, findings []report.Finding) error](<#func-simple-report>)
-- [type TOML](<#type-toml>)
- - [func (t TOML) Report(w io.Writer, findings []report.Finding) error](<#func-toml-report>)
-- [type Table](<#type-table>)
- - [func (t Table) Report(w io.Writer, findings []report.Finding) error](<#func-table-report>)
-- [type YAML](<#type-yaml>)
- - [func (y YAML) Report(w io.Writer, findings []report.Finding) error](<#func-yaml-report>)
-
-
-## type [Custom]()
-
-```go
-type Custom struct {
- // contains filtered or unexported fields
-}
-```
-
-### func \(Custom\) [Report]()
-
-```go
-func (c Custom) Report(w io.Writer, findings []report.Finding) error
-```
-
-### func \(\*Custom\) [WithTemplate]()
-
-```go
-func (c *Custom) WithTemplate(t string)
-```
-
-## type [HTML]()
-
-```go
-type HTML struct{}
-```
-
-### func \(HTML\) [Report]()
-
-```go
-func (h HTML) Report(w io.Writer, findings []report.Finding) error
-```
-
-## type [HTMLTable]()
-
-```go
-type HTMLTable struct{}
-```
-
-### func \(HTMLTable\) [Report]()
-
-```go
-func (h HTMLTable) Report(w io.Writer, findings []report.Finding) error
-```
-
-## type [JSON]()
-
-```go
-type JSON struct{}
-```
-
-### func \(JSON\) [Report]()
-
-```go
-func (j JSON) Report(w io.Writer, findings []report.Finding) error
-```
-
-## type [Markdown]()
-
-```go
-type Markdown struct{}
-```
-
-### func \(Markdown\) [Report]()
-
-```go
-func (m Markdown) Report(w io.Writer, findings []report.Finding) error
-```
-
-## type [Reporter]()
-
-Reporter is the interface that each of the canonical output formats implement\.
-
-```go
-type Reporter interface {
- Report(io.Writer, []report.Finding) error
-}
-```
-
-### func [StringToReporter]()
-
-```go
-func StringToReporter(s string) Reporter
-```
-
-StringToReporter takes in a string representation of the preferred reporter\.
-
-## type [Simple]()
-
-```go
-type Simple struct{}
-```
-
-### func \(Simple\) [Report]()
-
-```go
-func (s Simple) Report(w io.Writer, findings []report.Finding) error
-```
-
-## type [TOML]()
-
-```go
-type TOML struct{}
-```
-
-### func \(TOML\) [Report]()
-
-```go
-func (t TOML) Report(w io.Writer, findings []report.Finding) error
-```
-
-## type [Table]()
-
-```go
-type Table struct{}
-```
-
-### func \(Table\) [Report]()
-
-```go
-func (t Table) Report(w io.Writer, findings []report.Finding) error
-```
-
-## type [YAML]()
-
-```go
-type YAML struct{}
-```
-
-### func \(YAML\) [Report]()
-
-```go
-func (y YAML) Report(w io.Writer, findings []report.Finding) error
-```
-
-
-
-Generated by [gomarkdoc]()
diff --git a/pkg/format/format.go b/pkg/format/format.go
deleted file mode 100644
index e765868..0000000
--- a/pkg/format/format.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Package format contains the renderer and available output formats
-package format
-
-import (
- "strings"
-)
-
-// StringToReporter takes in a string representation of the preferred
-// reporter.
-func StringToReporter(s string) Reporter {
- switch strings.ToLower(s) {
- case "json":
- return JSON{}
- case "raw":
- return Raw{}
- case "yaml":
- return YAML{}
- case "toml":
- return TOML{}
- case "table":
- return Table{}
- case "html":
- return HTML{}
- case "html-table":
- return HTMLTable{}
- case "markdown":
- return Markdown{}
- case "custom":
- return Custom{}
- case "simple":
- return Simple{}
- default:
- return JSON{}
- }
-}
diff --git a/pkg/format/report.go b/pkg/format/report.go
deleted file mode 100644
index 2dd372f..0000000
--- a/pkg/format/report.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package format
-
-import (
- "encoding/json"
- "fmt"
- "io"
-
- "github.com/BurntSushi/toml"
- "github.com/brittonhayes/pillager/pkg/templates"
- "github.com/zricethezav/gitleaks/v8/report"
- "gopkg.in/yaml.v2"
-)
-
-// Reporter is the interface that each of the canonical output formats implement.
-type Reporter interface {
- Report(io.Writer, []report.Finding) error
-}
-
-type JSON struct{}
-
-func (j JSON) Report(w io.Writer, findings []report.Finding) error {
- encoder := json.NewEncoder(w)
- encoder.SetIndent("", "\t")
- if err := encoder.Encode(&findings); err != nil {
- return err
- }
-
- return nil
-}
-
-type Raw struct{}
-
-func (r Raw) Report(w io.Writer, findings []report.Finding) error {
- encoder := json.NewEncoder(w)
- if err := encoder.Encode(&findings); err != nil {
- return err
- }
-
- return nil
-}
-
-type YAML struct{}
-
-func (y YAML) Report(w io.Writer, findings []report.Finding) error {
- b, err := yaml.Marshal(&findings)
- if err != nil {
- return err
- }
- fmt.Fprintf(w, "%s\n", string(b))
-
- return nil
-}
-
-type TOML struct{}
-
-func (t TOML) Report(w io.Writer, findings []report.Finding) error {
- enc := toml.NewEncoder(w)
- return enc.Encode(&findings)
-}
-
-type HTML struct{}
-
-func (h HTML) Report(w io.Writer, findings []report.Finding) error {
- return templates.Render(w, templates.HTML, findings)
-}
-
-type HTMLTable struct{}
-
-func (h HTMLTable) Report(w io.Writer, findings []report.Finding) error {
- return templates.Render(w, templates.HTMLTable, findings)
-}
-
-type Markdown struct{}
-
-func (m Markdown) Report(w io.Writer, findings []report.Finding) error {
- return templates.Render(w, templates.Markdown, findings)
-}
-
-type Table struct{}
-
-func (t Table) Report(w io.Writer, findings []report.Finding) error {
- return templates.Render(w, templates.Table, findings)
-}
-
-type Custom struct {
- template string
-}
-
-func (c *Custom) WithTemplate(t string) {
- c.template = t
-}
-
-func (c Custom) Report(w io.Writer, findings []report.Finding) error {
- return templates.Render(w, c.template, findings)
-}
-
-type Simple struct{}
-
-func (s Simple) Report(w io.Writer, findings []report.Finding) error {
- return templates.Render(w, templates.Simple, findings)
-}
diff --git a/pkg/hunter/README.md b/pkg/hunter/README.md
deleted file mode 100644
index 6132d46..0000000
--- a/pkg/hunter/README.md
+++ /dev/null
@@ -1,156 +0,0 @@
-
-
-# hunter
-
-```go
-import "github.com/brittonhayes/pillager/pkg/hunter"
-```
-
-Package hunter contains secret hunting and file scanning tools\.
-
-## Index
-
-- [type Config](<#type-config>)
- - [func NewConfig(opts ...ConfigOption) *Config](<#func-newconfig>)
-- [type ConfigOption](<#type-configoption>)
- - [func WithFS(fs afero.Fs) ConfigOption](<#func-withfs>)
- - [func WithFormat(reporter format.Reporter) ConfigOption](<#func-withformat>)
- - [func WithGitleaksConfig(g config.Config) ConfigOption](<#func-withgitleaksconfig>)
- - [func WithLogLevel(level string) ConfigOption](<#func-withloglevel>)
- - [func WithRedact(redact bool) ConfigOption](<#func-withredact>)
- - [func WithScanPath(path string) ConfigOption](<#func-withscanpath>)
- - [func WithTemplate(template string) ConfigOption](<#func-withtemplate>)
- - [func WithVerbose(verbose bool) ConfigOption](<#func-withverbose>)
- - [func WithWorkers(count int) ConfigOption](<#func-withworkers>)
-- [type Hunter](<#type-hunter>)
- - [func New(opts ...ConfigOption) (*Hunter, error)](<#func-new>)
- - [func (h *Hunter) Hunt() ([]report.Finding, error)](<#func-hunter-hunt>)
- - [func (h *Hunter) Report(w io.Writer, findings []report.Finding) error](<#func-hunter-report>)
-
-
-## type [Config]()
-
-Config takes all of the configurable parameters for a Hunter\.
-
-```go
-type Config struct {
- Filesystem afero.Fs
- Reporter format.Reporter
- Gitleaks config.Config
-
- ScanPath string
- Verbose bool
- Redact bool
- Debug bool
- Workers int
- Template string
-}
-```
-
-### func [NewConfig]()
-
-```go
-func NewConfig(opts ...ConfigOption) *Config
-```
-
-NewConfig creates a Config instance\.
-
-## type [ConfigOption]()
-
-ConfigOption is a convenient type alias for func\(\*Config\)\.
-
-```go
-type ConfigOption func(*Config)
-```
-
-### func [WithFS]()
-
-```go
-func WithFS(fs afero.Fs) ConfigOption
-```
-
-### func [WithFormat]()
-
-```go
-func WithFormat(reporter format.Reporter) ConfigOption
-```
-
-### func [WithGitleaksConfig]()
-
-```go
-func WithGitleaksConfig(g config.Config) ConfigOption
-```
-
-### func [WithLogLevel]()
-
-```go
-func WithLogLevel(level string) ConfigOption
-```
-
-### func [WithRedact]()
-
-```go
-func WithRedact(redact bool) ConfigOption
-```
-
-### func [WithScanPath]()
-
-```go
-func WithScanPath(path string) ConfigOption
-```
-
-### func [WithTemplate]()
-
-```go
-func WithTemplate(template string) ConfigOption
-```
-
-### func [WithVerbose]()
-
-```go
-func WithVerbose(verbose bool) ConfigOption
-```
-
-### func [WithWorkers]()
-
-```go
-func WithWorkers(count int) ConfigOption
-```
-
-## type [Hunter]()
-
-Hunter is the secret scanner\.
-
-```go
-type Hunter struct {
- *Config
-}
-```
-
-### func [New]()
-
-```go
-func New(opts ...ConfigOption) (*Hunter, error)
-```
-
-New creates an instance of the Hunter\.
-
-### func \(\*Hunter\) [Hunt]()
-
-```go
-func (h *Hunter) Hunt() ([]report.Finding, error)
-```
-
-Hunt walks over the filesystem at the configured path\, looking for sensitive information\.
-
-### func \(\*Hunter\) [Report]()
-
-```go
-func (h *Hunter) Report(w io.Writer, findings []report.Finding) error
-```
-
-Report prints out the Findings in the preferred output format\.
-
-
-
-Generated by [gomarkdoc]()
diff --git a/pkg/hunter/config.go b/pkg/hunter/config.go
deleted file mode 100644
index 8c25925..0000000
--- a/pkg/hunter/config.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package hunter
-
-import (
- "errors"
- "os"
- "runtime"
-
- "github.com/rs/zerolog"
- "github.com/rs/zerolog/log"
-
- "github.com/brittonhayes/pillager/internal/validate"
-
- "github.com/brittonhayes/pillager/pkg/format"
- "github.com/brittonhayes/pillager/pkg/rules"
-
- "github.com/zricethezav/gitleaks/v8/config"
-)
-
-// Config takes all of the configurable parameters for a Hunter.
-type Config struct {
- Reporter format.Reporter
- Gitleaks config.Config
- ScanPath string
- Verbose bool
- Redact bool
- Debug bool
- Workers int
- Template string
-}
-
-// ConfigOption is a convenient type alias for func(*Config).
-type ConfigOption func(*Config)
-
-// NewConfig creates a Config instance.
-func NewConfig(opts ...ConfigOption) *Config {
- var (
- defaultVerbose = false
- defaultScanPath = "."
- defaultReporter = format.JSON{}
- defaultWorkers = runtime.NumCPU()
- defaultGitleaks = rules.NewLoader().Load()
- defaultTemplate = ""
- defaultLogLevel = zerolog.ErrorLevel
- )
-
- zerolog.SetGlobalLevel(defaultLogLevel)
- config := &Config{
- ScanPath: defaultScanPath,
- Reporter: defaultReporter,
- Workers: defaultWorkers,
- Gitleaks: defaultGitleaks,
- Verbose: defaultVerbose,
- Template: defaultTemplate,
- }
-
- for _, opt := range opts {
- opt(config)
- }
-
- if err := config.validate(); err != nil {
- log.Fatal().Err(err).Send()
- }
-
- return config
-}
-
-func WithScanPath(path string) ConfigOption {
- return func(c *Config) {
- if validate.PathExists(path) {
- c.ScanPath = path
- return
- }
-
- currentDir, err := os.Getwd()
- if err != nil {
- log.Fatal().Err(err).Msg("failed to get current dir")
- }
-
- log.Error().Msgf("scan path %q not found, defaulting to %q", path, currentDir)
- c.ScanPath = currentDir
- }
-}
-
-func WithLogLevel(level string) ConfigOption {
- return func(c *Config) {
- lvl, err := zerolog.ParseLevel(level)
- if err != nil {
- log.Fatal().Err(err).Send()
- }
- zerolog.SetGlobalLevel(lvl)
- }
-}
-
-func WithVerbose(verbose bool) ConfigOption {
- return func(c *Config) {
- c.Verbose = verbose
- }
-}
-
-func WithWorkers(count int) ConfigOption {
- return func(c *Config) {
- c.Workers = count
- }
-}
-
-func WithRedact(redact bool) ConfigOption {
- return func(c *Config) {
- c.Redact = redact
- }
-}
-
-func WithFormat(reporter format.Reporter) ConfigOption {
- return func(c *Config) {
- if c.Template != "" {
- custom := &format.Custom{}
- custom.WithTemplate(c.Template)
- c.Reporter = custom
- return
- }
-
- c.Reporter = reporter
- }
-}
-
-func WithTemplate(template string) ConfigOption {
- return func(c *Config) {
- c.Reporter = format.Custom{}
- c.Template = template
- }
-}
-
-func WithGitleaksConfig(g config.Config) ConfigOption {
- return func(c *Config) {
- c.Gitleaks = g
- }
-}
-
-func (c *Config) validate() error {
- if c.Gitleaks.Rules == nil {
- return errors.New("no gitleaks rules provided")
- }
-
- return nil
-}
diff --git a/pkg/hunter/docs.go b/pkg/hunter/docs.go
deleted file mode 100644
index 3e8881c..0000000
--- a/pkg/hunter/docs.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package hunter contains secret hunting and file scanning tools.
-package hunter
diff --git a/pkg/hunter/hunter.go b/pkg/hunter/hunter.go
deleted file mode 100644
index ae99a6b..0000000
--- a/pkg/hunter/hunter.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package hunter
-
-import (
- "io"
-
- "github.com/pkg/errors"
- "github.com/rs/zerolog/log"
- "github.com/zricethezav/gitleaks/v8/config"
- "github.com/zricethezav/gitleaks/v8/detect"
- "github.com/zricethezav/gitleaks/v8/report"
-)
-
-// Hunter is the secret scanner.
-type Hunter struct {
- *Config
-}
-
-// New creates an instance of the Hunter.
-func New(opts ...ConfigOption) (*Hunter, error) {
- return &Hunter{
- Config: NewConfig(opts...),
- }, nil
-}
-
-// Hunt walks over the filesystem at the configured path, looking for sensitive information.
-func (h *Hunter) Hunt() ([]report.Finding, error) {
- d, err := detect.NewDetectorDefaultConfig()
- if err != nil {
- return nil, errors.Wrap(err, "failed to setup hunter")
- }
-
- d.Verbose = h.Verbose
- d.Redact = h.Redact
- if h.Config != nil {
- d.Config = config.Config{Allowlist: h.Gitleaks.Allowlist, Rules: h.Gitleaks.Rules}
- }
-
- findings, err := d.DetectFiles(h.ScanPath)
- if err != nil {
- return nil, errors.Wrap(err, "failed to detect from files")
- }
-
- log.Debug().Bool("verbose", h.Verbose).Msg("scanner created")
-
- return findings, nil
-}
-
-// Report prints out the Findings in the preferred output format.
-func (h *Hunter) Report(w io.Writer, findings []report.Finding) error {
- return h.Reporter.Report(w, findings)
-}
diff --git a/pkg/report/format.go b/pkg/report/format.go
new file mode 100644
index 0000000..d82f0a4
--- /dev/null
+++ b/pkg/report/format.go
@@ -0,0 +1,99 @@
+package report
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/brittonhayes/pillager"
+ "github.com/brittonhayes/pillager/internal/templates"
+ "gopkg.in/yaml.v2"
+)
+
+type JSON struct{}
+
+func (j JSON) Report(w io.Writer, findings []pillager.Finding) error {
+ encoder := json.NewEncoder(w)
+ if err := encoder.Encode(&findings); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type JSONPretty struct{}
+
+func (j JSONPretty) Report(w io.Writer, findings []pillager.Finding) error {
+ encoder := json.NewEncoder(w)
+ encoder.SetIndent("", " ")
+ if err := encoder.Encode(&findings); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type Raw struct{}
+
+func (r Raw) Report(w io.Writer, findings []pillager.Finding) error {
+ encoder := json.NewEncoder(w)
+ if err := encoder.Encode(&findings); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type YAML struct{}
+
+func (y YAML) Report(w io.Writer, findings []pillager.Finding) error {
+ b, err := yaml.Marshal(&findings)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "%s\n", string(b))
+
+ return nil
+}
+
+type HTML struct{}
+
+func (h HTML) Report(w io.Writer, findings []pillager.Finding) error {
+ return Render(w, templates.HTML, findings)
+}
+
+type HTMLTable struct{}
+
+func (h HTMLTable) Report(w io.Writer, findings []pillager.Finding) error {
+ return Render(w, templates.HTMLTable, findings)
+}
+
+type Markdown struct{}
+
+func (m Markdown) Report(w io.Writer, findings []pillager.Finding) error {
+ return Render(w, templates.Markdown, findings)
+}
+
+type Table struct{}
+
+func (t Table) Report(w io.Writer, findings []pillager.Finding) error {
+ return Render(w, templates.Table, findings)
+}
+
+type Custom struct {
+ template string
+}
+
+func (c *Custom) WithTemplate(t string) {
+ c.template = t
+}
+
+func (c Custom) Report(w io.Writer, findings []pillager.Finding) error {
+ return Render(w, c.template, findings)
+}
+
+type Simple struct{}
+
+func (s Simple) Report(w io.Writer, findings []pillager.Finding) error {
+ return Render(w, templates.DefaultTemplate, findings)
+}
diff --git a/pkg/report/report.go b/pkg/report/report.go
new file mode 100644
index 0000000..90bf868
--- /dev/null
+++ b/pkg/report/report.go
@@ -0,0 +1,81 @@
+package report
+
+import (
+ "io"
+ "strings"
+ "text/template"
+
+ "encoding/json"
+
+ "github.com/Masterminds/sprig"
+ "github.com/brittonhayes/pillager"
+ "github.com/brittonhayes/pillager/internal/templates"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog/log"
+)
+
+// Reporter is the interface that each of the canonical output formats implement.
+type Reporter interface {
+ Report(io.Writer, []pillager.Finding) error
+}
+
+// StringToReporter takes in a string representation of the preferred
+// reporter.
+func StringToReporter(s string) Reporter {
+ switch strings.ToLower(s) {
+ case "json":
+ return JSON{}
+ case "json-pretty":
+ return JSONPretty{}
+ case "raw":
+ return Raw{}
+ case "yaml":
+ return YAML{}
+ case "table":
+ return Table{}
+ case "html":
+ return HTML{}
+ case "html-table":
+ return HTMLTable{}
+ case "markdown":
+ return Markdown{}
+ case "custom":
+ return Custom{}
+ case "simple":
+ return Simple{}
+ default:
+ return JSON{}
+ }
+}
+
+// Render renders a finding in a custom go template format to the provided writer.
+func Render(w io.Writer, tpl string, findings []pillager.Finding) error {
+ t := template.New("custom")
+ if tpl == "" {
+ log.Debug().Msg("using default template")
+ tpl = templates.DefaultTemplate
+ }
+
+ funcMap := sprig.TxtFuncMap()
+ funcMap["json"] = func(v interface{}) string {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return ""
+ }
+ // Escape quotes and backslashes for HTML attributes
+ escaped := strings.ReplaceAll(string(b), `"`, `"`)
+ escaped = strings.ReplaceAll(escaped, `\`, `\\`)
+ return escaped
+ }
+
+ t, err := t.Funcs(funcMap).Parse(tpl)
+ if err != nil {
+ return errors.Wrap(err, "failed to parse template")
+ }
+
+ if err := t.Execute(w, findings); err != nil {
+ return errors.Wrap(err, "Failed to use custom template")
+ }
+
+ return nil
+}
diff --git a/pkg/rules/README.md b/pkg/rules/README.md
deleted file mode 100755
index 62ca9be..0000000
--- a/pkg/rules/README.md
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-# rules
-
-```go
-import "github.com/brittonhayes/pillager/pkg/rules"
-```
-
-Package rules enables the parsing of Gitleaks rulesets\.
-
-## Index
-
-- [Constants](<#constants>)
-- [Variables](<#variables>)
-- [type Loader](<#type-loader>)
- - [func NewLoader(opts ...LoaderOption) *Loader](<#func-newloader>)
- - [func (l *Loader) Load() config.Config](<#func-loader-load>)
- - [func (l *Loader) WithStrict() LoaderOption](<#func-loader-withstrict>)
-- [type LoaderOption](<#type-loaderoption>)
- - [func FromFile(file string) LoaderOption](<#func-fromfile>)
-
-
-## Constants
-
-ErrReadConfig is the custom error message used if an error is encountered reading the gitleaks config\.
-
-```go
-const ErrReadConfig = "Failed to read gitleaks config"
-```
-
-## Variables
-
-These strings contain default configs\. They are initialized at compile time via go:embed\.
-
-```go
-var (
- //go:embed rules_simple.toml
- RulesDefault string
-
- //go:embed rules_strict.toml
- RulesStrict string
-)
-```
-
-## type [Loader]()
-
-Loader represents a gitleaks config loader\.
-
-```go
-type Loader struct {
- // contains filtered or unexported fields
-}
-```
-
-### func [NewLoader]()
-
-```go
-func NewLoader(opts ...LoaderOption) *Loader
-```
-
-NewLoader creates a Loader instance\.
-
-### func \(\*Loader\) [Load]()
-
-```go
-func (l *Loader) Load() config.Config
-```
-
-Load parses the gitleaks configuration\.
-
-### func \(\*Loader\) [WithStrict]()
-
-```go
-func (l *Loader) WithStrict() LoaderOption
-```
-
-WithStrict enables more strict pillager scanning\.
-
-## type [LoaderOption]()
-
-LoaderOption sets a parameter for the gitleaks config loader\.
-
-```go
-type LoaderOption func(*Loader)
-```
-
-### func [FromFile]()
-
-```go
-func FromFile(file string) LoaderOption
-```
-
-FromFile decodes a gitleaks config from a local file\.
-
-
-
-Generated by [gomarkdoc]()
diff --git a/pkg/rules/rules.go b/pkg/rules/rules.go
deleted file mode 100644
index fdd5d08..0000000
--- a/pkg/rules/rules.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// Package rules enables the parsing of Gitleaks rulesets.
-package rules
-
-import (
- _ "embed"
-
- "github.com/BurntSushi/toml"
- "github.com/brittonhayes/pillager/internal/validate"
- "github.com/rs/zerolog/log"
-
- "github.com/zricethezav/gitleaks/v8/config"
-)
-
-// ErrReadConfig is the custom error message used if an error is encountered
-// reading the gitleaks config.
-const ErrReadConfig = "Failed to read gitleaks config"
-
-// These strings contain default configs. They are initialized at compile time via go:embed.
-var (
- RulesDefault = config.DefaultConfig
-
- //go:embed rules_strict.toml
- RulesStrict string
-)
-
-// Loader represents a gitleaks config loader.
-type Loader struct {
- loader config.ViperConfig
-}
-
-// LoaderOption sets a parameter for the gitleaks config loader.
-type LoaderOption func(*Loader)
-
-// NewLoader creates a Loader instance.
-func NewLoader(opts ...LoaderOption) *Loader {
- var loader Loader
- if _, err := toml.Decode(RulesDefault, &loader.loader); err != nil {
- log.Fatal().Err(err).Msg(ErrReadConfig)
- }
-
- for _, opt := range opts {
- opt(&loader)
- }
-
- return &loader
-}
-
-// WithStrict enables more strict pillager scanning.
-func (l *Loader) WithStrict() LoaderOption {
- return func(l *Loader) {
- if _, err := toml.Decode(RulesStrict, &l.loader); err != nil {
- log.Fatal().Err(err).Msg(ErrReadConfig)
- }
- }
-}
-
-// Load parses the gitleaks configuration.
-func (l *Loader) Load() config.Config {
- config, err := l.loader.Translate()
- if err != nil {
- log.Fatal().Err(err).Msg(ErrReadConfig)
- }
-
- return config
-}
-
-// WithFile decodes a gitleaks config from a local file.
-func WithFile(file string) LoaderOption {
- return func(l *Loader) {
- if file == "" {
- if _, err := toml.Decode(config.DefaultConfig, &l.loader); err != nil {
- log.Fatal().Err(err).Msg(ErrReadConfig)
- }
- return
- }
-
- if validate.PathExists(file) {
- if _, err := toml.DecodeFile(file, &l.loader); err != nil {
- log.Fatal().Err(err).Msg(ErrReadConfig)
- }
- return
- }
-
- log.Fatal().Msgf("invalid - rules file '%s' does not exist", file)
- }
-}
diff --git a/pkg/rules/rules_simple.toml b/pkg/rules/rules_simple.toml
deleted file mode 100644
index f0fc62d..0000000
--- a/pkg/rules/rules_simple.toml
+++ /dev/null
@@ -1,29 +0,0 @@
-title = "pillager config"
-[[rules]]
- description = "AWS Access Key"
- regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'''
- tags = ["key", "AWS"]
-[[rules]]
- description = "AWS Secret Key"
- regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]'''
- tags = ["key", "AWS"]
-[[rules]]
- description = "Github"
- regex = '''(?i)github(.{0,20})?(?-i)[0-9a-zA-Z]{35,40}'''
- tags = ["key", "Github"]
-[[rules]]
- description = "Slack"
- regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''
- tags = ["key", "Slack"]
-[[rules]]
- description = "Asymmetric Private Key"
- regex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----'''
- tags = ["key", "AsymmetricPrivateKey"]
-[[rules]]
- description = "Slack Webhook"
- regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}'''
- tags = ["key", "slack"]
-
-[allowlist]
- description = "Allowlisted files"
- files = ['''^\.?gitleaks.toml$''', '''(.*?)(png|jpg|gif|doc|docx|pdf|bin|xls|pyc|zip)$''', '''(go.mod|go.sum)$''']
diff --git a/pkg/rules/rules_strict.toml b/pkg/rules/rules_strict.toml
deleted file mode 100644
index 32d4a0c..0000000
--- a/pkg/rules/rules_strict.toml
+++ /dev/null
@@ -1,226 +0,0 @@
-title = "Global gitleaks config"
-
-[allowlist]
- description = "Allowlisted files"
- files = ['''^\.?gitleaks.toml$''', # Ignoring this file
- '''(.*?)(jpg|gif|doc|pdf|bin)$''', # Ignoring common binaries
- '''^(.*?)_test\.go$''', # Ignoring Go test files
- '''^(.*?)\.(spec|test)\.(j|t)s$''', # Ignoring JavaScript and TypeScript test files
- '''(go.mod|go.sum)$''', # Ignoring Go manifests
- '''vendor\.json''',
- '''Gopkg\.(lock|toml)''',
- '''package-lock\.json''', # Ignoring Node/JS manifests
- '''package\.json''',
- '''composer\.json''',
- '''composer\.lock''', #Ignoring PHP manifests
- '''yarn\.lock''']
- paths = ["node_modules", # Ignoring Node dependencies
- "vendor", # Ignoring Go dependencies
- "test", # Ignoring test directories
- "tests"]
- regexes = ['''test'''] # Ignoring lines with test
-
-
-[[rules]]
- description = "AWS Secret Key"
- regex = '''(?i)aws(.{0,20})?(?-i)[0-9a-zA-Z\/+]{40}'''
- tags = ["key", "AWS"]
-
-[[rules]]
- description = "AWS MWS key"
- regex = '''amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
- tags = ["key", "AWS", "MWS"]
-
-[[rules]]
- description = "Facebook Secret Key"
- regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)[0-9a-f]{32}'''
- tags = ["key", "Facebook"]
-
-[[rules]]
- description = "Facebook Client ID"
- regex = '''(?i)(facebook|fb)(.{0,20})?[0-9]{13,17}'''
- tags = ["key", "Facebook"]
-
-[[rules]]
- description = "Twitter Secret Key"
- regex = '''(?i)twitter(.{0,20})?[0-9a-z]{35,44}'''
- tags = ["key", "Twitter"]
-
-[[rules]]
- description = "Twitter Client ID"
- regex = '''(?i)twitter(.{0,20})?[0-9a-z]{18,25}'''
- tags = ["client", "Twitter"]
-
-[[rules]]
- description = "Github"
- regex = '''(?i)github(.{0,20})?(?-i)[0-9a-zA-Z]{35,40}'''
- tags = ["key", "Github"]
-
-[[rules]]
- description = "LinkedIn Client ID"
- regex = '''(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}'''
- tags = ["client", "LinkedIn"]
-
-[[rules]]
- description = "LinkedIn Secret Key"
- regex = '''(?i)linkedin(.{0,20})?[0-9a-z]{16}'''
- tags = ["secret", "LinkedIn"]
-
-[[rules]]
- description = "Slack"
- regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?'''
- tags = ["key", "Slack"]
-
-[[rules]]
- description = "Asymmetric Private Key"
- regex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----'''
- tags = ["key", "AsymmetricPrivateKey"]
-
-[[rules]]
- description = "Google API key"
- regex = '''AIza[0-9A-Za-z\\-_]{35}'''
- tags = ["key", "Google"]
-
-[[rules]]
- description = "Google (GCP) Service Account"
- regex = '''"type": "service_account"'''
- tags = ["key", "Google"]
-
-[[rules]]
- description = "Heroku API key"
- regex = '''(?i)heroku(.{0,20})?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
- tags = ["key", "Heroku"]
-
-[[rules]]
- description = "MailChimp API key"
- regex = '''(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}'''
- tags = ["key", "Mailchimp"]
-
-[[rules]]
- description = "Mailgun API key"
- regex = '''((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}'''
- tags = ["key", "Mailgun"]
-
-[[rules]]
- description = "PayPal Braintree access token"
- regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}'''
- tags = ["key", "Paypal"]
-
-[[rules]]
- description = "Picatic API key"
- regex = '''sk_live_[0-9a-z]{32}'''
- tags = ["key", "Picatic"]
-
-[[rules]]
- description = "SendGrid API Key"
- regex = '''SG\.[\w_]{16,32}\.[\w_]{16,64}'''
- tags = ["key", "SendGrid"]
-
-[[rules]]
- description = "Slack Webhook"
- regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}'''
- tags = ["key", "slack"]
-
-[[rules]]
- description = "Stripe API key"
- regex = '''(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}'''
- tags = ["key", "Stripe"]
-
-[[rules]]
- description = "Square access token"
- regex = '''sq0atp-[0-9A-Za-z\-_]{22}'''
- tags = ["key", "square"]
-
-[[rules]]
- description = "Square OAuth secret"
- regex = '''sq0csp-[0-9A-Za-z\\-_]{43}'''
- tags = ["key", "square"]
-
-[[rules]]
- description = "Twilio API key"
- regex = '''(?i)twilio(.{0,20})?SK[0-9a-f]{32}'''
- tags = ["key", "twilio"]
-
-# The following rules check for credentials assigned to variables that its value has an entropy of more than 3 bits.
-# To achieve this there's a regexp for each language. The regexp checks for a variable with a suspicious name followed
-# by a value assignation (for example, := in Go, = in JS, etc.). Then, looks for a group of non-space characters enclosed
-# between quotes. If that group has an entropy higher than 3 bits the rule will trigger.
-
-[[rules]]
- description = "Hardcoded credentials in Go files"
- file = '''^(.*?)\.go$'''
- regex = '''(?i)(?:secret|key|signature|password|pwd|pass|token)(?:\w|\s*?)(?:=|:=)(?:\s*?)[\"'`](.{4,120}?)[\"'`]'''
- tags = ["credentials", "hardcoded", "go"]
- [[rules.Entropies]]
- Min = "3"
- Max = "7"
- Group = "1"
-
-[[rules]]
- description = "Hardcoded credentials in JavaScript or TypeScript files"
- file = '''^(.*?)\.(?:j|t)s$'''
- regex = '''(?i)(?:secret|key|signature|password|pwd|pass|token)(?:\w|\s*?)(?:=){1}(?:\s{0,10})[\"'`](.*?)[\"'`]'''
- tags = ["credentials", "hardcoded", "js"]
- [[rules.Entropies]]
- Min = "3"
- Max = "7"
- Group = "1"
-
-[[rules]]
- description = "Hardcoded credentials in PHP files"
- file = '''^(.*?)\.php$'''
- regex = '''(?i)(?:secret|key|signature|password|pwd|pass|token)(?:.{0,20})(?:=){1}(?:.{0,10})[\"'`](.{4,120})[\"'`]'''
- tags = ["credentials", "hardcoded", "php"]
- [[rules.Entropies]]
- Min = "3"
- Max = "7"
- Group = "1"
-
-[[rules]]
- description = "Hardcoded credentials in YAML files as quoted strings"
- file = '''^(.*?)\.y(a|)ml$'''
- regex = '''(?i)(?:secret|key|signature|password|pwd|pass|token)(?:.{0,20})(?::){1}(?:\s{0,10})(?:[\"'](.{4,120})[\"'])'''
- tags = ["credentials", "hardcoded", "yaml"]
- [[rules.Entropies]]
- Min = "3"
- Max = "7"
- Group = "1"
- [rules.allowlist]
- description = "Skip YAML Serverless variables, grabbed and concated values, encrypted secrets, and values with jinja2 placeholders"
- regexes = ['''\${(?:.)+}''', '''(?i)\(\((?:\s)*?(?:grab|concat)(?:.)*?(?:\s)*?\)\)''', '''(?i)!!enveloped:(?:\S)+''', '''(?:.)*?{{(?:.)*?}}''']
-
-[[rules]]
- description = "Hardcoded credentials in YAML files as unquoted strings"
- file = '''^(.*?)\.y(a|)ml$'''
- regex = '''(?i)(?:secret|key|signature|password|pwd|pass|token)(?:.{0,20})(?::){1}(?:\s{0,10})(\S{4,120})'''
- tags = ["credentials", "hardcoded", "yaml"]
- [[rules.Entropies]]
- Min = "3.5" # A higher entropy is required for this type of match, as unquoted can trigger many false positives
- Max = "7"
- Group = "1"
- [rules.allowlist]
- description = "Skip YAML Serverless variables, grabbed and concated values, encrypted secrets, and values with jinja2 placeholders"
- regexes = ['''\${(?:.)+}''', '''(?i)\(\((?:\s)*?(?:grab|concat)(?:.)*?(?:\s)*?\)\)''', '''(?i)!!enveloped:(?:\S)+''', '''(?:.)*?{{(?:.)*?}}''']
-
-[[rules]]
- description = "Hardcoded credentials in YAML files as multiline strings"
- file = '''^(.*?)\.y(a|)ml$'''
- regex = '''(?i)(?:secret|key|signature|password|pwd|pass|token)(?:.{0,20})(?::){1}(?:\s{0,10})(?:\|(?:-|))\n(?:\s{0,10})(\S{4,120})'''
- tags = ["credentials", "hardcoded", "yaml"]
- [[rules.Entropies]]
- Min = "4"
- Max = "7"
- Group = "1"
-
-[[rules]]
- description = "Hardcoded credentials in HCL files (*.tf)"
- file = '''^(.*?)\.tf$'''
- regex = '''(?i)(?:secret|key|signature|password|pwd|pass|token)(?:.{0,20})(?:=){1}(?:\s)*?"(.{4,120})"'''
- tags = ["credentials", "hardcoded", "hcl"]
- [[rules.Entropies]]
- Min = "3"
- Max = "7"
- Group = "1"
- [rules.allowlist]
- description = "Skip variable substitution"
- regexes = ['''\${(?:.)*?}''']
diff --git a/pkg/scanner/config.go b/pkg/scanner/config.go
new file mode 100644
index 0000000..8c6dac2
--- /dev/null
+++ b/pkg/scanner/config.go
@@ -0,0 +1,138 @@
+package scanner
+
+import (
+ "fmt"
+ "path/filepath"
+ "runtime"
+
+ "github.com/BurntSushi/toml"
+ "github.com/brittonhayes/pillager"
+ "github.com/pkg/errors"
+ "github.com/spf13/viper"
+ "github.com/zricethezav/gitleaks/v8/config"
+)
+
+// ConfigLoader handles loading configuration from files
+type ConfigLoader struct {
+ v *viper.Viper
+}
+
+// NewConfigLoader creates a new configuration loader
+func NewConfigLoader() *ConfigLoader {
+ v := viper.New()
+ v.SetDefault("verbose", false)
+ v.SetDefault("path", ".")
+ v.SetDefault("template", "")
+ v.SetDefault("workers", runtime.NumCPU())
+ v.SetDefault("redact", false)
+ v.SetDefault("reporter", "json")
+
+ return &ConfigLoader{v: v}
+}
+
+func convertDefaultConfig() (*pillager.Options, error) {
+ var defaultConfig config.Config
+
+ if err := toml.Unmarshal([]byte(config.DefaultConfig), &defaultConfig); err != nil {
+ return nil, errors.Wrap(err, "failed to parse default rules")
+ }
+
+ var opts pillager.Options
+
+ opts.Rules = gitleaksToPillagerRules(defaultConfig.Rules)
+ opts.Allowlist = gitleaksToPillagerAllowlist(defaultConfig.Allowlist)
+
+ return &opts, nil
+}
+
+// LoadConfig attempts to load configuration from a file
+func (c *ConfigLoader) LoadConfig(configPath string) (*pillager.Options, error) {
+
+ // Rules and allowlist defaults
+ defaultConfig, err := convertDefaultConfig()
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to convert default rules")
+ }
+
+ c.v.SetDefault("rules", defaultConfig.Rules)
+ c.v.SetDefault("allowlist.paths", defaultConfig.Allowlist.Paths)
+ c.v.SetDefault("allowlist.regexes", defaultConfig.Allowlist.Regexes)
+
+ if configPath != "" {
+ // Use specified config file
+ ext := filepath.Ext(configPath)
+ if ext != ".toml" {
+ return nil, fmt.Errorf("config file must have an extension .toml")
+ }
+ c.v.SetConfigType(ext[1:])
+ c.v.SetConfigFile(configPath)
+
+ if err := c.v.ReadInConfig(); err != nil {
+ return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
+ }
+ } else {
+ // Search for config in default locations
+ c.v.SetConfigName("pillager")
+ c.v.AddConfigPath(".")
+ c.v.AddConfigPath("$HOME/.pillager")
+ c.v.AddConfigPath("$HOME/.config/pillager")
+
+ c.v.ReadInConfig()
+ }
+
+ // Create options from config
+ opts := &pillager.Options{
+ Path: c.v.GetString("path"),
+ Workers: c.v.GetInt("workers"),
+ Verbose: c.v.GetBool("verbose"),
+ Template: c.v.GetString("template"),
+ Redact: c.v.GetBool("redact"),
+ Reporter: c.v.GetString("reporter"),
+ Allowlist: pillager.Allowlist{
+ Paths: c.v.GetStringSlice("allowlist.paths"),
+ Regexes: c.v.GetStringSlice("allowlist.regexes"),
+ },
+ }
+
+ // Unmarshal rules if they exist
+ var rules []pillager.Rule
+ if err := c.v.UnmarshalKey("rules", &rules); err != nil {
+ return nil, fmt.Errorf("failed to parse rules configuration: %w", err)
+ }
+ opts.Rules = rules
+
+ return opts, nil
+}
+
+// MergeWithFlags merges configuration with command line flags
+func (c *ConfigLoader) MergeWithFlags(opts *pillager.Options, flags *pillager.Options) {
+ if flags.Verbose {
+ opts.Verbose = flags.Verbose
+ }
+ if flags.Redact {
+ opts.Redact = flags.Redact
+ }
+ if flags.Workers > 0 {
+ opts.Workers = flags.Workers
+ }
+ if flags.Reporter != "" {
+ opts.Reporter = flags.Reporter
+ }
+ if flags.Template != "" {
+ opts.Template = flags.Template
+ }
+ if flags.Path != "" {
+ opts.Path = flags.Path
+ }
+ // Merge rules if provided
+ if len(flags.Rules) > 0 {
+ opts.Rules = flags.Rules
+ }
+ // Merge allowlist if provided
+ if len(flags.Allowlist.Paths) > 0 {
+ opts.Allowlist.Paths = flags.Allowlist.Paths
+ }
+ if len(flags.Allowlist.Regexes) > 0 {
+ opts.Allowlist.Regexes = flags.Allowlist.Regexes
+ }
+}
diff --git a/pkg/scanner/gitleaks.go b/pkg/scanner/gitleaks.go
new file mode 100644
index 0000000..00b09eb
--- /dev/null
+++ b/pkg/scanner/gitleaks.go
@@ -0,0 +1,183 @@
+package scanner
+
+import (
+ "regexp"
+
+ "github.com/brittonhayes/pillager"
+ "github.com/brittonhayes/pillager/pkg/report"
+ "github.com/pkg/errors"
+
+ "github.com/rs/zerolog/log"
+ "github.com/zricethezav/gitleaks/v8/config"
+ "github.com/zricethezav/gitleaks/v8/detect"
+ output "github.com/zricethezav/gitleaks/v8/report"
+)
+
+// GitleaksScanner implements the Scanner interface using gitleaks
+type GitleaksScanner struct {
+ detector *detect.Detector
+ reporter string
+}
+
+// NewGitleaksScanner creates a new scanner using gitleaks
+func NewGitleaksScanner(options pillager.Options) (Scanner, error) {
+ scanner := &GitleaksScanner{
+ reporter: options.Reporter,
+ }
+
+ cfg := config.Config{
+ Allowlist: convertToGitleaksAllowlist(options.Allowlist),
+ Rules: pillagerToGitleaksRules(options.Rules),
+ Path: options.Path,
+ }
+
+ scanner.detector = detect.NewDetector(cfg)
+ scanner.detector.Verbose = options.Verbose
+ scanner.detector.Redact = options.Redact
+
+ return scanner, nil
+}
+
+// Reporter returns the reporter for the scanner
+func (g *GitleaksScanner) Reporter() report.Reporter {
+ return report.StringToReporter(g.reporter)
+}
+
+// ScanPath returns the path that the scanner is scanning
+func (g *GitleaksScanner) ScanPath() string {
+ return g.detector.Config.Path
+}
+
+func (g *GitleaksScanner) Translate(f output.Finding) pillager.Finding {
+ finding := pillager.Finding{
+ Description: f.Description,
+ StartLine: f.StartLine,
+ EndLine: f.EndLine,
+ StartColumn: f.StartColumn,
+ EndColumn: f.EndColumn,
+ Match: f.Match,
+ Secret: f.Secret,
+ File: f.File,
+ Entropy: f.Entropy,
+ RuleID: f.RuleID,
+ }
+ return finding
+}
+
+// Scan implements the Scanner interface
+func (g *GitleaksScanner) Scan() ([]pillager.Finding, error) {
+ findings, err := g.detector.DetectFiles(g.ScanPath())
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to scan files")
+ }
+
+ var pillagerFindings []pillager.Finding
+ for _, f := range findings {
+ pillagerFindings = append(pillagerFindings, g.Translate(f))
+ }
+
+ if len(pillagerFindings) == 0 {
+ return pillagerFindings, nil
+ }
+
+ return pillagerFindings, nil
+}
+
+func convertToGitleaksAllowlist(a pillager.Allowlist) config.Allowlist {
+ paths := []*regexp.Regexp{}
+ for _, path := range a.Paths {
+ p, err := regexp.Compile(path)
+ if err != nil {
+ log.Fatal().Err(err).
+ Str("pattern", path).
+ Msg("failed to compile allowlist path regex")
+ }
+ paths = append(paths, p)
+ }
+
+ regexes := []*regexp.Regexp{}
+ for _, regex := range a.Regexes {
+ r, err := regexp.Compile(regex)
+ if err != nil {
+ log.Fatal().Err(err).
+ Str("pattern", regex).
+ Msg("failed to compile allowlist regex pattern")
+ }
+ regexes = append(regexes, r)
+ }
+
+ return config.Allowlist{
+ Paths: paths,
+ Regexes: regexes,
+ }
+}
+
+func gitleaksToPillagerAllowlist(a config.Allowlist) pillager.Allowlist {
+ paths := []string{}
+ for _, path := range a.Paths {
+ paths = append(paths, path.String())
+ }
+
+ regexes := []string{}
+ for _, regex := range a.Regexes {
+ regexes = append(regexes, regex.String())
+ }
+
+ return pillager.Allowlist{Paths: paths, Regexes: regexes}
+}
+
+func gitleaksToPillagerRules(rules []*config.Rule) []pillager.Rule {
+ converted := []pillager.Rule{}
+ for _, rule := range rules {
+ r := gitleaksToPillagerRule(rule)
+ converted = append(converted, r)
+ }
+ return converted
+}
+
+func gitleaksToPillagerRule(rule *config.Rule) pillager.Rule {
+ return pillager.Rule{
+ ID: rule.RuleID,
+ Description: rule.Description,
+ Regex: rule.Regex.String(),
+ Tags: rule.Tags,
+ Allowlist: gitleaksToPillagerAllowlist(rule.Allowlist),
+ }
+}
+
+func pillagerToGitleaksRules(rules []pillager.Rule) []*config.Rule {
+ converted := []*config.Rule{}
+ for _, rule := range rules {
+ r := pillagerToGitleaksRule(rule)
+ converted = append(converted, r)
+ }
+ return converted
+}
+
+func pillagerToGitleaksRule(rule pillager.Rule) *config.Rule {
+ path, err := regexp.Compile(rule.Path)
+ if err != nil {
+ log.Fatal().Err(err).
+ Str("rule_id", rule.ID).
+ Str("pattern", rule.Path).
+ Msg("failed to compile rule path regex")
+ }
+
+ regex, err := regexp.Compile(rule.Regex)
+ if err != nil {
+ log.Fatal().Err(err).
+ Str("rule_id", rule.ID).
+ Str("pattern", rule.Regex).
+ Msg("failed to compile rule regex pattern")
+ }
+
+ return &config.Rule{
+ RuleID: rule.ID,
+ Path: path,
+ Description: rule.Description,
+ Regex: regex,
+ Keywords: rule.Keywords,
+ Tags: rule.Tags,
+ Allowlist: convertToGitleaksAllowlist(rule.Allowlist),
+ }
+}
diff --git a/pkg/scanner/scanner.go b/pkg/scanner/scanner.go
new file mode 100644
index 0000000..1952c26
--- /dev/null
+++ b/pkg/scanner/scanner.go
@@ -0,0 +1,18 @@
+package scanner
+
+import (
+ "github.com/brittonhayes/pillager"
+ "github.com/brittonhayes/pillager/pkg/report"
+)
+
+// Scanner defines the interface for secret scanning implementations
+type Scanner interface {
+ // Scan performs the secret scanning operation on the given path
+ Scan() ([]pillager.Finding, error)
+
+ // Reporter returns the reporter for the scanner
+ Reporter() report.Reporter
+
+ // ScanPath returns the path that the scanner is scanning
+ ScanPath() string
+}
diff --git a/pkg/templates/html-table.tmpl b/pkg/templates/html-table.tmpl
deleted file mode 100644
index 5a704cb..0000000
--- a/pkg/templates/html-table.tmpl
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
- Pillager - Scan Results
-
-
-
-
-
-
- Pillager
-
-
- Results of your latest hunt
-
-
-
-
-
- File |
- Line |
- Leak |
-
-
-
- {{ range .Leaks }}
-
- {{.File}} |
- {{.LineNumber}} |
- {{.Offender}} |
-
- {{ end }}
-
-
-
-
-
-
-
-
diff --git a/pkg/templates/html.tmpl b/pkg/templates/html.tmpl
deleted file mode 100644
index ed88a9b..0000000
--- a/pkg/templates/html.tmpl
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
- Pillager - Scan Results
-
-
-
-
-
-
- Pillager
-
-
- Results of your latest hunt
-
-
- {{ range . }}
-
-
-
{{.File}}
-
- Tags:
-
- {{.Tags}}
-
-
-
- Leak:
- {{.Secret}}
-
-
- Line:
- {{.StartLine}}
-
-
-
- {{end}}
-
-
-
-
-
diff --git a/pkg/templates/markdown.tmpl b/pkg/templates/markdown.tmpl
deleted file mode 100644
index 45a75f8..0000000
--- a/pkg/templates/markdown.tmpl
+++ /dev/null
@@ -1,6 +0,0 @@
-# Results
-
-{{ range . -}}
- ## {{ .File }}
- - Location: {{.StartLine}}
-{{end}}
diff --git a/pkg/templates/templates.go b/pkg/templates/templates.go
deleted file mode 100644
index 9bfd622..0000000
--- a/pkg/templates/templates.go
+++ /dev/null
@@ -1,60 +0,0 @@
-// Package templates contains a compilation of go templates for rendering secret findings.
-package templates
-
-import (
- _ "embed"
- "io"
- "text/template"
-
- "github.com/Masterminds/sprig"
- "github.com/pkg/errors"
- "github.com/rs/zerolog/log"
- "github.com/zricethezav/gitleaks/v8/report"
-)
-
-var (
- //go:embed simple.tmpl
- Simple string
-
- //go:embed html.tmpl
- HTML string
-
- //go:embed markdown.tmpl
- Markdown string
-
- //go:embed table.tmpl
- Table string
-
- //go:embed html-table.tmpl
- HTMLTable string
-)
-
-// DefaultTemplate is the base template used to format a Finding into the
-// custom output format.
-const DefaultTemplate = `{{ with . -}}
-{{ range . -}}
-Line: {{ quote .StartLine}}
-File: {{ quote .File }}
-Secret: {{ quote .Secret }}
----
-{{ end -}}{{- end}}`
-
-// Render renders a finding in a custom go template format to the provided writer.
-func Render(w io.Writer, tpl string, findings []report.Finding) error {
- t := template.New("custom")
- if tpl == "" {
- log.Debug().Msg("using default template")
- tpl = DefaultTemplate
- }
-
- t, err := t.Funcs(sprig.TxtFuncMap()).Parse(tpl)
- if err != nil {
- return errors.Wrap(err, "failed to parse template")
- }
-
- if err := t.Execute(w, findings); err != nil {
- return errors.Wrap(err, "Failed to use custom template")
- }
-
- return nil
-}
diff --git a/pkg/tui/model/model.go b/pkg/tui/model/model.go
index a60867e..45edf59 100644
--- a/pkg/tui/model/model.go
+++ b/pkg/tui/model/model.go
@@ -4,13 +4,13 @@ import (
"fmt"
"os"
- "github.com/brittonhayes/pillager/pkg/hunter"
+ "github.com/brittonhayes/pillager"
+ "github.com/brittonhayes/pillager/pkg/scanner"
"github.com/brittonhayes/pillager/pkg/tui/style"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
- "github.com/zricethezav/gitleaks/v8/report"
"golang.org/x/term"
)
@@ -22,8 +22,8 @@ type model struct {
help help.Model
table table.Model
- hunter *hunter.Hunter
- results []report.Finding
+ scanner scanner.Scanner
+ results []pillager.Finding
err error
width int
@@ -51,15 +51,15 @@ type body struct {
message string
}
-type resultsMsg struct{ results []report.Finding }
+type resultsMsg struct{ results []pillager.Finding }
type errMsg struct{ err error }
func (e errMsg) Error() string {
- return fmt.Sprintf("🔥 Uh oh! Well that's not good. Looks like something went wrong and the application has exited: \n\n%s\n\n%s", style.Error.Render(e.Error()), "Press [q] to quit.")
+ return fmt.Sprintf("🔥 Uh oh! Well that's not good. Looks like something went wrong and the application has exited: \n\n%s\n\n%s", style.Error.Render(e.err.Error()), "Press [q] to quit.")
}
-func NewModel(hunt *hunter.Hunter) model {
+func NewModel(scan scanner.Scanner) model {
width, height, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
width = 80
@@ -76,7 +76,7 @@ func NewModel(hunt *hunter.Hunter) model {
h := help.New()
m := model{
- hunter: hunt,
+ scanner: scan,
header: header{
title: "Pillager",
subtitle: "Hunt inside the file system for valuable information",
@@ -102,11 +102,9 @@ func (m model) Dimensions() (int, int) {
return m.width, m.height
}
-func startScan(h *hunter.Hunter) tea.Cmd {
+func startScan(s scanner.Scanner) tea.Cmd {
return func() tea.Msg {
- h.Debug = false
- h.Verbose = false
- results, err := h.Hunt()
+ results, err := s.Scan()
if err != nil {
// There was an error making our request. Wrap the error we received
// in a message and return it.
diff --git a/pkg/tui/model/table.go b/pkg/tui/model/table.go
index 855eaf2..aa2dcf6 100644
--- a/pkg/tui/model/table.go
+++ b/pkg/tui/model/table.go
@@ -4,9 +4,9 @@ import (
"path/filepath"
"strings"
+ "github.com/brittonhayes/pillager"
"github.com/brittonhayes/pillager/pkg/tui/style"
"github.com/evertras/bubble-table/table"
- "github.com/zricethezav/gitleaks/v8/report"
)
const (
@@ -35,7 +35,7 @@ func newTable(width int) table.Model {
return t
}
-func addRowData(data []report.Finding) []table.Row {
+func addRowData(data []pillager.Finding) []table.Row {
rows := []table.Row{}
for i, entry := range data {
diff --git a/pkg/tui/model/view.go b/pkg/tui/model/view.go
index 5a38362..72dc921 100644
--- a/pkg/tui/model/view.go
+++ b/pkg/tui/model/view.go
@@ -4,11 +4,11 @@ import (
"fmt"
"strings"
+ "github.com/brittonhayes/pillager"
"github.com/brittonhayes/pillager/pkg/tui/style"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/zricethezav/gitleaks/v8/report"
)
func (m model) Init() tea.Cmd {
@@ -57,7 +57,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keymap.Start):
m.loading.active = true
- return m, tea.Batch(startScan(m.hunter), m.loading.spinner.Tick)
+ return m, tea.Batch(startScan(m.scanner), m.loading.spinner.Tick)
default:
return m, m.loading.spinner.Tick
}
@@ -69,7 +69,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m model) selectedView() string {
if m.results != nil && m.body.selected.visible {
w := &strings.Builder{}
- err := m.hunter.Report(w, []report.Finding{m.selectedRow()})
+ err := m.scanner.Reporter().Report(w, []pillager.Finding{m.selectedRow()})
if err != nil {
m.err = err
}
@@ -80,7 +80,7 @@ func (m model) selectedView() string {
return ""
}
-func (m model) selectedRow() report.Finding {
+func (m model) selectedRow() pillager.Finding {
return m.results[m.table.HighlightedRow().Data[columnKeyID].(int)-1]
}
@@ -91,9 +91,9 @@ func (m model) View() string {
m.body.toast = ""
if m.loading.active || m.loading.spinner.Visible() {
- m.body.message = fmt.Sprintf("%s Scanning for secrets in %q with %d workers", m.loading.spinner.View(), m.hunter.ScanPath, m.hunter.Workers)
+ m.body.message = fmt.Sprintf("%s Scanning for secrets in %q", m.loading.spinner.View(), m.scanner.ScanPath())
} else if m.results != nil {
- m.body.toast = style.Highlight.Render(fmt.Sprintf("Found %d secrets in path %q", len(m.results), m.hunter.ScanPath))
+ m.body.toast = style.Highlight.Render(fmt.Sprintf("Found %d secrets in path %q", len(m.results), m.scanner.ScanPath()))
m.body.message = m.table.View()
m.body.message += m.selectedView()
}