Skip to content

Commit c0bf190

Browse files
authored
CM-53929 Add full support of .cycodeignore for repository and commit range scans (#351)
1 parent 4e42488 commit c0bf190

File tree

13 files changed

+684
-21
lines changed

13 files changed

+684
-21
lines changed

cycode/cli/apps/report/sbom/path/path_command.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from cycode.cli.files_collector.zip_documents import zip_documents
1313
from cycode.cli.utils.get_api_client import get_report_cycode_client
1414
from cycode.cli.utils.progress_bar import SbomReportProgressBarSection
15+
from cycode.cli.utils.scan_utils import is_cycodeignore_allowed_by_scan_config
1516
from cycode.cli.utils.sentry import add_breadcrumb
1617

1718

@@ -37,7 +38,11 @@ def path_command(
3738

3839
try:
3940
documents = get_relevant_documents(
40-
progress_bar, SbomReportProgressBarSection.PREPARE_LOCAL_FILES, consts.SCA_SCAN_TYPE, (str(path),)
41+
progress_bar,
42+
SbomReportProgressBarSection.PREPARE_LOCAL_FILES,
43+
consts.SCA_SCAN_TYPE,
44+
(str(path),),
45+
is_cycodeignore_allowed=is_cycodeignore_allowed_by_scan_config(ctx),
4146
)
4247
# TODO(MarshalX): combine perform_pre_scan_documents_actions with get_relevant_document.
4348
# unhardcode usage of context in perform_pre_scan_documents_actions

cycode/cli/apps/scan/code_scanner.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
from cycode.cli.models import CliError, Document, LocalScanResult
2424
from cycode.cli.utils.progress_bar import ScanProgressBarSection
2525
from cycode.cli.utils.scan_batch import run_parallel_batched_scan
26-
from cycode.cli.utils.scan_utils import generate_unique_scan_id, set_issue_detected_by_scan_results
26+
from cycode.cli.utils.scan_utils import (
27+
generate_unique_scan_id,
28+
is_cycodeignore_allowed_by_scan_config,
29+
set_issue_detected_by_scan_results,
30+
)
2731
from cycode.cyclient.models import ZippedFileScanResult
2832
from cycode.logger import get_logger
2933

@@ -42,7 +46,13 @@ def scan_disk_files(ctx: typer.Context, paths: tuple[str, ...]) -> None:
4246
progress_bar = ctx.obj['progress_bar']
4347

4448
try:
45-
documents = get_relevant_documents(progress_bar, ScanProgressBarSection.PREPARE_LOCAL_FILES, scan_type, paths)
49+
documents = get_relevant_documents(
50+
progress_bar,
51+
ScanProgressBarSection.PREPARE_LOCAL_FILES,
52+
scan_type,
53+
paths,
54+
is_cycodeignore_allowed=is_cycodeignore_allowed_by_scan_config(ctx),
55+
)
4656
add_sca_dependencies_tree_documents_if_needed(ctx, scan_type, documents)
4757
scan_documents(ctx, documents, get_scan_parameters(ctx, paths))
4858
except Exception as e:

cycode/cli/apps/scan/commit_range_scanner.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
parse_commit_range_sast,
3030
parse_commit_range_sca,
3131
)
32+
from cycode.cli.files_collector.documents_walk_ignore import filter_documents_with_cycodeignore
3233
from cycode.cli.files_collector.file_excluder import excluder
3334
from cycode.cli.files_collector.models.in_memory_zip import InMemoryZip
3435
from cycode.cli.files_collector.sca.sca_file_collector import (
@@ -40,7 +41,11 @@
4041
from cycode.cli.utils.git_proxy import git_proxy
4142
from cycode.cli.utils.path_utils import get_path_by_os
4243
from cycode.cli.utils.progress_bar import ScanProgressBarSection
43-
from cycode.cli.utils.scan_utils import generate_unique_scan_id, set_issue_detected_by_scan_results
44+
from cycode.cli.utils.scan_utils import (
45+
generate_unique_scan_id,
46+
is_cycodeignore_allowed_by_scan_config,
47+
set_issue_detected_by_scan_results,
48+
)
4449
from cycode.cyclient.models import ZippedFileScanResult
4550
from cycode.logger import get_logger
4651

@@ -189,6 +194,12 @@ def _scan_sca_commit_range(ctx: typer.Context, repo_path: str, commit_range: str
189194
from_commit_documents = excluder.exclude_irrelevant_documents_to_scan(consts.SCA_SCAN_TYPE, from_commit_documents)
190195
to_commit_documents = excluder.exclude_irrelevant_documents_to_scan(consts.SCA_SCAN_TYPE, to_commit_documents)
191196

197+
is_cycodeignore_allowed = is_cycodeignore_allowed_by_scan_config(ctx)
198+
from_commit_documents = filter_documents_with_cycodeignore(
199+
from_commit_documents, repo_path, is_cycodeignore_allowed
200+
)
201+
to_commit_documents = filter_documents_with_cycodeignore(to_commit_documents, repo_path, is_cycodeignore_allowed)
202+
192203
perform_sca_pre_commit_range_scan_actions(
193204
repo_path, from_commit_documents, from_commit_rev, to_commit_documents, to_commit_rev
194205
)
@@ -204,6 +215,11 @@ def _scan_secret_commit_range(
204215
consts.SECRET_SCAN_TYPE, commit_diff_documents_to_scan
205216
)
206217

218+
is_cycodeignore_allowed = is_cycodeignore_allowed_by_scan_config(ctx)
219+
diff_documents_to_scan = filter_documents_with_cycodeignore(
220+
diff_documents_to_scan, repo_path, is_cycodeignore_allowed
221+
)
222+
207223
scan_documents(
208224
ctx, diff_documents_to_scan, get_scan_parameters(ctx, (repo_path,)), is_git_diff=True, is_commit_range=True
209225
)
@@ -221,9 +237,14 @@ def _scan_sast_commit_range(ctx: typer.Context, repo_path: str, commit_range: st
221237
to_commit_rev,
222238
reverse_diff=False,
223239
)
240+
224241
commit_documents = excluder.exclude_irrelevant_documents_to_scan(consts.SAST_SCAN_TYPE, commit_documents)
225242
diff_documents = excluder.exclude_irrelevant_documents_to_scan(consts.SAST_SCAN_TYPE, diff_documents)
226243

244+
is_cycodeignore_allowed = is_cycodeignore_allowed_by_scan_config(ctx)
245+
commit_documents = filter_documents_with_cycodeignore(commit_documents, repo_path, is_cycodeignore_allowed)
246+
diff_documents = filter_documents_with_cycodeignore(diff_documents, repo_path, is_cycodeignore_allowed)
247+
227248
_scan_commit_range_documents(ctx, commit_documents, diff_documents, scan_parameters=scan_parameters)
228249

229250

@@ -254,11 +275,18 @@ def _scan_sca_pre_commit(ctx: typer.Context, repo_path: str) -> None:
254275
progress_bar_section=ScanProgressBarSection.PREPARE_LOCAL_FILES,
255276
repo_path=repo_path,
256277
)
278+
257279
git_head_documents = excluder.exclude_irrelevant_documents_to_scan(consts.SCA_SCAN_TYPE, git_head_documents)
258280
pre_committed_documents = excluder.exclude_irrelevant_documents_to_scan(
259281
consts.SCA_SCAN_TYPE, pre_committed_documents
260282
)
261283

284+
is_cycodeignore_allowed = is_cycodeignore_allowed_by_scan_config(ctx)
285+
git_head_documents = filter_documents_with_cycodeignore(git_head_documents, repo_path, is_cycodeignore_allowed)
286+
pre_committed_documents = filter_documents_with_cycodeignore(
287+
pre_committed_documents, repo_path, is_cycodeignore_allowed
288+
)
289+
262290
perform_sca_pre_hook_range_scan_actions(repo_path, git_head_documents, pre_committed_documents)
263291

264292
_scan_commit_range_documents(
@@ -288,8 +316,12 @@ def _scan_secret_pre_commit(ctx: typer.Context, repo_path: str) -> None:
288316
is_git_diff_format=True,
289317
)
290318
)
319+
291320
documents_to_scan = excluder.exclude_irrelevant_documents_to_scan(consts.SECRET_SCAN_TYPE, documents_to_scan)
292321

322+
is_cycodeignore_allowed = is_cycodeignore_allowed_by_scan_config(ctx)
323+
documents_to_scan = filter_documents_with_cycodeignore(documents_to_scan, repo_path, is_cycodeignore_allowed)
324+
293325
scan_documents(ctx, documents_to_scan, get_scan_parameters(ctx), is_git_diff=True)
294326

295327

@@ -301,11 +333,18 @@ def _scan_sast_pre_commit(ctx: typer.Context, repo_path: str, **_) -> None:
301333
progress_bar_section=ScanProgressBarSection.PREPARE_LOCAL_FILES,
302334
repo_path=repo_path,
303335
)
336+
304337
pre_committed_documents = excluder.exclude_irrelevant_documents_to_scan(
305338
consts.SAST_SCAN_TYPE, pre_committed_documents
306339
)
307340
diff_documents = excluder.exclude_irrelevant_documents_to_scan(consts.SAST_SCAN_TYPE, diff_documents)
308341

