Skip to content

Commit

Permalink
CM-42904 - Add --by-cve option for cycode ignore command (#274)
Browse files Browse the repository at this point in the history
Co-authored-by: elsapet <elizabeth@cycode.com>
  • Loading branch information
MarshalX and elsapet authored Dec 17, 2024
1 parent aa534b3 commit 34b86d5
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 51 deletions.
76 changes: 53 additions & 23 deletions cycode/cli/commands/ignore/ignore_command.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import os
import re
from typing import Optional

import click

from cycode.cli import consts
from cycode.cli.config import config, configuration_manager
from cycode.cli.sentry import add_breadcrumb
from cycode.cli.utils.path_utils import get_absolute_path
from cycode.cli.utils.path_utils import get_absolute_path, is_path_exists
from cycode.cli.utils.string_utils import hash_string_to_sha256
from cycode.cyclient import logger


def _is_path_to_ignore_exists(path: str) -> bool:
return os.path.exists(path)


def _is_package_pattern_valid(package: str) -> bool:
return re.search('^[^@]+@[^@]+$', package) is not None

Expand Down Expand Up @@ -47,10 +43,16 @@ def _is_package_pattern_valid(package: str) -> bool:
required=False,
help='Ignore scanning a specific package version while running an SCA scan. Expected pattern: name@version.',
)
@click.option(
'--by-cve',
type=click.STRING,
required=False,
help='Ignore scanning a specific CVE while running an SCA scan. Expected pattern: CVE-YYYY-NNN.',
)
@click.option(
'--scan-type',
'-t',
default='secret',
default=consts.SECRET_SCAN_TYPE,
help='Specify the type of scan you wish to execute (the default is Secrets).',
type=click.Choice(config['scans']['supported_scans']),
required=False,
Expand All @@ -64,40 +66,68 @@ def _is_package_pattern_valid(package: str) -> bool:
required=False,
help='Add an ignore rule to the global CLI config.',
)
def ignore_command(
by_value: str, by_sha: str, by_path: str, by_rule: str, by_package: str, scan_type: str, is_global: bool
def ignore_command( # noqa: C901
by_value: Optional[str],
by_sha: Optional[str],
by_path: Optional[str],
by_rule: Optional[str],
by_package: Optional[str],
by_cve: Optional[str],
scan_type: str = consts.SECRET_SCAN_TYPE,
is_global: bool = False,
) -> None:
"""Ignores a specific value, path or rule ID."""
add_breadcrumb('ignore')

if not by_value and not by_sha and not by_path and not by_rule and not by_package:
raise click.ClickException('ignore by type is missing')
all_by_values = [by_value, by_sha, by_path, by_rule, by_package, by_cve]
if all(by is None for by in all_by_values):
raise click.ClickException('Ignore by type is missing')
if len([by for by in all_by_values if by is not None]) != 1:
raise click.ClickException('You must specify only one ignore by type')

if any(by is not None for by in [by_value, by_sha]) and scan_type != consts.SECRET_SCAN_TYPE:
raise click.ClickException('this exclude is supported only for secret scan type')
raise click.ClickException('This exclude is supported only for Secret scan type')
if (by_cve or by_package) and scan_type != consts.SCA_SCAN_TYPE:
raise click.ClickException('This exclude is supported only for SCA scan type')

# only one of the by values must be set
# at least one of the by values must be set
exclusion_type = exclusion_value = None

if by_value is not None:
if by_value:
exclusion_type = consts.EXCLUSIONS_BY_VALUE_SECTION_NAME
exclusion_value = hash_string_to_sha256(by_value)
elif by_sha is not None:

if by_sha:
exclusion_type = consts.EXCLUSIONS_BY_SHA_SECTION_NAME
exclusion_value = by_sha
elif by_path is not None:

if by_path:
absolute_path = get_absolute_path(by_path)
if not _is_path_to_ignore_exists(absolute_path):
raise click.ClickException('the provided path to ignore by is not exist')
if not is_path_exists(absolute_path):
raise click.ClickException('The provided path to ignore by does not exist')

exclusion_type = consts.EXCLUSIONS_BY_PATH_SECTION_NAME
exclusion_value = get_absolute_path(absolute_path)
elif by_package is not None:
if scan_type != consts.SCA_SCAN_TYPE:
raise click.ClickException('exclude by package is supported only for sca scan type')

if by_rule:
exclusion_type = consts.EXCLUSIONS_BY_RULE_SECTION_NAME
exclusion_value = by_rule

if by_package:
if not _is_package_pattern_valid(by_package):
raise click.ClickException('wrong package pattern. should be name@version.')

exclusion_type = consts.EXCLUSIONS_BY_PACKAGE_SECTION_NAME
exclusion_value = by_package
else:
exclusion_type = consts.EXCLUSIONS_BY_RULE_SECTION_NAME
exclusion_value = by_rule

if by_cve:
exclusion_type = consts.EXCLUSIONS_BY_CVE_SECTION_NAME
exclusion_value = by_cve

if not exclusion_type or not exclusion_value:
# should never happen
raise click.ClickException('Invalid ignore by type')

configuration_scope = 'global' if is_global else 'local'
logger.debug(
Expand Down
64 changes: 37 additions & 27 deletions cycode/cli/commands/scan/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,58 +764,68 @@ def _exclude_detections_by_exclusions_configuration(detections: List[Detection],


def _should_exclude_detection(detection: Detection, exclusions: Dict) -> bool:
# FIXME(MarshalX): what the difference between by_value and by_sha?
exclusions_by_value = exclusions.get(consts.EXCLUSIONS_BY_VALUE_SECTION_NAME, [])
if _is_detection_sha_configured_in_exclusions(detection, exclusions_by_value):
logger.debug(
'Going to ignore violations because they are on the values-to-ignore list, %s',
{'value_sha': detection.detection_details.get('sha512', '')},
'Ignoring violation because its value is on the ignore list, %s',
{'value_sha': detection.detection_details.get('sha512')},
)
return True

exclusions_by_sha = exclusions.get(consts.EXCLUSIONS_BY_SHA_SECTION_NAME, [])
if _is_detection_sha_configured_in_exclusions(detection, exclusions_by_sha):
logger.debug(
'Going to ignore violations because they are on the SHA ignore list, %s',
{'sha': detection.detection_details.get('sha512', '')},
'Ignoring violation because its SHA value is on the ignore list, %s',
{'sha': detection.detection_details.get('sha512')},
)
return True

exclusions_by_rule = exclusions.get(consts.EXCLUSIONS_BY_RULE_SECTION_NAME, [])
if exclusions_by_rule:
detection_rule = detection.detection_rule_id
if detection_rule in exclusions_by_rule:
logger.debug(
'Going to ignore violations because they are on the Rule ID ignore list, %s',
{'detection_rule': detection_rule},
)
return True
detection_rule_id = detection.detection_rule_id
if detection_rule_id in exclusions_by_rule:
logger.debug(
'Ignoring violation because its Detection Rule ID is on the ignore list, %s',
{'detection_rule_id': detection_rule_id},
)
return True

exclusions_by_package = exclusions.get(consts.EXCLUSIONS_BY_PACKAGE_SECTION_NAME, [])
if exclusions_by_package:
package = _get_package_name(detection)
if package in exclusions_by_package:
logger.debug(
'Going to ignore violations because they are on the packages-to-ignore list, %s', {'package': package}
)
return True
package = _get_package_name(detection)
if package and package in exclusions_by_package:
logger.debug('Ignoring violation because its package@version is on the ignore list, %s', {'package': package})
return True

exclusions_by_cve = exclusions.get(consts.EXCLUSIONS_BY_CVE_SECTION_NAME, [])
cve = _get_cve_identifier(detection)
if cve and cve in exclusions_by_cve:
logger.debug('Ignoring violation because its CVE is on the ignore list, %s', {'cve': cve})
return True

return False


def _is_detection_sha_configured_in_exclusions(detection: Detection, exclusions: List[str]) -> bool:
detection_sha = detection.detection_details.get('sha512', '')
detection_sha = detection.detection_details.get('sha512')
return detection_sha in exclusions


def _get_package_name(detection: Detection) -> str:
package_name = detection.detection_details.get('vulnerable_component', '')
package_version = detection.detection_details.get('vulnerable_component_version', '')
def _get_package_name(detection: Detection) -> Optional[str]:
package_name = detection.detection_details.get('vulnerable_component')
package_version = detection.detection_details.get('vulnerable_component_version')

if package_name is None:
package_name = detection.detection_details.get('package_name')
package_version = detection.detection_details.get('package_version')

if package_name and package_version:
return f'{package_name}@{package_version}'

return None

if package_name == '':
package_name = detection.detection_details.get('package_name', '')
package_version = detection.detection_details.get('package_version', '')

return f'{package_name}@{package_version}'
def _get_cve_identifier(detection: Detection) -> Optional[str]:
return detection.detection_details.get('alert', {}).get('cve_identifier')


def _get_document_by_file_name(
Expand Down
1 change: 1 addition & 0 deletions cycode/cli/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
EXCLUSIONS_BY_PATH_SECTION_NAME = 'paths'
EXCLUSIONS_BY_RULE_SECTION_NAME = 'rules'
EXCLUSIONS_BY_PACKAGE_SECTION_NAME = 'packages'
EXCLUSIONS_BY_CVE_SECTION_NAME = 'cves'

# 5MB in bytes (in decimal)
FILE_MAX_SIZE_LIMIT_IN_BYTES = 5000000
Expand Down
3 changes: 2 additions & 1 deletion cycode/cli/user_settings/configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ def add_exclusion(self, scope: str, scan_type: str, exclusion_type: str, value:
config_file_manager = self.get_config_file_manager(scope)
config_file_manager.add_exclusion(scan_type, exclusion_type, value)

def _merge_exclusions(self, local_exclusions: Dict, global_exclusions: Dict) -> Dict:
@staticmethod
def _merge_exclusions(local_exclusions: Dict, global_exclusions: Dict) -> Dict:
keys = set(list(local_exclusions.keys()) + list(global_exclusions.keys()))
return {key: local_exclusions.get(key, []) + global_exclusions.get(key, []) for key in keys}

Expand Down

0 comments on commit 34b86d5

Please sign in to comment.