"""Output module.
Prints messages and failed checks to console and sends them as Github
pull-request comments.
"""
import sys
import constants
class Output():
"""Abstract class to represent the generic output."""
def success(self, msg):
"""Send a text message representing the successful scan completion.
Args:
msg (str): Text message to send.
"""
raise NotImplementedError('Error: not implemented.')
def error(self, msg):
"""Send a text message representing application or scan failure.
Args:
msg (str): Text message to send.
"""
raise NotImplementedError('Error: not implemented.')
def finding(self, check):
"""Send failed check details.
The output can be buffered and released with the flush method.
Args:
check (dict): Failed check object to send.
"""
raise NotImplementedError('Error: not implemented.')
def flush(self):
"""Flush the output, sending all buffered findings."""
raise NotImplementedError('Error: not implemented.')
class ConsoleOutput(Output):
"""Console output implementation."""
def success(self, msg):
"""Print a text message representing the successful scan completion.
Args:
msg (str): Text message to print.
"""
print(msg)
def error(self, msg):
"""Print a text message representing application or scan failure.
Args:
msg (str): Text message to print.
"""
print(f'Error: {msg}')
def finding(self, check):
"""Print failed check details.
The output can be buffered and released with the flush method.
Args:
check (dict): Failed check object to print.
"""
filepath = (
f'{check["file_path"]}:'
f'{check["file_line_from"]}-'
f'{check["file_line_to"]}'
)
rows = []
rows.append(f'Check: {check["check_id"]}: "{check["check_name"]}"')
rows.append(f' {check["check_result"]} for resource: '
f'{check["resource"]}')
rows.append(f' File: {filepath}')
if check['caller_file_path']:
calling_file = (
f'{check["caller_file_path"]}:'
f'{check["caller_file_line_from"]}-'
f'{check["caller_file_line_to"]}'
)
rows.append(f' Calling File: {calling_file}')
if check['guideline']:
rows.append(f' Guide: {check["guideline"]}')
if check['code_block']:
rows.append('')
for item in check['code_block']:
line_num, line_content = item
line_content = line_content.rstrip()
rows.append(f' {line_num} | {line_content}')
rows.append('')
content = '\n'.join(rows)
print(content)
def flush(self):
"""Flush the output, printing all buffered findings."""
sys.stdout.flush()
class GithubOutput(Output):
"""Github pull-request comment output implementation."""
pr = None
buffer = []
def __init__(self, pr):
"""Init the collection with a list of output implementations.
Args:
pr (pull_request.PullRequest): Pull-request object to send comments
to.
"""
self.pr = pr
def success(self, msg):
"""Send a comment representing the successful scan completion.
Args:
msg (str): Text content to send.
"""
self.__send(f':heavy_check_mark: {msg}')
def error(self, msg):
"""Send a comment representing application or scan failure.
Args:
msg (str): Text content to send.
"""
self.__send(f':x: {msg}')
def finding(self, check):
"""Send a comment with failed check details.
The output can be buffered and released with the flush method.
Args:
check (dict): Failed check object to send.
"""
content = self.convert_to_markdown(check)
self.buffer.append(content)
if len(self.buffer) >= 5:
self.flush()
def flush(self):
"""Flush the output, sending all buffered findings."""
if self.buffer:
self.__send('\n\n---\n\n'.join(self.buffer))
self.buffer = []
def __send(self, content):
"""Send a comment to Github.
Args:
msg (content): Comment body in markdown format.
"""
content = f'{constants.GITHUB_NOTIFICATION_PREFIX}{content}'
self.pr.send_issue_comment(content)
def convert_to_markdown(self, check):
"""Convert a failed_check into markdown representation.
Args:
check (dict): Failed check object to send.
Returns:
str: Markdown-formatted failed check details.
"""
filepath = (
f'{check["file_path"]}:'
f'{check["file_line_from"]}-'
f'{check["file_line_to"]}'
)
if check['caller_file_path']:
calling_file = (
f'{check["caller_file_path"]}:'
f'{check["caller_file_line_from"]}-'
f'{check["caller_file_line_to"]}'
)
filepath = f'{calling_file} -> {filepath}'
rows = []
rows.append(
f'- [ ] `Checkov check {check["check_result"]}: '
f'{check["check_id"]}`'
)
rows.append(f'#### **{check["check_name"]}**')
rows.append('')
rows.append('')
rows.append('Details
')
rows.append('')
rows.append('| | |')
rows.append('|--|--|')
rows.append(f'| **Resource** | {check["resource"]} |')
rows.append(f'| **File** | {filepath} |')
if check['guideline']:
rows.append(f'| **More Info** | {check["guideline"]} |')
rows.append(' ')
if check['code_block']:
rows.append('')
rows.append('Resource Definition
')
rows.append('')
rows.append('```hcl')
for item in check['code_block']:
line_num, line_content = item
line_content = line_content.rstrip()
rows.append(f'{line_num} | {line_content}')
rows.append('```')
rows.append('')
rows.append(' ')
content = '\n'.join(rows)
return content
class GithubStatusCheckOutput(GithubOutput):
"""Github pull-request issue comment output implementation."""
def success(self, msg):
"""Send an issue comment representing the successful scan completion.
Args:
msg (str): Text content to send.
"""
self.__send(f':heavy_check_mark: {msg}')
def error(self, msg):
"""Send an issue comment representing application or scan failure.
Args:
msg (str): Text content to send.
"""
self.__send(f':x: {msg}')
def finding(self, check):
"""Send an issue comment with failed check details.
The output can be buffered and released with the flush method.
Args:
check (dict): Failed check object to send.
"""
content = self.convert_to_markdown(check)
self.buffer.append(content)
if len(self.buffer) >= 5:
self.__send('\n\n---\n\n'.join(self.buffer))
self.buffer = []
def flush(self):
"""Flush the output, sending all buffered findings."""
if self.buffer:
self.__send('\n\n---\n\n'.join(self.buffer))
self.buffer = []
self.__send(':heavy_exclamation_mark: The scan has failed. '
'Please check the listed findings and fix them before '
'continuing.')
def __send(self, content):
"""Send an issue comment to Github.
Args:
content (str): Comment body in markdown format.
"""
content = f'{constants.GITHUB_NOTIFICATION_PREFIX}{content}'
self.pr.send_issue_comment(content)
class OutputCollection(Output):
"""Collection of Output implementations."""
outputs = []
def __init__(self, outputs):
"""Init the collection with a list of output implementations.
Args:
outputs (list(Output)): List of Output implementations.
"""
self.outputs = outputs
def success(self, msg):
"""Send a text message representing the successful scan completion.
Args:
msg (str): Text message to send.
"""
for out in self.outputs:
out.success(msg)
def error(self, msg):
"""Send a text message representing application or scan failure.
Args:
msg (str): Text message to send.
"""
for out in self.outputs:
out.error(msg)
def finding(self, check):
"""Send failed check details.
Args:
check (dict): Failed check object to send.
"""
for out in self.outputs:
out.finding(check)
def flush(self):
"""Flush the output, sending all buffered findings."""
for out in self.outputs:
out.flush()