devsecops

Implementing Secret Scanning with Gitleaks

This skill covers implementing Gitleaks for detecting and preventing hardcoded secrets in git repositories. It addresses configuring pre-commit hooks, CI/CD pipeline integration, custom rule authoring for organization-specific secrets, baseline management for existing repositories, and remediation workflows for exposed credentials.

cicddevsecopsgitleakspre-commitsecret-scanningsecure-sdlc
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When developers may accidentally commit API keys, passwords, tokens, or private keys to repositories
  • When establishing pre-commit gates that prevent secrets from entering the git history
  • When scanning existing repository history for previously committed secrets that need rotation
  • When compliance requirements mandate secret detection across all source code repositories
  • When migrating from manual secret audits to automated continuous scanning

Do not use for detecting secrets in running applications or memory (use runtime secret detection), for managing secrets after detection (use Vault or AWS Secrets Manager), or for scanning container images (use Trivy or Grype).

Prerequisites

  • Gitleaks v8.18+ installed via binary, Go install, or Docker
  • Pre-commit framework installed for local hook integration
  • Git repository with history to scan
  • CI/CD platform access (GitHub Actions, GitLab CI, or equivalent)

Workflow

Step 1: Install and Run Initial Repository Scan

Perform a baseline scan of the repository to identify all existing secrets in the git history.

# Install Gitleaks
brew install gitleaks  # macOS
# or download binary from https://github.com/gitleaks/gitleaks/releases
 
# Scan entire git history for secrets
gitleaks detect --source . --report-format json --report-path gitleaks-report.json -v
 
# Scan only staged changes (for pre-commit)
gitleaks protect --staged --report-format json --report-path gitleaks-staged.json
 
# Scan specific commit range
gitleaks detect --source . --log-opts="HEAD~10..HEAD" --report-format json
 
# Scan without git history (filesystem only)
gitleaks detect --source . --no-git --report-format json

Step 2: Configure Pre-Commit Hook

Set up Gitleaks as a pre-commit hook to prevent secrets from being committed.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks
        name: gitleaks
        description: Detect hardcoded secrets using Gitleaks
        entry: gitleaks protect --staged --verbose --redact
        language: golang
        pass_filenames: false
# Install pre-commit framework
pip install pre-commit
 
# Install hooks defined in .pre-commit-config.yaml
pre-commit install
 
# Run against all files (not just staged)
pre-commit run gitleaks --all-files
 
# Test the hook with a deliberate secret
echo 'AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"' >> test.txt
git add test.txt
git commit -m "test"  # Should be blocked by gitleaks

Step 3: Integrate into GitHub Actions

# .github/workflows/secret-scanning.yml
name: Secret Scanning
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  gitleaks:
    name: Gitleaks Secret Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for comprehensive scanning
 
      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}  # Required for gitleaks-action v2
 
      # Alternative: Run Gitleaks directly
      - name: Install Gitleaks
        run: |
          wget -q https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz
          tar -xzf gitleaks_8.21.2_linux_x64.tar.gz
          chmod +x gitleaks
 
      - name: Scan for secrets
        run: |
          if [ "${{ github.event_name }}" == "pull_request" ]; then
            ./gitleaks detect \
              --source . \
              --log-opts="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" \
              --report-format sarif \
              --report-path gitleaks.sarif \
              --exit-code 1
          else
            ./gitleaks detect \
              --source . \
              --report-format sarif \
              --report-path gitleaks.sarif \
              --exit-code 1 \
              --baseline-path .gitleaks-baseline.json
          fi
 
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: gitleaks.sarif
          category: gitleaks

Step 4: Author Custom Detection Rules

Create organization-specific rules for internal secret patterns.

# .gitleaks.toml
title = "Organization Gitleaks Configuration"
 
[extend]
useDefault = true  # Include all default rules
 
# Custom rule for internal API tokens
[[rules]]
id = "internal-api-token"
description = "Internal API token for service-to-service auth"
regex = '''(?i)x-internal-token["\s:=]+["\']?([a-zA-Z0-9_\-]{40,})["\']?'''
entropy = 3.5
keywords = ["x-internal-token"]
tags = ["internal", "api"]
 
[[rules]]
id = "database-connection-string"
description = "Database connection string with embedded credentials"
regex = '''(?i)(postgres|mysql|mongodb|redis)://[^:]+:[^@]+@[^/]+/\w+'''
keywords = ["postgres://", "mysql://", "mongodb://", "redis://"]
tags = ["database", "credentials"]
 
