Esempio n. 1
0
def cloudsploit_error(event, context):
    """
    Handle errors in cloudsploit setup or populate. Invoked by step function.
    Adds a DNC score for each cloudsploit based requirement for the account
    that failed. Also records error in scan record.

    Expected input event format
    {
        'accountId': account_id,
        'scanId': scan_id,
    }
    """
    account_id = event['accountId']
    scan_id = event['scanId']
    error = event['error']
    # remove traceback for cloudsploit errors
    try:
        error['Cause'] = json.dumps({
            **json.loads(error['Cause']), 'trace': None
        })
    except:  # pylint: disable=bare-except
        pass
    account = accounts_table.get_account(account_id)

    cloudsploit_based_requirements = requirements_table.get_cloudsploit_based_requirements(
    )

    applying_requirements, not_applying_requirements = split_requirements(
        cloudsploit_based_requirements, account)

    scores_to_put = []
    for requirement in applying_requirements:
        scores_to_put.append(
            scores_table.new_score(
                scan_id,
                account_id,
                requirement,
                scores_table.DATA_NOT_COLLECTED,
                scores_table.DATA_NOT_COLLECTED,
            ))
    for requirement in not_applying_requirements:
        scores_to_put.append(
            scores_table.new_score(
                scan_id,
                account_id,
                requirement,
                scores_table.NOT_APPLICABLE,
                scores_table.NOT_APPLICABLE,
            ))

    scores_table.batch_put_records(scores_to_put)
    scans_table.add_error(scan_id, context.function_name, event['error'])
Esempio n. 2
0
def account_detailed_scores_handler(event, context):
    """
    :param event:
    :param context:
    :raises HttpInvalidException if missing 'scanId' or 'accountIds':
    :return account scores of all account_ids in event:
    """
    try:
        account_ids = event['pathParameters']['accountIds']
        scan_id = event['scanId']
    except KeyError:
        raise HttpInvalidException(
            'account ids or scan id not found in request')

    accounts = []
    account_ids = urllib.parse.unquote(account_ids).split(',')
    require_can_read_account(event['userRecord'], account_ids)

    for account_id in account_ids:
        try:
            account_name = accounts_table.get_account(account_id).get(
                'account_name', account_id)
        except KeyError:
            raise HttpNotFoundException(
                f'account record not found for {account_id}')

        if len(account_id) > 0:
            detailed_score = {
                'accountId': account_id,
                'accountName': account_name,
            }
            requirements = []
            to_parse = scores_table.query_all(
                KeyConditionExpression=Key('scanId').eq(scan_id)
                & Key('accntId_rqrmntId').begins_with(account_id), )
            for item in to_parse:
                requirements.append({
                    'requirementId': item['requirementId'],
                    'score': item['score']
                })

            detailed_score['requirementsScores'] = requirements
            accounts.append(detailed_score)

    return {'accounts': accounts}
Esempio n. 3
0
def cloudsploit_setup(event, context):
    """
    Create parameters for external cloudsploit scanning lambda based on
    account settings and requirement definition.

    Expected input event format
    {
        'accountId': account_id,
        'scanId': scan_id,
        'cloudsploitSettingsMap': {
            'default': {...},
            'settings1': {...},
            ...
        }
    }
    """
    account = accounts_table.get_account(event['accountId'])
    if 'cross_account_role' not in account:
        raise ValueError(
            f'cross_account_role not specified in account {event["accountId"]}'
        )
    cross_account_role = account.get('cross_account_role', None)
    if 'scorecard_profile' in account:
        try:
            settings = event['cloudsploitSettingsMap'][
                account['scorecard_profile']]
        except KeyError:
            raise KeyError(
                f'cloudsploit settings {account["scorecard_profile"]} specified in {event} does not exist'
            )
    else:
        settings = event['cloudsploitSettingsMap']['default']
    return {
        'cloud': 'aws',
        'cloudConfig': {
            'roleArn': cross_account_role
        },
        'settings': settings,
        's3Prefix': event['accountId']
    }
Esempio n. 4
0
def cloudsploit_populate(event, context):
    """
    Import cloudsploit findings from s3 and convert to NCRs based on
    requirement definitions

    Expected input event format
    {
        scanId: string,
        accountId: string,
    }
    """
    account_id = event['accountId']
    scan_id = event['scanId']
    if not isinstance(scan_id, str):
        raise TypeError(f'scanId should be str, not {type(scan_id)}')

    account = accounts_table.get_account(account_id)

    # keys here will be the ncr's sort key which will uniquely identify it
    # since the partion key will be the scanId for all ncrs created.
    all_ncrs = {}
    scores_to_put = []

    cloudsploit_based_requirements = requirements_table.get_cloudsploit_based_requirements(
    )

    # split requirements based on whether they apply to the account
    applying_requirements, not_applying_requirements = split_requirements(
        cloudsploit_based_requirements, account)

    s3_key = cloudsploit_prefix + '/' + account_id + '/latest.json'

    # load cloudsploit results
    logger.info('Getting Cloudsploit results from s3://%s/%s', bucket_name,
                s3_key)
    response = S3.get_object(Bucket=bucket_name, Key=s3_key)
    object_expiration_check(response, s3_key)
    result = json.loads(response['Body'].read())

    # group cloudsploit results by finding
    grouped_results_data = defaultdict(list)
    for result_object in result['resultsData']:
        grouped_results_data[result_object['title']].append(result_object)

    # add NCRs based on cloudsploit requirements
    for requirement in applying_requirements:
        requirement_titles = requirement['cloudsploit']['finding']
        if isinstance(requirement_titles, str):
            requirement_titles = [requirement_titles]
        relevant_results = []
        for requirement_title in requirement_titles:
            relevant_results.extend(
                grouped_results_data.get(requirement_title, []))

        failing_statuses = determine_failing_statuses(requirement)

        # create NCR for each cloudsploit result
        for finding_object in relevant_results:
            if finding_object['status'] in failing_statuses:
                resource_type = None
                # set resource if cloudsploit didn't provide one
                if finding_object[
                        'resource'] == ncr_table.CLOUDSPLOIT_FINDING_NA and requirement[
                            'cloudsploit'].get('regional', False) is True:
                    resource_id = finding_object['region']
                elif finding_object[
                        'resource'] == ncr_table.CLOUDSPLOIT_FINDING_NA:
                    resource_id = str(account_id)
                else:
                    (resource_type, resource_id) = extract_resource_id(
                        finding_object['resource'])

                # Set the resource type based on the requirement spec (not cloudsploit), if it wasn't overridden by extract_resource_id()
                if resource_type is None:
                    resource_type = f'{requirement["service"]}-{requirement["component"]}'

                ncr_key = ncr_table.create_sort_key(
                    account_id, resource_id, requirement['requirementId'])
                if ncr_key in all_ncrs:
                    all_ncrs[ncr_key]['reason'][
                        finding_object['message']] = None
                else:
                    all_ncrs[ncr_key] = ncr_table.new_ncr_record(
                        {
                            'accountId': account_id,
                            'accountName': account['account_name'],
                            'requirementId': requirement['requirementId'],
                            'resourceId': resource_id,
                            'resourceType': resource_type,
                            'region': finding_object['region'],
                            'reason': {
                                finding_object['message']: None
                            },  # dict as set with 1 item (set to deduplicate reasons)
                            'cloudsploitStatus': finding_object[
                                'status']  # Send the cloudsploit status to the NCR Record
                        },
                        scan_id)

        # determine number of resources for score
        if requirement['cloudsploit'].get('source'):
            service_name, api_call = requirement['cloudsploit'].get(
                'source').split('.')
            cloudsploit_data = result['collectionData']['aws'][service_name][
                api_call]
            try:
                num_resources = sum(
                    len(region_object['data'])
                    for region_object in cloudsploit_data.values()
                    if 'err' not in region_object)
            except KeyError:
                raise RuntimeError(
                    f'{service_name}, {api_call} collectionData contained region object lacking both "data" and "err" keys"'
                )
        else:
            num_resources = len(relevant_results)

        scores_to_put.append(
            scores_table.new_score(scan_id, account_id, requirement,
                                   num_resources))
    # add N/A score for requirements that don't apply
    for requirement in not_applying_requirements:
        scores_to_put.append(
            scores_table.new_score(scan_id, account_id, requirement,
                                   scores_table.NOT_APPLICABLE,
                                   scores_table.NOT_APPLICABLE))

    for ncr in all_ncrs.values():
        ncr['reason'] = '\n'.join(ncr['reason'].keys())
        logger.info('Adding ncrs: %s', json.dumps(all_ncrs))
    ncr_table.batch_put_records(all_ncrs.values())
    scores_table.batch_put_records(scores_to_put)
Esempio n. 5
0
def score_calc_handler(event, context):
    """
    :param event: {
        scanId: string,
        accountIds: list of accountIds,
    }
    :param context: dict
    :return: None
    """
    scan_id = event['openScan']['scanId']
    account_ids = event['load']['accountIds']
    date = scan_id[0:10]
    all_scores_to_put = []
    all_account_scores = []

    for account_id in account_ids:
        account_name = accounts_table.get_account(account_id).get('account_name')
        scores_to_put = {
            record['requirementId']: record
            for record in scores_table.query_all(
                KeyConditionExpression=Key('scanId').eq(scan_id) & Key('accntId_rqrmntId').begins_with(account_id)
            )
        }
        existing_ncr_records = ncr_table.query_all(
            KeyConditionExpression=Key('scanId').eq(scan_id) & Key('accntId_rsrceId_rqrmntId').begins_with(account_id),
        )

        grouped_ncr_data = defaultdict(list)
        for ncr_object in existing_ncr_records:
            grouped_ncr_data[ncr_object['requirementId']].append(ncr_object)

        for requirement_object in all_requirements:
            severity = requirement_object['severity']
            record_to_edit = scores_to_put.get(requirement_object['requirementId'], False)
            if record_to_edit is False:
                continue  # data not collected for this account for this scan for this requirement, moving on
            score_object = record_to_edit['score'][severity]

            # check if score is DNC if so we skip counting failing resources
            if scores_table.DATA_NOT_COLLECTED in score_object.values():
                continue

            if score_object['numFailing'] is None:
                score_object['numFailing'] = Decimal(0)

            matching_ncrs = grouped_ncr_data.get(requirement_object['requirementId'], [])
            for ncr_record in matching_ncrs:
                is_excluded = ncr_record.get('exclusionApplied', False)
                # TODO handle hidden ncr's also (decrement numResources)
                if is_excluded:
                    continue
                else:
                    score_object['numFailing'] += 1
            all_scores_to_put.append(record_to_edit)

        account_score = {
            'accountId': account_id,
            'accountName': account_name,
            'date': date,
            'scanId': scan_id,
            'score': scores_table.weighted_score_aggregate_calc(scores_to_put.values())
        }
        all_account_scores.append(account_score)

    scores_table.batch_put_records(all_scores_to_put)
    account_scores_table.batch_put_records(all_account_scores)