"""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()