[[rules]]
id = "jwt-secret"
description = "JWT signing secret"
regex = '''(?i)(jwt[_-]?secret|jwt[_-]?key)["\s:=]+["\']?([a-zA-Z0-9/+_\-]{32,})["\']?'''
entropy = 3.0
keywords = ["jwt_secret", "jwt-secret", "jwt_key", "jwt-key"]
 
# Allowlist for test files and known safe patterns
[allowlist]
description = "Global allowlist"
paths = [
  '''(^|/)test(s)?/''',
  '''(^|/)spec/''',
  '''\.test\.(js|ts|py)$''',
  '''\.spec\.(js|ts|py)$''',
  '''__mocks__/''',
  '''fixtures/''',
  '''(^|/)vendor/''',
  '''node_modules/'''
]
regexes = [
  '''EXAMPLE''',
  '''example\.com''',
  '''test[-_]?(key|secret|token|password)''',
  '''(?i)placeholder''',
  '''000000+'''
]

Step 5: Manage Baselines for Existing Repositories

Create a baseline of known findings to avoid blocking development while historical secrets are being rotated.

# Generate baseline from current state
gitleaks detect --source . --report-format json --report-path .gitleaks-baseline.json
 
# Subsequent scans compare against baseline (only new findings trigger failures)
gitleaks detect --source . --baseline-path .gitleaks-baseline.json --exit-code 1
 
# Review baseline periodically and remove entries as secrets are rotated
cat .gitleaks-baseline.json | python3 -m json.tool | head -50

Step 6: Remediate Exposed Secrets

When a secret is detected, follow the rotation and history cleanup procedure.

# 1. Immediately rotate the exposed credential
#    - Revoke the old API key/token in the service provider
#    - Generate a new credential
#    - Store the new credential in a secrets manager
 
# 2. Remove secret from git history using git-filter-repo
pip install git-filter-repo
 
# Create expressions file for secrets to remove
cat > /tmp/expressions.txt << 'EOF'
regex:AKIA[0-9A-Z]{16}==>REDACTED_AWS_KEY
regex:(?i)password\s*=\s*"[^"]*"==>password="REDACTED"
EOF
 
git filter-repo --replace-text /tmp/expressions.txt --force
 
# 3. Force-push the cleaned history (coordinate with team)
# git push --force --all  # WARNING: Requires team coordination
 
# 4. Add the secret pattern to .gitleaks.toml rules
# 5. Update the baseline file to remove the resolved finding

Key Concepts

Term Definition
Secret Any credential, token, key, or sensitive string that should not appear in source code
Pre-commit Hook Git hook that runs before a commit is created, blocking commits containing detected secrets
Entropy Measure of randomness in a string; high-entropy strings are more likely to be secrets
Baseline Snapshot of existing findings used to differentiate new secrets from pre-existing ones
Allowlist Configuration specifying paths, patterns, or commits to exclude from detection
SARIF Static Analysis Results Interchange Format for uploading findings to security dashboards
git-filter-repo Tool for rewriting git history to remove sensitive data from all commits

Tools & Systems

  • Gitleaks: Open-source secret detection tool supporting pre-commit hooks, CI/CD, and historical scanning
  • pre-commit: Framework for managing and maintaining multi-language pre-commit hooks
  • git-filter-repo: History rewriting tool for removing secrets from git history
  • TruffleHog: Alternative secret scanner with verified secret detection capabilities
  • GitHub Secret Scanning: Native GitHub feature that detects secrets matching partner patterns

Common Scenarios

Scenario: Onboarding Secret Scanning on a Legacy Repository

Context: A 5-year-old repository has never been scanned. The team needs to enable secret scanning without blocking all development while historical secrets are rotated.

Approach:

  1. Run gitleaks detect against full history and generate a baseline JSON file
  2. Triage each finding: classify as active (needs rotation), inactive (already rotated), or false positive
  3. Immediately rotate all active secrets and update consuming services
  4. Commit the baseline file (excluding active secrets that have been fixed)
  5. Enable pre-commit hooks for new development immediately
  6. Add CI/CD scanning with the baseline to catch only new secrets
  7. Progressively reduce the baseline as historical secrets are rotated

Pitfalls: Generating a baseline without triaging means accepting risk on unrotated secrets. Never assume a historical secret is inactive without verifying with the service provider. Running git-filter-repo on a shared repository without coordination will cause rebase conflicts for all team members.

Output Format

