Skip to content

Commit 72943e2

Browse files
authored
CM-12799 - add option to print results in json format (#8)
* factory skeleton + big refactor to current printer class * WIP * done with text printer * hello json printer * print pretty json * minor fixes for ResultsPrinter * remove debug
1 parent 4c1caa1 commit 72943e2

9 files changed

Lines changed: 249 additions & 47 deletions

File tree

cli/code_scanner.py

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from git import Repo, NULL_TREE, InvalidGitRepositoryError
99
from sys import getsizeof
1010
from cli import printer
11+
from cli.printers import ResultsPrinter
1112
from typing import List, Dict
12-
from cli.models import Document, DetectionDetails
13+
from cli.models import Document, DocumentDetections
1314
from cli.ci_integrations import get_commit_range
1415
from cli.consts import SECRET_SCAN_TYPE, INFRA_CONFIGURATION_SCAN_TYPE, INFRA_CONFIGURATION_SCAN_SUPPORTED_FILES, \
1516
SECRET_SCAN_FILE_EXTENSIONS_TO_IGNORE, EXCLUSIONS_BY_VALUE_SECTION_NAME, EXCLUSIONS_BY_SHA_SECTION_NAME, \
@@ -21,6 +22,7 @@
2122
from cli.zip_file import InMemoryZip
2223
from cli.exceptions.custom_exceptions import CycodeError, HttpUnauthorizedError, ZipTooLargeError
2324
from cyclient import logger
25+
from cyclient.models import ZippedFileScanResult
2426

2527
start_scan_time = time.time()
2628

@@ -43,7 +45,7 @@ def scan_repository(context: click.Context, path, branch):
4345
in get_git_repository_tree_file_entries(path, branch)]
4446
documents_to_scan = exclude_irrelevant_documents_to_scan(context, documents_to_scan)
4547
logger.debug('Found all relevant files for scanning %s', {'path': path, 'branch': branch})
46-
return scan_documents(context.obj["scan_type"], documents_to_scan, is_git_diff=False)
48+
return scan_documents(context, documents_to_scan, is_git_diff=False)
4749
except Exception as e:
4850
_handle_exception(context, e)
4951

@@ -64,12 +66,11 @@ def scan_repository_commit_history(context: click.Context, path: str, commit_ran
6466
""" Scan all the commits history in this git repository """
6567
try:
6668
logger.debug('Starting commit history scan process, %s', {'path': path, 'commit_range': commit_range})
67-
return scan_commit_range(path=path, commit_range=commit_range)
69+
return scan_commit_range(context, path=path, commit_range=commit_range)
6870
except Exception as e:
6971
_handle_exception(context, e)
7072

7173

72-
@click.pass_context
7374
def scan_commit_range(context: click.Context, path: str, commit_range: str):
7475
scan_type = context.obj["scan_type"]
7576

@@ -88,14 +89,15 @@ def scan_commit_range(context: click.Context, path: str, commit_range: str):
8889

8990
documents_to_scan = exclude_irrelevant_documents_to_scan(context, documents_to_scan)
9091
logger.debug('Found all relevant files for scanning %s', {'path': path, 'commit_range': commit_range})
91-
return scan_documents(context.obj["scan_type"], documents_to_scan, is_git_diff=True, is_commit_range=True)
92+
return scan_documents(context, documents_to_scan, is_git_diff=True, is_commit_range=True)
9293

9394

9495
@click.command()
95-
def scan_ci():
96+
@click.pass_context
97+
def scan_ci(context: click.Context):
9698
""" Execute scan in a CI environment which relies on the
9799
CYCODE_TOKEN and CYCODE_REPO_LOCATION environment variables """
98-
return scan_commit_range(path=os.getcwd(), commit_range=get_commit_range())
100+
return scan_commit_range(context, path=os.getcwd(), commit_range=get_commit_range())
99101

100102

101103
@click.command()
@@ -107,7 +109,7 @@ def scan_path(context: click.Context, path):
107109
files_to_scan = get_relevant_files_in_path(path=path, exclude_patterns=["**/.git/**", "**/.cycode/**"])
108110
files_to_scan = exclude_irrelevant_files(context, files_to_scan)
109111
logger.debug('Found all relevant files for scanning %s', {'path': path, 'file_to_scan_count': len(files_to_scan)})
110-
return scan_disk_files(context.obj["scan_type"], files_to_scan)
112+
return scan_disk_files(context, files_to_scan)
111113

112114

113115
@click.command()
@@ -117,27 +119,26 @@ def pre_commit_scan(context: click.Context, ignored_args: List[str]):
117119
""" Use this command to scan the content that was not committed yet """
118120
diff_files = Repo(os.getcwd()).index.diff("HEAD", create_patch=True, R=True)
119121
documents_to_scan = [Document(get_path_by_os(get_diff_file_path(file)), get_diff_file_content(file))
120-
for file in diff_files]
122+
for file in diff_files]
121123
documents_to_scan = exclude_irrelevant_documents_to_scan(context, documents_to_scan)
122-
return scan_documents(context.obj["scan_type"], documents_to_scan, is_git_diff=True)
124+
return scan_documents(context, documents_to_scan, is_git_diff=True)
123125

