Skip to content

Commit 92705f5

Browse files
Merge pull request #1195 from GitGuardian/ppetit/-/ai-vertical
[Refactoring] Create a new "AI" vertical
2 parents 939852c + 17f9363 commit 92705f5

File tree

17 files changed

+610
-589
lines changed

17 files changed

+610
-589
lines changed

.importlinter

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type = layers
1010
layers =
1111
ggshield.__main__
1212
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
13-
ggshield.verticals.auth | ggshield.verticals.hmsl | ggshield.verticals.secret
13+
ggshield.verticals.ai | ggshield.verticals.auth | ggshield.verticals.hmsl | ggshield.verticals.secret
1414
ggshield.core
1515
click | ggshield.utils | pygitguardian
1616
ignore_imports =
@@ -33,6 +33,7 @@ source_modules =
3333
ggshield.cmd.status
3434
ggshield.cmd.utils
3535
forbidden_modules =
36+
ggshield.verticals.ai
3637
ggshield.verticals.auth
3738
ggshield.verticals.hmsl
3839
ggshield.verticals.secret
@@ -46,7 +47,7 @@ ignore_imports =
4647
ggshield.cmd.hmsl.** -> ggshield.verticals.hmsl.**
4748
ggshield.cmd.honeytoken.** -> ggshield.verticals.honeytoken
4849
ggshield.cmd.honeytoken.** -> ggshield.verticals.honeytoken.**
49-
ggshield.cmd.install -> ggshield.verticals.secret.ai_hook
50+
ggshield.cmd.install -> ggshield.verticals.ai.installation
5051
ggshield.cmd.install.** -> ggshield.verticals.install
5152
ggshield.cmd.install.** -> ggshield.verticals.install.**
5253
ggshield.cmd.plugin.** -> ggshield.core.plugin
@@ -55,6 +56,7 @@ ignore_imports =
5556
ggshield.cmd.quota.** -> ggshield.verticals.quota.**
5657
ggshield.cmd.secret.** -> ggshield.verticals.secret
5758
ggshield.cmd.secret.** -> ggshield.verticals.secret.**
59+
ggshield.cmd.secret.scan.ai_hook -> ggshield.verticals.ai.hooks
5860
ggshield.cmd.status.** -> ggshield.verticals.status
5961
ggshield.cmd.status.** -> ggshield.verticals.status.**
6062
ggshield.cmd.utils.** -> ggshield.verticals.utils

ggshield/cmd/install.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from ggshield.core.dirs import get_data_dir
1111
from ggshield.core.errors import UnexpectedError
1212
from ggshield.utils.git_shell import check_git_dir, git
13-
from ggshield.verticals.secret.ai_hook import AI_FLAVORS, install_hooks
13+
from ggshield.verticals.ai.installation import AGENTS, install_hooks
1414

1515