Gitleaks Secret Scanning Report
=================================
Repository: org/web-application
Scan Type: Full History
Commits Scanned: 4,523
Date: 2026-02-23
 
FINDINGS:
  Total: 12
  New (not in baseline): 3
  Baseline (pre-existing): 9
 
NEW FINDINGS (blocking):
  [1] AWS Access Key ID
      Rule: aws-access-key-id
      File: src/config/aws.py:23
      Commit: a1b2c3d (2026-02-22, dev@company.com)
      Secret: AKIA...REDACTED
      Entropy: 3.8
 
  [2] GitHub Personal Access Token
      Rule: github-pat
      File: scripts/deploy.sh:15
      Commit: d4e5f6g (2026-02-21, ops@company.com)
      Secret: ghp_...REDACTED
      Entropy: 4.2
 
  [3] Internal API Token
      Rule: internal-api-token
      File: src/services/auth.py:89
      Commit: h7i8j9k (2026-02-20, dev@company.com)
 
QUALITY GATE: FAILED (3 new findings)
Action: Rotate exposed credentials immediately.
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md4.5 KB

API Reference: Gitleaks Secret Scanning

Libraries Used

Library Purpose
subprocess Execute gitleaks CLI commands
json Parse gitleaks JSON report output
pathlib Handle repository and report file paths
os Read GITLEAKS_CONFIG environment variable

Installation

# Install gitleaks binary
# macOS
brew install gitleaks
 
# Linux
curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64 -o gitleaks
chmod +x gitleaks && sudo mv gitleaks /usr/local/bin/
 
# Docker
docker pull ghcr.io/gitleaks/gitleaks:latest

CLI Commands

Scan a Git Repository

gitleaks git --source=/path/to/repo --report-format=json --report-path=results.json

Scan a Directory (Non-Git)

gitleaks dir --source=/path/to/code --report-format=json --report-path=results.json

Scan from stdin

echo "aws_secret_access_key=AKIAIOSFODNN7EXAMPLE" | gitleaks stdin

Key CLI Flags

Flag Description
--source Path to repository or directory to scan
--config, -c Path to custom gitleaks.toml config
--report-format, -f Output format: json, csv, junit, sarif
--report-path, -r Path to write the report file
--baseline-path Ignore known findings from baseline file
--exit-code Exit code when leaks found (default: 1)
--redact Redact secrets in output (percent: 0-100)
--verbose, -v Show verbose scan output
--no-git Treat source as plain directory
--log-level Log level: trace, debug, info, warn, error
--max-target-megabytes Skip files larger than this size

Custom Configuration (.gitleaks.toml)

title = "Custom Gitleaks Config"
 
[extend]
useDefault = true  # Extend the default ruleset
 
[[rules]]
id = "custom-internal-token"
description = "Internal API token pattern"
regex = '''(?i)internal[_-]?token\s*[:=]\s*['"]?([a-zA-Z0-9]{32,})'''
tags = ["internal", "token"]
keywords = ["internal_token", "internal-token"]
 
[[rules]]
id = "custom-db-password"
description = "Database password in config"
regex = '''(?i)(db|database|mysql|postgres)[_-]?pass(word)?\s*[:=]\s*['"]?[^\s'"]{8,}'''
tags = ["database", "password"]
 
[rules.allowlist]
paths = ['''test/.*''', '''mock/.*''']
regexTarget = "line"
regexes = ['''(?i)example|placeholder|changeme|test''']
 
[[allowlist.paths]]
regex = '''vendor/.*'''
 
[[allowlist.commits]]
sha = "abc123def456"

Python Integration

Run Gitleaks and Parse Results

import subprocess
import json
from pathlib import Path
 
def scan_repository(repo_path, config_path=None):
    cmd = [
        "gitleaks", "git",
        "--source", str(repo_path),
        "--report-format", "json",
        "--report-path", "/tmp/gitleaks-report.json",
        "--exit-code", "0",
    ]
    if config_path:
        cmd.extend(["--config", str(config_path)])
 
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
 
    report_path = Path("/tmp/gitleaks-report.json")
    if report_path.exists():
        with open(report_path) as f:
            findings = json.load(f)
        return findings
    return []

Categorize Findings by Severity

HIGH_SEVERITY_RULES = {
    "aws-access-key", "aws-secret-key", "gcp-api-key",
    "github-pat", "private-key", "generic-api-key",
}
 
