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
6 changes: 4 additions & 2 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type = layers
layers =
ggshield.__main__
ggshield.cmd.auth | ggshield.cmd.config | ggshield.cmd.hmsl | ggshield.cmd.honeytoken | ggshield.cmd.install | ggshield.cmd.plugin | ggshield.cmd.quota | ggshield.cmd.secret | ggshield.cmd.status | ggshield.cmd.utils
ggshield.verticals.auth | ggshield.verticals.hmsl | ggshield.verticals.secret
ggshield.verticals.ai | ggshield.verticals.auth | ggshield.verticals.hmsl | ggshield.verticals.secret
ggshield.core
click | ggshield.utils | pygitguardian
ignore_imports =
Expand All @@ -33,6 +33,7 @@ source_modules =
ggshield.cmd.status
ggshield.cmd.utils
forbidden_modules =
ggshield.verticals.ai
ggshield.verticals.auth
ggshield.verticals.hmsl
ggshield.verticals.secret
Expand All @@ -46,7 +47,7 @@ ignore_imports =
ggshield.cmd.hmsl.** -> ggshield.verticals.hmsl.**
ggshield.cmd.honeytoken.** -> ggshield.verticals.honeytoken
ggshield.cmd.honeytoken.** -> ggshield.verticals.honeytoken.**
ggshield.cmd.install -> ggshield.verticals.secret.ai_hook
ggshield.cmd.install -> ggshield.verticals.ai.installation
ggshield.cmd.install.** -> ggshield.verticals.install
ggshield.cmd.install.** -> ggshield.verticals.install.**
ggshield.cmd.plugin.** -> ggshield.core.plugin
Expand All @@ -55,6 +56,7 @@ ignore_imports =
ggshield.cmd.quota.** -> ggshield.verticals.quota.**
ggshield.cmd.secret.** -> ggshield.verticals.secret
ggshield.cmd.secret.** -> ggshield.verticals.secret.**
ggshield.cmd.secret.scan.ai_hook -> ggshield.verticals.ai.hooks
ggshield.cmd.status.** -> ggshield.verticals.status
ggshield.cmd.status.** -> ggshield.verticals.status.**
ggshield.cmd.utils.** -> ggshield.verticals.utils
Expand Down
6 changes: 3 additions & 3 deletions ggshield/cmd/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ggshield.core.dirs import get_data_dir
from ggshield.core.errors import UnexpectedError
from ggshield.utils.git_shell import check_git_dir, git
from ggshield.verticals.secret.ai_hook import AI_FLAVORS, install_hooks
from ggshield.verticals.ai.installation import AGENTS, install_hooks


