Skip to content
Open
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
7 changes: 6 additions & 1 deletion cycode/cli/apps/ai_guardrails/scan/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from cycode.cli.apps.scan.code_scanner import _get_scan_documents_thread_func
from cycode.cli.apps.scan.scan_parameters import get_scan_parameters
from cycode.cli.cli_types import ScanTypeOption, SeverityOption
from cycode.cli.files_collector.file_excluder import is_path_configured_in_exclusions
from cycode.cli.models import Document
from cycode.cli.utils.progress_bar import DummyProgressBar, ScanProgressBarSection
from cycode.cli.utils.scan_utils import build_violation_summary
Expand Down Expand Up @@ -337,7 +338,7 @@ def _perform_scan(

scan_id = local_scan_result.scan_id

if local_scan_result.detections_count > 0:
if local_scan_result.issue_detected:
violation_summary = build_violation_summary([local_scan_result])
return violation_summary, scan_id

Expand All @@ -360,6 +361,10 @@ def _scan_path_for_secrets(ctx: typer.Context, file_path: str, policy: dict) ->
if not file_path or not os.path.isfile(file_path):
return None, None

if is_path_configured_in_exclusions(str(ScanTypeOption.SECRET), os.path.abspath(file_path)):
logger.debug('Skipping scan; the path is in the ignore paths list, %s', {'file_path': file_path})
return None, None

max_bytes = get_policy_value(policy, 'secrets', 'max_bytes', default=200000)

with open(file_path, encoding='utf-8', errors='replace') as f:
Expand Down
4 changes: 2 additions & 2 deletions cycode/cli/files_collector/file_excluder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _is_subpath_of_cycode_configuration_folder(filename: str) -> bool:
)


def _is_path_configured_in_exclusions(scan_type: str, file_path: str) -> bool:
def is_path_configured_in_exclusions(scan_type: str, file_path: str) -> bool:
exclusions_by_path = configuration_manager.get_exclusions_by_scan_type(scan_type).get(
consts.EXCLUSIONS_BY_PATH_SECTION_NAME, []
)
Expand Down Expand Up @@ -106,7 +106,7 @@ def _is_relevant_file_to_scan_common(self, scan_type: str, filename: str) -> boo
)
return False

if _is_path_configured_in_exclusions(scan_type, filename):
if is_path_configured_in_exclusions(scan_type, filename):
logger.debug(
'The document is irrelevant because its path is in the ignore paths list, %s', {'filename': filename}
)
Expand Down
49 changes: 47 additions & 2 deletions tests/cli/commands/ai_guardrails/scan/test_handlers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for AI guardrails handlers."""

import os
from typing import Any
from unittest.mock import MagicMock, patch

Expand All @@ -8,12 +9,15 @@

from cycode.cli.apps.ai_guardrails.ides.base import DecisionAction, HookDecision
from cycode.cli.apps.ai_guardrails.scan.handlers import (
_perform_scan,
_scan_path_for_secrets,
handle_before_mcp_execution,
handle_before_read_file,
handle_before_submit_prompt,
)
from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType, AIHookOutcome, BlockReason
from cycode.cli.models import Document, LocalScanResult


@pytest.fixture
Expand Down Expand Up @@ -357,15 +361,56 @@ def test_handle_before_read_file_sensitive_path_scan_disabled_warns(

def test_scan_path_for_secrets_directory(mock_ctx: MagicMock, default_policy: dict[str, Any], fs: Any) -> None:
"""Test that _scan_path_for_secrets returns (None, None) for directories."""
from cycode.cli.apps.ai_guardrails.scan.handlers import _scan_path_for_secrets

fs.create_dir('/path/to/some_directory')

result = _scan_path_for_secrets(mock_ctx, '/path/to/some_directory', default_policy)

assert result == (None, None)


@patch('cycode.cli.apps.ai_guardrails.scan.handlers._perform_scan')
def test_scan_path_for_secrets_skips_path_configured_in_exclusions(
mock_perform_scan: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any], fs: Any
) -> None:
"""Test that a path ignored via `cycode ignore --by-path` is not scanned."""
# `cycode ignore --by-path` stores absolute paths; on Windows that includes the drive prefix
excluded_dir = os.path.abspath(os.path.join(os.sep, 'project', 'secrets'))
file_path = os.path.join(excluded_dir, 'creds.env')
fs.create_file(file_path, contents='password=hunter2')
mock_perform_scan.return_value = ('Cycode found 1 violations', 'scan-id-123')

with patch(
'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)

assert result == (None, None)
mock_perform_scan.assert_not_called()


def test_perform_scan_no_violation_when_all_detections_excluded(mock_ctx: MagicMock) -> None:
"""Test that detections filtered out by ignore rules do not produce a violation."""
local_scan_result = LocalScanResult(
scan_id='scan-id-123',
report_url=None,
document_detections=[],
issue_detected=False,
detections_count=1,
relevant_detections_count=0,
)
document = Document(path='prompt-content.txt', content='some content', is_git_diff_format=False)

with patch(
'cycode.cli.apps.ai_guardrails.scan.handlers._get_scan_documents_thread_func',
return_value=lambda batch: ('scan-id-123', None, local_scan_result),
):
violation_summary, scan_id = _perform_scan(mock_ctx, [document], {}, timeout_seconds=5.0)

assert violation_summary is None
assert scan_id == 'scan-id-123'


# Tests for handle_before_mcp_execution


Expand Down
Loading
Loading