342+
is_cycodeignore_allowed = is_cycodeignore_allowed_by_scan_config(ctx)
343+
pre_committed_documents = filter_documents_with_cycodeignore(
344+
pre_committed_documents, repo_path, is_cycodeignore_allowed
345+
)
346+
diff_documents = filter_documents_with_cycodeignore(diff_documents, repo_path, is_cycodeignore_allowed)
347+
309348
_scan_commit_range_documents(ctx, pre_committed_documents, diff_documents, scan_parameters=scan_parameters)
310349

311350

cycode/cli/apps/scan/repository/repository_command.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
from cycode.cli.apps.scan.code_scanner import scan_documents
99
from cycode.cli.apps.scan.scan_parameters import get_scan_parameters
1010
from cycode.cli.exceptions.handle_scan_errors import handle_scan_exception
11+
from cycode.cli.files_collector.documents_walk_ignore import filter_documents_with_cycodeignore
1112
from cycode.cli.files_collector.file_excluder import excluder
1213
from cycode.cli.files_collector.repository_documents import get_git_repository_tree_file_entries
1314
from cycode.cli.files_collector.sca.sca_file_collector import add_sca_dependencies_tree_documents_if_needed
1415
from cycode.cli.logger import logger
1516
from cycode.cli.models import Document
1617
from cycode.cli.utils.path_utils import get_path_by_os
1718
from cycode.cli.utils.progress_bar import ScanProgressBarSection
19+
from cycode.cli.utils.scan_utils import is_cycodeignore_allowed_by_scan_config
1820
from cycode.cli.utils.sentry import add_breadcrumb
1921

2022

@@ -60,6 +62,9 @@ def repository_command(
6062

6163
documents_to_scan = excluder.exclude_irrelevant_documents_to_scan(scan_type, documents_to_scan)
6264

65+
is_cycodeignore_allowed = is_cycodeignore_allowed_by_scan_config(ctx)
66+
documents_to_scan = filter_documents_with_cycodeignore(documents_to_scan, str(path), is_cycodeignore_allowed)
67+
6368
add_sca_dependencies_tree_documents_if_needed(ctx, scan_type, documents_to_scan)
6469

6570
logger.debug('Found all relevant files for scanning %s', {'path': path, 'branch': branch})

cycode/cli/apps/scan/scan_command.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import os
12
from pathlib import Path
23
from typing import Annotated, Optional
34

45
import click
56
import typer
67

8+
from cycode.cli.apps.scan.remote_url_resolver import _try_get_git_remote_url
79
from cycode.cli.cli_types import ExportTypeOption, ScanTypeOption, ScaScanTypeOption, SeverityOption
810
from cycode.cli.consts import (
911
ISSUE_DETECTED_STATUS_CODE,
@@ -161,10 +163,15 @@ def scan_command(
161163
scan_client = get_scan_cycode_client(ctx)
162164
ctx.obj['client'] = scan_client
163165

164-
remote_scan_config = scan_client.get_scan_configuration_safe(scan_type)
166+
# Get remote URL from current working directory
167+
remote_url = _try_get_git_remote_url(os.getcwd())
168+
169+
remote_scan_config = scan_client.get_scan_configuration_safe(scan_type, remote_url)
165170
if remote_scan_config:
166171
excluder.apply_scan_config(str(scan_type), remote_scan_config)
167172

173+
ctx.obj['scan_config'] = remote_scan_config
174+
168175
if export_type and export_file:
169176
console_printer = ctx.obj['console_printer']
170177
console_printer.enable_recording(export_type, export_file)

cycode/cli/consts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
IAC_SCAN_SUPPORTED_FILE_EXTENSIONS = ('.tf', '.tf.json', '.json', '.yaml', '.yml', '.dockerfile', '.containerfile')
1818
IAC_SCAN_SUPPORTED_FILE_PREFIXES = ('dockerfile', 'containerfile')
1919

20+
CYCODEIGNORE_FILENAME = '.cycodeignore'
21+
2022
SECRET_SCAN_FILE_EXTENSIONS_TO_IGNORE = (
2123
'.DS_Store',
2224
'.bmp',
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import os
2+
from typing import TYPE_CHECKING
3+
4+
from cycode.cli import consts
5+
from cycode.cli.logger import get_logger
6+
from cycode.cli.utils.ignore_utils import IgnoreFilterManager
7+
8+
if TYPE_CHECKING:
9+
from cycode.cli.models import Document
10+
11+
logger = get_logger('Documents Ignores')
12+
13+
14+
def _get_cycodeignore_path(repo_path: str) -> str:
15+
"""Get the path to .cycodeignore file in the repository root."""
16+
return os.path.join(repo_path, consts.CYCODEIGNORE_FILENAME)
17+
18+
19+
def _create_ignore_filter_manager(repo_path: str, cycodeignore_path: str) -> IgnoreFilterManager:
20+
"""Create IgnoreFilterManager with .cycodeignore file."""
21+
return IgnoreFilterManager.build(
22+
path=repo_path,
23+
global_ignore_file_paths=[cycodeignore_path],
24+
global_patterns=[],
25+
)
26+
27+
28+
def _log_ignored_files(repo_path: str, dirpath: str, ignored_dirnames: list[str], ignored_filenames: list[str]) -> None:
29+
"""Log ignored files for debugging (similar to walk_ignore function)."""
30+
rel_dirpath = '' if dirpath == repo_path else os.path.relpath(dirpath, repo_path)
31+
display_dir = rel_dirpath or '.'
32+
33+
for is_dir, names in (
34+
(True, ignored_dirnames),
35+
(False, ignored_filenames),
36+
):
37+
for name in names:
38+
full_path = os.path.join(repo_path, display_dir, name)
39+
if is_dir:
40+
full_path = os.path.join(full_path, '*')
41+
logger.debug('Ignoring match %s', full_path)
42+
43+
44+
def _build_allowed_paths_set(ignore_filter_manager: IgnoreFilterManager, repo_path: str) -> set[str]:
45+
"""Build set of allowed file paths using walk_with_ignored."""
46+
allowed_paths = set()
47+
48+
for dirpath, _dirnames, filenames, ignored_dirnames, ignored_filenames in ignore_filter_manager.walk_with_ignored():
49+
_log_ignored_files(repo_path, dirpath, ignored_dirnames, ignored_filenames)
50+
51+
for filename in filenames:
52+
file_path = os.path.join(dirpath, filename)
53+
allowed_paths.add(file_path)
54+
55+
return allowed_paths
56+
57+
58+
def _get_document_check_path(document: 'Document', repo_path: str) -> str:
59+
"""Get the normalized absolute path for a document to check against allowed paths."""
60+
check_path = document.absolute_path
61+
if not check_path:
62+
check_path = document.path if os.path.isabs(document.path) else os.path.join(repo_path, document.path)
63+
64+
return os.path.normpath(check_path)
65+
66+
67+
def _filter_documents_by_allowed_paths(
68+
documents: list['Document'], allowed_paths: set[str], repo_path: str
69+
) -> list['Document']:
70+
"""Filter documents by checking if their paths are in the allowed set."""
71+
filtered_documents = []
72+
73+
for document in documents:
74+
try:
75+
check_path = _get_document_check_path(document, repo_path)
76+
77+
if check_path in allowed_paths:
78+
filtered_documents.append(document)
79+
else:
80+
relative_path = os.path.relpath(check_path, repo_path)
81+
logger.debug('Filtered out document due to .cycodeignore: %s', relative_path)
82+
except Exception as e:
83+
logger.debug('Error processing document %s: %s', document.path, e)
84+
filtered_documents.append(document)
85+
86+
return filtered_documents
87+
88+
89+
def filter_documents_with_cycodeignore(
90+
documents: list['Document'], repo_path: str, is_cycodeignore_allowed: bool = True
91+
) -> list['Document']:
92+
"""Filter documents based on .cycodeignore patterns.
93+
94+
This function uses .cycodeignore file in the repository root to filter out
95+
documents whose paths match any of those patterns.
96+
97+
Args:
98+
documents: List of Document objects to filter
99+
repo_path: Path to the repository root
100+
is_cycodeignore_allowed: Whether .cycodeignore filtering is allowed by scan configuration
101+
102+
Returns:
103+
List of Document objects that don't match any .cycodeignore patterns
104+
"""
105+
if not is_cycodeignore_allowed:
106+
logger.debug('.cycodeignore filtering is not allowed by scan configuration')
107+
return documents
108+
109+
cycodeignore_path = _get_cycodeignore_path(repo_path)
110+
111+
if not os.path.exists(cycodeignore_path):
112+
logger.debug('.cycodeignore file does not exist in the repository root')
113+
return documents
114+
115+
logger.info('Using %s for filtering documents', cycodeignore_path)
116+
117+
ignore_filter_manager = _create_ignore_filter_manager(repo_path, cycodeignore_path)
118+
119+
allowed_paths = _build_allowed_paths_set(ignore_filter_manager, repo_path)
120+
121+
filtered_documents = _filter_documents_by_allowed_paths(documents, allowed_paths, repo_path)
122+
123+
logger.debug('Filtered %d documents using .cycodeignore patterns', len(documents) - len(filtered_documents))
124+
return filtered_documents

0 commit comments

Comments
 (0)