def categorize_findings(findings):
    high, medium, low = [], [], []
    for f in findings:
        rule = f.get("RuleID", "")
        if rule in HIGH_SEVERITY_RULES:
            high.append(f)
        elif "password" in rule or "token" in rule:
            medium.append(f)
        else:
            low.append(f)
    return {"high": high, "medium": medium, "low": low}

GitHub Actions Integration

name: Gitleaks Secret Scan
on: [push, pull_request]
jobs:
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Output Format

[
  {
    "Description": "Detected a Generic API Key",
    "StartLine": 42,
    "EndLine": 42,
    "StartColumn": 15,
    "EndColumn": 55,
    "Match": "REDACTED",
    "Secret": "REDACTED",
    "File": "config/settings.py",
    "Commit": "a1b2c3d4e5f6",
    "Author": "developer@example.com",
    "Date": "2025-01-15T10:30:00Z",
    "RuleID": "generic-api-key",
    "Tags": ["api", "key"],
    "Fingerprint": "a1b2c3d4:config/settings.py:generic-api-key:42"
  }
]
standards.md2.2 KB

Standards Reference: Secret Scanning with Gitleaks

OWASP Top 10 - A07:2021 Identification and Authentication Failures

  • Hardcoded credentials in source code enable unauthorized access
  • Gitleaks detects API keys, passwords, tokens, and private keys before they reach repositories
  • CWE-798: Use of Hard-coded Credentials is a direct mapping

NIST SSDF (SP 800-218)

PW.1: Design Software to Meet Security Requirements

  • PW.1.1: Identify security requirements including credential management
  • Secrets should never be stored in source code; use environment variables or secrets managers

PS.1: Protect All Forms of Code

  • PS.1.1: Store code securely with access controls and secret scanning
  • Implement pre-commit hooks to prevent secrets from entering version control

PS.2: Provide a Mechanism for Verifying Software Release Integrity

  • Code signing and secret scanning ensure software releases do not contain embedded credentials

CIS Software Supply Chain Security Guide

Source Code Controls

  • SC-1: Automated secret scanning on all commits
  • SC-5: No hardcoded credentials in source repositories
  • SC-6: Secrets detection integrated into CI/CD pipeline

OWASP SAMM - Secure Build

Maturity Level 1

  • Scan repositories for common secret patterns using default rulesets
  • Alert developers when secrets are detected in pull requests

Maturity Level 2

  • Custom rules for organization-specific secret patterns
  • Pre-commit hooks prevent secrets from entering history
  • Baseline management for legacy codebases

Maturity Level 3

  • Automated secret rotation workflow triggered by detection
  • Correlation with secrets management systems for validation
  • Historical scanning with git-filter-repo remediation

PCI DSS v4.0

  • Requirement 6.3.1: Security vulnerabilities identified through a defined process including code analysis
  • Requirement 8.6.1: Secrets stored in code or configuration files must be protected
  • Requirement 8.6.3: Passwords/passphrases for application and system accounts are protected against misuse

SOC 2 Trust Service Criteria

  • CC6.1: Logical access security over protected information assets including credential management
  • CC6.7: Restrict transmission of data to authorized external parties (prevent credential leakage)
workflows.md5.0 KB

Workflow Reference: Secret Scanning with Gitleaks

Secret Detection Pipeline

Developer Workstation          CI/CD Pipeline              Security Response
     │                              │                           │
     ▼                              │                           │
┌──────────────┐                    │                           │
│ Pre-commit   │                    │                           │
│ Hook (local) │                    │                           │
└──────┬───────┘                    │                           │
       │                            │                           │
  ┌────┴────┐                       │                           │
  │         │                       │                           │
PASS      FAIL                      │                           │
  │     (blocked)                   │                           │
  ▼                                 │                           │
Push to                             │                           │
Remote                              │                           │
  │                                 ▼                           │
  │                        ┌──────────────┐                     │
  └───────────────────────>│ Gitleaks CI  │                     │
                           │ Scan (PR)    │                     │
                           └──────┬───────┘                     │
                                  │                             │
                            ┌─────┴─────┐                      │
                            │           │                       │
                          PASS        FAIL                      │
                            │     ┌─────┴──────┐               │
                            │     │ Block PR   │               │
                            │     │ + Alert    │──────────────>│
                            │     └────────────┘    ┌──────────┴──────┐
                            │                       │ Rotate Secret   │
                            │                       │ Update Baseline │
                            │                       │ Clean History   │
                            │                       └─────────────────┘

                    Merge Permitted

