Skip to content

Commit 081eda6

Browse files
authored
CM-30139 - Add Company Guidelines (#184)
1 parent 0366b40 commit 081eda6

File tree

6 files changed

+170
-3
lines changed

6 files changed

+170
-3
lines changed

cycode/cli/commands/scan/code_scanner.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,35 @@ def set_issue_detected_by_scan_results(context: click.Context, scan_results: Lis
9999
set_issue_detected(context, any(scan_result.issue_detected for scan_result in scan_results))
100100

101101

102+
def _enrich_scan_result_with_data_from_detection_rules(
103+
cycode_client: 'ScanClient', scan_type: str, scan_result: ZippedFileScanResult
104+
) -> None:
105+
# TODO(MarshalX): remove scan_type arg after migration to new backend filter
106+
if scan_type != consts.SECRET_SCAN_TYPE:
107+
# not yet
108+
return
109+
110+
detection_rule_ids = set()
111+
for detections_per_file in scan_result.detections_per_file:
112+
for detection in detections_per_file.detections:
113+
detection_rule_ids.add(detection.detection_rule_id)
114+
115+
detection_rules = cycode_client.get_detection_rules(scan_type, detection_rule_ids)
116+
detection_rules_by_id = {detection_rule.detection_rule_id: detection_rule for detection_rule in detection_rules}
117+
118+
for detections_per_file in scan_result.detections_per_file:
119+
for detection in detections_per_file.detections:
120+
detection_rule = detection_rules_by_id.get(detection.detection_rule_id)
121+
if not detection_rule:
122+
# we want to make sure that BE returned it. better to not map data instead of failed scan
123+
continue
124+
125+
# TODO(MarshalX): here we can also map severity without migrating secrets to async flow
126+
127+
# detection_details never was typed properly. so not a problem for now
128+
detection.detection_details['custom_remediation_guidelines'] = detection_rule.custom_remediation_guidelines
129+
130+
102131
def _get_scan_documents_thread_func(
103132
context: click.Context, is_git_diff: bool, is_commit_range: bool, scan_parameters: dict
104133
) -> Callable[[List[Document]], Tuple[str, CliError, LocalScanResult]]:
@@ -123,6 +152,8 @@ def _scan_batch_thread_func(batch: List[Document]) -> Tuple[str, CliError, Local
123152
cycode_client, zipped_documents, scan_type, scan_id, is_git_diff, is_commit_range, scan_parameters
124153
)
125154

155+
_enrich_scan_result_with_data_from_detection_rules(cycode_client, scan_type, scan_result)
156+
126157
local_scan_result = create_local_scan_result(
127158
scan_result, batch, command_scan_type, scan_type, severity_threshold
128159
)

cycode/cli/printers/text_printer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def _print_detection_summary(
6868
self, detection: Detection, document_path: str, scan_id: str, report_url: Optional[str]
6969
) -> None:
7070
detection_name = detection.type if self.scan_type == SECRET_SCAN_TYPE else detection.message
71+
detection_name_styled = click.style(detection_name, fg='bright_red', bold=True)
7172

7273
detection_sha = detection.detection_details.get('sha512')
7374
detection_sha_message = f'\nSecret SHA: {detection_sha}' if detection_sha else ''
@@ -78,10 +79,19 @@ def _print_detection_summary(
7879
detection_commit_id = detection.detection_details.get('commit_id')
7980
detection_commit_id_message = f'\nCommit SHA: {detection_commit_id}' if detection_commit_id else ''
8081

82+
company_guidelines = detection.detection_details.get('custom_remediation_guidelines')
83+
company_guidelines_message = f'\nCompany Guideline: {company_guidelines}' if company_guidelines else ''
84+
8185
click.echo(
82-
f'⛔ Found issue of type: {click.style(detection_name, fg="bright_red", bold=True)} '
86+
f'⛔ '
87+
f'Found issue of type: {detection_name_styled} '
8388
f'(rule ID: {detection.detection_rule_id}) in file: {click.format_filename(document_path)} '
84-
f'{detection_sha_message}{scan_id_message}{report_url_message}{detection_commit_id_message} ⛔'
89+
f'{detection_sha_message}'
90+
f'{scan_id_message}'
91+
f'{report_url_message}'
92+
f'{detection_commit_id_message}'
93+
f'{company_guidelines_message}'
94+
f' ⛔'
8595
)
8696

8797
def _print_detection_code_segment(self, detection: Detection, document: Document, code_segment_size: int) -> None:

cycode/cyclient/models.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,39 @@ class Meta:
404404
@post_load
405405
def build_dto(self, data: Dict[str, Any], **_) -> SbomReport:
406406
return SbomReport(**data)
407+
408+
409+
@dataclass
410+
class ClassificationData:
411+
severity: str
412+
413+
414+
class ClassificationDataSchema(Schema):
415+
class Meta:
416+
unknown = EXCLUDE
417+
418+
severity = fields.String()
419+
420+
@post_load
421+
def build_dto(self, data: Dict[str, Any], **_) -> ClassificationData:
422+
return ClassificationData(**data)
423+
424+
425+
@dataclass
426+
class DetectionRule:
427+
classification_data: List[ClassificationData]
428+
detection_rule_id: str
429+
custom_remediation_guidelines: Optional[str] = None
430+
431+
432+
class DetectionRuleSchema(Schema):
433+
class Meta:
434+
unknown = EXCLUDE
435+
436+
classification_data = fields.Nested(ClassificationDataSchema, many=True)
437+
detection_rule_id = fields.String()
438+
custom_remediation_guidelines = fields.String(allow_none=True)
439+
440+
@post_load
441+
def build_dto(self, data: Dict[str, Any], **_) -> DetectionRule:
442+
return DetectionRule(**data)

cycode/cyclient/scan_client.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import json
2-
from typing import TYPE_CHECKING, List, Optional
2+
from typing import TYPE_CHECKING, List, Optional, Set, Union
33

44
from requests import Response
55

6+
from cycode.cli import consts
7+
from cycode.cli.exceptions.custom_exceptions import CycodeError
68
from cycode.cli.files_collector.models.in_memory_zip import InMemoryZip
79
from cycode.cyclient import models
810
from cycode.cyclient.cycode_client_base import CycodeClientBase
@@ -20,6 +22,7 @@ def __init__(
2022

2123
self.SCAN_CONTROLLER_PATH = 'api/v1/scan'
2224
self.DETECTIONS_SERVICE_CONTROLLER_PATH = 'api/v1/detections'
25+
self.POLICIES_SERVICE_CONTROLLER_PATH_V3 = 'api/v3/policies'
2326

2427
self._hide_response_log = hide_response_log
2528

@@ -95,6 +98,58 @@ def get_scan_details(self, scan_id: str) -> models.ScanDetailsResponse:
9598
response = self.scan_cycode_client.get(url_path=self.get_scan_details_path(scan_id))
9699
return models.ScanDetailsResponseSchema().load(response.json())
97100

101+
def get_detection_rules_path(self) -> str:
102+
return (
103+
f'{self.scan_config.get_detections_prefix()}/'
104+
f'{self.POLICIES_SERVICE_CONTROLLER_PATH_V3}/'
105+
f'detection_rules'
106+
)
107+
108+
@staticmethod
109+
def _get_policy_type_by_scan_type(scan_type: str) -> str:
110+
scan_type_to_policy_type = {
111+
consts.INFRA_CONFIGURATION_SCAN_TYPE: 'IaC',
112+
consts.SCA_SCAN_TYPE: 'SCA',
113+
consts.SECRET_SCAN_TYPE: 'SecretDetection',
114+
consts.SAST_SCAN_TYPE: 'SAST',
115+
}
116+
117+
if scan_type not in scan_type_to_policy_type:
118+
raise CycodeError('Invalid scan type')
119+
120+
return scan_type_to_policy_type[scan_type]
121+
122+
@staticmethod
123+
def _filter_detection_rules_by_ids(
124+
detection_rules: List[models.DetectionRule], detection_rules_ids: Union[Set[str], List[str]]
125+
) -> List[models.DetectionRule]:
126+
ids = set(detection_rules_ids) # cast to set to perform faster search
127+
return [rule for rule in detection_rules if rule.detection_rule_id in ids]
128+
129+
@staticmethod
130+
def parse_detection_rules_response(response: Response) -> List[models.DetectionRule]:
131+
return models.DetectionRuleSchema().load(response.json(), many=True)
132+
133+
def get_detection_rules(
134+
self, scan_type: str, detection_rules_ids: Union[Set[str], List[str]]
135+
) -> List[models.DetectionRule]:
136+
# TODO(MarshalX): use filter by list of IDs instead of policy_type when BE will be ready
137+
params = {
138+
'include_hidden': False,
139+
'include_only_enabled_detection_rules': True,
140+
'page_number': 0,
141+
'page_size': 5000,
142+
'policy_types_v2': self._get_policy_type_by_scan_type(scan_type),
143+
}
144+
response = self.scan_cycode_client.get(
145+
url_path=self.get_detection_rules_path(),
146+
params=params,
147+
hide_response_content_log=self._hide_response_log,
148+
)
149+
150+
# we are filtering rules by ids in-place for smooth migration when backend will be ready
151+
return self._filter_detection_rules_by_ids(self.parse_detection_rules_response(response), detection_rules_ids)
152+
98153
def get_scan_detections_path(self) -> str:
99154
return f'{self.scan_config.get_detections_prefix()}/{self.DETECTIONS_SERVICE_CONTROLLER_PATH}'
100155

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"classification_data": [
4+
{
5+
"severity": "High",
6+
"classification_rule_id": "e4e826bd-5820-4cc9-ae5b-bbfceb500a21"
7+
}
8+
],
9+
"detection_rule_id": "26ab3395-2522-4061-a50a-c69c2d622ca1"
10+
},
11+
{
12+
"classification_data": [
13+
{
14+
"severity": "High",
15+
"classification_rule_id": "e4e826bd-5820-4cc9-ae5b-bbfceb500a22"
16+
}
17+
],
18+
"detection_rule_id": "12345678-aea1-4304-a6e9-012345678901"
19+
}
20+
]

tests/cyclient/mocked_responses/scan_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,27 @@ def get_report_scan_status_response(url: str) -> responses.Response:
146146
return responses.Response(method=responses.POST, url=url, status=200)
147147

148148

149+
def get_detection_rules_url(scan_client: ScanClient) -> str:
150+
api_url = scan_client.scan_cycode_client.api_url
151+
service_url = scan_client.get_detection_rules_path()
152+
return f'{api_url}/{service_url}'
153+
154+
155+
def get_detection_rules_response(url: str) -> responses.Response:
156+
with open(MOCKED_RESPONSES_PATH.joinpath('detection_rules.json'), encoding='UTF-8') as f:
157+
json_response = json.load(f)
158+
159+
return responses.Response(method=responses.GET, url=url, json=json_response, status=200)
160+
161+
149162
def mock_scan_async_responses(
150163
responses_module: responses, scan_type: str, scan_client: ScanClient, scan_id: UUID, zip_content_path: Path
151164
) -> None:
152165
responses_module.add(
153166
get_zipped_file_scan_async_response(get_zipped_file_scan_async_url(scan_type, scan_client), scan_id)
154167
)
155168
responses_module.add(get_scan_details_response(get_scan_details_url(scan_id, scan_client), scan_id))
169+
responses_module.add(get_detection_rules_response(get_detection_rules_url(scan_client)))
156170
responses_module.add(get_scan_detections_count_response(get_scan_detections_count_url(scan_client)))
157171
responses_module.add(get_scan_detections_response(get_scan_detections_url(scan_client), scan_id, zip_content_path))
158172
responses_module.add(get_report_scan_status_response(get_report_scan_status_url(scan_type, scan_id, scan_client)))
@@ -164,4 +178,5 @@ def mock_scan_responses(
164178
responses_module.add(
165179
get_zipped_file_scan_response(get_zipped_file_scan_url(scan_type, scan_client), zip_content_path)
166180
)
181+
responses_module.add(get_detection_rules_response(get_detection_rules_url(scan_client)))
167182
responses_module.add(get_report_scan_status_response(get_report_scan_status_url(scan_type, scan_id, scan_client)))

0 commit comments

Comments
 (0)