124126

125-
@click.pass_context
126-
def scan_disk_files(context: click.Context, scan_type: str, paths: List[str]):
127+
def scan_disk_files(context: click.Context, paths: List[str]):
127128
is_git_diff = False
128129
documents = []
129130
for path in paths:
130131
with open(path, "r", encoding="utf-8") as f:
131132
content = f.read()
132133
documents.append(Document(path, content, is_git_diff))
133134

134-
return scan_documents(scan_type, documents, is_git_diff=is_git_diff)
135+
return scan_documents(context, documents, is_git_diff=is_git_diff)
135136

136137

137-
@click.pass_context
138-
def scan_documents(context: click.Context, scan_type: str, documents_to_scan: List[Document],
138+
def scan_documents(context: click.Context, documents_to_scan: List[Document],
139139
is_git_diff: bool = False, is_commit_range: bool = False):
140140
cycode_client = context.obj["client"]
141+
scan_type = context.obj["scan_type"]
141142
scan_command_type = context.info_name
142143
error_message = None
143144
all_detections_count = 0
@@ -148,9 +149,16 @@ def scan_documents(context: click.Context, scan_type: str, documents_to_scan: Li
148149
try:
149150
zipped_documents = zip_documents_to_scan(zipped_documents, documents_to_scan)
150151
scan_result = perform_scan(cycode_client, zipped_documents, scan_type, scan_id, is_git_diff, is_commit_range)
151-
issue_detected, all_detections_count, output_detections_count = print_result(scan_result, documents_to_scan,
152-
scan_type, scan_command_type)
153-
context.obj['issue_detected'] = issue_detected
152+
document_detections_list = enrich_scan_result(scan_result, documents_to_scan)
153+
relevant_document_detections_list = exclude_irrelevant_scan_results(document_detections_list, scan_type,
154+
scan_command_type)
155+
print_results(context, relevant_document_detections_list)
156+
157+
context.obj['issue_detected'] = len(relevant_document_detections_list) > 0
158+
all_detections_count = sum(
159+
[len(document_detections.detections) for document_detections in document_detections_list])
160+
output_detections_count = sum(
161+
[len(document_detections.detections) for document_detections in relevant_document_detections_list])
154162
scan_completed = True
155163
except Exception as e:
156164
_handle_exception(context, e)
@@ -190,37 +198,42 @@ def perform_scan(cycode_client, zipped_documents: InMemoryZip, scan_type: str, s
190198
return scan_result
191199

192200

193-
def print_result(scan_result, documents_to_scan: List[Document], scan_type: str, scan_command_type: str):
194-
all_detections_count = 0
195-
output_detections_count = 0
201+
def print_results(context: click.Context, document_detections_list: List[DocumentDetections]):
202+
output_type = context.obj['output']
203+
printer = ResultsPrinter()
204+
printer.print_results(context, document_detections_list, output_type)
205+
206+
207+
def enrich_scan_result(scan_result: ZippedFileScanResult, documents_to_scan: List[Document]) -> List[
208+
DocumentDetections]:
209+
logger.debug('enriching scan result')
210+
document_detections_list = []
211+
for detections_per_file in scan_result.detections_per_file:
212+
file_name = get_path_by_os(detections_per_file.file_name)
213+
logger.debug("going to find document of violated file, %s", {'file_name': file_name})
214+
document = _get_document_by_file_name(documents_to_scan, file_name)
215+
document_detections_list.append(
216+
DocumentDetections(document=document, detections=detections_per_file.detections))
196217

197-
issue_detected = False
198-
if scan_result.did_detect:
199-
for detections_per_file in scan_result.detections_per_file:
200-
all_detections_count += len(detections_per_file.detections)
201-
detections = exclude_irrelevant_detections(scan_type, scan_command_type, detections_per_file.detections)
202-
if not detections:
203-
continue
218+
return document_detections_list
204219

205-
issue_detected = True
206-
output_detections_count += len(detections)
207-
file_name = get_path_by_os(detections_per_file.file_name)
208-
logger.debug("going to find document of violated file, %s", {'file_name': file_name})
209-
document = _get_document_by_file_name(documents_to_scan, file_name)
210-
logger.debug('printing file\'s violations, %s',
211-
{'filename': file_name, 'socument_path': document.path,
212-
'unique_id': document.unique_id})
213-
print_file_result(document, detections)
214220

215-
if not issue_detected:
216-
click.secho("Good job! No issues were found!!! 👏👏👏", fg='green')
221+
def exclude_irrelevant_scan_results(document_detections_list: List[DocumentDetections], scan_type: str,
222+
scan_command_type: str) -> List[DocumentDetections]:
223+
relevant_document_detections_list = []
224+
for document_detections in document_detections_list:
225+
relevant_detections = exclude_irrelevant_detections(scan_type, scan_command_type,
226+
document_detections.detections)
227+
if relevant_detections:
228+
relevant_document_detections_list.append(DocumentDetections(document=document_detections.document,
229+
detections=relevant_detections))
217230

218-
return issue_detected, all_detections_count, output_detections_count
231+
return relevant_document_detections_list
219232

220233

221234
def print_file_result(document: Document, detections):
222235
printer.print_detections(
223-
detection_details=DetectionDetails(detections=detections, document=document))
236+
detection_details=DocumentDetections(detections=detections, document=document))
224237

225238

226239
def get_diff_file_path(file):

cli/cycode.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
@click.option('--scan-type', '-t', default="secret",
2525
help="""
2626
\b
27-
Specify the scan you wish to execute (secrets/iac),
28-
the default is secrets
27+
Specify the scan you wish to execute (secret/iac),
28+
the default is secret
2929
""",
3030
type=click.Choice(config['scans']['supported_scans']))
3131
@click.option('--secret',
@@ -50,8 +50,15 @@
5050
help='Run scan without failing, always return a non-error status code',
5151
type=bool,
5252
required=False)
53+
@click.option('--output', default='text',
54+
help="""
55+
\b
56+
Specify the results output (text/json),
57+
the default is text
58+
""",
59+
type=click.Choice(['text', 'json']))
5360
@click.pass_context
54-
def code_scan(context: click.Context, scan_type, client_id, secret, show_secret, soft_fail):
61+
def code_scan(context: click.Context, scan_type, client_id, secret, show_secret, soft_fail, output):
5562
""" Scan content for secrets/IaC violations, You need to specify which scan type: ci/commit_history/path/repository/etc """
5663
if show_secret:
5764
context.obj["show_secret"] = show_secret
@@ -64,6 +71,7 @@ def code_scan(context: click.Context, scan_type, client_id, secret, show_secret,
6471
context.obj["soft_fail"] = config["soft_fail"]
6572

6673
context.obj["scan_type"] = scan_type
74+
context.obj["output"] = output
6775
context.obj["client"] = get_cycode_client(client_id, secret, "code_scan")
6876

6977
return 1

cli/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __repr__(self) -> str:
1616
)
1717

1818

19-
class DetectionDetails:
19+
class DocumentDetections:
2020
def __init__(self, document: Document, detections: List[Detection]):
2121
self.document = document
2222
self.detections = detections

cli/printer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import click
22
import math
3-
from cli.models import DetectionDetails
3+
from cli.models import DocumentDetections
44
from cli.config import config
55
from cli.consts import SECRET_SCAN_TYPE
66
from cli.utils.string_utils import obfuscate_text
@@ -9,7 +9,7 @@
99

1010

1111
@click.pass_context
12-
def print_detections(context: click.Context, detection_details: DetectionDetails):
12+
def print_detections(context: click.Context, detection_details: DocumentDetections):
1313
lines_to_display = config['result_printer']['lines_to_display']
1414
show_secret = context.obj['show_secret']
1515
scan_type = context.obj['scan_type']

cli/printers/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from cli.printers.json_printer import JsonPrinter
2+
from cli.printers.text_printer import TextPrinter
3+
from cli.printers.results_printer import ResultsPrinter
4+
5+
6+
7+
__all__ = [
8+
"JsonPrinter",
9+
"TextPrinter",
10+
"ResultsPrinter"
11+
]

cli/printers/base_printer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import click
2+
from abc import ABC, abstractmethod
3+
from typing import List
4+
from cli.models import DocumentDetections
5+
6+
7+
class BasePrinter(ABC):
8+
9+
@abstractmethod
10+
def print_results(self, context: click.Context, results: List[DocumentDetections]):
11+
pass

cli/printers/json_printer.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import json
2+
import click
3+
from typing import List
4+
from cli.printers.base_printer import BasePrinter
5+
from cli.models import DocumentDetections
6+
from cyclient.models import DetectionSchema
7+
8+
9+
class JsonPrinter(BasePrinter):
10+
def print_results(self, context: click.Context, results: List[DocumentDetections]):
11+
detections = [detection for document_detections in results for detection in document_detections.detections]
12+
detections_schema = DetectionSchema(many=True)
13+
detections_dict = detections_schema.dump(detections)
14+
json_result = json.dumps(detections_dict, indent=4)
15+
click.secho(json_result, fg='white')

cli/printers/results_printer.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import click
2+
from typing import List
3+
from cli.printers import JsonPrinter, TextPrinter
4+
from cli.models import DocumentDetections
5+
6+
7+
class ResultsPrinter:
8+
printers = {
9+
'text': TextPrinter(),
10+
'json': JsonPrinter()
11+
}
12+
13+
def print_results(self, context: click.Context, detections_results_list: List[DocumentDetections],
14+
output_type: str):
15+
printer = self.get_printer(output_type)
16+
printer.print_results(context, detections_results_list)
17+
18+
def get_printer(self, output_type: str):
19+
printer = self.printers.get(output_type)
20+
if not printer:
21+
raise ValueError(f'the provided output is not supported - {output_type}')
22+
23+
return printer
24+

0 commit comments

Comments
 (0)