"""Checkov scanner module. 1. Runs checkov scan on a specific directory. 2. Parses JSON output to build a list of failed checks. """ import json import os import re import subprocess import sys import tempfile def scan(base_dir, github_token, scan_dir, aws_role=None): """Run checkov scan on a specific directory. Fill the list of failed checks with some reasonable defaults. Args: base_dir (str): Base directory to build relative path from. github_token (str): Github token for Checkov to use. scan_dir (str): Directory to run scan in, relative to base_dir. aws_role (str): AWS role to assume for dynamic policies. Returns: list[dict]: List of failed checks. """ # Check for forbidden skip comments in Terraform files BEFORE running Checkov full_scan_dir = os.path.join(base_dir, scan_dir) _check_for_forbidden_skips_in_files(full_scan_dir) checkov_output = _scan_with_checkov( base_dir, github_token, scan_dir, aws_role) data = json.loads(checkov_output) if not data.get('results', {}).get('failed_checks'): return [] checks = [] for failed_check in data['results']['failed_checks']: check = _extract_fields(failed_check, scan_dir) checks.append(check) return checks def _scan_with_checkov(base_dir, github_token, scan_dir, aws_role=None): """Run checkov scan on a specific directory. Args: base_dir (str): Base directory to build relative path from. github_token (str): Github token for Checkov to use. scan_dir (str): Directory to run scan in, relative to base_dir. aws_role (str): AWS role to assume for dynamic policies. Returns: str: Scan result in JSON format. """ try: dir = os.path.join(base_dir, scan_dir) if not aws_role: aws_role = '' modules_dir = tempfile.TemporaryDirectory( dir=dir, prefix='.terraform-modules-') env = os.environ.copy() env['GITHUB_PAT'] = github_token env['CHECKOV_SCAN_DIR'] = scan_dir env['CHECKOV_AWS_ROLE'] = aws_role process = subprocess.run( f'checkov --config-file checkov.yaml --directory {dir} --output json --soft-fail --external-modules-download-path {modules_dir.name}', # noqa stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, check=True, shell=True) modules_dir.cleanup() output = process.stdout.decode('utf-8') stderr = process.stderr.decode('utf-8') if stderr: print(stderr) except subprocess.CalledProcessError as exc: stdout = exc.stdout.decode('utf-8') stderr = exc.stderr.decode('utf-8') sys.exit(f'Error: failed to scan: {exc}\n{stdout}\n{stderr}') return output def _check_for_forbidden_skips_in_files(scan_dir): """Scan Terraform files for forbidden skip comments. Args: scan_dir (str): Directory to scan for Terraform files. """ # Pattern to match checkov:skip comments with ORCD_SNOWFLAKE_ rules skip_pattern = re.compile(r'#\s*checkov:skip=(ORCD_SNOWFLAKE_\w+)') forbidden_skips = [] for root, dirs, files in os.walk(scan_dir): for filename in files: if filename.endswith('.tf'): filepath = os.path.join(root, filename) try: with open(filepath, 'r') as f: for line_num, line in enumerate(f, 1): match = skip_pattern.search(line) if match: check_id = match.group(1) rel_path = os.path.relpath(filepath, scan_dir) forbidden_skips.append(f' - {check_id} in {rel_path} at line {line_num}') except IOError: pass if forbidden_skips: error_msg = 'Error: Suppression of custom ORCD_SNOWFLAKE rules is not allowed:\n' error_msg += '\n'.join(forbidden_skips) error_msg += '\n\nFor more details visit: https://www.notion.so/Checkov-guide-e5c30d67d35248ebbb0806df59775e0e#2ea97177520f808f941ecd1a254d4d1b' sys.exit(error_msg) def _extract_fields(failed_check, scan_dir): """Extract required fields from a failed check. Args: failed_check (dict): Checkov failed check. scan_dir (str): Directory that was scanned, needed to correctly fill in file_path. Returns: dict: Required fields with reasonable defaults. """ fields = {} fields['check_id'] = failed_check.get('check_id') fields['check_name'] = failed_check.get('check_name') fields['check_result'] = failed_check.get('check_result', {}).get('result') fields['resource'] = failed_check.get('resource') fields['guideline'] = failed_check.get('guideline') fields['code_block'] = failed_check.get('code_block', []) fields['file_path'] = None file_path = failed_check.get('file_path') if file_path: fields['file_path'] = os.path.join(scan_dir, file_path.lstrip('/')) fields['file_line_from'] = None fields['file_line_to'] = None file_line_range = failed_check.get('file_line_range', []) if len(file_line_range) == 2: fields['file_line_from'], fields['file_line_to'] = file_line_range fields['caller_file_path'] = None caller_file_path = failed_check.get('caller_file_path') if caller_file_path: fields['caller_file_path'] = os.path.join( scan_dir, caller_file_path.lstrip('/')) fields['caller_file_line_from'] = None fields['caller_file_line_to'] = None caller_file_line_range = failed_check.get('caller_file_line_range', []) if caller_file_line_range and len(caller_file_line_range) == 2: fields['caller_file_line_from'], fields['caller_file_line_to'] = \ caller_file_line_range return fields