Gitleaks Rule Configuration Deep Dive

Rule Anatomy

[[rules]]
id = "rule-unique-identifier"          # Unique rule ID
description = "Human-readable desc"     # What this rule detects
regex = '''pattern'''                   # Detection regex
entropy = 3.5                           # Minimum entropy threshold (optional)
secretGroup = 1                         # Regex capture group containing secret
keywords = ["key1", "key2"]             # Fast pre-filter keywords
tags = ["aws", "credential"]            # Categorization tags
path = '''\.env$'''                     # Path filter regex (optional)

Built-in Rule Categories

Category Example Rules Count
Cloud Provider Keys aws-access-key-id, gcp-service-account 15+
API Tokens github-pat, gitlab-pat, slack-token 20+
Private Keys private-key, rsa-private-key 5+
Database Credentials generic-password, connection-string 10+
Service Tokens stripe-api-key, sendgrid-api-key 30+

Entropy Scoring

  • Entropy measures string randomness (Shannon entropy)
  • Random-looking strings (API keys) have entropy > 3.5
  • Regular English text has entropy around 2.0-3.0
  • Setting entropy threshold reduces false positives on non-random strings
  • Combine entropy with regex for highest accuracy

Remediation Process

Secret Rotation Checklist

  1. Identify the exposed secret type and associated service
  2. Log into the service provider and revoke the exposed credential
  3. Generate a new credential with the same permissions
  4. Store the new credential in a secrets manager (Vault, AWS SM, etc.)
  5. Update all consuming services to use the new credential
  6. Verify service functionality with the new credential
  7. Update the Gitleaks baseline to remove the resolved finding
  8. Optionally clean git history with git-filter-repo

History Cleanup Decision Matrix

Factor Clean History Keep History
Secret is rotated Optional Acceptable
Repo is public Required Never
Compliance mandate Required Not compliant
Active contributor count < 10 preferred > 10 difficult
Secret exposure duration Long (high risk) Short (lower risk)

Scripts 2

agent.py7.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Gitleaks secret scanning agent.

Wraps the Gitleaks CLI to scan git repositories, directories, or
specific commits for hardcoded secrets, API keys, tokens, and
credentials. Parses JSON output into structured findings.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone


def find_gitleaks_binary():
    """Locate the gitleaks binary on the system."""
    custom_path = os.environ.get("GITLEAKS_PATH")
    if custom_path and os.path.isfile(custom_path):
        return custom_path
    for name in ["gitleaks", "gitleaks.exe"]:
        for directory in os.environ.get("PATH", "").split(os.pathsep):
            full_path = os.path.join(directory, name)
            if os.path.isfile(full_path):
                return full_path
    print("[!] gitleaks binary not found. Install: https://github.com/gitleaks/gitleaks",
          file=sys.stderr)
    sys.exit(1)


def run_detect(gitleaks_bin, target, config=None, baseline=None,
               log_opts=None, no_git=False, verbose=False):
    """Run gitleaks detect on a repository or directory."""
    cmd = [gitleaks_bin, "detect"]
    if no_git:
        cmd.extend(["--no-git", "--source", target])
    else:
        cmd.extend(["--source", target])
    cmd.extend(["--report-format", "json", "--report-path", "/dev/stdout"])
    if config:
        cmd.extend(["--config", config])
    if baseline:
        cmd.extend(["--baseline-path", baseline])
    if log_opts:
        cmd.extend(["--log-opts", log_opts])
    if verbose:
        cmd.append("--verbose")
    cmd.extend(["--exit-code", "0"])

    print(f"[*] Running: {' '.join(cmd)}")
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=600,
    )
    if result.returncode not in (0, 1):
        print(f"[!] Gitleaks error (exit {result.returncode}): {result.stderr}",
              file=sys.stderr)
    return result.stdout, result.stderr, result.returncode


def run_protect(gitleaks_bin, target, config=None, staged=False, verbose=False):
    """Run gitleaks protect for pre-commit scanning."""
    cmd = [gitleaks_bin, "protect", "--source", target]
    cmd.extend(["--report-format", "json", "--report-path", "/dev/stdout"])
    if staged:
        cmd.append("--staged")
    if config:
        cmd.extend(["--config", config])
    if verbose:
        cmd.append("--verbose")
    cmd.extend(["--exit-code", "0"])

    print(f"[*] Running protect mode: {' '.join(cmd)}")
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=300,
    )
    return result.stdout, result.stderr, result.returncode


