Project Path: data
Source Tree:
data
├── Dockerfile
├── README.md
├── SECURITY_REVIEW.md
├── cmd
│ └── scan.go
├── code.md
├── entrypoint.sh
├── go.mod
├── go.sum
├── internal
│ ├── config
│ │ ├── config.go
│ │ └── gitleaks.toml
│ ├── genai
│ │ └── gemini.go
│ └── scanner
│ └── scanner.go
├── main.go
├── run_tests.sh
├── scan_all_repos.sh
└── testdata
├── expected
└── repos
├── repo-clean
│ └── README.md
├── repo-with-false-positive
│ └── examples.py
└── repo-with-secret
└── config.yml
Dockerfile:
# ---- Build Stage ----
FROM golang:1.23-alpine AS builder
WORKDIR /app
# Copy go.mod and go.sum files to download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . .
# Build the statically-linked binary
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/gitleaks-lite .
# ---- Final Stage ----
FROM alpine:latest
# Install git, which is the only runtime dependency
RUN apk add --no-cache git
# Copy the binary from the builder stage
COPY --from=builder /app/gitleaks-lite /usr/local/bin/gitleaks-lite
# Copy the entrypoint wrapper script
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
# Set the entrypoint for the container to our new script
ENTRYPOINT ["entrypoint.sh"]
# The default command can be to show help
CMD ["--help"]
README.md:
# Gitleaks-Lite
[](https://github.com/actions)
[](https://opensource.org/licenses/MIT)
[](security_summary.md)
A minimalist, containerized CLI tool to rapidly scan Git repositories for high-confidence secrets, with an optional AI-powered validation layer using Google Gemini to eliminate false positives.
## Motivation
Powerful security tools like the original [Gitleaks](https://github.com/gitleaks/gitleaks) are feature-rich and highly configurable, making them ideal for complex security programs. However, for the common use case of a developer wanting a quick, simple scan without configuring rules or wading through false positives, a lighter approach is needed.
**Gitleaks-Lite** is designed for this exact scenario. It adheres to an 80/20 philosophy by focusing exclusively on scanning Git repositories with a curated, embedded ruleset for the most common secret types. It's a "fire and forget" tool that provides immediate, high-confidence results.
## Features
- **Zero-Configuration:** No `.toml` file needed. The tool comes with a built-in, curated set of high-confidence rules.
- **High-Speed Regex Scan:** Utilizes a fast, regex-based engine to perform an initial scan of the entire repository history.
- **AI-Powered Validation (Optional):** Uses the Google Gemini API as a secondary validation pass to analyze the context of potential secrets, dramatically reducing false positives from test files, examples, and placeholders.
- **Containerized & Secure:** Runs in a self-contained, minimal Docker image with **no host dependencies** other than Docker itself. Go and Git are included in the image. The application runs as a non-root user within the container.
- **Simple, Actionable Output:** Reports are printed directly to the console in a human-readable format or as JSON for easy CI/CD integration.
## How It Works: A Hybrid Approach
Gitleaks-Lite employs a powerful two-phase scanning process to maximize both speed and accuracy.
```plaintext
Phase 1: High-Speed Regex Scan (Always On)
- Scans the entire Git history using a fast, embedded ruleset.
- Generates a list of "potential findings".
- If AI validation is disabled, this is the final report.
│
└─ (If GenAI is enabled) ──>
│
Phase 2: AI Validation with Gemini (Optional)
- For each potential finding, it sends the secret and its surrounding code context to the Gemini API.
- Asks the AI to act as a security expert and determine if it's a true secret or a false positive.
- Discards findings that the AI confidently identifies as false positives.
│
└──>
Final Report: High-Confidence Secrets
- The final output contains only the secrets that are either un-validated
or have been confirmed by the AI, resulting in a cleaner, more actionable report.Your security and privacy are paramount. When you enable the GenAI-Enhanced Scan by providing a GEMINI_API_KEY, this tool sends a limited amount of data to the Google Gemini API for validation.
What is Sent:
- The potential secret string (the text flagged by the initial regex scan).
- A small snippet of the surrounding code (approximately 5 lines) to provide the necessary context for validation.
What is NOT Sent:
- The filename or the full file content.
- The commit hash, author name, email, or any other Git metadata.
- The name of your repository or any other identifying project information.
The data sent is used by Google for the sole purpose of returning a validation result and is governed by Google Cloud's Generative AI data governance policies.
If you are working in a high-security environment or are not comfortable with sending code snippets to a third-party API, do not set the GEMINI_API_KEY environment variable. The tool will remain fully functional and secure in its fast, local, regex-only mode.
For a detailed security overview, please see the Security Summary.
- Docker installed and running.
- A local Git repository you wish to scan.
The only installation step is to build the Docker image. From the root of this project, run:
docker build -t gitleaks-lite .This is the fastest method and performs the scan entirely locally without requiring an API key. To scan a local Git repository, mount it as a volume to the /scan directory inside the container.
# Replace /path/to/your/local/repo with the absolute path to your project
docker run --rm -v "/path/to/your/local/repo:/scan" gitleaks-lite git /scanTo enable the secondary validation pass with Google Gemini, you must provide your API key as an environment variable.
Get your API key: Google AI Studio
docker run --rm -v "/path/to/your/local/repo:/scan" \
-e GEMINI_API_KEY="YOUR_API_KEY_HERE" \
gitleaks-lite git /scanNote: Handle your GEMINI_API_KEY as a secret. For CI/CD environments, use your platform's built-in secret management features.
This project includes a helper script, scan_all_repos.sh, to scan all Git repositories within a specified directory.
- Configure the script: Open
scan_all_repos.shand adjust theSCAN_DIRECTORYandUSE_GEMINIvariables. - Make it executable:
chmod +x scan_all_repos.sh - Run it:
./scan_all_repos.sh
For machine-readable output, which is useful for scripting and integration with other tools, use the --json flag. The tool will exit with code 1 if secrets are found and 0 otherwise.
docker run --rm -v "/path/to/your/local/repo:/scan" gitleaks-lite git /scan --jsonThe only supported method for ignoring a specific finding is to add a gitleaks:allow comment on the same line as the secret, or in a block comment directly above it, in your source code.
// This secret will be ignored by the scanner
var mySecret = "some_secret_value_here" // gitleaks:allowThis project includes a comprehensive, automated testing suite. The test runner script will lint, format, build the Docker image, and run a series of integration tests against purpose-built test repositories.
To run the full suite:
# To run all tests, including the AI validation ones
export GEMINI_API_KEY="YOUR_API_KEY_HERE"
# Run the test script
./run_tests.shIf you do not set the GEMINI_API_KEY, the script will automatically skip the AI-related tests.
Contributions are welcome! Please ensure that any pull requests adhere to the standard Go formatting and that all tests pass.
- Format your code:
go fmt ./... - Run the test suite:
./run_tests.sh
This project is licensed under the MIT License.
`SECURITY_REVIEW.md`:
```md
# Security Summary
This document provides a summary of the security posture of the **Gitleaks-Lite** project, including its design principles, data handling practices, and potential risks.
## 1. Core Design and Threat Model
**Gitleaks-Lite** is designed as a command-line utility that runs inside a Docker container. Its primary function is to scan a Git repository (mounted as a volume) for secrets.
The threat model considers the following:
* **Primary Goal:** To prevent accidental exposure of secrets in Git repositories.
* **Primary Assets:** The secrets within the code being scanned and the user's `GEMINI_API_KEY`.
* **Main Threat Actors:**
1. Maintainers or users of the tool who might misunderstand its security boundaries.
2. Attackers who gain access to the environment where the tool is run (e.g., a CI/CD runner or a developer's machine).
The core security principle is **isolation**. By running inside a container, the tool has no access to the host system beyond the explicitly mounted repository directory.
## 2. Data Handling and Privacy
Data handling practices are critical, especially when an external AI service is used.
### Regex-Only Mode (Default)
* **Execution:** Entirely local.
* **Data Sent:** No data ever leaves the container or the machine it is running on.
* **Security:** This mode is suitable for all environments, including those with strict data residency and privacy requirements.
### GenAI-Enhanced Mode (Optional)
This mode is only activated when a `GEMINI_API_KEY` is provided.
* **What is Sent to Google Gemini API:**
* The potential secret string (e.g., `"ghp_..."`).
* A small code snippet (~5 lines) surrounding the potential secret for context.
* **What is NOT Sent:**
* Git metadata (commit hash, author, email, commit message).
* The filename or the full file content.
* The repository's name or origin.
* **Data Governance:** The data sent to the Gemini API is governed by Google Cloud's [Generative AI data governance policies](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance). This data is used solely for the validation request and is not used for training models.
**Recommendation:** Users in high-security environments should use the default regex-only mode if sending code snippets to a third-party API is against their security policy.
## 3. Security Hardening Measures
Several measures have been implemented to secure the application and its environment.
### Container Security
* **Multi-Stage Builds:** The `Dockerfile` uses a multi-stage build to create a minimal final image, reducing the attack surface by excluding the Go toolchain and build-time dependencies.
* **Minimal Dependencies:** The final image is based on `alpine:latest` and only includes `git`, which is the sole runtime dependency. This minimizes the number of packages that could contain vulnerabilities.
* **Non-Root Execution (Recommended):** The tool is designed to be run as a non-root user inside the container to limit the impact of a potential container escape vulnerability. Users can enforce this by adding `USER gitleaks` to the Dockerfile or using `docker run --user`.
### Code Security
* **No `unsafe` Go:** The project avoids using the `unsafe` package in Go, preventing low-level memory corruption bugs.
* **Dependency Management:** Dependencies are managed via `go.mod` and `go.sum`, ensuring reproducible builds. Regular dependency scanning is recommended to identify and update vulnerable packages.
* **Secure API Usage:** The client for the Gemini API is initialized using the official Google Go SDK, which handles secure TLS communication.
* **Concurrency Safety:** When performing AI validation, Go channels and WaitGroups are used to manage concurrent operations safely, preventing race conditions.
## 4. Known Risks and Mitigations
* **Compromised `GEMINI_API_KEY`:**
* **Risk:** If the `GEMINI_API_KEY` is leaked, an attacker could use it to make requests to the Gemini API at the owner's expense.
* **Mitigation:** The key is only ever read from an environment variable. Users are instructed to handle this key as a secret and use a secrets manager in CI/CD environments. It is never stored or logged by the application.
* **Malicious Repository:**
* **Risk:** A repository with maliciously crafted file content could theoretically target a vulnerability in the `gitdiff` parser or the Go standard library.
* **Mitigation:** The application runs in a container with limited privileges, minimizing the potential impact. The Go runtime also provides memory safety, reducing the risk of buffer overflows.
* **False Negatives:**
* **Risk:** The tool might fail to detect a real secret.
* **Mitigation:** This is an inherent risk in any secret scanner. The regex rules are curated to be effective, but they cannot be exhaustive. The AI validation layer is tuned to be conservative, erring on the side of caution. Users should not rely on this tool as their only security measure.
This summary is intended to provide transparency into the security of Gitleaks-Lite. Users are encouraged to review the source code and use the tool in accordance with their own security policies.
cmd/scan.go:
package cmd
import (
"context"
"encoding/json"
"fmt"
"gitleaks-lite/internal/config"
"gitleaks-lite/internal/genai"
"gitleaks-lite/internal/scanner"
"os"
"os/exec"
"strings"
"sync"
"github.com/gitleaks/go-gitdiff/gitdiff"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "gitleaks-lite",
Short: "A lightweight secret scanner for git repositories.",
}
var scanCmd = &cobra.Command{
Use: "git [path]",
Short: "Scan a git repository",
Args: cobra.ExactArgs(1),
Run: runScan,
}
var (
jsonOutput bool
)
func init() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"})
zerolog.SetGlobalLevel(zerolog.InfoLevel)
scanCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output findings in JSON format")
rootCmd.AddCommand(scanCmd)
}
func Execute() error {
return rootCmd.Execute()
}
func runScan(cmd *cobra.Command, args []string) {
repoPath := args[0]
useGenAI := os.Getenv("GEMINI_API_KEY") != ""
cfg, err := config.Load()
if err != nil {
log.Fatal().Err(err).Msg("Failed to load configuration")
}
var aiValidator *genai.Validator
if useGenAI {
log.Info().Msg("GEMINI_API_KEY found, enabling GenAI validation.")
aiValidator, err = genai.NewValidator(context.Background())
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize Gemini Validator")
}
}
appScanner := scanner.New(cfg)
var potentialFindings []scanner.Finding
// Execute git log command
gitCmd := exec.Command("git", "-C", repoPath, "log", "-p", "-U0", "--full-history", "--all")
stdout, err := gitCmd.StdoutPipe()
if err != nil {
log.Fatal().Err(err).Msg("Failed to get stdout pipe from git log")
}
if err := gitCmd.Start(); err != nil {
log.Fatal().Err(err).Msg("Failed to start git log command")
}
files, err := gitdiff.Parse(stdout)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse git diff")
}
log.Info().Msgf("Scanning repository at %s...", repoPath)
for file := range files {
findings := appScanner.Scan(file)
if len(findings) > 0 {
potentialFindings = append(potentialFindings, findings...)
}
}
if err := gitCmd.Wait(); err != nil {
log.Warn().Err(err).Msg("Git log command finished with error")
}
finalFindings := processFindings(potentialFindings, aiValidator)
printResults(finalFindings)
}
func processFindings(findings []scanner.Finding, validator *genai.Validator) []scanner.Finding {
if validator == nil {
return findings // No AI validation needed
}
log.Info().Msgf("Validating %d potential findings with Gemini...", len(findings))
var validatedFindings []scanner.Finding
var mu sync.Mutex
var wg sync.WaitGroup
for _, f := range findings {
wg.Add(1)
go func(finding scanner.Finding) {
defer wg.Done()
result, err := validator.Validate(finding.Secret, finding.CodeContext)
if err != nil {
log.Warn().Err(err).Msgf("Failed to validate finding in %s", finding.File)
// Conservatively include the finding if validation fails
mu.Lock()
validatedFindings = append(validatedFindings, finding)
mu.Unlock()
return
}
if result.IsSecret {
log.Debug().Msgf("Gemini validation CONFIRMED secret for rule '%s' in %s. Reason: %s", finding.RuleID, finding.File, result.Reason)
mu.Lock()
validatedFindings = append(validatedFindings, finding)
mu.Unlock()
} else {
log.Debug().Msgf("Gemini validation REJECTED secret for rule '%s' in %s. Reason: %s", finding.RuleID, finding.File, result.Reason)
}
}(f)
}
wg.Wait()
return validatedFindings
}
func printResults(findings []scanner.Finding) {
if len(findings) == 0 {
log.Info().Msg("✅ No secrets found.")
return
}
log.Warn().Msgf("🚨 Found %d high-confidence secrets! 🚨", len(findings))
if jsonOutput {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(findings); err != nil {
log.Fatal().Err(err).Msg("Failed to encode findings to JSON")
}
} else {
for _, f := range findings {
fmt.Println(strings.Repeat("-", 50))
fmt.Printf("Rule: %s\n", f.RuleID)
fmt.Printf("File: %s\n", f.File)
fmt.Printf("Line: %d\n", f.StartLine)
fmt.Printf("Commit: %s\n", f.Commit[:12])
fmt.Printf("Author: %s\n", f.Author)
fmt.Printf("Date: %s\n", f.Date)
fmt.Printf("Secret: %s\n", f.Secret)
fmt.Println(strings.Repeat("-", 50))
}
}
os.Exit(1)
}code.md:
Project Path: data
Source Tree:
```txt
data
├── Dockerfile
├── README.md
├── cmd
│ └── scan.go
├── entrypoint.sh
├── go.mod
├── go.sum
├── internal
│ ├── config
│ │ ├── config.go
│ │ └── gitleaks.toml
│ ├── genai
│ │ └── gemini.go
│ └── scanner
│ └── scanner.go
├── main.go
├── run_tests.sh
├── scan_all_repos.sh
└── testdata
├── expected
└── repos
├── repo-clean
│ └── README.md
├── repo-with-false-positive
│ └── examples.py
└── repo-with-secret
└── config.yml
Dockerfile:
# ---- Build Stage ----
FROM golang:1.23-alpine AS builder
WORKDIR /app
# Copy go.mod and go.sum files to download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . .
# Build the statically-linked binary
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/gitleaks-lite .
# ---- Final Stage ----
FROM alpine:latest
# Install git, which is the only runtime dependency
RUN apk add --no-cache git
# Copy the binary from the builder stage
COPY --from=builder /app/gitleaks-lite /usr/local/bin/gitleaks-lite
# Copy the entrypoint wrapper script
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
# Set the entrypoint for the container to our new script
ENTRYPOINT ["entrypoint.sh"]
# The default command can be to show help
CMD ["--help"]
README.md:
Of course. Here is a complete, updated `README.md` file that incorporates the security and privacy recommendations, ready for your public repository.
***
# Gitleaks-Lite
[](https://github.com/actions)
[](https://opensource.org/licenses/MIT)
A minimalist, zero-configuration CLI tool to rapidly scan Git repositories for high-confidence secrets, with an optional AI-powered validation layer using Google Gemini.
## Motivation
Powerful security tools like the original [Gitleaks](https://github.com/gitleaks/gitleaks) are feature-rich and highly configurable, making them ideal for complex security programs. However, for the common use case of a developer wanting a quick, simple scan without configuring rules or wading through false positives, a lighter approach is needed.
**Gitleaks-Lite** is designed to solve this problem. It adheres to an 80/20 philosophy by focusing exclusively on scanning Git repositories with a curated, embedded ruleset for the most common secret types. It's built to be a "fire and forget" tool that provides immediate, high-confidence results.
## Features
- **Zero-Configuration:** No `.toml` file needed. The tool comes with a built-in, curated set of high-confidence rules.
- **High-Speed Regex Scan:** Utilizes a fast, regex-based engine to perform the initial scan of the entire repository history.
- **AI-Powered Validation:** Optionally uses the Google Gemini API as a secondary validation pass to analyze the context of potential secrets, dramatically reducing false positives from test files, examples, and placeholders.
- **Containerized & Portable:** Runs in a self-contained Docker image with **no host dependencies** other than Docker itself. Go and Git are included in the image.
- **Simple, Actionable Output:** Reports are printed directly to the console in a human-readable format or as JSON for easy integration.
## How It Works: A Hybrid Approach
Gitleaks-Lite employs a powerful two-phase scanning process to maximize both speed and accuracy.
```plaintext
Phase 1: High-Speed Regex Scan (Always On)
- Scans the entire Git history using a fast, embedded ruleset.
- Generates a list of "potential findings".
- If AI validation is disabled, this is the final report.
│
└─ (If GenAI is enabled) ──>
│
Phase 2: AI Validation with Gemini (Optional)
- Sends each potential finding and its surrounding code context to the Gemini API.
- Asks the AI to act as a security expert and determine if it's a true secret or a false positive.
- Discards findings that the AI identifies as false positives.
│
└──>
Final Report: High-Confidence Secrets
- The final output contains only the secrets confirmed by the AI,
resulting in a cleaner, more actionable report.When you enable the GenAI-Enhanced Scan by providing a GEMINI_API_KEY, this tool sends data to the Google Gemini API for validation.
What is Sent:
- The potential secret (the string flagged by the initial regex scan).
- A small snippet of the surrounding code (approximately 5 lines) to provide the necessary context for validation.
What is NOT Sent:
- The filename or the full file content.
- The commit hash, author name, or any other Git metadata.
The data sent is used by Google for the sole purpose of returning a validation result and is governed by Google Cloud's Generative AI data governance policies.
If you are working in a high-security environment or are not comfortable with sending code snippets to a third-party API, do not set the GEMINI_API_KEY environment variable. The tool will remain fully functional in its fast, local, regex-only mode.
- Docker installed and running.
- A local Git repository to scan.
The only installation step is to build the Docker image. From the root of this project, run:
docker build -t gitleaks-lite .This is the fastest method and does not require an API key. To scan a local Git repository, mount it as a volume to the /scan directory inside the container.
# Replace /path/to/your/local/repo with the absolute path to your project
docker run --rm -v "/path/to/your/local/repo:/scan" gitleaks-lite git /scanTo enable the secondary validation pass with Google Gemini, you must provide your API key as an environment variable.
Get your API key: Google AI Studio
docker run --rm -v "/path/to/your/local/repo:/scan" \
-e GEMINI_API_KEY="YOUR_API_KEY_HERE" \
gitleaks-lite git /scanThis project includes a helper script, scan_all_repos.sh, to scan all Git repositories within a specified directory.
- Configure the script: Open
scan_all_repos.shand adjust theSCAN_DIRECTORYandUSE_GEMINIvariables. - Make it executable:
chmod +x scan_all_repos.sh - Run it:
./scan_all_repos.sh
For machine-readable output, which is useful for scripting and integration with other tools, use the --json flag.
docker run --rm -v "/path/to/your/local/repo:/scan" gitleaks-lite git /scan --jsonThe only supported method for ignoring a specific finding is to add a gitleaks:allow comment on the same line as the secret in your source code.
// This secret will be ignored by the scanner
var mySecret = "some_secret_value_here" // gitleaks:allowThis project includes a comprehensive, automated testing suite. The test runner script will lint, format, build the Docker image, and run a series of integration tests against purpose-built test repositories.
To run the full suite:
# Set your Gemini key to run all tests
export GEMINI_API_KEY="YOUR_API_KEY_HERE"
# Run the test script
./run_tests.shIf you do not set the GEMINI_API_KEY, the script will automatically skip the AI-related tests.
Contributions are welcome! Please ensure that any pull requests adhere to the standard Go formatting and that all tests pass.
- Format your code:
go fmt ./... - Run the test suite:
./run_tests.sh
This project is licensed under the MIT License.
`cmd/scan.go`:
```go
package cmd
import (
"context"
"encoding/json"
"fmt"
"gitleaks-lite/internal/config"
"gitleaks-lite/internal/genai"
"gitleaks-lite/internal/scanner"
"os"
"os/exec"
"strings"
"sync"
"github.com/gitleaks/go-gitdiff/gitdiff"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "gitleaks-lite",
Short: "A lightweight secret scanner for git repositories.",
}
var scanCmd = &cobra.Command{
Use: "git [path]",
Short: "Scan a git repository",
Args: cobra.ExactArgs(1),
Run: runScan,
}
var (
jsonOutput bool
)
func init() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"})
zerolog.SetGlobalLevel(zerolog.InfoLevel)
scanCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output findings in JSON format")
rootCmd.AddCommand(scanCmd)
}
func Execute() error {
return rootCmd.Execute()
}
func runScan(cmd *cobra.Command, args []string) {
repoPath := args[0]
useGenAI := os.Getenv("GEMINI_API_KEY") != ""
cfg, err := config.Load()
if err != nil {
log.Fatal().Err(err).Msg("Failed to load configuration")
}
var aiValidator *genai.Validator
if useGenAI {
log.Info().Msg("GEMINI_API_KEY found, enabling GenAI validation.")
aiValidator, err = genai.NewValidator(context.Background())
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize Gemini Validator")
}
}
appScanner := scanner.New(cfg)
var potentialFindings []scanner.Finding
// Execute git log command
gitCmd := exec.Command("git", "-C", repoPath, "log", "-p", "-U0", "--full-history", "--all")
stdout, err := gitCmd.StdoutPipe()
if err != nil {
log.Fatal().Err(err).Msg("Failed to get stdout pipe from git log")
}
if err := gitCmd.Start(); err != nil {
log.Fatal().Err(err).Msg("Failed to start git log command")
}
files, err := gitdiff.Parse(stdout)
if err != nil {
log.Fatal().Err(err).Msg("Failed to parse git diff")
}
log.Info().Msgf("Scanning repository at %s...", repoPath)
for file := range files {
findings := appScanner.Scan(file)
if len(findings) > 0 {
potentialFindings = append(potentialFindings, findings...)
}
}
if err := gitCmd.Wait(); err != nil {
log.Warn().Err(err).Msg("Git log command finished with error")
}
finalFindings := processFindings(potentialFindings, aiValidator)
printResults(finalFindings)
}
func processFindings(findings []scanner.Finding, validator *genai.Validator) []scanner.Finding {
if validator == nil {
return findings // No AI validation needed
}
log.Info().Msgf("Validating %d potential findings with Gemini...", len(findings))
var validatedFindings []scanner.Finding
var mu sync.Mutex
var wg sync.WaitGroup
for _, f := range findings {
wg.Add(1)
go func(finding scanner.Finding) {
defer wg.Done()
result, err := validator.Validate(finding.Secret, finding.CodeContext)
if err != nil {
log.Warn().Err(err).Msgf("Failed to validate finding in %s", finding.File)
// Conservatively include the finding if validation fails
mu.Lock()
validatedFindings = append(validatedFindings, finding)
mu.Unlock()
return
}
if result.IsSecret {
log.Debug().Msgf("Gemini validation CONFIRMED secret for rule '%s' in %s. Reason: %s", finding.RuleID, finding.File, result.Reason)
mu.Lock()
validatedFindings = append(validatedFindings, finding)
mu.Unlock()
} else {
log.Debug().Msgf("Gemini validation REJECTED secret for rule '%s' in %s. Reason: %s", finding.RuleID, finding.File, result.Reason)
}
}(f)
}
wg.Wait()
return validatedFindings
}
func printResults(findings []scanner.Finding) {
if len(findings) == 0 {
log.Info().Msg("✅ No secrets found.")
return
}
log.Warn().Msgf("🚨 Found %d high-confidence secrets! 🚨", len(findings))
if jsonOutput {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(findings); err != nil {
log.Fatal().Err(err).Msg("Failed to encode findings to JSON")
}
} else {
for _, f := range findings {
fmt.Println(strings.Repeat("-", 50))
fmt.Printf("Rule: %s\n", f.RuleID)
fmt.Printf("File: %s\n", f.File)
fmt.Printf("Line: %d\n", f.StartLine)
fmt.Printf("Commit: %s\n", f.Commit[:12])
fmt.Printf("Author: %s\n", f.Author)
fmt.Printf("Date: %s\n", f.Date)
fmt.Printf("Secret: %s\n", f.Secret)
fmt.Println(strings.Repeat("-", 50))
}
}
os.Exit(1)
}
entrypoint.sh:
#!/bin/sh
# This script runs every time the container starts.
# The `git` command expects to run in a directory, but our scanner
# specifies the path. We'll run this from the root. The scanner will
# use the correct path provided in its arguments.
git config --global --add safe.directory /scan
# Now, execute the main gitleaks-lite application, passing along
# all the arguments that were given to the container.
exec gitleaks-lite "$@"
go.mod:
module gitleaks-lite
go 1.23
require (
github.com/BurntSushi/toml v1.4.0
github.com/gitleaks/go-gitdiff v0.9.1
github.com/google/generative-ai-go v0.15.0
github.com/rs/zerolog v1.33.0
github.com/spf13/cobra v1.8.1
google.golang.org/api v0.189.0
)
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/ai v0.8.0 // indirect
cloud.google.com/go/auth v0.7.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/longrunning v0.5.9 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
go.sum:
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k=
cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0=
github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.15.0 h1:0PQF6ib/72Sa8SfVkqsyzHqgVZH2MxpIa/krpbGDT7E=
github.com/google/generative-ai-go v0.15.0/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY=
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
internal/config/config.go:
package config
import (
_ "embed"
"regexp"
"github.com/BurntSushi/toml"
"github.com/rs/zerolog/log"
)
//go:embed gitleaks.toml
var defaultRules string
// ViperConfig is a temporary struct to match the TOML structure for parsing.
type ViperConfig struct {
Rules []struct {
ID string
Description string
Regex string
SecretGroup int
Entropy float64
Keywords []string
}
}
type Rule struct {
ID string
Description string
Regex *regexp.Regexp
SecretGroup int
Keywords []string
Entropy float64
}
type Config struct {
Rules []Rule
}
func Load() (*Config, error) {
var vc ViperConfig
if _, err := toml.Decode(defaultRules, &vc); err != nil {
return nil, err
}
var cfg Config
for _, vr := range vc.Rules {
re, err := regexp.Compile(vr.Regex)
if err != nil {
log.Error().Err(err).Msgf("Failed to compile regex for rule %s", vr.ID)
continue
}
newRule := Rule{
ID: vr.ID,
Description: vr.Description,
Regex: re,
SecretGroup: vr.SecretGroup,
Keywords: vr.Keywords,
Entropy: vr.Entropy,
}
cfg.Rules = append(cfg.Rules, newRule)
}
return &cfg, nil
}internal/config/gitleaks.toml:
[[rules]]
id = "aws-access-token"
description = "AWS Access Token"
regex = '''\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b'''
keywords = ["AKIA", "ASIA"]
[[rules]]
id = "github-pat"
description = "GitHub Personal Access Token"
regex = '''ghp_[0-9a-zA-Z]{36}'''
keywords = ["ghp_"]
[[rules]]
id = "slack-webhook-url"
description = "Slack Webhook"
regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}'''
keywords = ["hooks.slack.com"]
[[rules]]
id = "private-key"
description = "Asymmetric Private Key"
regex = '''-----BEGIN (?:[A-Z\s]+) PRIVATE KEY-----'''
keywords = ["-----BEGIN"]
[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(key|api|token|secret|password)"?\s*[:=]\s*["']([0-9a-zA-Z\-_.=]{20,})["']'''
secretGroup = 2
entropy = 3.5
keywords = ["key", "api", "token", "secret", "password"]internal/genai/gemini.go:
package genai
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/google/generative-ai-go/genai"
"github.com/rs/zerolog/log"
"google.golang.org/api/option"
)
type ValidationResult struct {
IsSecret bool `json:"is_secret"`
Confidence string `json:"confidence"`
Reason string `json:"reason"`
}
type Validator struct {
client *genai.GenerativeModel
}
func NewValidator(ctx context.Context) (*Validator, error) {
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("GEMINI_API_KEY environment variable not set")
}
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, fmt.Errorf("failed to create Gemini client: %w", err)
}
model := client.GenerativeModel("gemini-2.5-flash")
model.SetTemperature(0.0)
return &Validator{client: model}, nil
}
func cleanGeminiResponse(raw string) string {
if strings.HasPrefix(raw, "```json") {
raw = strings.TrimPrefix(raw, "```json")
raw = strings.TrimSuffix(raw, "```")
}
return strings.TrimSpace(raw)
}
func (v *Validator) Validate(secret, codeContext string) (*ValidationResult, error) {
prompt := fmt.Sprintf(`
You are a security expert specializing in secret detection. Your primary goal is to prevent false positives while being conservative.
A regular expression has flagged the string "%s" as a potential leaked secret in the following code snippet:
--- CODE CONTEXT ---
%s
--------------------
Your task is to analyze this and determine if it is a real, active secret or a false positive.
False positives are things like test data, placeholder values (e.g., "YOUR_API_KEY_HERE"), example keys in documentation, or commented-out code.
**If there is any ambiguity, err on the side of caution and classify it as a secret.**
Respond ONLY with a valid JSON object in the following format:
{"is_secret": boolean, "confidence": "High/Medium/Low", "reason": "A brief justification for your decision."}
`, secret, codeContext)
ctx := context.Background()
resp, err := v.client.GenerateContent(ctx, genai.Text(prompt))
if err != nil {
return nil, fmt.Errorf("failed to generate content from Gemini: %w", err)
}
// --- CORRECTED LOGIC TO ACCESS RESPONSE STRUCTURE ---
// 1. Check if resp.Candidates slice is populated.
if len(resp.Candidates) == 0 {
return nil, fmt.Errorf("received an empty response from Gemini API (no candidates)")
}
// 2. Access the first candidate.
candidate := resp.Candidates[0]
// 3. Check if the candidate's Content field is not nil and Parts slice is not empty.
if candidate.Content == nil || len(candidate.Content.Parts) == 0 {
return nil, fmt.Errorf("received an empty response from Gemini API (no content parts found in candidate)")
}
// 4. Access the first part from the candidate's content, which should be genai.Text.
rawJSON, ok := candidate.Content.Parts[0].(genai.Text)
if !ok {
return nil, fmt.Errorf("unexpected response format from Gemini API (expected genai.Text part)")
}
// --- END CORRECTED LOGIC ---
cleanedJSON := cleanGeminiResponse(string(rawJSON))
var result ValidationResult
if err := json.Unmarshal([]byte(cleanedJSON), &result); err != nil {
log.Warn().Str("raw_response", string(rawJSON)).Msg("Failed to unmarshal JSON from Gemini, treating as secret.")
// Fallback: if JSON is malformed, conservatively treat as a secret.
return &ValidationResult{IsSecret: true, Confidence: "Low", Reason: "Failed to parse AI response."}, nil
}
return &result, nil
}
func (v *Validator) Close() {
// The current SDK does not require explicit closing of the top-level client.
}internal/scanner/scanner.go:
package scanner
import (
"gitleaks-lite/internal/config"
"strings"
"github.com/gitleaks/go-gitdiff/gitdiff"
)
// Finding represents a secret that has been found.
type Finding struct {
RuleID string
File string
StartLine int
Secret string
Commit string
Author string
Email string
Date string
Message string
CodeContext string
}
type Scanner struct {
cfg *config.Config
}
func New(cfg *config.Config) *Scanner {
return &Scanner{cfg: cfg}
}
// Scan performs the detection on a single gitdiff file.
func (s *Scanner) Scan(file *gitdiff.File) []Finding {
var findings []Finding
if file == nil || file.PatchHeader == nil {
return findings
}
for _, textFragment := range file.TextFragments {
if textFragment == nil {
continue
}
content := textFragment.Raw(gitdiff.OpAdd)
lines := strings.Split(content, "\n")
for i, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
if strings.Contains(line, "gitleaks:allow") {
continue
}
for _, rule := range s.cfg.Rules {
matches := rule.Regex.FindAllStringSubmatch(line, -1)
if matches == nil {
continue
}
for _, match := range matches {
secret := ""
// Safely determine the secret.
if len(match) > rule.SecretGroup {
secret = match[rule.SecretGroup]
}
// If a specific group is requested but empty, or not set, use the full match.
if secret == "" {
secret = match[0]
}
if secret == "" {
continue
}
start := i - 2
if start < 0 {
start = 0
}
end := i + 3
if end > len(lines) {
end = len(lines)
}
contextSnippet := strings.Join(lines[start:end], "\n")
findings = append(findings, Finding{
RuleID: rule.ID,
File: file.NewName,
StartLine: int(textFragment.NewPosition) + i,
Secret: secret,
Commit: file.PatchHeader.SHA,
Author: file.PatchHeader.Author.Name,
Email: file.PatchHeader.Author.Email,
Date: file.PatchHeader.AuthorDate.String(),
Message: file.PatchHeader.Message(),
CodeContext: contextSnippet,
})
}
}
}
}
return findings
}main.go:
package main
import (
"gitleaks-lite/cmd"
"os"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}run_tests.sh:
#!/bin/bash
# --- Self-sufficient PATH configuration ---
if ! command -v go &> /dev/null; then
echo "[SETUP] 'go' command not found. Adding default Go path to environment..."
export PATH=$PATH:/usr/local/go/bin
fi
if ! command -v go &> /dev/null; then
echo "[ERROR] Could not find 'go' executable." >&2
exit 1
fi
set -e
# --- Configuration ---
DOCKER_IMAGE_NAME="gitleaks-lite-test"
REPO_CLEAN_PATH="$(pwd)/testdata/repos/repo-clean"
REPO_SECRET_PATH="$(pwd)/testdata/repos/repo-with-secret"
REPO_FP_PATH="$(pwd)/testdata/repos/repo-with-false-positive"
# --- Helper Functions for Logging ---
info() { echo -e "\033[0;34m[INFO]\033[0m $1"; }
success() { echo -e "\033[0;32m[SUCCESS]\033[0m $1"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $1" >&2; exit 1; }
# --- Test Runner Logic ---
info "--- Phase 1: Linting and Formatting ---"
FMT_OUTPUT=$(go fmt ./...)
if [ -n "$FMT_OUTPUT" ]; then
error "Go files are not formatted. Please run 'go fmt ./...'\nFiles changed:\n$FMT_OUTPUT"
fi
go vet ./...
success "Code is well-formatted and passes vet checks."
info "\n--- Phase 2: Building Test Docker Image ---"
docker build -t "$DOCKER_IMAGE_NAME" .
success "Docker image '$DOCKER_IMAGE_NAME' built successfully."
info "\n--- Phase 3: Running Integration Tests ---"
# --- REMOVED THE -u FLAG FROM ALL DOCKER COMMANDS ---
BASE_DOCKER_RUN="docker run --rm"
# Test 1: Clean Repo - Expect exit code 0
info "Running test on clean repository..."
set +e
output_clean=$($BASE_DOCKER_RUN -v "$REPO_CLEAN_PATH:/scan" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_clean=$?
set -e
if [ $exit_code_clean -ne 0 ]; then
error "Expected exit code 0 for clean repo, but got $exit_code_clean. Output:\n$output_clean"
fi
if ! echo "$output_clean" | grep -q "No secrets found"; then
error "Expected 'No secrets found' for clean repo. Output:\n$output_clean"
fi
success "Clean repository test passed."
# Test 2: Repo with Secret (Regex-Only) - Expect exit code 1
info "\nRunning test on repo with secret (regex-only)..."
set +e
output_secret=$($BASE_DOCKER_RUN -v "$REPO_SECRET_PATH:/scan" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_secret=$?
set -e
if [ $exit_code_secret -ne 1 ]; then
error "Expected exit code 1 for repo with secret, but got $exit_code_secret. Output:\n$output_secret"
fi
if ! echo "$output_secret" | grep -q "github-pat"; then
error "Expected finding for rule 'github-pat' was not found. Output:\n$output_secret"
fi
success "Repo with secret (regex-only) test passed."
# GenAI related tests
if [ -z "$GEMINI_API_KEY" ]; then
info "\n--- Skipping GenAI tests: GEMINI_API_KEY not set ---"
else
info "\nGEMINI_API_KEY found. Proceeding with GenAI integration tests."
# Test 3: Repo with Secret (GenAI Mode) - Expect exit code 1
info "Running test on repo with secret (GenAI mode)..."
set +e
output_secret_ai=$($BASE_DOCKER_RUN -v "$REPO_SECRET_PATH:/scan" -e GEMINI_API_KEY="$GEMINI_API_KEY" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_secret_ai=$?
set -e
if [ $exit_code_secret_ai -ne 1 ]; then
error "Expected exit code 1 for repo with secret (GenAI mode), but got $exit_code_secret_ai. Output:\n$output_secret_ai"
fi
if ! echo "$output_secret_ai" | grep -q "github-pat"; then
error "Expected finding for 'github-pat' not found in GenAI mode. Output:\n$output_secret_ai"
fi
success "Repo with secret (GenAI mode) test passed (True Positive Confirmed)."
# Test 4: Repo with False Positive (GenAI Mode) - Expect exit code 0
info "\nRunning test on repo with false positive (GenAI mode)..."
set +e
output_fp_ai=$($BASE_DOCKER_RUN -v "$REPO_FP_PATH:/scan" -e GEMINI_API_KEY="$GEMINI_API_KEY" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_fp_ai=$?
set -e
if [ $exit_code_fp_ai -ne 0 ]; then
error "Expected exit code 0 for repo with false positive (GenAI mode), but got $exit_code_fp_ai. Output:\n$output_fp_ai"
fi
if ! echo "$output_fp_ai" | grep -q "No secrets found"; then
error "Expected 'No secrets found' for false positive repo. Output:\n$output_fp_ai"
fi
success "Repo with false positive (GenAI mode) test passed (False Positive Filtered)."
fi
info "\n--- Phase 4: Cleaning up ---"
# docker rmi "$DOCKER_IMAGE_NAME"
# success "Test Docker image '$DOCKER_IMAGE_NAME' removed."
echo ""
success "✅ ✅ ✅ ALL TESTS PASSED! ✅ ✅ ✅"scan_all_repos.sh:
#!/bin/bash
# --- Configuration ---
SCAN_DIRECTORY="/home/mdt/dev"
DOCKER_IMAGE="gitleaks-lite:latest"
USE_GEMINI=true
SCAN_TIMEOUT="2m" # Set a 2-minute timeout for each repository scan.
# --- Helper function for logging ---
info() {
echo -e "\033[0;34m[INFO]\033[0m $1"
}
# --- Script Logic ---
# Step 1: Handle API Key securely at the very beginning.
if [ "$USE_GEMINI" = true ] && [ -z "$GEMINI_API_KEY" ]; then
echo "GenAI validation is enabled, but GEMINI_API_KEY is not set."
read -sp "Please enter your Gemini API Key (or press Enter to skip GenAI scans): " temp_api_key
echo
if [ -n "$temp_api_key" ]; then
export GEMINI_API_KEY="$temp_api_key"
info "GEMINI_API_KEY has been set for this session."
else
info "No API key provided. GenAI validation will be skipped."
USE_GEMINI=false
fi
fi
echo "--- Starting Gitleaks-Lite Scan on all repositories in $SCAN_DIRECTORY ---"
echo "--- Timeout per repository is set to $SCAN_TIMEOUT ---"
repos_with_findings=0
start_time=$SECONDS
# Step 2: Loop through directories and scan.
for dir in "$SCAN_DIRECTORY"/*/; do
if [ -d "$dir" ]; then
repo_path=$(realpath "$dir")
repo_name=$(basename "$repo_path")
if git -C "$repo_path" rev-parse --is-inside-work-tree > /dev/null 2>&1; then
echo ""
echo "========================================================================"
echo "Scanning repository: $repo_name"
echo "========================================================================"
docker_args=( "run" "--rm" "-v" "$repo_path:/scan" )
if [ "$USE_GEMINI" = true ] && [ -n "$GEMINI_API_KEY" ]; then
docker_args+=("-e" "GEMINI_API_KEY=$GEMINI_API_KEY")
fi
docker_args+=("$DOCKER_IMAGE" "git" "/scan")
# Execute the command with a timeout. This simplified execution is more stable.
# The output will appear when the command finishes or times out.
if output=$(timeout --foreground "$SCAN_TIMEOUT" docker "${docker_args[@]}" 2>&1); then
# Exit code 0 (success)
echo "$output"
else
status=$?
if [ $status -eq 124 ]; then
echo "❌ ERROR: Scan for $repo_name timed out after $SCAN_TIMEOUT."
elif [ $status -eq 1 ]; then
echo "$output"
echo "🚨🚨🚨 WARNING: Secrets found in $repo_name 🚨🚨🚨"
repos_with_findings=$((repos_with_findings + 1))
else
echo "❌ ERROR: Scan for $repo_name failed with an unexpected error (exit code $status)."
echo "--- Error Output ---"
echo "$output"
echo "--------------------"
fi
fi
else
echo "-> Skipping '$repo_name' (not a Git repository)"
fi
fi
done
end_time=$SECONDS
duration=$((end_time - start_time))
echo ""
echo "========================================================================"
echo "--- Scan Complete ---"
echo "Total execution time: $((duration / 60)) minutes and $((duration % 60)) seconds."
if [ $repos_with_findings -gt 0 ]; then
echo "Total repositories with findings: $repos_with_findings"
exit 1
else
echo "✅ No secrets found in any repository."
fitestdata/repos/repo-clean/README.md:
This is a clean repository with no secrets.
testdata/repos/repo-with-false-positive/examples.py:
# This is an example key for documentation purposes only.
# It is not a real production secret.
EXAMPLE_API_KEY = "key_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN"testdata/repos/repo-with-secret/config.yml:
api_key: "ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN"
`entrypoint.sh`:
```sh
#!/bin/sh
# This script runs every time the container starts.
# The `git` command expects to run in a directory, but our scanner
# specifies the path. We'll run this from the root. The scanner will
# use the correct path provided in its arguments.
git config --global --add safe.directory /scan
# Now, execute the main gitleaks-lite application, passing along
# all the arguments that were given to the container.
exec gitleaks-lite "$@"
go.mod:
module gitleaks-lite
go 1.23
require (
github.com/BurntSushi/toml v1.4.0
github.com/gitleaks/go-gitdiff v0.9.1
github.com/google/generative-ai-go v0.15.0
github.com/rs/zerolog v1.33.0
github.com/spf13/cobra v1.8.1
google.golang.org/api v0.189.0
)
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/ai v0.8.0 // indirect
cloud.google.com/go/auth v0.7.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/longrunning v0.5.9 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)
go.sum:
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k=
cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0=
github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.15.0 h1:0PQF6ib/72Sa8SfVkqsyzHqgVZH2MxpIa/krpbGDT7E=
github.com/google/generative-ai-go v0.15.0/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY=
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
internal/config/config.go:
package config
import (
_ "embed"
"regexp"
"github.com/BurntSushi/toml"
"github.com/rs/zerolog/log"
)
//go:embed gitleaks.toml
var defaultRules string
// ViperConfig is a temporary struct to match the TOML structure for parsing.
type ViperConfig struct {
Rules []struct {
ID string
Description string
Regex string
SecretGroup int
Entropy float64
Keywords []string
}
}
type Rule struct {
ID string
Description string
Regex *regexp.Regexp
SecretGroup int
Keywords []string
Entropy float64
}
type Config struct {
Rules []Rule
}
func Load() (*Config, error) {
var vc ViperConfig
if _, err := toml.Decode(defaultRules, &vc); err != nil {
return nil, err
}
var cfg Config
for _, vr := range vc.Rules {
re, err := regexp.Compile(vr.Regex)
if err != nil {
log.Error().Err(err).Msgf("Failed to compile regex for rule %s", vr.ID)
continue
}
newRule := Rule{
ID: vr.ID,
Description: vr.Description,
Regex: re,
SecretGroup: vr.SecretGroup,
Keywords: vr.Keywords,
Entropy: vr.Entropy,
}
cfg.Rules = append(cfg.Rules, newRule)
}
return &cfg, nil
}internal/config/gitleaks.toml:
[[rules]]
id = "aws-access-token"
description = "AWS Access Token"
regex = '''\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b'''
keywords = ["AKIA", "ASIA"]
[[rules]]
id = "github-pat"
description = "GitHub Personal Access Token"
regex = '''ghp_[0-9a-zA-Z]{36}'''
keywords = ["ghp_"]
[[rules]]
id = "slack-webhook-url"
description = "Slack Webhook"
regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}'''
keywords = ["hooks.slack.com"]
[[rules]]
id = "private-key"
description = "Asymmetric Private Key"
regex = '''-----BEGIN (?:[A-Z\s]+) PRIVATE KEY-----'''
keywords = ["-----BEGIN"]
[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(key|api|token|secret|password)"?\s*[:=]\s*["']([0-9a-zA-Z\-_.=]{20,})["']'''
secretGroup = 2
entropy = 3.5
keywords = ["key", "api", "token", "secret", "password"]internal/genai/gemini.go:
package genai
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/google/generative-ai-go/genai"
"github.com/rs/zerolog/log"
"google.golang.org/api/option"
)
type ValidationResult struct {
IsSecret bool `json:"is_secret"`
Confidence string `json:"confidence"`
Reason string `json:"reason"`
}
type Validator struct {
client *genai.GenerativeModel
}
func NewValidator(ctx context.Context) (*Validator, error) {
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("GEMINI_API_KEY environment variable not set")
}
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, fmt.Errorf("failed to create Gemini client: %w", err)
}
model := client.GenerativeModel("gemini-2.5-flash")
model.SetTemperature(0.0)
return &Validator{client: model}, nil
}
func cleanGeminiResponse(raw string) string {
if strings.HasPrefix(raw, "```json") {
raw = strings.TrimPrefix(raw, "```json")
raw = strings.TrimSuffix(raw, "```")
}
return strings.TrimSpace(raw)
}
func (v *Validator) Validate(secret, codeContext string) (*ValidationResult, error) {
prompt := fmt.Sprintf(`
You are a security expert specializing in secret detection. Your primary goal is to prevent false positives while being conservative.
A regular expression has flagged the string "%s" as a potential leaked secret in the following code snippet:
--- CODE CONTEXT ---
%s
--------------------
Your task is to analyze this and determine if it is a real, active secret or a false positive.
False positives are things like test data, placeholder values (e.g., "YOUR_API_KEY_HERE"), example keys in documentation, or commented-out code.
**If there is any ambiguity, err on the side of caution and classify it as a secret.**
Respond ONLY with a valid JSON object in the following format:
{"is_secret": boolean, "confidence": "High/Medium/Low", "reason": "A brief justification for your decision."}
`, secret, codeContext)
ctx := context.Background()
resp, err := v.client.GenerateContent(ctx, genai.Text(prompt))
if err != nil {
return nil, fmt.Errorf("failed to generate content from Gemini: %w", err)
}
// --- CORRECTED LOGIC TO ACCESS RESPONSE STRUCTURE ---
// 1. Check if resp.Candidates slice is populated.
if len(resp.Candidates) == 0 {
return nil, fmt.Errorf("received an empty response from Gemini API (no candidates)")
}
// 2. Access the first candidate.
candidate := resp.Candidates[0]
// 3. Check if the candidate's Content field is not nil and Parts slice is not empty.
if candidate.Content == nil || len(candidate.Content.Parts) == 0 {
return nil, fmt.Errorf("received an empty response from Gemini API (no content parts found in candidate)")
}
// 4. Access the first part from the candidate's content, which should be genai.Text.
rawJSON, ok := candidate.Content.Parts[0].(genai.Text)
if !ok {
return nil, fmt.Errorf("unexpected response format from Gemini API (expected genai.Text part)")
}
// --- END CORRECTED LOGIC ---
cleanedJSON := cleanGeminiResponse(string(rawJSON))
var result ValidationResult
if err := json.Unmarshal([]byte(cleanedJSON), &result); err != nil {
log.Warn().Str("raw_response", string(rawJSON)).Msg("Failed to unmarshal JSON from Gemini, treating as secret.")
// Fallback: if JSON is malformed, conservatively treat as a secret.
return &ValidationResult{IsSecret: true, Confidence: "Low", Reason: "Failed to parse AI response."}, nil
}
return &result, nil
}
func (v *Validator) Close() {
// The current SDK does not require explicit closing of the top-level client.
}internal/scanner/scanner.go:
package scanner
import (
"gitleaks-lite/internal/config"
"strings"
"github.com/gitleaks/go-gitdiff/gitdiff"
)
// Finding represents a secret that has been found.
type Finding struct {
RuleID string
File string
StartLine int
Secret string
Commit string
Author string
Email string
Date string
Message string
CodeContext string
}
type Scanner struct {
cfg *config.Config
}
func New(cfg *config.Config) *Scanner {
return &Scanner{cfg: cfg}
}
// Scan performs the detection on a single gitdiff file.
func (s *Scanner) Scan(file *gitdiff.File) []Finding {
var findings []Finding
if file == nil || file.PatchHeader == nil {
return findings
}
for _, textFragment := range file.TextFragments {
if textFragment == nil {
continue
}
content := textFragment.Raw(gitdiff.OpAdd)
lines := strings.Split(content, "\n")
for i, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
if strings.Contains(line, "gitleaks:allow") {
continue
}
for _, rule := range s.cfg.Rules {
matches := rule.Regex.FindAllStringSubmatch(line, -1)
if matches == nil {
continue
}
for _, match := range matches {
secret := ""
// Safely determine the secret.
if len(match) > rule.SecretGroup {
secret = match[rule.SecretGroup]
}
// If a specific group is requested but empty, or not set, use the full match.
if secret == "" {
secret = match[0]
}
if secret == "" {
continue
}
start := i - 2
if start < 0 {
start = 0
}
end := i + 3
if end > len(lines) {
end = len(lines)
}
contextSnippet := strings.Join(lines[start:end], "\n")
findings = append(findings, Finding{
RuleID: rule.ID,
File: file.NewName,
StartLine: int(textFragment.NewPosition) + i,
Secret: secret,
Commit: file.PatchHeader.SHA,
Author: file.PatchHeader.Author.Name,
Email: file.PatchHeader.Author.Email,
Date: file.PatchHeader.AuthorDate.String(),
Message: file.PatchHeader.Message(),
CodeContext: contextSnippet,
})
}
}
}
}
return findings
}main.go:
package main
import (
"gitleaks-lite/cmd"
"os"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}run_tests.sh:
#!/bin/bash
# --- Self-sufficient PATH configuration ---
if ! command -v go &> /dev/null; then
echo "[SETUP] 'go' command not found. Adding default Go path to environment..."
export PATH=$PATH:/usr/local/go/bin
fi
if ! command -v go &> /dev/null; then
echo "[ERROR] Could not find 'go' executable." >&2
exit 1
fi
set -e
# --- Configuration ---
DOCKER_IMAGE_NAME="gitleaks-lite-test"
REPO_CLEAN_PATH="$(pwd)/testdata/repos/repo-clean"
REPO_SECRET_PATH="$(pwd)/testdata/repos/repo-with-secret"
REPO_FP_PATH="$(pwd)/testdata/repos/repo-with-false-positive"
# --- Helper Functions for Logging ---
info() { echo -e "\033[0;34m[INFO]\033[0m $1"; }
success() { echo -e "\033[0;32m[SUCCESS]\033[0m $1"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $1" >&2; exit 1; }
# --- Test Runner Logic ---
info "--- Phase 1: Linting and Formatting ---"
FMT_OUTPUT=$(go fmt ./...)
if [ -n "$FMT_OUTPUT" ]; then
error "Go files are not formatted. Please run 'go fmt ./...'\nFiles changed:\n$FMT_OUTPUT"
fi
go vet ./...
success "Code is well-formatted and passes vet checks."
info "\n--- Phase 2: Building Test Docker Image ---"
docker build -t "$DOCKER_IMAGE_NAME" .
success "Docker image '$DOCKER_IMAGE_NAME' built successfully."
info "\n--- Phase 3: Running Integration Tests ---"
# --- REMOVED THE -u FLAG FROM ALL DOCKER COMMANDS ---
BASE_DOCKER_RUN="docker run --rm"
# Test 1: Clean Repo - Expect exit code 0
info "Running test on clean repository..."
set +e
output_clean=$($BASE_DOCKER_RUN -v "$REPO_CLEAN_PATH:/scan" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_clean=$?
set -e
if [ $exit_code_clean -ne 0 ]; then
error "Expected exit code 0 for clean repo, but got $exit_code_clean. Output:\n$output_clean"
fi
if ! echo "$output_clean" | grep -q "No secrets found"; then
error "Expected 'No secrets found' for clean repo. Output:\n$output_clean"
fi
success "Clean repository test passed."
# Test 2: Repo with Secret (Regex-Only) - Expect exit code 1
info "\nRunning test on repo with secret (regex-only)..."
set +e
output_secret=$($BASE_DOCKER_RUN -v "$REPO_SECRET_PATH:/scan" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_secret=$?
set -e
if [ $exit_code_secret -ne 1 ]; then
error "Expected exit code 1 for repo with secret, but got $exit_code_secret. Output:\n$output_secret"
fi
if ! echo "$output_secret" | grep -q "github-pat"; then
error "Expected finding for rule 'github-pat' was not found. Output:\n$output_secret"
fi
success "Repo with secret (regex-only) test passed."
# GenAI related tests
if [ -z "$GEMINI_API_KEY" ]; then
info "\n--- Skipping GenAI tests: GEMINI_API_KEY not set ---"
else
info "\nGEMINI_API_KEY found. Proceeding with GenAI integration tests."
# Test 3: Repo with Secret (GenAI Mode) - Expect exit code 1
info "Running test on repo with secret (GenAI mode)..."
set +e
output_secret_ai=$($BASE_DOCKER_RUN -v "$REPO_SECRET_PATH:/scan" -e GEMINI_API_KEY="$GEMINI_API_KEY" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_secret_ai=$?
set -e
if [ $exit_code_secret_ai -ne 1 ]; then
error "Expected exit code 1 for repo with secret (GenAI mode), but got $exit_code_secret_ai. Output:\n$output_secret_ai"
fi
if ! echo "$output_secret_ai" | grep -q "github-pat"; then
error "Expected finding for 'github-pat' not found in GenAI mode. Output:\n$output_secret_ai"
fi
success "Repo with secret (GenAI mode) test passed (True Positive Confirmed)."
# Test 4: Repo with False Positive (GenAI Mode) - Expect exit code 0
info "\nRunning test on repo with false positive (GenAI mode)..."
set +e
output_fp_ai=$($BASE_DOCKER_RUN -v "$REPO_FP_PATH:/scan" -e GEMINI_API_KEY="$GEMINI_API_KEY" "$DOCKER_IMAGE_NAME" git /scan 2>&1)
exit_code_fp_ai=$?
set -e
if [ $exit_code_fp_ai -ne 0 ]; then
error "Expected exit code 0 for repo with false positive (GenAI mode), but got $exit_code_fp_ai. Output:\n$output_fp_ai"
fi
if ! echo "$output_fp_ai" | grep -q "No secrets found"; then
error "Expected 'No secrets found' for false positive repo. Output:\n$output_fp_ai"
fi
success "Repo with false positive (GenAI mode) test passed (False Positive Filtered)."
fi
info "\n--- Phase 4: Cleaning up ---"
# docker rmi "$DOCKER_IMAGE_NAME"
# success "Test Docker image '$DOCKER_IMAGE_NAME' removed."
echo ""
success "✅ ✅ ✅ ALL TESTS PASSED! ✅ ✅ ✅"scan_all_repos.sh:
#!/bin/bash
# --- Configuration ---
SCAN_DIRECTORY="/home/mdt/dev"
DOCKER_IMAGE="gitleaks-lite:latest"
USE_GEMINI=true
SCAN_TIMEOUT="2m" # Set a 2-minute timeout for each repository scan.
# --- Helper function for logging ---
info() {
echo -e "\033[0;34m[INFO]\033[0m $1"
}
# --- Script Logic ---
# Step 1: Handle API Key securely at the very beginning.
if [ "$USE_GEMINI" = true ] && [ -z "$GEMINI_API_KEY" ]; then
echo "GenAI validation is enabled, but GEMINI_API_KEY is not set."
read -sp "Please enter your Gemini API Key (or press Enter to skip GenAI scans): " temp_api_key
echo
if [ -n "$temp_api_key" ]; then
export GEMINI_API_KEY="$temp_api_key"
info "GEMINI_API_KEY has been set for this session."
else
info "No API key provided. GenAI validation will be skipped."
USE_GEMINI=false
fi
fi
echo "--- Starting Gitleaks-Lite Scan on all repositories in $SCAN_DIRECTORY ---"
echo "--- Timeout per repository is set to $SCAN_TIMEOUT ---"
repos_with_findings=0
start_time=$SECONDS
# Step 2: Loop through directories and scan.
for dir in "$SCAN_DIRECTORY"/*/; do
if [ -d "$dir" ]; then
repo_path=$(realpath "$dir")
repo_name=$(basename "$repo_path")
if git -C "$repo_path" rev-parse --is-inside-work-tree > /dev/null 2>&1; then
echo ""
echo "========================================================================"
echo "Scanning repository: $repo_name"
echo "========================================================================"
docker_args=( "run" "--rm" "-v" "$repo_path:/scan" )
if [ "$USE_GEMINI" = true ] && [ -n "$GEMINI_API_KEY" ]; then
docker_args+=("-e" "GEMINI_API_KEY=$GEMINI_API_KEY")
fi
docker_args+=("$DOCKER_IMAGE" "git" "/scan")
# Execute the command with a timeout. This simplified execution is more stable.
# The output will appear when the command finishes or times out.
if output=$(timeout --foreground "$SCAN_TIMEOUT" docker "${docker_args[@]}" 2>&1); then
# Exit code 0 (success)
echo "$output"
else
status=$?
if [ $status -eq 124 ]; then
echo "❌ ERROR: Scan for $repo_name timed out after $SCAN_TIMEOUT."
elif [ $status -eq 1 ]; then
echo "$output"
echo "🚨🚨🚨 WARNING: Secrets found in $repo_name 🚨🚨🚨"
repos_with_findings=$((repos_with_findings + 1))
else
echo "❌ ERROR: Scan for $repo_name failed with an unexpected error (exit code $status)."
echo "--- Error Output ---"
echo "$output"
echo "--------------------"
fi
fi
else
echo "-> Skipping '$repo_name' (not a Git repository)"
fi
fi
done
end_time=$SECONDS
duration=$((end_time - start_time))
echo ""
echo "========================================================================"
echo "--- Scan Complete ---"
echo "Total execution time: $((duration / 60)) minutes and $((duration % 60)) seconds."
if [ $repos_with_findings -gt 0 ]; then
echo "Total repositories with findings: $repos_with_findings"
exit 1
else
echo "✅ No secrets found in any repository."
fitestdata/repos/repo-clean/README.md:
This is a clean repository with no secrets.
testdata/repos/repo-with-false-positive/examples.py:
# This is an example key for documentation purposes only.
# It is not a real production secret.
EXAMPLE_API_KEY = "key_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN"testdata/repos/repo-with-secret/config.yml:
api_key: "ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN"