Skip to content

Commit

Permalink
platform(general): Do not crash the run if S3 integration fails durin…
Browse files Browse the repository at this point in the history
…g setup, upload, or finalize (#5691)

* handle failure to get upload creds (other then auth error)

* use const for upload error message

* handle failed PUT integration request

* save debug logs locally when --support is used but s3 setup failed

* handle failed s3 upload

* add comments, fix type, fix lint

* add danger ignore ability
  • Loading branch information
mikeurbanski1 authored Oct 26, 2023
1 parent 8e8225d commit f6a4502
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 63 deletions.
58 changes: 36 additions & 22 deletions checkov/common/bridgecrew/platform_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def __init__(self) -> None:
self.prisma_policies_response = None
self.public_metadata_response = None
self.use_s3_integration = False
self.s3_setup_failed = False
self.platform_integration_configured = False
self.http: urllib3.PoolManager | urllib3.ProxyManager | None = None
self.http_timeout = urllib3.Timeout(connect=REQUEST_CONNECT_TIMEOUT, read=REQUEST_READ_TIMEOUT)
Expand Down Expand Up @@ -314,6 +315,9 @@ def set_s3_integration(self) -> None:
try:
self.skip_fixes = True # no need to run fixes on CI integration
repo_full_path, support_path, response = self.get_s3_role(self.repo_id) # type: ignore
if not repo_full_path: # happens if the setup fails with something other than an auth error - we continue locally
return

self.bucket, self.repo_path = repo_full_path.split("/", 1)

self.timestamp = self.repo_path.split("/")[-2]
Expand Down Expand Up @@ -358,7 +362,7 @@ def set_s3_integration(self) -> None:
logging.error("Received an error response during authentication")
raise

def get_s3_role(self, repo_id: str) -> tuple[str, str | None, dict[str, Any]]:
def get_s3_role(self, repo_id: str) -> tuple[str, str, dict[str, Any]] | tuple[None, None, dict[str, Any]]:
token = self.get_auth_token()

if not self.http:
Expand All @@ -369,28 +373,34 @@ def get_s3_role(self, repo_id: str) -> tuple[str, str | None, dict[str, Any]]:
while ('Message' in response or 'message' in response):
if response.get('Message') and response['Message'] == UNAUTHORIZED_MESSAGE:
raise BridgecrewAuthError()
if response.get('message') and ASSUME_ROLE_UNUATHORIZED_MESSAGE in response['message']:
elif response.get('message') and ASSUME_ROLE_UNUATHORIZED_MESSAGE in response['message']:
raise BridgecrewAuthError(
"Checkov got an unexpected authorization error that may not be due to your credentials. Please contact support.")
if response.get('message') and "cannot be found" in response['message']:
elif response.get('message') and "cannot be found" in response['message']:
self.loading_output("creating role")
response = self._get_s3_creds(repo_id, token)
if response.get('message') is None and response.get('Message') is None:
else:
if tries < 3:
tries += 1
response = self._get_s3_creds(repo_id, token)
else:
raise BridgecrewAuthError(
"Checkov got an unexpected error that may be due to backend issues. Please contact support.")
logging.error('Checkov got an unexpected error that may be due to backend issues. The scan will continue, '
'but results will not be sent to the platform. Please contact support for assistance.')
logging.error(f'Error from platform: {response.get("message") or response.get("Message")}')
self.s3_setup_failed = True
return None, None, response
repo_full_path = response["path"]
support_path = response.get("supportPath")
return repo_full_path, support_path, response

def _get_s3_creds(self, repo_id: str, token: str) -> dict[str, Any]:
logging.debug(f'Getting S3 upload credentials from {self.integrations_api_url}')
request = self.http.request("POST", self.integrations_api_url, # type:ignore[union-attr]
body=json.dumps({"repoId": repo_id, "support": self.support_flag_enabled}),
headers=merge_dicts({"Authorization": token, "Content-Type": "application/json"},
get_user_agent_header()))
logging.debug(f'Request ID: {request.headers.get("x-amzn-requestid")}')
logging.debug(f'Trace ID: {request.headers.get("x-amzn-trace-id")}')
if request.status == 403:
error_message = get_auth_error_message(request.status, self.is_prisma_integration(), True)
raise BridgecrewAuthError(error_message)
Expand Down Expand Up @@ -421,7 +431,7 @@ def persist_repository(
"""
excluded_paths = excluded_paths if excluded_paths is not None else []

if not self.use_s3_integration:
if not self.use_s3_integration or self.s3_setup_failed:
return
files_to_persist: List[FileToPersist] = []
if files:
Expand Down Expand Up @@ -450,7 +460,7 @@ def persist_repository(
self.persist_files(files_to_persist)

def persist_git_configuration(self, root_dir: str | Path, git_config_folders: list[str]) -> None:
if not self.use_s3_integration:
if not self.use_s3_integration or self.s3_setup_failed:
return
files_to_persist: list[FileToPersist] = []

Expand All @@ -475,7 +485,7 @@ def persist_scan_results(self, scan_reports: list[Report]) -> None:
Persist checkov's scan result into bridgecrew's platform.
:param scan_reports: List of checkov scan reports
"""
if not self.use_s3_integration or not self.s3_client:
if not self.use_s3_integration or not self.s3_client or self.s3_setup_failed:
return
if not self.bucket or not self.repo_path:
logging.error(f"Something went wrong: bucket {self.bucket}, repo path {self.repo_path}")
Expand All @@ -491,7 +501,7 @@ def persist_scan_results(self, scan_reports: list[Report]) -> None:
persist_checks_results(reduced_scan_reports, self.s3_client, self.bucket, self.repo_path)

async def persist_reachability_alias_mapping(self, alias_mapping: Dict[str, Any]) -> None:
if not self.use_s3_integration or not self.s3_client:
if not self.use_s3_integration or not self.s3_client or self.s3_setup_failed:
return
if not self.bucket or not self.repo_path:
logging.error(f"Something went wrong: bucket {self.bucket}, repo path {self.repo_path}")
Expand Down Expand Up @@ -558,7 +568,7 @@ def persist_enriched_secrets(self, enriched_secrets: list[EnrichedSecret]) -> st
return s3_path

def persist_run_metadata(self, run_metadata: dict[str, str | list[str]]) -> None:
if not self.use_s3_integration or not self.s3_client:
if not self.use_s3_integration or not self.s3_client or self.s3_setup_failed:
return
if not self.bucket or not self.repo_path:
logging.error(f"Something went wrong: bucket {self.bucket}, repo path {self.repo_path}")
Expand All @@ -570,7 +580,7 @@ def persist_run_metadata(self, run_metadata: dict[str, str | list[str]]) -> None
persist_run_metadata(run_metadata, self.s3_client, self.support_bucket, self.support_repo_path, False)

def persist_logs_stream(self, logs_stream: StringIO) -> None:
if not self.use_s3_integration or not self.s3_client:
if not self.use_s3_integration or not self.s3_client or self.s3_setup_failed:
return
if not self.support_bucket or not self.support_repo_path:
logging.error(
Expand All @@ -581,7 +591,7 @@ def persist_logs_stream(self, logs_stream: StringIO) -> None:
persist_logs_stream(logs_stream, self.s3_client, self.support_bucket, log_path)

def persist_graphs(self, graphs: dict[str, list[tuple[LibraryGraph, Optional[str]]]], absolute_root_folder: str = '') -> None:
if not self.use_s3_integration or not self.s3_client:
if not self.use_s3_integration or not self.s3_client or self.s3_setup_failed:
return
if not self.bucket or not self.repo_path:
logging.error(f"Something went wrong: bucket {self.bucket}, repo path {self.repo_path}")
Expand All @@ -590,7 +600,7 @@ def persist_graphs(self, graphs: dict[str, list[tuple[LibraryGraph, Optional[str
absolute_root_folder=absolute_root_folder)

def persist_resource_subgraph_maps(self, resource_subgraph_maps: dict[str, dict[str, str]]) -> None:
if not self.use_s3_integration or not self.s3_client:
if not self.use_s3_integration or not self.s3_client or self.s3_setup_failed:
return
if not self.bucket or not self.repo_path:
logging.error(f"Something went wrong: bucket {self.bucket}, repo path {self.repo_path}")
Expand All @@ -604,7 +614,7 @@ def commit_repository(self, branch: str) -> str | None:
"""
try_num = 0
while try_num < MAX_RETRIES:
if not self.use_s3_integration:
if not self.use_s3_integration or self.s3_setup_failed:
return None

request = None
Expand All @@ -620,6 +630,7 @@ def commit_repository(self, branch: str) -> str | None:
# no need to upload something
return None

logging.debug(f'Submitting finalize upload request to {self.integrations_api_url}')
request = self.http.request("PUT", f"{self.integrations_api_url}?source={self.bc_source.name}", # type:ignore[no-untyped-call]
body=json.dumps(
{"path": self.repo_path, "branch": branch,
Expand All @@ -640,20 +651,21 @@ def commit_repository(self, branch: str) -> str | None:
get_user_agent_header()
))
response = json.loads(request.data.decode("utf8"))
logging.debug(f'Request ID: {request.headers.get("x-amzn-requestid")}')
logging.debug(f'Trace ID: {request.headers.get("x-amzn-trace-id")}')
url: str = self.get_sso_prismacloud_url(response.get("url", None))
return url
except HTTPError:
logging.error(f"Failed to commit repository {self.repo_path}", exc_info=True)
raise
self.s3_setup_failed = True
except JSONDecodeError:
if request:
logging.warning(
f"Response (status: {request.status}) of {self.integrations_api_url}: {request.data.decode('utf8')}")
logging.warning(f"Response (status: {request.status}) of {self.integrations_api_url}: {request.data.decode('utf8')}") # danger:ignore - we won't be here if the response contains valid data
logging.error(f"Response of {self.integrations_api_url} is not a valid JSON", exc_info=True)
raise
self.s3_setup_failed = True
finally:
if request and request.status == 201 and response and response.get("result") == "Success":
logging.info(f"Finalize repository {self.repo_id} in bridgecrew's platform")
logging.info(f"Finalize repository {self.repo_id} in the platform")
elif (
response
and try_num < MAX_RETRIES
Expand All @@ -664,8 +676,8 @@ def commit_repository(self, branch: str) -> str | None:
try_num += 1
sleep(SLEEP_SECONDS)
else:
raise Exception(
f"Failed to finalize repository {self.repo_id} in bridgecrew's platform\n{response}")
logging.error(f"Failed to finalize repository {self.repo_id} in the platform with the following error:\n{response}")
self.s3_setup_failed = True

return None

Expand Down Expand Up @@ -753,6 +765,8 @@ def get_customer_run_config(self) -> None:
url = self.get_run_config_url()
logging.debug(f'Platform run config URL: {url}')
request = self.http.request("GET", url, headers=headers) # type:ignore[no-untyped-call]
logging.debug(f'Request ID: {request.headers.get("x-amzn-requestid")}')
logging.debug(f'Trace ID: {request.headers.get("x-amzn-trace-id")}')
if request.status != 200:
error_message = get_auth_error_message(request.status, self.is_prisma_integration(), False)
logging.error(error_message)
Expand Down
8 changes: 5 additions & 3 deletions checkov/common/output/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from checkov.common.output.ai import OpenAi
from checkov.common.typing import _ExitCodeThresholds, _ScaExitCodeThresholds
from checkov.common.output.record import Record, SCA_PACKAGE_SCAN_CHECK_NAME
from checkov.common.util.consts import PARSE_ERROR_FAIL_FLAG, CHECKOV_RUN_SCA_PACKAGE_SCAN_V2
from checkov.common.util.consts import PARSE_ERROR_FAIL_FLAG, CHECKOV_RUN_SCA_PACKAGE_SCAN_V2, S3_UPLOAD_DETAILS_MESSAGE
from checkov.common.util.json_utils import CustomJSONEncoder
from checkov.runner_filter import RunnerFilter
from checkov.sast.consts import POLICIES_ERRORS, POLICIES_ERRORS_COUNT, ENGINE_NAME, SOURCE_FILES_COUNT, POLICY_COUNT
Expand Down Expand Up @@ -97,9 +97,11 @@ def get_json(self) -> str:
def get_all_records(self) -> List[Record]:
return self.failed_checks + self.passed_checks + self.skipped_checks

def get_dict(self, is_quiet: bool = False, url: str | None = None, full_report: bool = False) -> dict[str, Any]:
if not url:
def get_dict(self, is_quiet: bool = False, url: str | None = None, full_report: bool = False, s3_setup_failed: bool = False) -> dict[str, Any]:
if not url and not s3_setup_failed:
url = "Add an api key '--bc-api-key <api-key>' to see more detailed insights via https://bridgecrew.cloud"
elif s3_setup_failed:
url = S3_UPLOAD_DETAILS_MESSAGE
if is_quiet:
return {
"check_type": self.check_type,
Expand Down
12 changes: 9 additions & 3 deletions checkov/common/runners/runner_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from checkov.common.typing import _ExitCodeThresholds, _BaseRunner, _ScaExitCodeThresholds, LibraryGraph
from checkov.common.util import data_structures_utils
from checkov.common.util.banner import tool as tool_name
from checkov.common.util.consts import S3_UPLOAD_DETAILS_MESSAGE
from checkov.common.util.data_structures_utils import pickle_deepcopy
from checkov.common.util.json_utils import CustomJSONEncoder
from checkov.common.util.secrets_omitter import SecretsOmitter
Expand Down Expand Up @@ -390,7 +391,7 @@ def print_reports(
for report in scan_reports:
if not report.is_empty():
if "json" in config.output:
report_jsons.append(report.get_dict(is_quiet=config.quiet, url=url))
report_jsons.append(report.get_dict(is_quiet=config.quiet, url=url, s3_setup_failed=bc_integration.s3_setup_failed))
if "junitxml" in config.output:
junit_reports.append(report)
if "github_failed_only" in config.output:
Expand Down Expand Up @@ -477,8 +478,11 @@ def print_reports(

del output_formats["sarif"]

if "cli" not in config.output and url:
print(f"More details: {url}")
if "cli" not in config.output:
if url:
print(f"More details: {url}")
elif bc_integration.s3_setup_failed:
print(S3_UPLOAD_DETAILS_MESSAGE)
if CONSOLE_OUTPUT in output_formats.values():
print(OUTPUT_DELIMITER)

Expand Down Expand Up @@ -617,6 +621,8 @@ def _print_to_console(self, output_formats: dict[str, str], output_format: str,
print(output)
if url:
print(f"More details: {url}")
elif bc_integration.s3_setup_failed:
print(S3_UPLOAD_DETAILS_MESSAGE)

if CONSOLE_OUTPUT in output_formats.values():
print(OUTPUT_DELIMITER)
Expand Down
3 changes: 3 additions & 0 deletions checkov/common/util/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@
CHECKOV_RUN_SCA_PACKAGE_SCAN_V2 = os.getenv('CHECKOV_RUN_SCA_PACKAGE_SCAN_V2', 'true').lower() == 'true'

RESOURCE_ATTRIBUTES_TO_OMIT_UNIVERSAL_MASK = '*'

S3_UPLOAD_DETAILS_MESSAGE = 'An error occurred uploading results to the platform. A details URL is not available for this run. ' \
'See the error log output and enable debug logs for more information.'
3 changes: 3 additions & 0 deletions checkov/contributor_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ def report_contributor_metrics(repository: str, source: str,
contributors_report_api_url = f"{bc_integration.api_url}/api/v2/contributors/report"
if request_body:
while number_of_attempts <= 4:
logging.debug(f'Uploading contributor metrics to {contributors_report_api_url}')
response = request_wrapper(
"POST", contributors_report_api_url,
headers=bc_integration.get_default_headers("POST"), data=json.dumps(request_body)
)
logging.debug(f'Request ID: {response.headers.get("x-amzn-requestid")}')
logging.debug(f'Trace ID: {response.headers.get("x-amzn-trace-id")}')
if response.status_code < 300:
logging.debug(
f"Successfully uploaded contributor metrics with status: {response.status_code}. number of attempts: {number_of_attempts}")
Expand Down
Loading

0 comments on commit f6a4502

Please sign in to comment.