# This snippet is used by the global hook to call the hook defined in the
Expand Down Expand Up @@ -39,7 +39,7 @@
@click.option(
"--hook-type",
"-t",
type=click.Choice(["pre-commit", "pre-push"] + list(AI_FLAVORS.keys())),
type=click.Choice(["pre-commit", "pre-push"] + list(AGENTS.keys())),
help="Type of hook to install.",
default="pre-commit",
)
Expand All @@ -61,7 +61,7 @@ def install_cmd(
It can also install ggshield as a Cursor IDE or Claude Code agent hook.
"""

if hook_type in AI_FLAVORS:
if hook_type in AGENTS:
return install_hooks(name=hook_type, mode=mode, force=force)

return_code = (
Expand Down
6 changes: 4 additions & 2 deletions ggshield/cmd/secret/scan/ai_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
from ggshield.core import ui
from ggshield.core.client import create_client_from_config
from ggshield.core.scan import ScanContext, ScanMode
from ggshield.verticals.ai.hooks import AIHookScanner
from ggshield.verticals.secret import SecretScanner
from ggshield.verticals.secret.ai_hook import AIHookScanner
from ggshield.verticals.secret.ai_hook.models import MAX_READ_SIZE


MAX_READ_SIZE = 1024 * 1024 * 10 # We restrict stdin read to 10MB


@click.command()
Expand Down
4 changes: 4 additions & 0 deletions ggshield/core/scan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .scan_context import ScanContext
from .scan_mode import ScanMode
from .scannable import DecodeError, NonSeekableFileError, Scannable, StringScannable
from .scanner import ResultsProtocol, ScannerProtocol, SecretProtocol


__all__ = [
Expand All @@ -11,8 +12,11 @@
"DecodeError",
"File",
"NonSeekableFileError",
"ResultsProtocol",
"ScanContext",
"ScanMode",
"Scannable",
"ScannerProtocol",
"SecretProtocol",
"StringScannable",
]
50 changes: 50 additions & 0 deletions ggshield/core/scan/scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Protocols for SecretScanner and its results,
so that other verticals can use the scanner if they are provided one.
"""

from collections.abc import Sequence
from typing import Iterable, Optional, Protocol

from pygitguardian.models import Match

from ggshield.core.scanner_ui import ScannerUI

from . import Scannable


class SecretProtocol(Protocol):
"""Abstract base class for secrets.

We use getters instead of properties to have a .
"""

@property
def detector_display_name(self) -> str: ...

@property
def validity(self) -> str: ...

@property
def matches(self) -> Sequence[Match]: ...


class ResultProtocol(Protocol):
@property
def secrets(self) -> Sequence[SecretProtocol]: ...


class ResultsProtocol(Protocol):
@property
def results(self) -> Sequence[ResultProtocol]: ...


class ScannerProtocol(Protocol):
"""Protocol for scanners."""

def scan(
self,
files: Iterable[Scannable],
scanner_ui: ScannerUI,
scan_threads: Optional[int] = None,
) -> ResultsProtocol: ...
10 changes: 10 additions & 0 deletions ggshield/verticals/ai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .agents import AGENTS
from .hooks import AIHookScanner
from .installation import install_hooks


__all__ = [
"AGENTS",
"AIHookScanner",
"install_hooks",
]
9 changes: 9 additions & 0 deletions ggshield/verticals/ai/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .claude_code import Claude
from .copilot import Copilot
from .cursor import Cursor


AGENTS = {agent.name: agent for agent in [Cursor(), Claude(), Copilot()]}


__all__ = ["AGENTS", "Claude", "Copilot", "Cursor"]
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@

import click

from .models import EventType, Flavor, Result
from ..models import Agent, EventType, HookResult


class Claude(Flavor):
class Claude(Agent):
"""Behavior specific to Claude Code."""

name = "Claude Code"
@property
def name(self) -> str:
return "claude-code"

@property
def display_name(self) -> str:
return "Claude Code"

def output_result(self, result: Result) -> int:
def output_result(self, result: HookResult) -> int:
response = {}
if result.block:
if result.payload.event_type in [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import click

from ..models import EventType, HookResult
from .claude_code import Claude
from .models import EventType, Result


class Copilot(Claude):
Expand All @@ -13,9 +13,15 @@ class Copilot(Claude):
Inherits most of its behavior from Claude Code.
"""

name = "Copilot"
@property
def name(self) -> str:
return "copilot"

@property
def display_name(self) -> str:
return "Copilot Chat"

def output_result(self, result: Result) -> int:
def output_result(self, result: HookResult) -> int:
response = {}
if result.block:
if result.payload.event_type == EventType.PRE_TOOL_USE:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@

import click

from .models import EventType, Flavor, Result
from ..models import Agent, EventType, HookResult


class Cursor(Flavor):
class Cursor(Agent):
"""Behavior specific to Cursor."""

name = "Cursor"
@property
def name(self) -> str:
return "cursor"

@property
def display_name(self) -> str:
return "Cursor"

def output_result(self, result: Result) -> int:
def output_result(self, result: HookResult) -> int:
response = {}
if result.payload.event_type == EventType.USER_PROMPT:
response["continue"] = not result.block
Expand Down
Loading
Loading