cycode 3.8.9.dev1__py3-none-any.whl → 3.8.11.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. cycode/__init__.py +1 -1
  2. cycode/cli/app.py +2 -1
  3. cycode/cli/apps/ai_guardrails/__init__.py +19 -0
  4. cycode/cli/apps/ai_guardrails/command_utils.py +66 -0
  5. cycode/cli/apps/ai_guardrails/consts.py +78 -0
  6. cycode/cli/apps/ai_guardrails/hooks_manager.py +200 -0
  7. cycode/cli/apps/ai_guardrails/install_command.py +78 -0
  8. cycode/cli/apps/ai_guardrails/scan/__init__.py +1 -0
  9. cycode/cli/apps/ai_guardrails/scan/consts.py +48 -0
  10. cycode/cli/apps/ai_guardrails/scan/handlers.py +341 -0
  11. cycode/cli/apps/ai_guardrails/scan/payload.py +72 -0
  12. cycode/cli/apps/ai_guardrails/scan/policy.py +85 -0
  13. cycode/cli/apps/ai_guardrails/scan/response_builders.py +86 -0
  14. cycode/cli/apps/ai_guardrails/scan/scan_command.py +134 -0
  15. cycode/cli/apps/ai_guardrails/scan/types.py +54 -0
  16. cycode/cli/apps/ai_guardrails/scan/utils.py +72 -0
  17. cycode/cli/apps/ai_guardrails/status_command.py +92 -0
  18. cycode/cli/apps/ai_guardrails/uninstall_command.py +73 -0
  19. cycode/cli/apps/scan/code_scanner.py +1 -1
  20. cycode/cli/cli_types.py +13 -0
  21. cycode/cli/printers/tables/table_printer.py +3 -1
  22. cycode/cli/printers/utils/code_snippet_syntax.py +3 -1
  23. cycode/cli/printers/utils/rich_helpers.py +3 -1
  24. cycode/cli/utils/get_api_client.py +15 -2
  25. cycode/cli/utils/scan_utils.py +24 -0
  26. cycode/cli/utils/string_utils.py +9 -0
  27. cycode/cyclient/ai_security_manager_client.py +86 -0
  28. cycode/cyclient/ai_security_manager_service_config.py +27 -0
  29. cycode/cyclient/client_creator.py +20 -0
  30. {cycode-3.8.9.dev1.dist-info → cycode-3.8.11.dev1.dist-info}/METADATA +1 -1
  31. {cycode-3.8.9.dev1.dist-info → cycode-3.8.11.dev1.dist-info}/RECORD +34 -16
  32. {cycode-3.8.9.dev1.dist-info → cycode-3.8.11.dev1.dist-info}/WHEEL +0 -0
  33. {cycode-3.8.9.dev1.dist-info → cycode-3.8.11.dev1.dist-info}/entry_points.txt +0 -0
  34. {cycode-3.8.9.dev1.dist-info → cycode-3.8.11.dev1.dist-info}/licenses/LICENCE +0 -0
cycode/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '3.8.9.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
1
+ __version__ = '3.8.11.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
cycode/cli/app.py CHANGED
@@ -9,7 +9,7 @@ from typer._completion_shared import Shells
9
9
  from typer.completion import install_callback, show_callback
10
10
 
11
11
  from cycode import __version__
12
- from cycode.cli.apps import ai_remediation, auth, configure, ignore, report, report_import, scan, status
12
+ from cycode.cli.apps import ai_guardrails, ai_remediation, auth, configure, ignore, report, report_import, scan, status
13
13
 
14
14
  if sys.version_info >= (3, 10):
15
15
  from cycode.cli.apps import mcp
@@ -45,6 +45,7 @@ app = typer.Typer(
45
45
  add_completion=False, # we add it manually to control the rich help panel
46
46
  )
47
47
 
48
+ app.add_typer(ai_guardrails.app)
48
49
  app.add_typer(ai_remediation.app)
49
50
  app.add_typer(auth.app)
50
51
  app.add_typer(configure.app)
@@ -0,0 +1,19 @@
1
+ import typer
2
+
3
+ from cycode.cli.apps.ai_guardrails.install_command import install_command
4
+ from cycode.cli.apps.ai_guardrails.scan.scan_command import scan_command
5
+ from cycode.cli.apps.ai_guardrails.status_command import status_command
6
+ from cycode.cli.apps.ai_guardrails.uninstall_command import uninstall_command
7
+
8
+ app = typer.Typer(name='ai-guardrails', no_args_is_help=True, hidden=True)
9
+
10
+ app.command(hidden=True, name='install', short_help='Install AI guardrails hooks for supported IDEs.')(install_command)
11
+ app.command(hidden=True, name='uninstall', short_help='Remove AI guardrails hooks from supported IDEs.')(
12
+ uninstall_command
13
+ )
14
+ app.command(hidden=True, name='status', short_help='Show AI guardrails hook installation status.')(status_command)
15
+ app.command(
16
+ hidden=True,
17
+ name='scan',
18
+ short_help='Scan content from AI IDE hooks for secrets (reads JSON from stdin).',
19
+ )(scan_command)
@@ -0,0 +1,66 @@
1
+ """Common utilities for AI guardrails commands."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from cycode.cli.apps.ai_guardrails.consts import AIIDEType
11
+
12
+ console = Console()
13
+
14
+
15
+ def validate_and_parse_ide(ide: str) -> AIIDEType:
16
+ """Validate IDE parameter and convert to AIIDEType enum.
17
+
18
+ Args:
19
+ ide: IDE name string (e.g., 'cursor')
20
+
21
+ Returns:
22
+ AIIDEType enum value
23
+
24
+ Raises:
25
+ typer.Exit: If IDE is invalid
26
+ """
27
+ try:
28
+ return AIIDEType(ide.lower())
29
+ except ValueError:
30
+ valid_ides = ', '.join([ide_type.value for ide_type in AIIDEType])
31
+ console.print(
32
+ f'[red]Error:[/] Invalid IDE "{ide}". Supported IDEs: {valid_ides}',
33
+ style='bold red',
34
+ )
35
+ raise typer.Exit(1) from None
36
+
37
+
38
+ def validate_scope(scope: str, allowed_scopes: tuple[str, ...] = ('user', 'repo')) -> None:
39
+ """Validate scope parameter.
40
+
41
+ Args:
42
+ scope: Scope string to validate
43
+ allowed_scopes: Tuple of allowed scope values
44
+
45
+ Raises:
46
+ typer.Exit: If scope is invalid
47
+ """
48
+ if scope not in allowed_scopes:
49
+ scopes_list = ', '.join(f'"{s}"' for s in allowed_scopes)
50
+ console.print(f'[red]Error:[/] Invalid scope. Use {scopes_list}.', style='bold red')
51
+ raise typer.Exit(1)
52
+
53
+
54
+ def resolve_repo_path(scope: str, repo_path: Optional[Path]) -> Optional[Path]:
55
+ """Resolve repository path, defaulting to current directory for repo scope.
56
+
57
+ Args:
58
+ scope: The command scope ('user' or 'repo')
59
+ repo_path: Provided repo path or None
60
+
61
+ Returns:
62
+ Resolved Path for repo scope, None for user scope
63
+ """
64
+ if scope == 'repo' and repo_path is None:
65
+ return Path(os.getcwd())
66
+ return repo_path
@@ -0,0 +1,78 @@
1
+ """Constants for AI guardrails hooks management.
2
+
3
+ Currently supports:
4
+ - Cursor
5
+
6
+ To add a new IDE (e.g., Claude Code):
7
+ 1. Add new value to AIIDEType enum
8
+ 2. Create _get_<ide>_hooks_dir() function with platform-specific paths
9
+ 3. Add entry to IDE_CONFIGS dict with IDE-specific hook event names
10
+ 4. Unhide --ide option in commands (install, uninstall, status)
11
+ """
12
+
13
+ import platform
14
+ from enum import Enum
15
+ from pathlib import Path
16
+ from typing import NamedTuple
17
+
18
+
19
+ class AIIDEType(str, Enum):
20
+ """Supported AI IDE types."""
21
+
22
+ CURSOR = 'cursor'
23
+
24
+
25
+ class IDEConfig(NamedTuple):
26
+ """Configuration for an AI IDE."""
27
+
28
+ name: str
29
+ hooks_dir: Path
30
+ repo_hooks_subdir: str # Subdirectory in repo for hooks (e.g., '.cursor')
31
+ hooks_file_name: str
32
+ hook_events: list[str] # List of supported hook event names for this IDE
33
+
34
+
35
+ def _get_cursor_hooks_dir() -> Path:
36
+ """Get Cursor hooks directory based on platform."""
37
+ if platform.system() == 'Darwin':
38
+ return Path.home() / '.cursor'
39
+ if platform.system() == 'Windows':
40
+ return Path.home() / 'AppData' / 'Roaming' / 'Cursor'
41
+ # Linux
42
+ return Path.home() / '.config' / 'Cursor'
43
+
44
+
45
+ # IDE-specific configurations
46
+ IDE_CONFIGS: dict[AIIDEType, IDEConfig] = {
47
+ AIIDEType.CURSOR: IDEConfig(
48
+ name='Cursor',
49
+ hooks_dir=_get_cursor_hooks_dir(),
50
+ repo_hooks_subdir='.cursor',
51
+ hooks_file_name='hooks.json',
52
+ hook_events=['beforeSubmitPrompt', 'beforeReadFile', 'beforeMCPExecution'],
53
+ ),
54
+ }
55
+
56
+ # Default IDE
57
+ DEFAULT_IDE = AIIDEType.CURSOR
58
+
59
+ # Command used in hooks
60
+ CYCODE_SCAN_PROMPT_COMMAND = 'cycode ai-guardrails scan'
61
+
62
+
63
+ def get_hooks_config(ide: AIIDEType) -> dict:
64
+ """Get the hooks configuration for a specific IDE.
65
+
66
+ Args:
67
+ ide: The AI IDE type
68
+
69
+ Returns:
70
+ Dict with hooks configuration for the specified IDE
71
+ """
72
+ config = IDE_CONFIGS[ide]
73
+ hooks = {event: [{'command': CYCODE_SCAN_PROMPT_COMMAND}] for event in config.hook_events}
74
+
75
+ return {
76
+ 'version': 1,
77
+ 'hooks': hooks,
78
+ }
@@ -0,0 +1,200 @@
1
+ """
2
+ Hooks manager for AI guardrails.
3
+
4
+ Handles installation, removal, and status checking of AI IDE hooks.
5
+ Supports multiple IDEs: Cursor, Claude Code (future).
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from cycode.cli.apps.ai_guardrails.consts import (
13
+ CYCODE_SCAN_PROMPT_COMMAND,
14
+ DEFAULT_IDE,
15
+ IDE_CONFIGS,
16
+ AIIDEType,
17
+ get_hooks_config,
18
+ )
19
+ from cycode.logger import get_logger
20
+
21
+ logger = get_logger('AI Guardrails Hooks')
22
+
23
+
24
+ def get_hooks_path(scope: str, repo_path: Optional[Path] = None, ide: AIIDEType = DEFAULT_IDE) -> Path:
25
+ """Get the hooks.json path for the given scope and IDE.
26
+
27
+ Args:
28
+ scope: 'user' for user-level hooks, 'repo' for repository-level hooks
29
+ repo_path: Repository path (required if scope is 'repo')
30
+ ide: The AI IDE type (default: Cursor)
31
+ """
32
+ config = IDE_CONFIGS[ide]
33
+ if scope == 'repo' and repo_path:
34
+ return repo_path / config.repo_hooks_subdir / config.hooks_file_name
35
+ return config.hooks_dir / config.hooks_file_name
36
+
37
+
38
+ def load_hooks_file(hooks_path: Path) -> Optional[dict]:
39
+ """Load existing hooks.json file."""
40
+ if not hooks_path.exists():
41
+ return None
42
+ try:
43
+ content = hooks_path.read_text(encoding='utf-8')
44
+ return json.loads(content)
45
+ except Exception as e:
46
+ logger.debug('Failed to load hooks file', exc_info=e)
47
+ return None
48
+
49
+
50
+ def save_hooks_file(hooks_path: Path, hooks_config: dict) -> bool:
51
+ """Save hooks.json file."""
52
+ try:
53
+ hooks_path.parent.mkdir(parents=True, exist_ok=True)
54
+ hooks_path.write_text(json.dumps(hooks_config, indent=2), encoding='utf-8')
55
+ return True
56
+ except Exception as e:
57
+ logger.error('Failed to save hooks file', exc_info=e)
58
+ return False
59
+
60
+
61
+ def is_cycode_hook_entry(entry: dict) -> bool:
62
+ """Check if a hook entry is from cycode-cli."""
63
+ command = entry.get('command', '')
64
+ return CYCODE_SCAN_PROMPT_COMMAND in command
65
+
66
+
67
+ def install_hooks(
68
+ scope: str = 'user', repo_path: Optional[Path] = None, ide: AIIDEType = DEFAULT_IDE
69
+ ) -> tuple[bool, str]:
70
+ """
71
+ Install Cycode AI guardrails hooks.
72
+
73
+ Args:
74
+ scope: 'user' for user-level hooks, 'repo' for repository-level hooks
75
+ repo_path: Repository path (required if scope is 'repo')
76
+ ide: The AI IDE type (default: Cursor)
77
+
78
+ Returns:
79
+ Tuple of (success, message)
80
+ """
81
+ hooks_path = get_hooks_path(scope, repo_path, ide)
82
+
83
+ # Load existing hooks or create new
84
+ existing = load_hooks_file(hooks_path) or {'version': 1, 'hooks': {}}
85
+ existing.setdefault('version', 1)
86
+ existing.setdefault('hooks', {})
87
+
88
+ # Get IDE-specific hooks configuration
89
+ hooks_config = get_hooks_config(ide)
90
+
91
+ # Add/update Cycode hooks
92
+ for event, entries in hooks_config['hooks'].items():
93
+ existing['hooks'].setdefault(event, [])
94
+
95
+ # Remove any existing Cycode entries for this event
96
+ existing['hooks'][event] = [e for e in existing['hooks'][event] if not is_cycode_hook_entry(e)]
97
+
98
+ # Add new Cycode entries
99
+ for entry in entries:
100
+ existing['hooks'][event].append(entry)
101
+
102
+ # Save
103
+ if save_hooks_file(hooks_path, existing):
104
+ return True, f'AI guardrails hooks installed: {hooks_path}'
105
+ return False, f'Failed to install hooks to {hooks_path}'
106
+
107
+
108
+ def uninstall_hooks(
109
+ scope: str = 'user', repo_path: Optional[Path] = None, ide: AIIDEType = DEFAULT_IDE
110
+ ) -> tuple[bool, str]:
111
+ """
112
+ Remove Cycode AI guardrails hooks.
113
+
114
+ Args:
115
+ scope: 'user' for user-level hooks, 'repo' for repository-level hooks
116
+ repo_path: Repository path (required if scope is 'repo')
117
+ ide: The AI IDE type (default: Cursor)
118
+
119
+ Returns:
120
+ Tuple of (success, message)
121
+ """
122
+ hooks_path = get_hooks_path(scope, repo_path, ide)
123
+
124
+ existing = load_hooks_file(hooks_path)
125
+ if existing is None:
126
+ return True, f'No hooks file found at {hooks_path}'
127
+
128
+ # Remove Cycode entries from all events
129
+ modified = False
130
+ for event in list(existing.get('hooks', {}).keys()):
131
+ original_count = len(existing['hooks'][event])
132
+ existing['hooks'][event] = [e for e in existing['hooks'][event] if not is_cycode_hook_entry(e)]
133
+ if len(existing['hooks'][event]) != original_count:
134
+ modified = True
135
+ # Remove empty event lists
136
+ if not existing['hooks'][event]:
137
+ del existing['hooks'][event]
138
+
139
+ if not modified:
140
+ return True, 'No Cycode hooks found to remove'
141
+
142
+ # Save or delete if empty
143
+ if not existing.get('hooks'):
144
+ try:
145
+ hooks_path.unlink()
146
+ return True, f'Removed hooks file: {hooks_path}'
147
+ except Exception as e:
148
+ logger.debug('Failed to delete hooks file', exc_info=e)
149
+ return False, f'Failed to remove hooks file: {hooks_path}'
150
+
151
+ if save_hooks_file(hooks_path, existing):
152
+ return True, f'Cycode hooks removed from: {hooks_path}'
153
+ return False, f'Failed to update hooks file: {hooks_path}'
154
+
155
+
156
+ def get_hooks_status(scope: str = 'user', repo_path: Optional[Path] = None, ide: AIIDEType = DEFAULT_IDE) -> dict:
157
+ """
158
+ Get the status of AI guardrails hooks.
159
+
160
+ Args:
161
+ scope: 'user' for user-level hooks, 'repo' for repository-level hooks
162
+ repo_path: Repository path (required if scope is 'repo')
163
+ ide: The AI IDE type (default: Cursor)
164
+
165
+ Returns:
166
+ Dict with status information
167
+ """
168
+ hooks_path = get_hooks_path(scope, repo_path, ide)
169
+
170
+ status = {
171
+ 'scope': scope,
172
+ 'ide': ide.value,
173
+ 'ide_name': IDE_CONFIGS[ide].name,
174
+ 'hooks_path': str(hooks_path),
175
+ 'file_exists': hooks_path.exists(),
176
+ 'cycode_installed': False,
177
+ 'hooks': {},
178
+ }
179
+
180
+ existing = load_hooks_file(hooks_path)
181
+ if existing is None:
182
+ return status
183
+
184
+ # Check each hook event for this IDE
185
+ ide_config = IDE_CONFIGS[ide]
186
+ has_cycode_hooks = False
187
+ for event in ide_config.hook_events:
188
+ entries = existing.get('hooks', {}).get(event, [])
189
+ cycode_entries = [e for e in entries if is_cycode_hook_entry(e)]
190
+ if cycode_entries:
191
+ has_cycode_hooks = True
192
+ status['hooks'][event] = {
193
+ 'total_entries': len(entries),
194
+ 'cycode_entries': len(cycode_entries),
195
+ 'enabled': len(cycode_entries) > 0,
196
+ }
197
+
198
+ status['cycode_installed'] = has_cycode_hooks
199
+
200
+ return status
@@ -0,0 +1,78 @@
1
+ """Install command for AI guardrails hooks."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+
8
+ from cycode.cli.apps.ai_guardrails.command_utils import (
9
+ console,
10
+ resolve_repo_path,
11
+ validate_and_parse_ide,
12
+ validate_scope,
13
+ )
14
+ from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS
15
+ from cycode.cli.apps.ai_guardrails.hooks_manager import install_hooks
16
+ from cycode.cli.utils.sentry import add_breadcrumb
17
+
18
+
19
+ def install_command(
20
+ ctx: typer.Context,
21
+ scope: Annotated[
22
+ str,
23
+ typer.Option(
24
+ '--scope',
25
+ '-s',
26
+ help='Installation scope: "user" for all projects, "repo" for current repository only.',
27
+ ),
28
+ ] = 'user',
29
+ ide: Annotated[
30
+ str,
31
+ typer.Option(
32
+ '--ide',
33
+ help='IDE to install hooks for (e.g., "cursor"). Defaults to cursor.',
34
+ ),
35
+ ] = 'cursor',
36
+ repo_path: Annotated[
37
+ Optional[Path],
38
+ typer.Option(
39
+ '--repo-path',
40
+ help='Repository path for repo-scoped installation (defaults to current directory).',
41
+ exists=True,
42
+ file_okay=False,
43
+ dir_okay=True,
44
+ resolve_path=True,
45
+ ),
46
+ ] = None,
47
+ ) -> None:
48
+ """Install AI guardrails hooks for supported IDEs.
49
+
50
+ This command configures the specified IDE to use Cycode for scanning prompts, file reads,
51
+ and MCP tool calls for secrets before they are sent to AI models.
52
+
53
+ Examples:
54
+ cycode ai-guardrails install # Install for all projects (user scope)
55
+ cycode ai-guardrails install --scope repo # Install for current repo only
56
+ cycode ai-guardrails install --ide cursor # Install for Cursor IDE
57
+ cycode ai-guardrails install --scope repo --repo-path /path/to/repo
58
+ """
59
+ add_breadcrumb('ai-guardrails-install')
60
+
61
+ # Validate inputs
62
+ validate_scope(scope)
63
+ repo_path = resolve_repo_path(scope, repo_path)
64
+ ide_type = validate_and_parse_ide(ide)
65
+ ide_name = IDE_CONFIGS[ide_type].name
66
+ success, message = install_hooks(scope, repo_path, ide=ide_type)
67
+
68
+ if success:
69
+ console.print(f'[green]✓[/] {message}')
70
+ console.print()
71
+ console.print('[bold]Next steps:[/]')
72
+ console.print(f'1. Restart {ide_name} to activate the hooks')
73
+ console.print('2. (Optional) Customize policy in ~/.cycode/ai-guardrails.yaml')
74
+ console.print()
75
+ console.print('[dim]The hooks will scan prompts, file reads, and MCP tool calls for secrets.[/]')
76
+ else:
77
+ console.print(f'[red]✗[/] {message}', style='bold red')
78
+ raise typer.Exit(1)
@@ -0,0 +1 @@
1
+ # Prompt scan command for AI guardrails (hooks)
@@ -0,0 +1,48 @@
1
+ """
2
+ Constants and default configuration for AI guardrails.
3
+
4
+ These defaults can be overridden by:
5
+ 1. User-level config: ~/.cycode/ai-guardrails.yaml
6
+ 2. Repo-level config: <workspace>/.cycode/ai-guardrails.yaml
7
+ """
8
+
9
+ # Policy file name
10
+ POLICY_FILE_NAME = 'ai-guardrails.yaml'
11
+
12
+ # Default policy configuration
13
+ DEFAULT_POLICY = {
14
+ 'version': 1,
15
+ 'mode': 'block', # block | warn
16
+ 'fail_open': True, # allow if scan fails/timeouts
17
+ 'secrets': {
18
+ 'scan_type': 'secret',
19
+ 'timeout_ms': 30000,
20
+ 'max_bytes': 200000,
21
+ },
22
+ 'prompt': {
23
+ 'enabled': True,
24
+ 'action': 'block',
25
+ },
26
+ 'file_read': {
27
+ 'enabled': True,
28
+ 'action': 'block',
29
+ 'deny_globs': [
30
+ '.env',
31
+ '.env.*',
32
+ '*.pem',
33
+ '*.p12',
34
+ '*.key',
35
+ '.aws/**',
36
+ '.ssh/**',
37
+ '*kubeconfig*',
38
+ '.npmrc',
39
+ '.netrc',
40
+ ],
41
+ 'scan_content': True,
42
+ },
43
+ 'mcp': {
44
+ 'enabled': True,
45
+ 'action': 'block',
46
+ 'scan_arguments': True,
47
+ },
48
+ }