Skip to content
Draft
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
11 changes: 8 additions & 3 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ name = ggshield-layers
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.cmd.ai | 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.ai | ggshield.verticals.auth | ggshield.verticals.hmsl | ggshield.verticals.secret
ggshield.core
click | ggshield.utils | pygitguardian
ignore_imports =
Expand All @@ -22,6 +22,7 @@ unmatched_ignore_imports_alerting = warn
name = verticals-cmd-transversals
type = forbidden
source_modules =
ggshield.cmd.ai
ggshield.cmd.auth
ggshield.cmd.config
ggshield.cmd.hmsl
Expand All @@ -33,10 +34,13 @@ source_modules =
ggshield.cmd.status
ggshield.cmd.utils
forbidden_modules =
ggshield.verticals.ai
ggshield.verticals.auth
ggshield.verticals.hmsl
ggshield.verticals.secret
ignore_imports =
ggshield.cmd.ai.** -> ggshield.verticals.ai
ggshield.cmd.ai.** -> ggshield.verticals.ai.**
ggshield.cmd.auth.** -> ggshield.verticals.auth
ggshield.cmd.auth.** -> ggshield.verticals.auth.**
ggshield.cmd.auth.** -> ggshield.verticals.hmsl.**
Expand All @@ -46,7 +50,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 +59,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
2 changes: 2 additions & 0 deletions ggshield/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import click

from ggshield import __version__
from ggshield.cmd.ai import ai_group
from ggshield.cmd.auth import auth_group
from ggshield.cmd.config import config_group
from ggshield.cmd.hmsl import hmsl_group
Expand Down Expand Up @@ -88,6 +89,7 @@ def _load_plugins() -> PluginRegistry:
@click.group(
context_settings={"help_option_names": ["-h", "--help"]},
commands={
"ai": ai_group,
"auth": auth_group,
"config": config_group,
"plugin": plugin_group,
Expand Down
12 changes: 12 additions & 0 deletions ggshield/cmd/ai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Any

import click

from ggshield.cmd.ai.discover import discover_cmd
from ggshield.cmd.utils.common_options import add_common_options


@click.group(commands={"discover": discover_cmd})
@add_common_options()
def ai_group(**kwargs: Any) -> None:
"""Commands to work with MCP (Model Context Protocol) servers."""
70 changes: 70 additions & 0 deletions ggshield/cmd/ai/discover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
MCP Discover command - Discovers MCP servers and optionally probes them
for tools, resources, and prompts.
"""

import json
from typing import Any

import click
from rich import print

from ggshield.cmd.utils.common_options import add_common_options
from ggshield.cmd.utils.context_obj import ContextObj
from ggshield.core import ui
from ggshield.core.client import create_client_from_config
from ggshield.core.errors import APIKeyCheckError, UnknownInstanceError
from ggshield.verticals.ai.discovery import (
discover_ai_configuration,
save_discovery_cache,
submit_ai_discovery,
)


@click.command(name="discover")
@click.option(
"--json",
"use_json",
is_flag=True,
default=False,
help="Output as JSON",
)
@add_common_options()
@click.pass_context
def discover_cmd(
ctx: click.Context,
use_json: bool,
**kwargs: Any,
) -> None:
"""
Discover MCP servers and their configuration.

Parses MCP configuration files from supported assistants

Examples:
ggshield mcp discover
ggshield mcp discover --json
"""

config = discover_ai_configuration()

if use_json:
click.echo(json.dumps(config.model_dump(mode="json"), indent=2))
else:
print(config)

ctx_obj = ContextObj.get(ctx)
try:
client = create_client_from_config(ctx_obj.config)
except (APIKeyCheckError, UnknownInstanceError) as exc:
ui.display_warning(
f"Skipping upload of AI discovery to GitGuardian ({exc}). "
"Authenticate with `ggshield auth login` to enable upload."
)
return

try:
config = submit_ai_discovery(client, config)
save_discovery_cache(config)
except Exception as exc:
ui.display_warning(f"Could not upload AI discovery to GitGuardian: {exc}")
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: ...
20 changes: 20 additions & 0 deletions ggshield/verticals/ai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from .agents import AGENTS
from .config import load_mcp_config
from .discovery import (
discover_ai_configuration,
load_discovery_cache,
save_discovery_cache,
)
from .hooks import AIHookScanner
from .installation import install_hooks


__all__ = [
"AGENTS",
"AIHookScanner",
"discover_ai_configuration",
"install_hooks",
"load_discovery_cache",
"load_mcp_config",
"save_discovery_cache",
]
14 changes: 14 additions & 0 deletions ggshield/verticals/ai/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Dict

from ..models import Agent
from .claude_code import Claude
from .copilot import Copilot
from .cursor import Cursor


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


__all__ = ["AGENTS", "Claude", "Copilot", "Cursor"]
Loading
Loading