def parse_findings(raw_json):
    """Parse gitleaks JSON output into structured findings."""
    findings = []
    if not raw_json or not raw_json.strip():
        return findings
    try:
        results = json.loads(raw_json)
    except json.JSONDecodeError:
        return findings
    if not isinstance(results, list):
        results = [results]
    for item in results:
        secret_val = item.get("Secret", "")
        findings.append({
            "rule_id": item.get("RuleID", "unknown"),
            "description": item.get("Description", ""),
            "secret": secret_val[:8] + "..." if secret_val else "",
            "file": item.get("File", ""),
            "line": item.get("StartLine", 0),
            "commit": (item.get("Commit", "") or "")[:12],
            "author": item.get("Author", ""),
            "email": item.get("Email", ""),
            "date": item.get("Date", ""),
            "message": (item.get("Message", "") or "")[:80],
            "entropy": item.get("Entropy", 0),
            "fingerprint": item.get("Fingerprint", ""),
            "tags": item.get("Tags", []),
        })
    return findings


def format_summary(findings, target):
    """Print human-readable summary of secrets found."""
    print(f"\n{'='*60}")
    print(f"  Gitleaks Secret Scan Report")
    print(f"{'='*60}")
    print(f"  Target    : {target}")
    print(f"  Secrets   : {len(findings)}")

    if not findings:
        print(f"  Status    : CLEAN - No secrets detected")
        print(f"{'='*60}")
        return

    by_rule = {}
    for f in findings:
        rule = f["rule_id"]
        by_rule.setdefault(rule, []).append(f)

    print(f"\n  Findings by Rule:")
    for rule, items in sorted(by_rule.items(), key=lambda x: -len(x[1])):
        print(f"    {rule:40s}: {len(items)} occurrence(s)")

    by_file = {}
    for f in findings:
        by_file.setdefault(f["file"], []).append(f)

    print(f"\n  Affected Files: {len(by_file)}")
    for filepath, items in sorted(by_file.items(), key=lambda x: -len(x[1]))[:10]:
        print(f"    {filepath} ({len(items)} secret(s))")

    print(f"\n  Top Findings:")
    for f in findings[:15]:
        print(f"    [{f['rule_id']:30s}] {f['file']}:{f['line']} "
              f"(commit: {f['commit']}, secret: {f['secret']})")


def main():
    parser = argparse.ArgumentParser(
        description="Gitleaks secret scanning agent"
    )
    parser.add_argument("--target", required=True,
                        help="Repository path or directory to scan")
    parser.add_argument("--mode", choices=["detect", "protect"], default="detect",
                        help="Scan mode: detect (full history) or protect (pre-commit)")
    parser.add_argument("--config", help="Path to custom .gitleaks.toml config")
    parser.add_argument("--baseline", help="Path to baseline file for known findings")
    parser.add_argument("--log-opts", help="Git log options (e.g., '--since=2026-01-01')")
    parser.add_argument("--no-git", action="store_true",
                        help="Scan files without git history")
    parser.add_argument("--staged", action="store_true",
                        help="Only scan staged changes (protect mode)")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    gitleaks_bin = find_gitleaks_binary()
    print(f"[*] Using gitleaks: {gitleaks_bin}")

    if args.mode == "protect":
        raw_json, stderr, exit_code = run_protect(
            gitleaks_bin, args.target, args.config, args.staged, args.verbose
        )
    else:
        raw_json, stderr, exit_code = run_detect(
            gitleaks_bin, args.target, args.config, args.baseline,
            args.log_opts, args.no_git, args.verbose
        )

    findings = parse_findings(raw_json)
    format_summary(findings, args.target)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "gitleaks",
        "target": args.target,
        "mode": args.mode,
        "secrets_found": len(findings),
        "findings": findings,
        "risk_level": (
            "CRITICAL" if len(findings) > 10
            else "HIGH" if len(findings) > 0
            else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))

    if findings:
        print(f"\n[!] {len(findings)} secret(s) detected - remediation required")
    else:
        print(f"\n[+] No secrets detected")


if __name__ == "__main__":
    main()
process.py9.8 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
Gitleaks Secret Scanning Pipeline Script

Runs Gitleaks scans, manages baselines, evaluates findings,
and generates remediation reports.

Usage:
    python process.py --repo-path /path/to/repo --scan-type detect
    python process.py --repo-path . --scan-type protect --staged
    python process.py --repo-path . --baseline .gitleaks-baseline.json --output report.json
