Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions gitshield/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ def uninstall_hook() -> None:
group["hooks"] = remaining
filtered.append(group)

settings["hooks"]["PreToolUse"] = filtered
settings.setdefault("hooks", {})["PreToolUse"] = filtered

# Clean up empty structures
if not settings["hooks"]["PreToolUse"]:
del settings["hooks"]["PreToolUse"]
if not settings["hooks"]:
del settings["hooks"]
if not settings.get("hooks", {}).get("PreToolUse"):
settings.get("hooks", {}).pop("PreToolUse", None)
if not settings.get("hooks"):
settings.pop("hooks", None)

_save_settings(settings)
click.echo(colorize("GitShield hook removed from Claude Code.", Colors.GREEN))
Expand Down
11 changes: 4 additions & 7 deletions gitshield/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import click

from . import __version__
from .config import filter_findings, load_config, load_ignore_list, find_git_root
from .config import build_custom_patterns, filter_findings, load_config, load_ignore_list, find_git_root
from .formatter import print_findings, print_json, print_blocked_message, colorize, Colors
from .scanner import scan_path, ScannerError

Expand All @@ -29,11 +29,11 @@ def main():
def scan(path: str, staged: bool, no_git: bool, as_json: bool, sarif: bool, quiet: bool):
"""Scan for secrets in PATH (default: current directory)."""
try:
findings = scan_path(path, staged_only=staged, no_git=no_git)
config = load_config(Path(path))
findings = scan_path(path, staged_only=staged, no_git=no_git, scan_tests=config.scan_tests)

# Filter ignored
ignores = load_ignore_list(Path(path))
config = load_config(Path(path))
findings = filter_findings(findings, ignores, config=config)

# Output
Expand Down Expand Up @@ -277,12 +277,9 @@ def patrol(repo: str, limit: int, dry_run: bool, stats: bool):
click.echo(f" Secrets found: {total_findings}")
click.echo(f" Notifications: {notified_count}")

except GitHubError as e:
except (GitHubError, ScannerError) as e:
click.echo(colorize(f"Error: {e}", Colors.RED), err=True)
sys.exit(1)
except Exception as e:
click.echo(colorize(f"Error: {e}", Colors.RED), err=True)
sys.exit(2)


if __name__ == "__main__":
Expand Down
62 changes: 59 additions & 3 deletions gitshield/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
from __future__ import annotations

import fnmatch
import functools
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Set

from .scanner import Finding
from .models import Finding
from .patterns import Pattern

# ---------------------------------------------------------------------------
# TOML parser import -- gracefully degrade when unavailable
Expand Down Expand Up @@ -205,14 +207,22 @@ def create_default_config(path: Path, force: bool = False) -> Path:
# ---------------------------------------------------------------------------
# Filtering
# ---------------------------------------------------------------------------
@functools.lru_cache(maxsize=512)
def _compile_glob(pattern: str):
"""Compile a glob pattern to a regex (cached)."""
import re
return re.compile(fnmatch.translate(pattern))


def _matches_any_glob(filepath: str, patterns: List[str]) -> bool:
"""Check if *filepath* matches any of the given glob patterns."""
for pattern in patterns:
compiled = _compile_glob(pattern)
# Match against the full relative path
if fnmatch.fnmatch(filepath, pattern):
if compiled.fullmatch(filepath):
return True
# Also match against just the filename
if fnmatch.fnmatch(Path(filepath).name, pattern):
if compiled.fullmatch(Path(filepath).name):
return True
return False

Expand Down Expand Up @@ -256,6 +266,52 @@ def filter_findings(
return filtered


# ---------------------------------------------------------------------------
# Custom pattern builder
# ---------------------------------------------------------------------------

def build_custom_patterns(config: "GitShieldConfig") -> List[Pattern]:
"""Convert config.custom_patterns dicts into Pattern objects.

Skips entries with invalid severity or un-compilable regex (logs to stderr).
Returns an empty list when config has no custom patterns.
"""
import re
import sys

built: List[Pattern] = []
for raw in config.custom_patterns:
pattern_id = str(raw.get("name", "custom-pattern"))
regex_str = str(raw.get("regex", ""))
description = str(raw.get("description", ""))
severity = str(raw.get("severity", "medium"))
entropy_threshold = raw.get("entropy_threshold")

if not regex_str:
print(f"gitshield: custom pattern '{pattern_id}' has no regex, skipping", file=sys.stderr)
continue

try:
compiled = re.compile(regex_str)
except re.error as exc:
print(f"gitshield: custom pattern '{pattern_id}' has invalid regex: {exc}", file=sys.stderr)
continue

try:
built.append(Pattern(
id=pattern_id,
name=pattern_id,
regex=compiled,
description=description,
severity=severity,
entropy_threshold=float(entropy_threshold) if entropy_threshold is not None else None,
))
except ValueError as exc:
print(f"gitshield: custom pattern '{pattern_id}' error: {exc}", file=sys.stderr)

return built


# ---------------------------------------------------------------------------
# Legacy ignore file creation (unchanged API)
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading