diff --git a/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py b/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py index 416262b8..b40cb4f9 100644 --- a/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +++ b/cycode/cli/apps/ai_guardrails/scan/guardrail_config.py @@ -100,9 +100,8 @@ def apply_platform_config(policy: dict, config: Optional[GuardrailConfig], ide_n The platform is the only mode source: no cache (cold start) means the built-in defaults - Report everywhere with the default globs - which equal an unconfigured tenant's platform - config, so behaviour is uniform either way. Each matrix cell lands on its own per-feature - action, so the two FileRead guardrails (content scan vs. sensitive path) keep independent modes. - An all-Off event never reaches here at all: scan_command skips it. + config, so behaviour is uniform either way. The sensitive-path mode comes back in the server's + verdict. An all-Off event never reaches here at all: scan_command skips it. """ def cell(guardrail_key: str) -> str: @@ -121,7 +120,6 @@ def action(guardrail_key: str) -> str: if cell(BlockReason.SENSITIVE_PATH) != GuardrailCellMode.OFF else [] ) - file_read['path_action'] = action(BlockReason.SENSITIVE_PATH) policy.setdefault('mcp', {})['action'] = action(BlockReason.SECRETS_IN_MCP_ARGS) diff --git a/cycode/cli/apps/ai_guardrails/scan/handlers.py b/cycode/cli/apps/ai_guardrails/scan/handlers.py index c4a37b35..357345a0 100644 --- a/cycode/cli/apps/ai_guardrails/scan/handlers.py +++ b/cycode/cli/apps/ai_guardrails/scan/handlers.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from cycode.cli.apps.ai_guardrails.scan.guardrail_config import GuardrailConfig -from cycode.cli.apps.ai_guardrails.consts import GuardrailsMode, PolicyMode +from cycode.cli.apps.ai_guardrails.consts import GuardrailsMode from cycode.cli.apps.ai_guardrails.ides.base import HookDecision from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload from cycode.cli.apps.ai_guardrails.scan.policy import get_policy_value @@ -81,17 +81,12 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli error_message = None try: - scan_outcome = _scan_text_for_secrets( - ctx, - clipped, - timeout_ms, - payload=payload, - event_type=AiHookEventType.PROMPT, - ) + guardrail = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[AiHookEventType.PROMPT] + scan_outcome = _scan_text_for_secrets(ctx, clipped, timeout_ms, payload=payload, guardrail=guardrail) scan_id = scan_outcome.scan_id if scan_outcome.violation_summary: - block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[AiHookEventType.PROMPT] + block_reason = guardrail if scan_outcome.verdict == GuardrailsMode.BLOCK: outcome = AIHookOutcome.BLOCKED user_message = f'Remove secrets before sending. {scan_outcome.violation_summary}' @@ -117,12 +112,10 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy: dict) -> HookDecision: - """Block sensitive paths and scan file content for secrets.""" + """Scan the file for secrets under the sensitive-path or secrets-in-file guardrail.""" ai_client = ctx.obj['ai_security_client'] - file_read_config = get_policy_value(policy, 'file_read', default={}) file_path = payload.file_path or '' - path_mode = get_effective_mode(file_read_config, action_key='path_action') scan_id = None block_reason = None @@ -130,61 +123,36 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy: error_message = None try: - is_sensitive_path = is_denied_path(file_path, policy) - if is_sensitive_path: - block_reason = BlockReason.SENSITIVE_PATH - if path_mode == GuardrailsMode.BLOCK: - outcome = AIHookOutcome.BLOCKED - user_message = f'Cycode blocked sending {file_path} to the AI (sensitive path policy).' - return HookDecision.deny( - AiHookEventType.FILE_READ, - user_message, - 'This file path is classified as sensitive; do not read/send it to the model.', - ) - # Warn mode: if content scan is enabled, emit a separate event for the - # sensitive path so the finally block can independently track the scan result. - outcome = AIHookOutcome.WARNED - if get_policy_value(file_read_config, 'scan_content', default=True): - ai_client.create_event( - payload, - AiHookEventType.FILE_READ, - outcome, - block_reason=BlockReason.SENSITIVE_PATH, - file_path=payload.file_path, - ) - block_reason = None - outcome = AIHookOutcome.ALLOWED - - if get_policy_value(file_read_config, 'scan_content', default=True): - scan_outcome = _scan_path_for_secrets(ctx, file_path, policy, payload=payload) - scan_id = scan_outcome.scan_id - if scan_outcome.violation_summary: - block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[AiHookEventType.FILE_READ] - if scan_outcome.verdict == GuardrailsMode.BLOCK: - outcome = AIHookOutcome.BLOCKED - user_message = f'Cycode blocked reading {file_path}. {scan_outcome.violation_summary}' - return HookDecision.deny( - AiHookEventType.FILE_READ, - user_message, - 'Secrets detected; do not send this file to the model.', - ) - outcome = AIHookOutcome.WARNED - user_message = f'Cycode detected secrets in {file_path}. {scan_outcome.violation_summary}' - return HookDecision.ask( - AiHookEventType.FILE_READ, - user_message, - 'Possible secrets detected; proceed with caution.', - ) - - if is_sensitive_path: - user_message = f'Cycode flagged {file_path} as sensitive. Allow reading?' - return HookDecision.ask( + if is_denied_path(file_path, policy): + guardrail = BlockReason.SENSITIVE_PATH + elif get_policy_value(policy, 'file_read', 'scan_content', default=True): + guardrail = BlockReason.SECRETS_IN_FILE + else: + return HookDecision.allow(AiHookEventType.FILE_READ) + + scan_outcome = _scan_path_for_secrets(ctx, file_path, policy, payload=payload, guardrail=guardrail) + scan_id = scan_outcome.scan_id + if not scan_outcome.violation_summary: + return HookDecision.allow(AiHookEventType.FILE_READ) + + block_reason = guardrail + subject = f'the sensitive file {file_path}' if guardrail == BlockReason.SENSITIVE_PATH else file_path + if scan_outcome.verdict == GuardrailsMode.BLOCK: + outcome = AIHookOutcome.BLOCKED + user_message = f'Cycode blocked reading {subject}. {scan_outcome.violation_summary}' + return HookDecision.deny( AiHookEventType.FILE_READ, user_message, - 'This file path is classified as sensitive; proceed with caution.', + 'Secrets detected; do not send this file to the model.', ) - return HookDecision.allow(AiHookEventType.FILE_READ) + outcome = AIHookOutcome.WARNED + user_message = f'Cycode detected secrets in {subject}. {scan_outcome.violation_summary}' + return HookDecision.ask( + AiHookEventType.FILE_READ, + user_message, + 'Possible secrets detected; proceed with caution.', + ) except Exception as e: outcome = ( AIHookOutcome.ALLOWED if get_policy_value(policy, 'fail_open', default=True) else AIHookOutcome.BLOCKED @@ -239,16 +207,11 @@ def _handle_arg_scan( error_message = None try: - scan_outcome = _scan_text_for_secrets( - ctx, - clipped, - timeout_ms, - payload=payload, - event_type=feature.event_type, - ) + guardrail = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[feature.event_type] + scan_outcome = _scan_text_for_secrets(ctx, clipped, timeout_ms, payload=payload, guardrail=guardrail) scan_id = scan_outcome.scan_id if scan_outcome.violation_summary: - block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[feature.event_type] + block_reason = guardrail if scan_outcome.verdict == GuardrailsMode.BLOCK: outcome = AIHookOutcome.BLOCKED return HookDecision.deny( @@ -312,12 +275,6 @@ def get_handler_for_event(event_type: str) -> Optional[HandlerFn]: return handlers.get(event_type) -def get_effective_mode(feature_config: dict, action_key: str = 'action') -> GuardrailsMode: - """A guardrail's action is its matrix cell: block, or warn (report) for everything else.""" - action = get_policy_value(feature_config, action_key, default=PolicyMode.BLOCK) - return GuardrailsMode.BLOCK if action == PolicyMode.BLOCK else GuardrailsMode.REPORT - - def should_detach_scan( config: Optional['GuardrailConfig'], policy: dict, @@ -341,12 +298,13 @@ def build_ai_guardrails_scan_parameters( ctx: typer.Context, paths: Optional[tuple[str, ...]], payload: AIHookPayload, - event_type: AiHookEventType, + guardrail: BlockReason, ) -> dict: + """`guardrail` is sent as detection_source and picks the server's floors.""" scan_parameters = get_scan_parameters(ctx, paths) scan_parameters.setdefault('metadata', {})['ai_guardrails'] = { 'ide_provider': payload.ide_provider, - 'detection_source': SECRETS_BLOCK_REASON_BY_EVENT_TYPE[event_type].value, + 'detection_source': guardrail.value, 'device_id': get_serial_number(), 'device_hostname': get_hostname(), 'conversation_id': payload.conversation_id, @@ -422,16 +380,16 @@ def _scan_text_for_secrets( text: str, timeout_ms: int, payload: AIHookPayload, - event_type: AiHookEventType, + guardrail: BlockReason, ) -> ScanOutcome: - """Scan text content for secrets using Cycode CLI.""" + """Scan text for secrets.""" if not text: return NO_SCAN document = Document(path='prompt-content.txt', content=text, is_git_diff_format=False) scan_ctx = _setup_scan_context(ctx) timeout_seconds = timeout_ms / 1000.0 - scan_parameters = build_ai_guardrails_scan_parameters(scan_ctx, None, payload, event_type) + scan_parameters = build_ai_guardrails_scan_parameters(scan_ctx, None, payload, guardrail) return _perform_scan(scan_ctx, [document], scan_parameters, timeout_seconds) @@ -440,8 +398,9 @@ def _scan_path_for_secrets( file_path: str, policy: dict, payload: AIHookPayload, + guardrail: BlockReason, ) -> ScanOutcome: - """Scan a file path for secrets.""" + """Scan a file for secrets.""" if not file_path or not os.path.isfile(file_path): return NO_SCAN @@ -459,5 +418,5 @@ def _scan_path_for_secrets( document = Document(path=os.path.basename(file_path), content=content, is_git_diff_format=False) scan_ctx = _setup_scan_context(ctx) - scan_parameters = build_ai_guardrails_scan_parameters(scan_ctx, (file_path,), payload, AiHookEventType.FILE_READ) + scan_parameters = build_ai_guardrails_scan_parameters(scan_ctx, (file_path,), payload, guardrail) return _perform_scan(scan_ctx, [document], scan_parameters, timeout_seconds) diff --git a/tests/cli/commands/ai_guardrails/scan/test_guardrail_config.py b/tests/cli/commands/ai_guardrails/scan/test_guardrail_config.py index efde1d26..3c241178 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_guardrail_config.py +++ b/tests/cli/commands/ai_guardrails/scan/test_guardrail_config.py @@ -105,7 +105,6 @@ def test_apply_platform_config_without_cache_uses_report_defaults() -> None: assert policy['prompt']['action'] == 'warn' assert policy['file_read']['action'] == 'warn' - assert policy['file_read']['path_action'] == 'warn' assert policy['mcp']['action'] == 'warn' assert policy['file_read']['deny_globs'] == DEFAULT_SENSITIVE_PATH_GLOBS @@ -119,17 +118,13 @@ def test_apply_platform_config_block_cell_sets_block_action() -> None: assert policy['mcp']['action'] == 'warn' -def test_apply_platform_config_file_read_cells_keep_independent_actions() -> None: - # Content scan and sensitive path share the FileRead event but are separate matrix cells. +def test_apply_platform_config_sensitive_path_block_keeps_its_globs_and_content_action() -> None: + # The sensitive-path mode never sets the content action. policy: dict = {} - apply_platform_config(policy, _config(file_read='Block', sensitive_path='Report'), 'cursor') - assert policy['file_read']['action'] == 'block' - assert policy['file_read']['path_action'] == 'warn' - - policy = {} - apply_platform_config(policy, _config(file_read='Report', sensitive_path='Block'), 'cursor') + config = _config(file_read='Report', sensitive_path='Block') + apply_platform_config(policy, config, 'cursor') assert policy['file_read']['action'] == 'warn' - assert policy['file_read']['path_action'] == 'block' + assert policy['file_read']['deny_globs'] == config.sensitive_globs() def test_apply_platform_config_off_cells_disable_subfeatures() -> None: diff --git a/tests/cli/commands/ai_guardrails/scan/test_handlers.py b/tests/cli/commands/ai_guardrails/scan/test_handlers.py index 5235f1a3..d0692990 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_handlers.py +++ b/tests/cli/commands/ai_guardrails/scan/test_handlers.py @@ -17,7 +17,6 @@ _scan_path_for_secrets, _scan_text_for_secrets, build_ai_guardrails_scan_parameters, - get_effective_mode, handle_before_mcp_execution, handle_before_read_file, handle_before_submit_prompt, @@ -161,11 +160,13 @@ def test_handle_before_submit_prompt_scan_failure_fail_closed( @patch('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path') -def test_handle_before_read_file_sensitive_path( - mock_is_denied: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any] +@patch('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets') +def test_handle_before_read_file_sensitive_path_blocked_by_verdict( + mock_scan: MagicMock, mock_is_denied: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any] ) -> None: - """Test that sensitive path is blocked.""" + """A Block verdict blocks a sensitive path.""" mock_is_denied.return_value = True + mock_scan.return_value = ScanOutcome('Found 1 secret: API key', 'scan-id-456', GuardrailsMode.BLOCK) payload = AIHookPayload( event_name='FileRead', ide_provider='cursor', @@ -174,26 +175,18 @@ def test_handle_before_read_file_sensitive_path( result = handle_before_read_file(mock_ctx, payload, default_policy) + assert mock_scan.call_args.kwargs['guardrail'] == BlockReason.SENSITIVE_PATH assert result.action == DecisionAction.DENY assert result.event_type == AiHookEventType.FILE_READ - assert '.env' in result.user_message + assert 'sensitive file /path/to/.env' in result.user_message + assert 'Found 1 secret: API key' in result.user_message mock_ctx.obj['ai_security_client'].create_event.assert_called_once() call_args = mock_ctx.obj['ai_security_client'].create_event.call_args assert call_args.args[2] == AIHookOutcome.BLOCKED assert call_args.kwargs['block_reason'] == BlockReason.SENSITIVE_PATH + assert call_args.kwargs['scan_id'] == 'scan-id-456' assert call_args.kwargs['file_path'] == '/path/to/.env' - # The path guardrail has its own action: content scan in block mode must not make a - # report-mode path match block. - mock_ctx.obj['ai_security_client'].create_event.reset_mock() - default_policy['file_read']['path_action'] = 'warn' - default_policy['file_read']['scan_content'] = False - - result = handle_before_read_file(mock_ctx, payload, default_policy) - - assert result.action == DecisionAction.ASK - assert mock_ctx.obj['ai_security_client'].create_event.call_args.args[2] == AIHookOutcome.WARNED - @patch('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path') @patch('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets') @@ -241,8 +234,7 @@ def test_handle_before_read_file_with_secrets( assert call_args.kwargs['block_reason'] == BlockReason.SECRETS_IN_FILE assert call_args.kwargs['file_path'] == '/path/to/file.txt' - # A block-mode path guardrail must not make a Report verdict block; the developer is asked instead - default_policy['file_read']['path_action'] = 'block' + # A Report verdict asks instead mock_scan.return_value = ScanOutcome('Found 1 secret: password', 'scan-id-456', GuardrailsMode.REPORT) result = handle_before_read_file(mock_ctx, payload, default_policy) @@ -274,13 +266,12 @@ def test_handle_before_read_file_scan_disabled( @patch('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path') @patch('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets') -def test_handle_before_read_file_sensitive_path_warn_mode_scans_content( +def test_handle_before_read_file_sensitive_path_report_verdict_asks( mock_scan: MagicMock, mock_is_denied: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any] ) -> None: - """Test that sensitive path in warn mode still scans file content and emits two events.""" + """A Report verdict asks.""" mock_is_denied.return_value = True - mock_scan.return_value = ScanOutcome(scan_id='scan-id-123') - default_policy['file_read']['path_action'] = 'warn' + mock_scan.return_value = ScanOutcome('Found 1 secret: API key', 'scan-id-456', GuardrailsMode.REPORT) payload = AIHookPayload( event_name='FileRead', ide_provider='cursor', @@ -289,30 +280,22 @@ def test_handle_before_read_file_sensitive_path_warn_mode_scans_content( result = handle_before_read_file(mock_ctx, payload, default_policy) - mock_scan.assert_called_once() assert result.action == DecisionAction.ASK - assert result.event_type == AiHookEventType.FILE_READ - assert '.env' in result.user_message - - assert mock_ctx.obj['ai_security_client'].create_event.call_count == 2 - first_event = mock_ctx.obj['ai_security_client'].create_event.call_args_list[0] - assert first_event.args[2] == AIHookOutcome.WARNED - assert first_event.kwargs['block_reason'] == BlockReason.SENSITIVE_PATH - second_event = mock_ctx.obj['ai_security_client'].create_event.call_args_list[1] - assert second_event.args[2] == AIHookOutcome.ALLOWED - assert second_event.kwargs['block_reason'] is None + assert 'sensitive file /path/to/.env' in result.user_message + mock_ctx.obj['ai_security_client'].create_event.assert_called_once() + call_args = mock_ctx.obj['ai_security_client'].create_event.call_args + assert call_args.args[2] == AIHookOutcome.WARNED + assert call_args.kwargs['block_reason'] == BlockReason.SENSITIVE_PATH @patch('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path') @patch('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets') -def test_handle_before_read_file_sensitive_path_warn_mode_with_secrets( +def test_handle_before_read_file_sensitive_path_without_secrets_is_allowed( mock_scan: MagicMock, mock_is_denied: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any] ) -> None: - """Test that sensitive path in warn mode reports secrets and emits two events.""" + """No secrets means allowed.""" mock_is_denied.return_value = True - mock_scan.return_value = ScanOutcome('Found 1 secret: API key', 'scan-id-456', GuardrailsMode.REPORT) - default_policy['file_read']['path_action'] = 'warn' - default_policy['file_read']['action'] = 'warn' + mock_scan.return_value = ScanOutcome(scan_id='scan-id-123') payload = AIHookPayload( event_name='FileRead', ide_provider='cursor', @@ -321,28 +304,22 @@ def test_handle_before_read_file_sensitive_path_warn_mode_with_secrets( result = handle_before_read_file(mock_ctx, payload, default_policy) - mock_scan.assert_called_once() - assert result.action == DecisionAction.ASK - assert result.event_type == AiHookEventType.FILE_READ - assert '.env' in result.user_message - - assert mock_ctx.obj['ai_security_client'].create_event.call_count == 2 - first_event = mock_ctx.obj['ai_security_client'].create_event.call_args_list[0] - assert first_event.args[2] == AIHookOutcome.WARNED - assert first_event.kwargs['block_reason'] == BlockReason.SENSITIVE_PATH - second_event = mock_ctx.obj['ai_security_client'].create_event.call_args_list[1] - assert second_event.args[2] == AIHookOutcome.WARNED - assert second_event.kwargs['block_reason'] == BlockReason.SECRETS_IN_FILE + assert result == HookDecision.allow(AiHookEventType.FILE_READ) + mock_ctx.obj['ai_security_client'].create_event.assert_called_once() + call_args = mock_ctx.obj['ai_security_client'].create_event.call_args + assert call_args.args[2] == AIHookOutcome.ALLOWED + assert call_args.kwargs['block_reason'] is None + assert call_args.kwargs['scan_id'] == 'scan-id-123' @patch('cycode.cli.apps.ai_guardrails.scan.handlers.is_denied_path') @patch('cycode.cli.apps.ai_guardrails.scan.handlers._scan_path_for_secrets') -def test_handle_before_read_file_sensitive_path_scan_disabled_warns( +def test_handle_before_read_file_sensitive_path_scans_with_content_scan_off( mock_scan: MagicMock, mock_is_denied: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any] ) -> None: - """Test that sensitive path in warn mode with scan disabled emits a single event.""" + """Secrets-in-file Off still scans sensitive paths.""" mock_is_denied.return_value = True - default_policy['file_read']['path_action'] = 'warn' + mock_scan.return_value = ScanOutcome(scan_id='scan-id-123') default_policy['file_read']['scan_content'] = False payload = AIHookPayload( event_name='FileRead', @@ -350,17 +327,10 @@ def test_handle_before_read_file_sensitive_path_scan_disabled_warns( file_path='/path/to/.env', ) - result = handle_before_read_file(mock_ctx, payload, default_policy) - - mock_scan.assert_not_called() - assert result.action == DecisionAction.ASK - assert result.event_type == AiHookEventType.FILE_READ - assert '.env' in result.user_message + handle_before_read_file(mock_ctx, payload, default_policy) - mock_ctx.obj['ai_security_client'].create_event.assert_called_once() - call_args = mock_ctx.obj['ai_security_client'].create_event.call_args - assert call_args.args[2] == AIHookOutcome.WARNED - assert call_args.kwargs['block_reason'] == BlockReason.SENSITIVE_PATH + mock_scan.assert_called_once() + assert mock_scan.call_args.kwargs['guardrail'] == BlockReason.SENSITIVE_PATH def test_scan_path_for_secrets_directory( @@ -369,7 +339,9 @@ def test_scan_path_for_secrets_directory( """Test that _scan_path_for_secrets returns (None, None) for directories.""" fs.create_dir('/path/to/some_directory') - result = _scan_path_for_secrets(mock_ctx, '/path/to/some_directory', default_policy, payload=mock_payload) + result = _scan_path_for_secrets( + mock_ctx, '/path/to/some_directory', default_policy, payload=mock_payload, guardrail=BlockReason.SECRETS_IN_FILE + ) assert result == ScanOutcome() @@ -393,7 +365,9 @@ def test_scan_path_for_secrets_skips_path_configured_in_exclusions( 'cycode.cli.files_collector.file_excluder.configuration_manager.get_exclusions_by_scan_type', return_value={'paths': [excluded_dir]}, ): - result = _scan_path_for_secrets(mock_ctx, file_path, default_policy, payload=mock_payload) + result = _scan_path_for_secrets( + mock_ctx, file_path, default_policy, payload=mock_payload, guardrail=BlockReason.SECRETS_IN_FILE + ) assert result == ScanOutcome() mock_perform_scan.assert_not_called() @@ -582,16 +556,6 @@ def test_handle_before_mcp_execution_with_secrets_warned( assert call_args.args[2] == AIHookOutcome.WARNED -def test_get_effective_mode_reads_the_guardrails_action() -> None: - assert get_effective_mode({'action': 'block'}) == GuardrailsMode.BLOCK - assert get_effective_mode({'action': 'warn'}) == GuardrailsMode.REPORT - assert get_effective_mode({}) == GuardrailsMode.BLOCK - # A feature may carry more than one action; the caller picks which cell to read. - feature = {'action': 'warn', 'path_action': 'block'} - assert get_effective_mode(feature) == GuardrailsMode.REPORT - assert get_effective_mode(feature, action_key='path_action') == GuardrailsMode.BLOCK - - @patch('cycode.cli.apps.ai_guardrails.scan.handlers.get_serial_number', return_value='SER-123') @patch('cycode.cli.apps.ai_guardrails.scan.handlers.get_hostname', return_value='test-host') def test_build_ai_guardrails_scan_parameters( @@ -600,7 +564,7 @@ def test_build_ai_guardrails_scan_parameters( """The built scan parameters embed the full hook context alongside the standard scan parameters.""" mock_ctx.info_name = 'ai_guardrails' - params = build_ai_guardrails_scan_parameters(mock_ctx, None, mock_payload, AiHookEventType.PROMPT) + params = build_ai_guardrails_scan_parameters(mock_ctx, None, mock_payload, BlockReason.SECRETS_IN_PROMPT) assert params['command_type'] == 'ai_guardrails' assert params['metadata']['ai_guardrails'] == { @@ -618,6 +582,19 @@ def test_build_ai_guardrails_scan_parameters( } +@patch('cycode.cli.apps.ai_guardrails.scan.handlers.get_serial_number', return_value='SER-123') +@patch('cycode.cli.apps.ai_guardrails.scan.handlers.get_hostname', return_value='test-host') +def test_build_ai_guardrails_scan_parameters_names_the_given_guardrail( + mock_hostname: MagicMock, mock_serial: MagicMock, mock_ctx: MagicMock, mock_payload: AIHookPayload +) -> None: + """The guardrail becomes the detection_source.""" + mock_ctx.info_name = 'ai_guardrails' + + params = build_ai_guardrails_scan_parameters(mock_ctx, None, mock_payload, BlockReason.SENSITIVE_PATH) + + assert params['metadata']['ai_guardrails']['detection_source'] == 'sensitive_path' + + @patch('cycode.cli.apps.ai_guardrails.scan.handlers._perform_scan') def test_scan_text_for_secrets_injects_ai_guardrails_scan_parameter( mock_perform_scan: MagicMock, mock_ctx: MagicMock, mock_payload: AIHookPayload @@ -631,7 +608,7 @@ def test_scan_text_for_secrets_injects_ai_guardrails_scan_parameter( 'some text', 1000, payload=mock_payload, - event_type=AiHookEventType.PROMPT, + guardrail=BlockReason.SECRETS_IN_PROMPT, ) ai_guardrails = mock_perform_scan.call_args.args[2]['metadata']['ai_guardrails']