From 5b5b0b040587d51d5a236eefc19bd5da3325894f Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Wed, 19 Oct 2022 15:21:07 +0200 Subject: [PATCH] feat(securityhub_check): Add check and service for SecurityHub (#1360) Co-authored-by: Toni de la Fuente Co-authored-by: sergargar Co-authored-by: Pepe Fagoaga --- .../accessanalyzer/accessanalyzer_service.py | 1 - .../aws/services/securityhub/__init__.py | 0 .../aws/services/securityhub/check_extra799 | 38 -------- .../securityhub/securityhub_client.py | 4 + .../securityhub_enabled/__init__.py | 0 .../securityhub_enabled.metadata.json | 35 +++++++ .../securityhub_enabled.py | 23 +++++ .../securityhub_enabled_test.py | 62 +++++++++++++ .../securityhub/securityhub_service.py | 91 +++++++++++++++++++ .../securityhub/securityhub_service_test.py | 75 +++++++++++++++ 10 files changed, 290 insertions(+), 39 deletions(-) create mode 100644 providers/aws/services/securityhub/__init__.py delete mode 100644 providers/aws/services/securityhub/check_extra799 create mode 100644 providers/aws/services/securityhub/securityhub_client.py create mode 100644 providers/aws/services/securityhub/securityhub_enabled/__init__.py create mode 100644 providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json create mode 100644 providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py create mode 100644 providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py create mode 100644 providers/aws/services/securityhub/securityhub_service.py create mode 100644 providers/aws/services/securityhub/securityhub_service_test.py diff --git a/providers/aws/services/accessanalyzer/accessanalyzer_service.py b/providers/aws/services/accessanalyzer/accessanalyzer_service.py index 838a613d..e77a4658 100644 --- a/providers/aws/services/accessanalyzer/accessanalyzer_service.py +++ b/providers/aws/services/accessanalyzer/accessanalyzer_service.py @@ -87,7 +87,6 @@ class AccessAnalyzer: f"{regional_client.region} -- {error.__class__.__name__}: {error}" ) - @dataclass class Analyzer: arn: str diff --git a/providers/aws/services/securityhub/__init__.py b/providers/aws/services/securityhub/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/securityhub/check_extra799 b/providers/aws/services/securityhub/check_extra799 deleted file mode 100644 index 131f6829..00000000 --- a/providers/aws/services/securityhub/check_extra799 +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2018) by Toni de la Fuente -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -CHECK_ID_extra799="7.99" -CHECK_TITLE_extra799="[extra799] Check if Security Hub is enabled and its standard subscriptions" -CHECK_SCORED_extra799="NOT_SCORED" -CHECK_CIS_LEVEL_extra799="EXTRA" -CHECK_SEVERITY_extra799="High" -CHECK_ASFF_RESOURCE_TYPE_extra799="AwsSecurityHubHub" -CHECK_ALTERNATE_check799="extra799" -CHECK_SERVICENAME_extra799="securityhub" -CHECK_RISK_extra799='AWS Security Hub gives you a comprehensive view of your security alerts and security posture across your AWS accounts.' -CHECK_REMEDIATION_extra799='Security Hub is Regional. When you enable or disable a security standard; it is enabled or disabled only in the current Region or in the Region that you specify.' -CHECK_DOC_extra799='https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable.html' -CHECK_CAF_EPIC_extra799='Logging and Monitoring' - -extra799(){ - for regx in $REGIONS; do - # If command below fails get nothing then it there are no subscriptions and Security Hub is not enabled. - LIST_OF_SECHUB_SUBSCRIPTIONS=$($AWSCLI $PROFILE_OPT --region $regx securityhub get-enabled-standards --query 'StandardsSubscriptions[?StandardsStatus == `READY`].StandardsSubscriptionArn' --output json 2>/dev/null | awk -F "/" '{ print $2 }' | tr '\n' ' ' ) - if [[ $LIST_OF_SECHUB_SUBSCRIPTIONS ]]; then - textPass "$regx: Security Hub is enabled with standards $LIST_OF_SECHUB_SUBSCRIPTIONS" "$regx" - else - textInfo "$regx: Security Hub is not enabled" "$regx" - #textFail "$regx: Security Hub is not enabled" "$regx" - fi - done -} diff --git a/providers/aws/services/securityhub/securityhub_client.py b/providers/aws/services/securityhub/securityhub_client.py new file mode 100644 index 00000000..a04f6548 --- /dev/null +++ b/providers/aws/services/securityhub/securityhub_client.py @@ -0,0 +1,4 @@ +from providers.aws.lib.audit_info.audit_info import current_audit_info +from providers.aws.services.securityhub.securityhub_service import SecurityHub + +securityhub_client = SecurityHub(current_audit_info) diff --git a/providers/aws/services/securityhub/securityhub_enabled/__init__.py b/providers/aws/services/securityhub/securityhub_enabled/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json b/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json new file mode 100644 index 00000000..19e87a61 --- /dev/null +++ b/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "securityhub_enabled", + "CheckTitle": "Check if Security Hub is enabled and its standard subscriptions.", + "CheckType": ["Logging and Monitoring"], + "ServiceName": "securityhub", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:securityhub:region:account-id:hub/hub-id", + "Severity": "high", + "ResourceType": "AwsSecurityHubHub", + "Description": "Check if Security Hub is enabled and its standard subscriptions.", + "Risk": "AWS Security Hub gives you a comprehensive view of your security alerts and security posture across your AWS accounts.", + "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable.html", + "Remediation": { + "Code": { + "CLI": "aws securityhub enable-security-hub --enable-default-standards", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Security Hub is Regional. When you enable or disable a security standard, it is enabled or disabled only in the current Region or in the Region that you specify.", + "Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] +} diff --git a/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py b/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py new file mode 100644 index 00000000..14dea3ce --- /dev/null +++ b/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py @@ -0,0 +1,23 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.securityhub.securityhub_client import securityhub_client + + +class securityhub_enabled(Check): + def execute(self): + findings = [] + for securityhub in securityhub_client.securityhubs: + report = Check_Report(self.metadata) + report.region = securityhub.region + if securityhub.status == "ACTIVE": + report.status = "PASS" + report.status_extended = ( + f"Security Hub is enabled with standards {securityhub.standards}" + ) + else: + report.status = "FAIL" + report.status_extended = f"Security Hub is not enabled" + report.resource_id = securityhub.id + report.resource_arn = securityhub.arn + findings.append(report) + + return findings diff --git a/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py b/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py new file mode 100644 index 00000000..575882d0 --- /dev/null +++ b/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py @@ -0,0 +1,62 @@ +from unittest import mock + +from providers.aws.services.securityhub.securityhub_service import SecurityHubHub + + +class Test_accessanalyzer_enabled_without_findings: + def test_securityhub_hub_inactive(self): + securityhub_client = mock.MagicMock + securityhub_client.securityhubs = [ + SecurityHubHub( + "", + "Security Hub", + "NOT_AVAILABLE", + "", + "eu-west-1", + ) + ] + with mock.patch( + "providers.aws.services.securityhub.securityhub_service.SecurityHub", + new=securityhub_client, + ): + # Test Check + from providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled import ( + securityhub_enabled, + ) + + check = securityhub_enabled() + result = check.execute() + + assert result[0].status == "FAIL" + assert result[0].status_extended == "Security Hub is not enabled" + assert result[0].resource_id == "Security Hub" + + def test_securityhub_hub_active(self): + securityhub_client = mock.MagicMock + securityhub_client.securityhubs = [ + SecurityHubHub( + "arn:aws:securityhub:us-east-1:0123456789012:hub/default", + "default", + "ACTIVE", + "cis-aws-foundations-benchmark/v/1.2.0", + "eu-west-1", + ) + ] + with mock.patch( + "providers.aws.services.securityhub.securityhub_service.SecurityHub", + new=securityhub_client, + ): + # Test Check + from providers.aws.services.securityhub.securityhub_enabled.securityhub_enabled import ( + securityhub_enabled, + ) + + check = securityhub_enabled() + result = check.execute() + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Security Hub is enabled with standards cis-aws-foundations-benchmark/v/1.2.0" + ) + assert result[0].resource_id == "default" diff --git a/providers/aws/services/securityhub/securityhub_service.py b/providers/aws/services/securityhub/securityhub_service.py new file mode 100644 index 00000000..f1c695dc --- /dev/null +++ b/providers/aws/services/securityhub/securityhub_service.py @@ -0,0 +1,91 @@ +import threading +from dataclasses import dataclass + +from lib.logger import logger +from providers.aws.aws_provider import generate_regional_clients + + +################## SecurityHub +class SecurityHub: + def __init__(self, audit_info): + self.service = "securityhub" + self.session = audit_info.audit_session + self.audited_account = audit_info.audited_account + self.regional_clients = generate_regional_clients(self.service, audit_info) + self.securityhubs = [] + self.__threading_call__(self.__describe_hub__) + + def __get_session__(self): + return self.session + + def __threading_call__(self, call): + threads = [] + for regional_client in self.regional_clients.values(): + threads.append(threading.Thread(target=call, args=(regional_client,))) + for t in threads: + t.start() + for t in threads: + t.join() + + def __describe_hub__(self, regional_client): + logger.info("SecurityHub - Describing Hub...") + try: + get_enabled_standards_paginator = regional_client.get_paginator( + "get_enabled_standards" + ) + standards = "" + for page in get_enabled_standards_paginator.paginate(): + for standard in page["StandardsSubscriptions"]: + standards += f" {standard['StandardsArn'].split('/')[1]}" + # Security Hub is not enabled in region + if standards == "": + self.securityhubs.append( + SecurityHubHub( + "", + "Security Hub", + "NOT_AVAILABLE", + "", + regional_client.region, + ) + ) + else: + # SecurityHub is active so get HubArn + hub_arn = regional_client.describe_hub()["HubArn"] + hub_id = hub_arn.split("/")[1] + self.securityhubs.append( + SecurityHubHub( + hub_arn, + hub_id, + "ACTIVE", + standards, + regional_client.region, + ) + ) + + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +@dataclass +class SecurityHubHub: + arn: str + id: str + status: str + standards: str + region: str + + def __init__( + self, + arn, + id, + status, + standards, + region, + ): + self.arn = arn + self.id = id + self.status = status + self.standards = standards + self.region = region diff --git a/providers/aws/services/securityhub/securityhub_service_test.py b/providers/aws/services/securityhub/securityhub_service_test.py new file mode 100644 index 00000000..7a6666f8 --- /dev/null +++ b/providers/aws/services/securityhub/securityhub_service_test.py @@ -0,0 +1,75 @@ +from unittest.mock import patch + +import botocore + +from providers.aws.lib.audit_info.audit_info import current_audit_info +from providers.aws.services.securityhub.securityhub_service import SecurityHub + +# Mock Test Region +AWS_REGION = "eu-west-1" + +# Mocking Access Analyzer Calls +make_api_call = botocore.client.BaseClient._make_api_call + +# As you can see the operation_name has the list_analyzers snake_case form but +# we are using the ListAnalyzers form. +# Rationale -> https://github.com/boto/botocore/blob/develop/botocore/client.py#L810:L816 +# +# We have to mock every AWS API call using Boto3 +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "GetEnabledStandards": + return { + "StandardsSubscriptions": [ + { + "StandardsArn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0", + "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:0123456789012:subscription/cis-aws-foundations-benchmark/v/1.2.0", + "StandardsInput": {"string": "string"}, + "StandardsStatus": "READY", + }, + ] + } + if operation_name == "DescribeHub": + return { + "HubArn": "arn:aws:securityhub:us-east-1:0123456789012:hub/default", + } + return make_api_call(self, operation_name, kwarg) + + +# Mock generate_regional_clients() +def mock_generate_regional_clients(service, audit_info): + regional_client = audit_info.audit_session.client(service, region_name=AWS_REGION) + regional_client.region = AWS_REGION + return {AWS_REGION: regional_client} + + +# Patch every AWS call using Boto3 and generate_regional_clients to have 1 client +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +@patch( + "providers.aws.services.securityhub.securityhub_service.generate_regional_clients", + new=mock_generate_regional_clients, +) +class Test_SecurityHub_Service: + # Test SecurityHub Client + def test__get_client__(self): + access_analyzer = SecurityHub(current_audit_info) + assert ( + access_analyzer.regional_clients[AWS_REGION].__class__.__name__ + == "SecurityHub" + ) + + # Test SecurityHub Session + def test__get_session__(self): + access_analyzer = SecurityHub(current_audit_info) + assert access_analyzer.session.__class__.__name__ == "Session" + + def test__describe_hub__(self): + # Set partition for the service + current_audit_info.audited_partition = "aws" + securityhub = SecurityHub(current_audit_info) + assert len(securityhub.securityhubs) == 1 + assert ( + securityhub.securityhubs[0].arn + == "arn:aws:securityhub:us-east-1:0123456789012:hub/default" + ) + assert securityhub.securityhubs[0].id == "default" + assert securityhub.securityhubs[0].standards == " cis-aws-foundations-benchmark"