"""

import argparse
import json
import os
import subprocess
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional


@dataclass
class SecretFinding:
    rule_id: str
    description: str
    file: str
    line: int
    commit: str
    author: str
    date: str
    secret: str
    entropy: float
    match: str
    tags: list = field(default_factory=list)
    is_new: bool = True


@dataclass
class ScanResult:
    findings: list = field(default_factory=list)
    new_findings: list = field(default_factory=list)
    baseline_findings: list = field(default_factory=list)
    commits_scanned: int = 0
    scan_duration: float = 0.0
    error: str = ""


def run_gitleaks(repo_path: str, scan_type: str = "detect",
                 baseline_path: Optional[str] = None,
                 commit_range: Optional[str] = None,
                 staged: bool = False,
                 config_path: Optional[str] = None) -> dict:
    """Execute Gitleaks and return JSON results."""
    cmd = ["gitleaks", scan_type, "--source", repo_path,
           "--report-format", "json", "--report-path", "/dev/stdout"]

    if baseline_path and os.path.exists(baseline_path):
        cmd.extend(["--baseline-path", baseline_path])

    if commit_range:
        cmd.extend(["--log-opts", commit_range])

    if staged:
        cmd.append("--staged")

    if config_path:
        cmd.extend(["--config", config_path])

    cmd.append("--verbose")

    try:
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=300
        )

        findings = []
        if proc.stdout.strip():
            try:
                findings = json.loads(proc.stdout)
            except json.JSONDecodeError:
                pass

        return {
            "findings": findings if isinstance(findings, list) else [],
            "exit_code": proc.returncode,
            "stderr": proc.stderr
        }

    except subprocess.TimeoutExpired:
        return {"findings": [], "exit_code": -1, "stderr": "Scan timed out after 300s"}
    except FileNotFoundError:
        return {"findings": [], "exit_code": -1,
                "stderr": "gitleaks not found. Install from https://github.com/gitleaks/gitleaks"}


def parse_findings(raw_findings: list) -> list:
    """Parse raw Gitleaks JSON findings into SecretFinding objects."""
    findings = []
    for f in raw_findings:
        redacted_secret = redact_secret(f.get("Secret", ""))
        findings.append(SecretFinding(
            rule_id=f.get("RuleID", "unknown"),
            description=f.get("Description", ""),
            file=f.get("File", ""),
            line=f.get("StartLine", 0),
            commit=f.get("Commit", "")[:8],
            author=f.get("Author", ""),
            date=f.get("Date", ""),
            secret=redacted_secret,
            entropy=f.get("Entropy", 0.0),
            match=f.get("Match", "")[:100],
            tags=f.get("Tags", [])
        ))
    return findings


def redact_secret(secret: str) -> str:
    """Redact a secret, showing only first 4 and last 4 characters."""
    if len(secret) <= 12:
        return "*" * len(secret)
    return secret[:4] + "..." + secret[-4:]


def load_baseline(baseline_path: str) -> set:
    """Load baseline fingerprints for comparison."""
    if not os.path.exists(baseline_path):
        return set()

    with open(baseline_path, "r") as f:
        try:
            baseline = json.load(f)
        except json.JSONDecodeError:
            return set()

    fingerprints = set()
    for entry in baseline:
        fp = f"{entry.get('RuleID', '')}:{entry.get('File', '')}:{entry.get('Commit', '')}"
        fingerprints.add(fp)

    return fingerprints


def classify_findings(findings: list, baseline_fingerprints: set) -> tuple:
    """Separate findings into new and baseline (pre-existing)."""
    new_findings = []
    baseline_findings = []

    for f in findings:
        fp = f"{f.rule_id}:{f.file}:{f.commit}"
        if fp in baseline_fingerprints:
            f.is_new = False
            baseline_findings.append(f)
        else:
            f.is_new = True
            new_findings.append(f)

    return new_findings, baseline_findings


def generate_report(scan_result: ScanResult, repo_path: str) -> dict:
    """Generate a structured scan report."""
    rule_summary = {}
    for f in scan_result.findings:
        rule_summary[f.rule_id] = rule_summary.get(f.rule_id, 0) + 1

    author_summary = {}
    for f in scan_result.new_findings:
        author_summary[f.author] = author_summary.get(f.author, 0) + 1

    return {
        "report_metadata": {
            "repository": repo_path,
            "scan_date": datetime.now(timezone.utc).isoformat(),
            "duration_seconds": scan_result.scan_duration,
            "commits_scanned": scan_result.commits_scanned
        },
        "summary": {
            "total_findings": len(scan_result.findings),
            "new_findings": len(scan_result.new_findings),
            "baseline_findings": len(scan_result.baseline_findings),
            "unique_rules_triggered": len(rule_summary),
            "rules_breakdown": rule_summary,
            "authors_with_new_findings": author_summary
        },
        "quality_gate": {
            "passed": len(scan_result.new_findings) == 0,
            "blocking_count": len(scan_result.new_findings)
        },
        "new_findings": [
            {
                "rule_id": f.rule_id,
                "description": f.description,
                "file": f.file,
                "line": f.line,
                "commit": f.commit,
                "author": f.author,
                "date": f.date,
                "secret_preview": f.secret,
                "entropy": f.entropy
            }
            for f in scan_result.new_findings
        ],
        "remediation_steps": [
            f"1. Rotate the {f.rule_id} credential found in {f.file}"
            for f in scan_result.new_findings
        ]
    }


def main():
    parser = argparse.ArgumentParser(description="Gitleaks Secret Scanning Pipeline")
    parser.add_argument("--repo-path", required=True, help="Path to git repository")
    parser.add_argument("--scan-type", default="detect", choices=["detect", "protect"],
                        help="Scan type: detect (history) or protect (staged/pre-commit)")
    parser.add_argument("--baseline", default=None, help="Path to baseline JSON file")
    parser.add_argument("--commit-range", default=None,
                        help="Git log range (e.g., HEAD~10..HEAD)")
    parser.add_argument("--staged", action="store_true",
                        help="Scan only staged changes (for pre-commit)")
    parser.add_argument("--config", default=None, help="Path to .gitleaks.toml config")
    parser.add_argument("--output", default="gitleaks-report.json", help="Output report path")
    parser.add_argument("--fail-on-findings", action="store_true",
                        help="Exit non-zero on new findings")
    parser.add_argument("--create-baseline", action="store_true",
                        help="Generate baseline from current findings")
    args = parser.parse_args()

    repo_path = os.path.abspath(args.repo_path)
    start_time = datetime.now(timezone.utc)

    print(f"[*] Running Gitleaks {args.scan_type} on {repo_path}")

    raw_result = run_gitleaks(
        repo_path,
        scan_type=args.scan_type,
        baseline_path=args.baseline,
        commit_range=args.commit_range,
        staged=args.staged,
        config_path=args.config
    )

    if raw_result["exit_code"] == -1:
        print(f"[ERROR] {raw_result['stderr']}")
        sys.exit(2)

    scan_result = ScanResult()
    scan_result.findings = parse_findings(raw_result["findings"])
    scan_result.scan_duration = (datetime.now(timezone.utc) - start_time).total_seconds()

    if args.baseline:
        baseline_fps = load_baseline(args.baseline)
        scan_result.new_findings, scan_result.baseline_findings = classify_findings(
            scan_result.findings, baseline_fps
        )
    else:
        scan_result.new_findings = scan_result.findings
        scan_result.baseline_findings = []

    if args.create_baseline:
        baseline_path = os.path.join(repo_path, ".gitleaks-baseline.json")
        with open(baseline_path, "w") as f:
            json.dump(raw_result["findings"], f, indent=2)
        print(f"[*] Baseline created: {baseline_path}")
        print(f"    Contains {len(raw_result['findings'])} findings")
        return

    report = generate_report(scan_result, repo_path)

    output_path = os.path.abspath(args.output)
    with open(output_path, "w") as f:
        json.dump(report, f, indent=2)
    print(f"[*] Report: {output_path}")

    print(f"\n[*] Total: {len(scan_result.findings)} | "
          f"New: {len(scan_result.new_findings)} | "
          f"Baseline: {len(scan_result.baseline_findings)}")

    if scan_result.new_findings:
        print("\n[!] New secrets detected:")
        for f in scan_result.new_findings:
            print(f"  [{f.rule_id}] {f.file}:{f.line} (commit: {f.commit}, author: {f.author})")
            print(f"    Secret: {f.secret}")

    if report["quality_gate"]["passed"]:
        print("\n[PASS] No new secrets detected.")
    else:
        print(f"\n[FAIL] {len(scan_result.new_findings)} new secrets found. Rotate immediately.")

    if args.fail_on_findings and not report["quality_gate"]["passed"]:
        sys.exit(1)


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 4.7 KB
Keep exploring