1616
# This snippet is used by the global hook to call the hook defined in the
@@ -39,7 +39,7 @@
3939
@click.option(
4040
"--hook-type",
4141
"-t",
42-
type=click.Choice(["pre-commit", "pre-push"] + list(AI_FLAVORS.keys())),
42+
type=click.Choice(["pre-commit", "pre-push"] + list(AGENTS.keys())),
4343
help="Type of hook to install.",
4444
default="pre-commit",
4545
)
@@ -61,7 +61,7 @@ def install_cmd(
6161
It can also install ggshield as a Cursor IDE or Claude Code agent hook.
6262
"""
6363

64-
if hook_type in AI_FLAVORS:
64+
if hook_type in AGENTS:
6565
return install_hooks(name=hook_type, mode=mode, force=force)
6666

6767
return_code = (

ggshield/cmd/secret/scan/ai_hook.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
from ggshield.core import ui
1111
from ggshield.core.client import create_client_from_config
1212
from ggshield.core.scan import ScanContext, ScanMode
13+
from ggshield.verticals.ai.hooks import AIHookScanner
1314
from ggshield.verticals.secret import SecretScanner
14-
from ggshield.verticals.secret.ai_hook import AIHookScanner
15-
from ggshield.verticals.secret.ai_hook.models import MAX_READ_SIZE
15+
16+
17+
MAX_READ_SIZE = 1024 * 1024 * 10 # We restrict stdin read to 10MB
1618

1719

1820
@click.command()

ggshield/core/scan/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .scan_context import ScanContext
44
from .scan_mode import ScanMode
55
from .scannable import DecodeError, NonSeekableFileError, Scannable, StringScannable
6+
from .scanner import ResultsProtocol, ScannerProtocol, SecretProtocol
67

78

89
__all__ = [
@@ -11,8 +12,11 @@
1112
"DecodeError",
1213
"File",
1314
"NonSeekableFileError",
15+
"ResultsProtocol",
1416
"ScanContext",
1517
"ScanMode",
1618
"Scannable",
19+
"ScannerProtocol",
20+
"SecretProtocol",
1721
"StringScannable",
1822
]

ggshield/core/scan/scanner.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Protocols for SecretScanner and its results,
3+
so that other verticals can use the scanner if they are provided one.
4+
"""
5+
6+
from collections.abc import Sequence
7+
from typing import Iterable, Optional, Protocol
8+
9+
from pygitguardian.models import Match
10+
11+
from ggshield.core.scanner_ui import ScannerUI
12+
13+
from . import Scannable
14+
15+
16+
class SecretProtocol(Protocol):
17+
"""Abstract base class for secrets.
18+
19+
We use getters instead of properties to have a .
20+
"""
21+
22+
@property
23+
def detector_display_name(self) -> str: ...
24+
25+
@property
26+
def validity(self) -> str: ...
27+
28+
@property
29+
def matches(self) -> Sequence[Match]: ...
30+
31+
32+
class ResultProtocol(Protocol):
33+
@property
34+
def secrets(self) -> Sequence[SecretProtocol]: ...
35+
36+
37+
class ResultsProtocol(Protocol):
38+
@property
39+
def results(self) -> Sequence[ResultProtocol]: ...
40+
41+
42+
class ScannerProtocol(Protocol):
43+
"""Protocol for scanners."""
44+
45+
def scan(
46+
self,
47+
files: Iterable[Scannable],
48+
scanner_ui: ScannerUI,
49+
scan_threads: Optional[int] = None,
50+
) -> ResultsProtocol: ...

ggshield/verticals/ai/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .agents import AGENTS
2+
from .hooks import AIHookScanner
3+
from .installation import install_hooks
4+
5+
6+
__all__ = [
7+
"AGENTS",
8+
"AIHookScanner",
9+
"install_hooks",
10+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .claude_code import Claude
2+
from .copilot import Copilot
3+
from .cursor import Cursor
4+
5+
6+
AGENTS = {agent.name: agent for agent in [Cursor(), Claude(), Copilot()]}
7+
8+
9+
__all__ = ["AGENTS", "Claude", "Copilot", "Cursor"]

ggshield/verticals/secret/ai_hook/claude_code.py renamed to ggshield/verticals/ai/agents/claude_code.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44

55
import click
66

7-
from .models import EventType, Flavor, Result
7+
from ..models import Agent, EventType, HookResult
88

99

10-
class Claude(Flavor):
10+
class Claude(Agent):
1111
"""Behavior specific to Claude Code."""
1212

13-
name = "Claude Code"
13+
@property
14+
def name(self) -> str:
15+
return "claude-code"
16+
17+
@property
18+
def display_name(self) -> str:
19+
return "Claude Code"
1420

15-
def output_result(self, result: Result) -> int:
21+
def output_result(self, result: HookResult) -> int:
1622
response = {}
1723
if result.block:
1824
if result.payload.event_type in [

ggshield/verticals/secret/ai_hook/copilot.py renamed to ggshield/verticals/ai/agents/copilot.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import click
55

6+
from ..models import EventType, HookResult
67
from .claude_code import Claude
7-
from .models import EventType, Result
88

99

1010
class Copilot(Claude):
@@ -13,9 +13,15 @@ class Copilot(Claude):
1313
Inherits most of its behavior from Claude Code.
1414
"""
1515

16-
name = "Copilot"
16+
@property
17+
def name(self) -> str:
18+
return "copilot"
19+
20+
@property
21+
def display_name(self) -> str:
22+
return "Copilot Chat"
1723

18-
def output_result(self, result: Result) -> int:
24+
def output_result(self, result: HookResult) -> int:
1925
response = {}
2026
if result.block:
2127
if result.payload.event_type == EventType.PRE_TOOL_USE:

ggshield/verticals/secret/ai_hook/cursor.py renamed to ggshield/verticals/ai/agents/cursor.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44

55
import click
66

7-
from .models import EventType, Flavor, Result
7+
from ..models import Agent, EventType, HookResult
88

99

10-
class Cursor(Flavor):
10+
class Cursor(Agent):
1111
"""Behavior specific to Cursor."""
1212

13-
name = "Cursor"
13+
@property
14+
def name(self) -> str:
15+
return "cursor"
16+
17+
@property
18+
def display_name(self) -> str:
19+
return "Cursor"
1420

15-
def output_result(self, result: Result) -> int:
21+
def output_result(self, result: HookResult) -> int:
1622
response = {}
1723
if result.payload.event_type == EventType.USER_PROMPT:
1824
response["continue"] = not result.block

0 commit comments

Comments
 (0)