mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-21 13:20:57 +00:00
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a27029cd95 | ||
|
|
9a9cc9a17a | ||
|
|
9bb4329b7f | ||
|
|
210207a5fa | ||
|
|
d587d40451 | ||
|
|
f0cd88bd0e | ||
|
|
aca17904fa | ||
|
|
0157802ac1 | ||
|
|
10766d708d | ||
|
|
f231d8b080 | ||
|
|
590a7b2697 | ||
|
|
3c3421644f | ||
|
|
f1f68da25d | ||
|
|
48df7fdebf | ||
|
|
f2e8691bf4 | ||
|
|
344d54155a | ||
|
|
8ecffa3039 | ||
|
|
efbbfc1c68 | ||
|
|
dc68c1b955 | ||
|
|
5de13bdd8a | ||
|
|
5d0f498425 | ||
|
|
716558ffcb | ||
|
|
23929b3e68 | ||
|
|
a5612abc8c | ||
|
|
78dddc1e03 | ||
|
|
76020d4d47 | ||
|
|
b0af1390b5 | ||
|
|
bc3cd43126 | ||
|
|
087dae07d8 | ||
|
|
0baf4fb224 | ||
|
|
0f8ea48f2f | ||
|
|
ec207c50ce | ||
|
|
b59b40b822 | ||
|
|
aa51045329 | ||
|
|
1a9f854063 | ||
|
|
6bdcb509e1 | ||
|
|
ce1e9de104 | ||
|
|
2471bc569a | ||
|
|
d0ef75d8d9 | ||
|
|
aa79a289ce | ||
|
|
0340ab9570 | ||
|
|
a2929f2efb | ||
|
|
bf4db86dec | ||
|
|
a339dafcc6 | ||
|
|
f376516aad | ||
|
|
816b49fac5 | ||
|
|
6851350093 | ||
|
|
d5873c0437 | ||
|
|
a2dba30869 | ||
|
|
0662dff13f | ||
|
|
0ae26bddfc | ||
|
|
43efabef6c | ||
|
|
e73fc14f62 | ||
|
|
89fe8fa8e2 | ||
|
|
634ef2e599 | ||
|
|
4efb70a508 | ||
|
|
c3ae0aa873 | ||
|
|
a109cd2816 | ||
|
|
78fb540bbb | ||
|
|
5b543bf058 |
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.83.2
|
||||
uses: trufflesecurity/trufflehog@v3.83.6
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
# Update system dependencies and install essential tools
|
||||
#hadolint ignore=DL3018
|
||||
RUN apk --no-cache upgrade && apk --no-cache add curl git
|
||||
RUN apk --no-cache upgrade && apk --no-cache add curl git g++
|
||||
|
||||
# Create nonroot user
|
||||
# Create non-root user
|
||||
RUN mkdir -p /home/prowler && \
|
||||
echo 'prowler:x:1000:1000:prowler:/home/prowler:' > /etc/passwd && \
|
||||
echo 'prowler:x:1000:' > /etc/group && \
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog"><img width="30" height="30" alt="Prowler community on Slack" src="https://github.com/prowler-cloud/prowler/assets/38561120/3c8b4ec5-6849-41a5-b5e1-52bbb94af73a"></a>
|
||||
<a href="https://goto.prowler.com/slack"><img width="30" height="30" alt="Prowler community on Slack" src="https://github.com/prowler-cloud/prowler/assets/38561120/3c8b4ec5-6849-41a5-b5e1-52bbb94af73a"></a>
|
||||
<br>
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-2oinmgmw6-cl7gOrljSEqo_aoripVPFA">Join our Prowler community!</a>
|
||||
<a href="https://goto.prowler.com/slack">Join our Prowler community!</a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center">
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog"><img alt="Slack Shield" src="https://img.shields.io/badge/slack-prowler-brightgreen.svg?logo=slack"></a>
|
||||
<a href="https://goto.prowler.com/slack"><img alt="Slack Shield" src="https://img.shields.io/badge/slack-prowler-brightgreen.svg?logo=slack"></a>
|
||||
<a href="https://pypi.org/project/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/v/prowler.svg"></a>
|
||||
<a href="https://pypi.python.org/pypi/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/pyversions/prowler.svg"></a>
|
||||
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Prowler Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=prowler%20downloads"></a>
|
||||
@@ -63,9 +63,9 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 457 | 67 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
|
||||
| AWS | 553 | 77 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
|
||||
| GCP | 77 | 13 -> `prowler gcp --list-services` | 2 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
|
||||
| Azure | 136 | 17 -> `prowler azure --list-services` | 3 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Azure | 138 | 17 -> `prowler azure --list-services` | 3 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
|
||||
|
||||
# 💻 Installation
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
For technical support or any type of inquiries, you are very welcome to:
|
||||
|
||||
- Reach out to community members on the [**Prowler Slack channel**](https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog)
|
||||
- Reach out to community members on the [**Prowler Slack channel**](https://goto.prowler.com/slack)
|
||||
|
||||
- Open an Issue or a Pull Request in our [**GitHub repository**](https://github.com/prowler-cloud/prowler).
|
||||
|
||||
|
||||
@@ -67,4 +67,4 @@ If you create or review a PR in https://github.com/prowler-cloud/prowler please
|
||||
|
||||
## Want some swag as appreciation for your contribution?
|
||||
|
||||
If you are like us and you love swag, we are happy to thank you for your contribution with some laptop stickers or whatever other swag we may have at that time. Please, tell us more details and your pull request link in our [Slack workspace here](https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog). You can also reach out to Toni de la Fuente on Twitter [here](https://twitter.com/ToniBlyx), his DMs are open.
|
||||
If you are like us and you love swag, we are happy to thank you for your contribution with some laptop stickers or whatever other swag we may have at that time. Please, tell us more details and your pull request link in our [Slack workspace here](https://goto.prowler.com/slack). You can also reach out to Toni de la Fuente on Twitter [here](https://twitter.com/ToniBlyx), his DMs are open.
|
||||
|
||||
@@ -190,18 +190,18 @@ from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.<new_provider_name>.models import (
|
||||
# All providers models needed
|
||||
ProvierSessionModel,
|
||||
ProvierIdentityModel,
|
||||
ProvierOutputOptionsModel
|
||||
ProviderSessionModel,
|
||||
ProviderIdentityModel,
|
||||
ProviderOutputOptionsModel
|
||||
)
|
||||
|
||||
class NewProvider(Provider):
|
||||
# All properties from the class, some of this are properties in the base class
|
||||
_type: str = "<provider_name>"
|
||||
_session: <ProvierSessionModel>
|
||||
_identity: <ProvierIdentityModel>
|
||||
_session: <ProviderSessionModel>
|
||||
_identity: <ProviderIdentityModel>
|
||||
_audit_config: dict
|
||||
_output_options: ProvierOutputOptionsModel
|
||||
_output_options: ProviderOutputOptionsModel
|
||||
_mutelist: dict
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
@@ -212,13 +212,13 @@ class NewProvider(Provider):
|
||||
arguments (dict): A dictionary containing configuration arguments.
|
||||
"""
|
||||
logger.info("Setting <NewProviderName> provider ...")
|
||||
# First get from arguments the necesary from the cloud acount (subscriptions or projects or whatever the provider use for storing services)
|
||||
# First get from arguments the necessary from the cloud account (subscriptions or projects or whatever the provider use for storing services)
|
||||
|
||||
# Set the session with the method enforced by parent class
|
||||
self._session = self.setup_session(credentials_file)
|
||||
|
||||
# Set the Identity class normaly the provider class give by Python provider library
|
||||
self._identity = <ProvierIdentityModel>()
|
||||
self._identity = <ProviderIdentityModel>()
|
||||
|
||||
# Set the provider configuration
|
||||
self._audit_config = load_and_validate_config_file(
|
||||
@@ -254,7 +254,7 @@ class NewProvider(Provider):
|
||||
<all_needed_for_auth> Can include all necessary arguments to setup the session
|
||||
|
||||
Returns:
|
||||
Credentials necesary to communicate with the provider.
|
||||
Credentials necessary to communicate with the provider.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -125,5 +125,5 @@ prowler <provider> --list-categories
|
||||
```
|
||||
- Execute specific category(s):
|
||||
```console
|
||||
prowler <provider> --categories
|
||||
prowler <provider> --categories secrets
|
||||
```
|
||||
|
||||
Generated
+1150
-1045
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,7 @@ from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
from prowler.providers.gcp.models import GCPOutputOptions
|
||||
from prowler.providers.kubernetes.models import KubernetesOutputOptions
|
||||
from prowler.providers.microsoft365.models import Microsoft365OutputOptions
|
||||
|
||||
|
||||
def prowler():
|
||||
@@ -257,6 +258,10 @@ def prowler():
|
||||
output_options = KubernetesOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "microsoft365":
|
||||
output_options = Microsoft365OutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
|
||||
# Run the quick inventory for the provider if available
|
||||
if hasattr(args, "quick_inventory") and args.quick_inventory:
|
||||
|
||||
@@ -485,7 +485,7 @@
|
||||
"codeartifact_packages_external_public_publishing_disabled",
|
||||
"ecr_repositories_not_publicly_accessible",
|
||||
"efs_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"elb_internet_facing",
|
||||
"elbv2_internet_facing",
|
||||
"s3_account_level_public_access_blocks",
|
||||
@@ -664,7 +664,7 @@
|
||||
"awslambda_function_not_publicly_accessible",
|
||||
"apigateway_restapi_waf_acl_attached",
|
||||
"cloudfront_distributions_using_waf",
|
||||
"eks_control_plane_endpoint_access_restricted",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"sagemaker_models_network_isolation_enabled",
|
||||
"sagemaker_models_vpc_settings_configured",
|
||||
"sagemaker_notebook_instance_vpc_settings_configured",
|
||||
|
||||
@@ -1509,9 +1509,9 @@
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"networkfirewall_in_all_vpc",
|
||||
"eks_cluster_network_policy_enabled",
|
||||
"eks_control_plane_endpoint_access_restricted",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_cluster_private_nodes_enabled",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"kafka_cluster_is_public",
|
||||
"kafka_cluster_unrestricted_access_disabled",
|
||||
"vpc_peering_routing_tables_with_least_privilege",
|
||||
|
||||
@@ -1509,9 +1509,9 @@
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"networkfirewall_in_all_vpc",
|
||||
"eks_cluster_network_policy_enabled",
|
||||
"eks_control_plane_endpoint_access_restricted",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_cluster_private_nodes_enabled",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"kafka_cluster_is_public",
|
||||
"kafka_cluster_unrestricted_access_disabled",
|
||||
"vpc_peering_routing_tables_with_least_privilege",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_profile_attached",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"iam_aws_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
@@ -61,7 +61,7 @@
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_profile_attached",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"iam_aws_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
@@ -102,7 +102,7 @@
|
||||
"Checks": [
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"awslambda_function_not_publicly_accessible",
|
||||
"awslambda_function_url_public",
|
||||
|
||||
@@ -971,7 +971,7 @@
|
||||
"Checks": [
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"awslambda_function_url_public",
|
||||
"rds_instance_no_public_access",
|
||||
|
||||
@@ -3043,9 +3043,7 @@
|
||||
{
|
||||
"Id": "9.4",
|
||||
"Description": "Ensure that Register with Entra ID is enabled on App Service",
|
||||
"Checks": [
|
||||
""
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "9. AppService",
|
||||
@@ -3175,9 +3173,7 @@
|
||||
{
|
||||
"Id": "9.10",
|
||||
"Description": "Ensure Azure Key Vaults are Used to Store Secrets",
|
||||
"Checks": [
|
||||
""
|
||||
],
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "9. AppService",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Framework": "CIS",
|
||||
"Version": "4.0",
|
||||
"Provider": "Microsoft365",
|
||||
"Description": "The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for establishing a secure configuration posture for Microsoft 365 Cloud offerings running on any OS.",
|
||||
"Requirements": [
|
||||
{
|
||||
"Id": "1.1.1",
|
||||
"Description": "Ensure that 'Administrative accounts' are 'cloud-only'",
|
||||
"Checks": [
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "1.Microsoft 365 admin center",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Automated",
|
||||
"Description": "",
|
||||
"RationaleStatement": "",
|
||||
"ImpactStatement": "",
|
||||
"RemediationProcedure": "",
|
||||
"AuditProcedure": "",
|
||||
"AdditionalInformation": "",
|
||||
"DefaultValue": "",
|
||||
"References": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "4.5.0"
|
||||
prowler_version = "4.6.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -482,6 +482,24 @@ class Check_Report_Kubernetes(Check_Report):
|
||||
self.namespace = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check_Report_Microsoft365(Check_Report):
|
||||
# TODO change class name to CheckReportMicrosoft365
|
||||
"""Contains the Microsoft365 Check's finding information."""
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
subscription: str
|
||||
location: str
|
||||
|
||||
def __init__(self, metadata):
|
||||
super().__init__(metadata)
|
||||
self.resource_name = ""
|
||||
self.resource_id = ""
|
||||
self.subscription = ""
|
||||
self.location = "global"
|
||||
|
||||
|
||||
# Testing Pending
|
||||
def load_check_metadata(metadata_file: str) -> CheckMetadata:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 9000 to 9999 are reserved for Jira exceptions
|
||||
class JiraBaseException(ProwlerException):
|
||||
"""Base class for Jira exceptions."""
|
||||
|
||||
JIRA_ERROR_CODES = {
|
||||
(9000, "JiraNoProjectsError"): {
|
||||
"message": "No projects were found in Jira.",
|
||||
"remediation": "Please create a project in Jira.",
|
||||
},
|
||||
(9001, "JiraAuthenticationError"): {
|
||||
"message": "Failed to authenticate with Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again. Needed scopes are: read:jira-user read:jira-work write:jira-work",
|
||||
},
|
||||
(9002, "JiraTestConnectionError"): {
|
||||
"message": "Failed to connect to Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9003, "JiraCreateIssueError"): {
|
||||
"message": "Failed to create an issue in Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9004, "JiraGetProjectsError"): {
|
||||
"message": "Failed to get projects from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9005, "JiraGetCloudIDError"): {
|
||||
"message": "Failed to get the cloud ID from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9006, "JiraGetCloudIDNoResourcesError"): {
|
||||
"message": "No resources were found in Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9007, "JiraGetCloudIDResponseError"): {
|
||||
"message": "Failed to get the cloud ID from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9008, "JiraRefreshTokenResponseError"): {
|
||||
"message": "Failed to refresh the access token, response code did not match 200.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9009, "JiraRefreshTokenError"): {
|
||||
"message": "Failed to refresh the access token.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9010, "JiraGetAccessTokenError"): {
|
||||
"message": "Failed to get the access token.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9011, "JiraGetAuthResponseError"): {
|
||||
"message": "Failed to authenticate with Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9012, "JiraGetProjectsResponseError"): {
|
||||
"message": "Failed to get projects from Jira, response code did not match 200.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9013, "JiraSendFindingsResponseError"): {
|
||||
"message": "Failed to send findings to Jira, response code did not match 201.",
|
||||
"remediation": "Please check the finding format and try again.",
|
||||
},
|
||||
(9014, "JiraGetAvailableIssueTypesError"): {
|
||||
"message": "Failed to get available issue types from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9015, "JiraGetAvailableIssueTypesResponseError"): {
|
||||
"message": "Failed to get available issue types from Jira, response code did not match 200.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9016, "JiraInvalidIssueTypeError"): {
|
||||
"message": "The issue type is invalid.",
|
||||
"remediation": "Please check the issue type and try again.",
|
||||
},
|
||||
(9017, "JiraNoTokenError"): {
|
||||
"message": "No token was found.",
|
||||
"remediation": "Make sure the token is set when using the Jira integration.",
|
||||
},
|
||||
(9018, "JiraInvalidProjectKeyError"): {
|
||||
"message": "The project key is invalid.",
|
||||
"remediation": "Please check the project key and try again.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
module = "Jira"
|
||||
error_info = self.JIRA_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code=code,
|
||||
source=module,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
)
|
||||
|
||||
|
||||
class JiraNoProjectsError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraAuthenticationError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraTestConnectionError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9002, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraCreateIssueError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9003, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetProjectsError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9004, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetCloudIDError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9005, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetCloudIDNoResourcesError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9006, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetCloudIDResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9007, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraRefreshTokenResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9008, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraRefreshTokenError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9009, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAccessTokenError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9010, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAuthResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9011, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetProjectsResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9012, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraSendFindingsResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9013, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAvailableIssueTypesError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9014, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAvailableIssueTypesResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9015, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraInvalidIssueTypeError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9016, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraNoTokenError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9017, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraInvalidProjectKeyError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9018, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -185,7 +185,7 @@ class Slack:
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
"url": "https://goto.prowler.com/slack",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,6 +40,9 @@ def display_summary_table(
|
||||
elif provider.type == "kubernetes":
|
||||
entity_type = "Context"
|
||||
audited_entities = provider.identity.context
|
||||
elif provider.type == "microsoft365":
|
||||
entity_type = "Tenant Domain"
|
||||
audited_entities = provider.identity.tenant_domain
|
||||
|
||||
# Check if there are findings and that they are not all MANUAL
|
||||
if findings and not all(finding.status == "MANUAL" for finding in findings):
|
||||
|
||||
@@ -1262,7 +1262,9 @@ class AwsProvider(Provider):
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
if raise_on_exception:
|
||||
raise error
|
||||
return Connection(error=error)
|
||||
|
||||
@staticmethod
|
||||
def create_sts_session(
|
||||
|
||||
@@ -1270,6 +1270,7 @@
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -1280,6 +1281,7 @@
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -1294,6 +1296,7 @@
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -1304,6 +1307,7 @@
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -1318,6 +1322,7 @@
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -1328,6 +1333,7 @@
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -3288,6 +3294,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4390,6 +4397,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7615,7 +7623,6 @@
|
||||
"opsworkscm": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
@@ -9201,6 +9208,7 @@
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
@@ -9255,7 +9263,6 @@
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -9263,10 +9270,7 @@
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.aws.services.appsync.appsync_service import AppSync
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
appsync_client = AppSync(Provider.get_global_provider())
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "appsync_field_level_logging_enabled",
|
||||
"CheckTitle": "AWS AppSync should have field-level logging enabled",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "appsync",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:appsync:{region}:{account-id}:apis/{api-id}",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsAppSyncGraphQLApi",
|
||||
"Description": "This control checks whether an AWS AppSync API (only GraphQL APIs since boto3 doesnt have a method to return other APIs) field-level logging turned on. The control fails if the field resolver log level is set to None.",
|
||||
"Risk": "Without field-level logging enabled, it's difficult to monitor, troubleshoot, and optimize GraphQL API queries effectively.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/appsync-logging-enabled.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws appsync update-graphql-api --api-id <api-id> --log-config fieldLogLevel=<fieldLoggingLevel>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/appsync-controls.html#appsync-2",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable field-level logging for your AWS AppSync API to monitor and troubleshoot GraphQL queries effectively.",
|
||||
"Url": "https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#setup-and-configuration"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.appsync.appsync_client import appsync_client
|
||||
|
||||
|
||||
class appsync_field_level_logging_enabled(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
# Check only GraphQL APIs because boto3 does not have a method to get other types of AppSync APIs (list_apis is not working)
|
||||
for api in appsync_client.graphql_apis.values():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = api.region
|
||||
report.resource_id = api.id
|
||||
report.resource_arn = api.arn
|
||||
report.resource_tags = api.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"AppSync API {api.name} has field log level enabled."
|
||||
)
|
||||
if api.field_log_level != "ALL" and api.field_log_level != "ERROR":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"AppSync API {api.name} does not have field log level enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "appsync_graphql_api_no_api_key_authentication",
|
||||
"CheckTitle": "AWS AppSync GraphQL APIs should not be authenticated with API keys",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "appsync",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:appsync:{region}:{account-id}:apis/{api-id}",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsAppSyncGraphQLApi",
|
||||
"Description": "This control checks whether your application uses an API key to interact with an AWS AppSync GraphQL API. The control fails if an AWS AppSync GraphQL API is authenticated with an API key.",
|
||||
"Risk": "API keys in AppSync can expose applications to unauthorized access if compromised. Avoiding API keys helps reduce the risk of unintended access.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/appsync-authorization-check.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws appsync update-graphql-api --api-id <api-id> --authentication-type <authentication-type>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/appsync-controls.html#appsync-5",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use authentication methods other than API keys for AWS AppSync GraphQL APIs, such as AWS_IAM or Amazon Cognito.",
|
||||
"Url": "https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"trustboundaries"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.appsync.appsync_client import appsync_client
|
||||
|
||||
|
||||
class appsync_graphql_api_no_api_key_authentication(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for api in appsync_client.graphql_apis.values():
|
||||
if api.type == "GRAPHQL":
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = api.region
|
||||
report.resource_id = api.id
|
||||
report.resource_arn = api.arn
|
||||
report.resource_tags = api.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"AppSync GraphQL API {api.name} is not using an API KEY for authentication."
|
||||
if api.authentication_type == "API_KEY":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"AppSync GraphQL API {api.name} is using an API KEY for authentication."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
|
||||
class AppSync(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.graphql_apis = {}
|
||||
self.__threading_call__(self._list_graphql_apis)
|
||||
|
||||
def _list_graphql_apis(self, regional_client):
|
||||
logger.info("AppSync - Describing APIs...")
|
||||
try:
|
||||
list_graphql_apis_paginator = regional_client.get_paginator(
|
||||
"list_graphql_apis"
|
||||
)
|
||||
for page in list_graphql_apis_paginator.paginate():
|
||||
for api in page["graphqlApis"]:
|
||||
api_arn = api["arn"]
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(
|
||||
api_arn,
|
||||
self.audit_resources,
|
||||
)
|
||||
):
|
||||
self.graphql_apis[api_arn] = GraphqlApi(
|
||||
id=api["apiId"],
|
||||
name=api["name"],
|
||||
arn=api_arn,
|
||||
region=regional_client.region,
|
||||
type=api.get("apiType", "GRAPHQL"),
|
||||
field_log_level=api.get("logConfig", {}).get(
|
||||
"fieldLogLevel", ""
|
||||
),
|
||||
authentication_type=api.get(
|
||||
"authenticationType", "API_KEY"
|
||||
),
|
||||
tags=[api.get("tags", {})],
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class GraphqlApi(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
arn: str
|
||||
region: str
|
||||
type: str
|
||||
field_log_level: str
|
||||
authentication_type: str
|
||||
tags: Optional[list] = []
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-add-availability-zone.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "dms_endpoint_redis_in_transit_encryption_enabled",
|
||||
"CheckTitle": "Check if DMS endpoints for Redis OSS are encrypted in transit.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "dms",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:dms:region:account-id:endpoint/endpoint-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsDmsEndpoint",
|
||||
"Description": "This control checks whether an AWS DMS endpoint for Redis OSS is configured with a TLS connection. The control fails if the endpoint doesn't have TLS enabled.",
|
||||
"Risk": "Without TLS, data transmitted between databases may be vulnerable to interception or eavesdropping, increasing the risk of data breaches and other security incidents.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Source.Redis.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws dms modify-endpoint --endpoint-arn <endpoint-arn> --redis-settings '{'SslSecurityProtocol': 'ssl-encryption'}'",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-12",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable TLS for DMS endpoints for Redis OSS to ensure encrypted communication during data migration.",
|
||||
"Url": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.Redis.html#CHAP_Target.Redis.EndpointSettings"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.dms.dms_client import dms_client
|
||||
|
||||
|
||||
class dms_endpoint_redis_in_transit_encryption_enabled(Check):
|
||||
"""
|
||||
Check if AWS DMS Endpoints for Redis OSS have TLS enabled.
|
||||
|
||||
This class verifies whether each AWS DMS Endpoint configured for Redis OSS is encrypted in transit
|
||||
by checking the `TlsEnabled` property in the endpoint's configuration. The check ensures that
|
||||
TLS is enabled to secure data in transit, preventing unauthorized access and ensuring data integrity.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""
|
||||
Execute the DMS Redis TLS enabled check.
|
||||
|
||||
Iterates over all DMS Endpoints and generates a report indicating whether
|
||||
each Redis OSS endpoint is encrypted in transit.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of report objects with the results of the check.
|
||||
"""
|
||||
findings = []
|
||||
for endpoint_arn, endpoint in dms_client.endpoints.items():
|
||||
if endpoint.engine_name == "redis":
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = endpoint.id
|
||||
report.resource_arn = endpoint_arn
|
||||
report.region = endpoint.region
|
||||
report.resource_tags = endpoint.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Endpoint {endpoint.id} for Redis OSS is not encrypted in transit."
|
||||
if endpoint.redis_ssl_protocol == "ssl-encryption":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"DMS Endpoint {endpoint.id} for Redis OSS is encrypted in transit."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DMS/multi-az.html#"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "dms_replication_task_source_logging_enabled",
|
||||
"CheckTitle": "Check if DMS replication tasks for the source database have logging enabled.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "dms",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:dms:region:account-id:task/task-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsDmsReplicationTask",
|
||||
"Description": "This control checks whether logging is enabled with the minimum severity level of LOGGER_SEVERITY_DEFAULT for DMS replication tasks SOURCE_CAPTURE and SOURCE_UNLOAD. The control fails if logging isn't enabled for these tasks or if the minimum severity level is less than LOGGER_SEVERITY_DEFAULT.",
|
||||
"Risk": "Without logging enabled, issues in data migration may go undetected, affecting the integrity and compliance of replicated data.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Monitoring.html#CHAP_Monitoring.ManagingLogs",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws dms modify-replication-task --replication-task-arn <task-arn> --task-settings '{\"Logging\":{\"EnableLogging\":true,\"LogComponents\":[{\"Id\":\"SOURCE_CAPTURE\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"},{\"Id\":\"SOURCE_UNLOAD\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"}]}}'",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-8",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable logging for source database DMS replication tasks with a minimum severity level of LOGGER_SEVERITY_DEFAULT.",
|
||||
"Url": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Tasks.CustomizingTasks.TaskSettings.Logging.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.dms.dms_client import dms_client
|
||||
|
||||
|
||||
class dms_replication_task_source_logging_enabled(Check):
|
||||
"""
|
||||
Check if AWS DMS replication tasks have logging enabled with the required
|
||||
logging components and severity levels.
|
||||
|
||||
This class verifies that each DMS replication task has logging enabled
|
||||
and that the components SOURCE_CAPTURE and SOURCE_UNLOAD are configured with
|
||||
at least LOGGER_SEVERITY_DEFAULT severity level. If either component is missing
|
||||
or does not meet the minimum severity requirement, the check will fail.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""
|
||||
Execute the DMS replication task logging requirements check.
|
||||
|
||||
Iterates over all DMS replication tasks and generates a report indicating
|
||||
whether each task has logging enabled and meets the logging requirements
|
||||
for SOURCE_CAPTURE and SOURCE_UNLOAD components.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of report objects with the results of the check.
|
||||
"""
|
||||
MINIMUM_SEVERITY_LEVELS = [
|
||||
"LOGGER_SEVERITY_DEFAULT",
|
||||
"LOGGER_SEVERITY_DEBUG",
|
||||
"LOGGER_SEVERITY_DETAILED_DEBUG",
|
||||
]
|
||||
findings = []
|
||||
for (
|
||||
replication_task_arn,
|
||||
replication_task,
|
||||
) in dms_client.replication_tasks.items():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = replication_task.id
|
||||
report.resource_arn = replication_task_arn
|
||||
report.region = replication_task.region
|
||||
report.resource_tags = replication_task.tags
|
||||
|
||||
if not replication_task.logging_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not have logging enabled for source events."
|
||||
else:
|
||||
missing_components = []
|
||||
source_capture_compliant = False
|
||||
source_unload_compliant = False
|
||||
|
||||
for component in replication_task.log_components:
|
||||
if (
|
||||
component["Id"] == "SOURCE_CAPTURE"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_capture_compliant = True
|
||||
elif (
|
||||
component["Id"] == "SOURCE_UNLOAD"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_unload_compliant = True
|
||||
|
||||
if not source_capture_compliant:
|
||||
missing_components.append("Source Capture")
|
||||
if not source_unload_compliant:
|
||||
missing_components.append("Source Unload")
|
||||
|
||||
if source_capture_compliant and source_unload_compliant:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} has logging enabled with the minimum severity level in source events."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not meet the minimum severity level of logging in {' and '.join(missing_components)} events."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "dms_replication_task_target_logging_enabled",
|
||||
"CheckTitle": "Check if DMS replication tasks for the target database have logging enabled.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "dms",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:dms:region:account-id:task/task-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsDmsReplicationTask",
|
||||
"Description": "This control checks whether logging is enabled with the minimum severity level of LOGGER_SEVERITY_DEFAULT for DMS replication tasks TARGET_APPLY and TARGET_LOAD. The control fails if logging isn't enabled for these tasks or if the minimum severity level is less than LOGGER_SEVERITY_DEFAULT.",
|
||||
"Risk": "Without logging enabled, issues in data migration may go undetected, affecting the integrity and compliance of replicated data.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Monitoring.html#CHAP_Monitoring.ManagingLogs",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws dms modify-replication-task --replication-task-arn <task-arn> --task-settings '{\"Logging\":{\"EnableLogging\":true,\"LogComponents\":[{\"Id\":\"TARGET_APPLY\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"},{\"Id\":\"TARGET_LOAD\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"}]}}'",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-7",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable logging for target database DMS replication tasks with a minimum severity level of LOGGER_SEVERITY_DEFAULT.",
|
||||
"Url": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Tasks.CustomizingTasks.TaskSettings.Logging.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.dms.dms_client import dms_client
|
||||
|
||||
|
||||
class dms_replication_task_target_logging_enabled(Check):
|
||||
"""
|
||||
Check if AWS DMS replication tasks have logging enabled with the required
|
||||
logging components and severity levels.
|
||||
|
||||
This class verifies that each DMS replication task has logging enabled
|
||||
and that the components TARGET_APPLY and TARGET_LOAD are configured with
|
||||
at least LOGGER_SEVERITY_DEFAULT severity level. If either component is missing
|
||||
or does not meet the minimum severity requirement, the check will fail.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""
|
||||
Execute the DMS replication task logging requirements check.
|
||||
|
||||
Iterates over all DMS replication tasks and generates a report indicating
|
||||
whether each task has logging enabled and meets the logging requirements
|
||||
for TARGET_APPLY and TARGET_LOAD components.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of report objects with the results of the check.
|
||||
"""
|
||||
MINIMUM_SEVERITY_LEVELS = [
|
||||
"LOGGER_SEVERITY_DEFAULT",
|
||||
"LOGGER_SEVERITY_DEBUG",
|
||||
"LOGGER_SEVERITY_DETAILED_DEBUG",
|
||||
]
|
||||
findings = []
|
||||
for (
|
||||
replication_task_arn,
|
||||
replication_task,
|
||||
) in dms_client.replication_tasks.items():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = replication_task.id
|
||||
report.resource_arn = replication_task_arn
|
||||
report.region = replication_task.region
|
||||
report.resource_tags = replication_task.tags
|
||||
|
||||
if not replication_task.logging_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not have logging enabled for target events."
|
||||
else:
|
||||
missing_components = []
|
||||
source_capture_compliant = False
|
||||
source_unload_compliant = False
|
||||
|
||||
for component in replication_task.log_components:
|
||||
if (
|
||||
component["Id"] == "TARGET_APPLY"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_capture_compliant = True
|
||||
elif (
|
||||
component["Id"] == "TARGET_LOAD"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_unload_compliant = True
|
||||
|
||||
if not source_capture_compliant:
|
||||
missing_components.append("Target Apply")
|
||||
if not source_unload_compliant:
|
||||
missing_components.append("Target Load")
|
||||
|
||||
if source_capture_compliant and source_unload_compliant:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} has logging enabled with the minimum severity level in target events."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not meet the minimum severity level of logging in {' and '.join(missing_components)} events."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -13,10 +14,14 @@ class DMS(AWSService):
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.instances = []
|
||||
self.endpoints = {}
|
||||
self.replication_tasks = {}
|
||||
self.__threading_call__(self._describe_replication_instances)
|
||||
self.__threading_call__(self._list_tags, self.instances)
|
||||
self.__threading_call__(self._describe_endpoints)
|
||||
self.__threading_call__(self._describe_replication_tasks)
|
||||
self.__threading_call__(self._list_tags, self.endpoints.values())
|
||||
self.__threading_call__(self._describe_replication_tasks)
|
||||
self.__threading_call__(self._list_tags, self.replication_tasks.values())
|
||||
|
||||
def _describe_replication_instances(self, regional_client):
|
||||
logger.info("DMS - Describing DMS Replication Instances...")
|
||||
@@ -71,6 +76,9 @@ class DMS(AWSService):
|
||||
id=endpoint["EndpointIdentifier"],
|
||||
region=regional_client.region,
|
||||
ssl_mode=endpoint.get("SslMode", False),
|
||||
redis_ssl_protocol=endpoint.get("RedisSettings", {}).get(
|
||||
"SslSecurityProtocol", "plaintext"
|
||||
),
|
||||
mongodb_auth_type=endpoint.get("MongoDbSettings", {}).get(
|
||||
"AuthType", "no"
|
||||
),
|
||||
@@ -84,6 +92,37 @@ class DMS(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_replication_tasks(self, regional_client):
|
||||
logger.info("DMS - Describing DMS Replication Tasks for Logging Settings...")
|
||||
try:
|
||||
paginator = regional_client.get_paginator("describe_replication_tasks")
|
||||
for page in paginator.paginate():
|
||||
for task in page["ReplicationTasks"]:
|
||||
arn = task["ReplicationTaskArn"]
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(arn, self.audit_resources)
|
||||
):
|
||||
task_settings = json.loads(
|
||||
task.get("ReplicationTaskSettings", "")
|
||||
)
|
||||
self.replication_tasks[arn] = ReplicationTasks(
|
||||
arn=arn,
|
||||
id=task["ReplicationTaskIdentifier"],
|
||||
region=regional_client.region,
|
||||
source_endpoint_arn=task["SourceEndpointArn"],
|
||||
target_endpoint_arn=task["TargetEndpointArn"],
|
||||
logging_enabled=task_settings.get("Logging", {}).get(
|
||||
"EnableLogging", False
|
||||
),
|
||||
log_components=task_settings.get("Logging", {}).get(
|
||||
"LogComponents", []
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags(self, resource: any):
|
||||
try:
|
||||
resource.tags = self.regional_clients[
|
||||
@@ -100,10 +139,11 @@ class Endpoint(BaseModel):
|
||||
id: str
|
||||
region: str
|
||||
ssl_mode: str
|
||||
tags: Optional[list]
|
||||
redis_ssl_protocol: str
|
||||
mongodb_auth_type: str
|
||||
neptune_iam_auth_enabled: bool = False
|
||||
engine_name: str
|
||||
tags: Optional[list]
|
||||
|
||||
|
||||
class RepInstance(BaseModel):
|
||||
@@ -117,3 +157,14 @@ class RepInstance(BaseModel):
|
||||
multi_az: bool
|
||||
region: str
|
||||
tags: Optional[list] = []
|
||||
|
||||
|
||||
class ReplicationTasks(BaseModel):
|
||||
arn: str
|
||||
id: str
|
||||
region: str
|
||||
source_endpoint_arn: str
|
||||
target_endpoint_arn: str
|
||||
logging_enabled: bool = False
|
||||
log_components: list[dict] = []
|
||||
tags: Optional[list] = []
|
||||
|
||||
+30
-26
@@ -17,34 +17,38 @@ class ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports(Check
|
||||
and vpc_client.vpcs[security_group.vpc_id].in_use
|
||||
and len(security_group.network_interfaces) > 0
|
||||
):
|
||||
check_ports = ec2_client.audit_config.get(
|
||||
"ec2_high_risk_ports",
|
||||
[25, 110, 135, 143, 445, 3000, 4333, 5000, 5500, 8080, 8088],
|
||||
)
|
||||
for port in check_ports:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = security_group.region
|
||||
report.resource_details = security_group.name
|
||||
report.resource_id = security_group.id
|
||||
report.resource_arn = security_group_arn
|
||||
report.resource_tags = security_group.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Security group {security_group.name} ({security_group.id}) does not have port {port} open to the Internet."
|
||||
# only proceed if check "..._to_all_ports" did not run or did not FAIL to avoid to report open ports twice
|
||||
if not ec2_client.is_failed_check(
|
||||
ec2_securitygroup_allow_ingress_from_internet_to_all_ports.__name__,
|
||||
security_group_arn,
|
||||
):
|
||||
# Loop through every security group's ingress rule and check it
|
||||
for ingress_rule in security_group.ingress_rules:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = security_group.region
|
||||
report.resource_details = security_group.name
|
||||
report.resource_id = security_group.id
|
||||
report.resource_arn = security_group_arn
|
||||
report.resource_tags = security_group.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Security group {security_group.name} ({security_group.id}) does not have any high-risk port open to the Internet."
|
||||
# only proceed if check "..._to_all_ports" did not run or did not FAIL to avoid to report open ports twice
|
||||
if not ec2_client.is_failed_check(
|
||||
ec2_securitygroup_allow_ingress_from_internet_to_all_ports.__name__,
|
||||
security_group_arn,
|
||||
):
|
||||
check_ports = ec2_client.audit_config.get(
|
||||
"ec2_high_risk_ports",
|
||||
[25, 110, 135, 143, 445, 3000, 4333, 5000, 5500, 8080, 8088],
|
||||
)
|
||||
# Loop through every security group's ingress rule and check it
|
||||
open_ports = []
|
||||
for ingress_rule in security_group.ingress_rules:
|
||||
for port in check_ports:
|
||||
if check_security_group(
|
||||
ingress_rule, "tcp", [port], any_address=True
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Security group {security_group.name} ({security_group.id}) has port {port} (high risk port) open to the Internet."
|
||||
break
|
||||
else:
|
||||
report.status_extended = f"Security group {security_group.name} ({security_group.id}) has all ports open to the Internet and therefore was not checked against port {port}."
|
||||
findings.append(report)
|
||||
open_ports.append(port)
|
||||
|
||||
if open_ports:
|
||||
report.status = "FAIL"
|
||||
open_ports_str = ", ".join(map(str, open_ports))
|
||||
report.status_extended = f"Security group {security_group.name} ({security_group.id}) has the following high-risk ports open to the Internet: {open_ports_str}."
|
||||
else:
|
||||
report.status_extended = f"Security group {security_group.name} ({security_group.id}) has all ports open to the Internet and therefore was not checked against high-risk ports."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://redis.io/blog/highly-available-in-memory-cloud-datastores/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/ElastiCache/elasticache-multi-az.html#"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-disable-crosszone-lb.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-subnets.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "glue_etl_jobs_logging_enabled",
|
||||
"CheckTitle": "Check if Glue ETL Jobs have logging enabled.",
|
||||
"CheckTitle": "[DEPRECATED] Check if Glue ETL Jobs have logging enabled.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
|
||||
],
|
||||
@@ -10,7 +10,7 @@
|
||||
"ResourceIdTemplate": "arn:partition:glue:region:account-id:job/job-name",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsGlueJob",
|
||||
"Description": "Ensure that Glue ETL Jobs have CloudWatch logs enabled.",
|
||||
"Description": "[DEPRECATED] Ensure that Glue ETL Jobs have CloudWatch logs enabled.",
|
||||
"Risk": "Without logging enabled, AWS Glue jobs lack visibility into job activities and failures, making it difficult to detect unauthorized access, troubleshoot issues, and ensure compliance. This may result in untracked security incidents or operational issues that affect data processing.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/monitor-continuous-logging.html",
|
||||
"Remediation": {
|
||||
@@ -28,5 +28,5 @@
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
"Notes": "This check is being removed since logs for all AWS Glue jobs are now always sent to Amazon CloudWatch."
|
||||
}
|
||||
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/amazon-mq/latest/developer-guide/rabbitmq-broker-architecture.html#rabbitmq-broker-architecture-cluster"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "mq_broker_not_publicly_accessible",
|
||||
"CheckTitle": "MQ brokers should not be publicly accessible.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
|
||||
],
|
||||
"ServiceName": "mq",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:mq:region:account-id:broker:broker-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsAmazonMQBroker",
|
||||
"Description": "Brokers created without public accessibility can't be accessed from outside of your VPC. This greatly reduces your broker's susceptibility to Distributed Denial of Service (DDoS) attacks from the public internet.",
|
||||
"Risk": "Public Amazon MQ brokers can be accessed directly, outside of a Virtual Private Cloud (VPC), therefore every machine on the Internet can reach your brokers through their public endpoints and this can increase the opportunity for malicious activity such as cross-site scripting (XSS) and clickjacking attacks. ",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/amazon-mq/latest/developer-guide/using-amazon-mq-securely.html#prefer-brokers-without-public-accessibility",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/MQ/publicly-accessible.html#",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Ensure that the Amazon MQ brokers provisioned in your AWS account are not publicly accessible from the Internet in order to avoid exposing sensitive data and minimize security risks.",
|
||||
"Url": "https://docs.aws.amazon.com/amazon-mq/latest/developer-guide/using-amazon-mq-securely.html#prefer-brokers-without-public-accessibility"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.mq.mq_client import mq_client
|
||||
|
||||
|
||||
class mq_broker_not_publicly_accessible(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for broker in mq_client.brokers.values():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = broker.region
|
||||
report.resource_id = broker.id
|
||||
report.resource_arn = broker.arn
|
||||
report.resource_tags = broker.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"MQ Broker {broker.name} is publicly accessible."
|
||||
|
||||
if not broker.publicly_accessible:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"MQ Broker {broker.name} is not publicly accessible."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -56,6 +56,9 @@ class MQ(AWSService):
|
||||
broker.audit_logging_enabled = describe_broker.get("Logs", {}).get(
|
||||
"Audit", False
|
||||
)
|
||||
broker.publicly_accessible = describe_broker.get(
|
||||
"PubliclyAccessible", False
|
||||
)
|
||||
broker.tags = [describe_broker.get("Tags", {})]
|
||||
|
||||
except Exception as error:
|
||||
@@ -87,6 +90,7 @@ class Broker(BaseModel):
|
||||
id: str
|
||||
region: str
|
||||
auto_minor_version_upgrade: bool = Field(default=False)
|
||||
publicly_accessible: bool = Field(default=False)
|
||||
general_logging_enabled: bool = Field(default=False)
|
||||
audit_logging_enabled: bool = Field(default=False)
|
||||
engine_type: EngineType = EngineType.ACTIVEMQ
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/neptune-controls.html#neptune-9"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
"Url": "https://aws.amazon.com/es/blogs/networking-and-content-delivery/deployment-models-for-aws-network-firewall/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://aws.amazon.com/rds/features/multi-az/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "rds_cluster_protected_by_backup_plan",
|
||||
"CheckTitle": "Check if RDS clusters are protected by a backup plan.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks, AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "rds",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsRdsDbInstance",
|
||||
"Description": "Check if RDS clusters are protected by a backup plan.",
|
||||
"Risk": "Without a backup plan, RDS clusters are vulnerable to data loss, accidental deletion, or corruption. This could lead to significant operational disruptions or loss of critical data.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws backup create-backup-plan --backup-plan , aws backup tag-resource --resource-arn <rds-cluster-arn> --tags Key=backup,Value=true",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-26",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Create a backup plan for the RDS cluster to protect it from data loss, accidental deletion, or corruption.",
|
||||
"Url": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.backup.backup_client import backup_client
|
||||
from prowler.providers.aws.services.rds.rds_client import rds_client
|
||||
|
||||
|
||||
class rds_cluster_protected_by_backup_plan(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for db_cluster_arn, db_cluster in rds_client.db_clusters.items():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = db_cluster.region
|
||||
report.resource_id = db_cluster.id
|
||||
report.resource_arn = db_cluster_arn
|
||||
report.resource_tags = db_cluster.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"RDS Cluster {db_cluster.id} is not protected by a backup plan."
|
||||
)
|
||||
|
||||
if (
|
||||
db_cluster_arn in backup_client.protected_resources
|
||||
or f"arn:{rds_client.audited_partition}:rds:*:*:cluster:*"
|
||||
in backup_client.protected_resources
|
||||
or "*" in backup_client.protected_resources
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"RDS Cluster {db_cluster.id} is protected by a backup plan."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://aws.amazon.com/rds/features/multi-az/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from prowler.providers.aws.services.servicecatalog.servicecatalog_service import (
|
||||
ServiceCatalog,
|
||||
)
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
servicecatalog_client = ServiceCatalog(Provider.get_global_provider())
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "servicecatalog_portfolio_shared_within_organization_only",
|
||||
"CheckTitle": "Service Catalog portfolios should be shared within an AWS organization only",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "servicecatalog",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:servicecatalog:{region}:{account-id}:portfolio/{portfolio-id}",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsServiceCatalogPortfolio",
|
||||
"Description": "This control checks whether AWS Service Catalog shares portfolios within an organization when the integration with AWS Organizations is enabled. The control fails if portfolios aren't shared within an organization.",
|
||||
"Risk": "Sharing Service Catalog portfolios outside of an organization may result in access granted to unintended AWS accounts, potentially exposing sensitive resources.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/servicecatalog/latest/adminguide/catalogs_portfolios_sharing.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws servicecatalog create-portfolio-share --portfolio-id <portfolio-id> --organization-ids <org-id>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/servicecatalog/latest/adminguide/catalogs_portfolios_sharing.html",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Configure AWS Service Catalog to share portfolios only within your AWS Organization for more secure access management.",
|
||||
"Url": "https://docs.aws.amazon.com/servicecatalog/latest/adminguide/catalogs_portfolios_sharing.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"trustboundaries"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.organizations.organizations_client import (
|
||||
organizations_client,
|
||||
)
|
||||
from prowler.providers.aws.services.servicecatalog.servicecatalog_client import (
|
||||
servicecatalog_client,
|
||||
)
|
||||
|
||||
|
||||
class servicecatalog_portfolio_shared_within_organization_only(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for org in organizations_client.organizations:
|
||||
if org.status == "ACTIVE":
|
||||
for portfolio in servicecatalog_client.portfolios.values():
|
||||
if portfolio.shares is not None:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = portfolio.region
|
||||
report.resource_id = portfolio.id
|
||||
report.resource_arn = portfolio.arn
|
||||
report.resource_tags = portfolio.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared within your AWS Organization."
|
||||
for portfolio_share in portfolio.shares:
|
||||
if portfolio_share.type == "ACCOUNT":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared with an account."
|
||||
break
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -0,0 +1,107 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
PORTFOLIO_SHARE_TYPES = [
|
||||
"ACCOUNT",
|
||||
"ORGANIZATION",
|
||||
"ORGANIZATIONAL_UNIT",
|
||||
"ORGANIZATION_MEMBER_ACCOUNT",
|
||||
]
|
||||
|
||||
|
||||
class ServiceCatalog(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.portfolios = {}
|
||||
self.__threading_call__(self._list_portfolios)
|
||||
self.__threading_call__(
|
||||
self._describe_portfolio_shares, self.portfolios.values()
|
||||
)
|
||||
self.__threading_call__(self._describe_portfolio, self.portfolios.values())
|
||||
|
||||
def _list_portfolios(self, regional_client):
|
||||
logger.info("ServiceCatalog - listing portfolios...")
|
||||
try:
|
||||
response = regional_client.list_portfolios()
|
||||
for portfolio in response["PortfolioDetails"]:
|
||||
portfolio_arn = portfolio["ARN"]
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(portfolio_arn, self.audit_resources)
|
||||
):
|
||||
self.portfolios[portfolio_arn] = Portfolio(
|
||||
arn=portfolio_arn,
|
||||
id=portfolio["Id"],
|
||||
name=portfolio["DisplayName"],
|
||||
region=regional_client.region,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_portfolio_shares(self, portfolio):
|
||||
try:
|
||||
logger.info("ServiceCatalog - describing portfolios shares...")
|
||||
regional_client = self.regional_clients[portfolio.region]
|
||||
for portfolio_type in PORTFOLIO_SHARE_TYPES:
|
||||
try:
|
||||
for share in regional_client.describe_portfolio_shares(
|
||||
PortfolioId=portfolio.id,
|
||||
Type=portfolio_type,
|
||||
).get("PortfolioShareDetails", []):
|
||||
portfolio_share = PortfolioShare(
|
||||
type=portfolio_type,
|
||||
accepted=share["Accepted"],
|
||||
)
|
||||
portfolio.shares.append(portfolio_share)
|
||||
except Exception as error:
|
||||
if error.response["Error"]["Code"] == "AccessDeniedException":
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
portfolio.shares = None
|
||||
else:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_portfolio(self, portfolio):
|
||||
try:
|
||||
logger.info("ServiceCatalog - describing portfolios...")
|
||||
try:
|
||||
regional_client = self.regional_clients[portfolio.region]
|
||||
portfolio.tags = regional_client.describe_portfolio(
|
||||
Id=portfolio.id,
|
||||
)["Tags"]
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class PortfolioShare(BaseModel):
|
||||
type: str
|
||||
accepted: bool
|
||||
|
||||
|
||||
class Portfolio(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
arn: str
|
||||
region: str
|
||||
shares: Optional[list[PortfolioShare]] = []
|
||||
tags: Optional[list] = []
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"redundancy"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -161,54 +161,61 @@ class Provider(ABC):
|
||||
if not isinstance(Provider._global, provider_class):
|
||||
if "aws" in provider_class_name.lower():
|
||||
provider_class(
|
||||
arguments.aws_retries_max_attempts,
|
||||
arguments.role,
|
||||
arguments.session_duration,
|
||||
arguments.external_id,
|
||||
arguments.role_session_name,
|
||||
arguments.mfa,
|
||||
arguments.profile,
|
||||
set(arguments.region) if arguments.region else None,
|
||||
arguments.organizations_role,
|
||||
arguments.scan_unused_services,
|
||||
arguments.resource_tag,
|
||||
arguments.resource_arn,
|
||||
arguments.config_file,
|
||||
arguments.mutelist_file,
|
||||
retries_max_attempts=arguments.aws_retries_max_attempts,
|
||||
role_arn=arguments.role,
|
||||
session_duration=arguments.session_duration,
|
||||
external_id=arguments.external_id,
|
||||
role_session_name=arguments.role_session_name,
|
||||
mfa=arguments.mfa,
|
||||
profile=arguments.profile,
|
||||
regions=set(arguments.region) if arguments.region else None,
|
||||
organizations_role_arn=arguments.organizations_role,
|
||||
scan_unused_services=arguments.scan_unused_services,
|
||||
resource_tags=arguments.resource_tag,
|
||||
resource_arn=arguments.resource_arn,
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
elif "azure" in provider_class_name.lower():
|
||||
provider_class(
|
||||
arguments.az_cli_auth,
|
||||
arguments.sp_env_auth,
|
||||
arguments.browser_auth,
|
||||
arguments.managed_identity_auth,
|
||||
arguments.tenant_id,
|
||||
arguments.azure_region,
|
||||
arguments.subscription_id,
|
||||
arguments.config_file,
|
||||
arguments.mutelist_file,
|
||||
az_cli_auth=arguments.az_cli_auth,
|
||||
sp_env_auth=arguments.sp_env_auth,
|
||||
browser_auth=arguments.browser_auth,
|
||||
managed_identity_auth=arguments.managed_identity_auth,
|
||||
tenant_id=arguments.tenant_id,
|
||||
region=arguments.azure_region,
|
||||
subscription_ids=arguments.subscription_id,
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
elif "gcp" in provider_class_name.lower():
|
||||
provider_class(
|
||||
arguments.organization_id,
|
||||
arguments.project_id,
|
||||
arguments.excluded_project_id,
|
||||
arguments.credentials_file,
|
||||
arguments.impersonate_service_account,
|
||||
arguments.list_project_id,
|
||||
arguments.config_file,
|
||||
arguments.mutelist_file,
|
||||
organization_id=arguments.organization_id,
|
||||
project_ids=arguments.project_id,
|
||||
excluded_project_ids=arguments.excluded_project_id,
|
||||
credentials_file=arguments.credentials_file,
|
||||
impersonate_service_account=arguments.impersonate_service_account,
|
||||
list_project_ids=arguments.list_project_id,
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
elif "kubernetes" in provider_class_name.lower():
|
||||
provider_class(
|
||||
arguments.kubeconfig_file,
|
||||
arguments.context,
|
||||
arguments.namespace,
|
||||
arguments.config_file,
|
||||
arguments.mutelist_file,
|
||||
kubeconfig_file=arguments.kubeconfig_file,
|
||||
context=arguments.context,
|
||||
namespace=arguments.namespace,
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
elif "microsoft365" in provider_class_name.lower():
|
||||
provider_class(
|
||||
app_env_auth=arguments.app_env_auth,
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -411,7 +411,7 @@ class GcpProvider(Provider):
|
||||
|
||||
@staticmethod
|
||||
def get_projects(
|
||||
credentials: Credentials, organization_id: str
|
||||
credentials: Credentials, organization_id: str = None
|
||||
) -> dict[str, GCPProject]:
|
||||
"""
|
||||
Get the projects accessible by the provided credentials. If an organization ID is provided, only the projects under that organization are returned.
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 2000 to 2999 are reserved for Microsoft365 exceptions
|
||||
class Microsoft365BaseException(ProwlerException):
|
||||
"""Base class for Microsoft365 Errors."""
|
||||
|
||||
AZURE_ERROR_CODES = {
|
||||
(2000, "Microsoft365EnvironmentVariableError"): {
|
||||
"message": "Microsoft365 environment variable error",
|
||||
"remediation": "Check the Microsoft365 environment variables and ensure they are properly set.",
|
||||
},
|
||||
(2001, "Microsoft365NoSubscriptionsError"): {
|
||||
"message": "No Microsoft365 subscriptions found",
|
||||
"remediation": "Check the Microsoft365 subscriptions and ensure they are properly set up.",
|
||||
},
|
||||
(2002, "Microsoft365SetUpIdentityError"): {
|
||||
"message": "Microsoft365 identity setup error related with credentials",
|
||||
"remediation": "Check credentials and ensure they are properly set up for Microsoft365 and the identity provider.",
|
||||
},
|
||||
(2003, "Microsoft365NoAuthenticationMethodError"): {
|
||||
"message": "No Microsoft365 authentication method found",
|
||||
"remediation": "Check that any authentication method is properly set up for Microsoft365.",
|
||||
},
|
||||
(2004, "Microsoft365BrowserAuthNoTenantIDError"): {
|
||||
"message": "Microsoft365 browser authentication error: no tenant ID found",
|
||||
"remediation": "To use browser authentication, ensure the tenant ID is properly set.",
|
||||
},
|
||||
(2005, "Microsoft365TenantIDNoBrowserAuthError"): {
|
||||
"message": "Microsoft365 tenant ID error: browser authentication not found",
|
||||
"remediation": "To use browser authentication, both the tenant ID and browser authentication must be properly set.",
|
||||
},
|
||||
(2006, "Microsoft365ArgumentTypeValidationError"): {
|
||||
"message": "Microsoft365 argument type validation error",
|
||||
"remediation": "Check the provided argument types specific to Microsoft365 and ensure they meet the required format.",
|
||||
},
|
||||
(2007, "Microsoft365SetUpRegionConfigError"): {
|
||||
"message": "Microsoft365 region configuration setup error",
|
||||
"remediation": "Check the Microsoft365 region configuration and ensure it is properly set up.",
|
||||
},
|
||||
(2008, "Microsoft365DefaultMicrosoft365CredentialError"): {
|
||||
"message": "Error in DefaultMicrosoft365Credential",
|
||||
"remediation": "Check that all the attributes are properly set up for the DefaultMicrosoft365Credential.",
|
||||
},
|
||||
(2009, "Microsoft365InteractiveBrowserCredentialError"): {
|
||||
"message": "Error retrieving InteractiveBrowserCredential",
|
||||
"remediation": "Check your browser and ensure that the tenant ID and browser authentication are properly set.",
|
||||
},
|
||||
(2010, "Microsoft365HTTPResponseError"): {
|
||||
"message": "Error in HTTP response from Microsoft365",
|
||||
"remediation": "",
|
||||
},
|
||||
(2011, "Microsoft365CredentialsUnavailableError"): {
|
||||
"message": "Error trying to configure Microsoft365 credentials because they are unavailable",
|
||||
"remediation": "Check the dictionary and ensure it is properly set up for Microsoft365 credentials. TENANT_ID, CLIENT_ID and CLIENT_SECRET are required.",
|
||||
},
|
||||
(2012, "Microsoft365GetTokenIdentityError"): {
|
||||
"message": "Error trying to get token from Microsoft365 Identity",
|
||||
"remediation": "Check the Microsoft365 Identity and ensure it is properly set up.",
|
||||
},
|
||||
(2013, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): {
|
||||
"message": "The provided credentials are not a tenant ID but a client ID and client secret",
|
||||
"remediation": "Tenant Id, Client Id and Client Secret are required for Microsoft365 credentials. Make sure you are using the correct credentials.",
|
||||
},
|
||||
(2014, "Microsoft365ClientAuthenticationError"): {
|
||||
"message": "Error in client authentication",
|
||||
"remediation": "Check the client authentication and ensure it is properly set up.",
|
||||
},
|
||||
(2015, "Microsoft365SetUpSessionError"): {
|
||||
"message": "Error setting up session",
|
||||
"remediation": "Check the session setup and ensure it is properly set up.",
|
||||
},
|
||||
(2016, "Microsoft365NotValidTenantIdError"): {
|
||||
"message": "The provided tenant ID is not valid",
|
||||
"remediation": "Check the tenant ID and ensure it is a valid ID.",
|
||||
},
|
||||
(2017, "Microsoft365NotValidClientIdError"): {
|
||||
"message": "The provided client ID is not valid",
|
||||
"remediation": "Check the client ID and ensure it is a valid ID.",
|
||||
},
|
||||
(2018, "Microsoft365NotValidClientSecretError"): {
|
||||
"message": "The provided client secret is not valid",
|
||||
"remediation": "Check the client secret and ensure it is a valid secret.",
|
||||
},
|
||||
(2019, "Microsoft365ConfigCredentialsError"): {
|
||||
"message": "Error in configuration of Microsoft365 credentials",
|
||||
"remediation": "Check the configuration of Microsoft365 credentials and ensure it is properly set up.",
|
||||
},
|
||||
(2020, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): {
|
||||
"message": "The provided client ID and client secret do not belong to the provided tenant ID",
|
||||
"remediation": "Check the client ID and client secret and ensure they belong to the provided tenant ID.",
|
||||
},
|
||||
(2021, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): {
|
||||
"message": "The provided tenant ID and client secret do not belong to the provided client ID",
|
||||
"remediation": "Check the tenant ID and client secret and ensure they belong to the provided client ID.",
|
||||
},
|
||||
(2022, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): {
|
||||
"message": "The provided tenant ID and client ID do not belong to the provided client secret",
|
||||
"remediation": "Check the tenant ID and client ID and ensure they belong to the provided client secret.",
|
||||
},
|
||||
(2023, "Microsoft365InvalidProviderIdError"): {
|
||||
"message": "The provided provider_id does not match with the available subscriptions",
|
||||
"remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
provider = "Microsoft365"
|
||||
error_info = self.AZURE_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code=code,
|
||||
source=provider,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365CredentialsError(Microsoft365BaseException):
|
||||
"""Base class for Microsoft365 credentials errors."""
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
super().__init__(code, file, original_exception, message)
|
||||
|
||||
|
||||
class Microsoft365EnvironmentVariableError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365NoSubscriptionsError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365SetUpIdentityError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2002, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365NoAuthenticationMethodError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2003, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365BrowserAuthNoTenantIDError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2004, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365TenantIDNoBrowserAuthError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2005, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365ArgumentTypeValidationError(Microsoft365BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2006, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365SetUpRegionConfigError(Microsoft365BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2007, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365DefaultMicrosoft365CredentialError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2008, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365InteractiveBrowserCredentialError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2009, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365HTTPResponseError(Microsoft365BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2010, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365CredentialsUnavailableError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2011, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365GetTokenIdentityError(Microsoft365BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2012, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365NotTenantIdButClientIdAndClienSecretError(
|
||||
Microsoft365CredentialsError
|
||||
):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2013, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365ClientAuthenticationError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2014, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365SetUpSessionError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2015, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365NotValidTenantIdError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2016, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365NotValidClientIdError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2017, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365NotValidClientSecretError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2018, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365ConfigCredentialsError(Microsoft365CredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2019, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError(
|
||||
Microsoft365CredentialsError
|
||||
):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2020, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError(
|
||||
Microsoft365CredentialsError
|
||||
):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2021, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError(
|
||||
Microsoft365CredentialsError
|
||||
):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2022, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365InvalidProviderIdError(Microsoft365BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
2023, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
from argparse import ArgumentTypeError
|
||||
|
||||
|
||||
def init_parser(self):
|
||||
"""Init the Microsoft365 Provider CLI parser"""
|
||||
microsoft365_parser = self.subparsers.add_parser(
|
||||
"microsoft365",
|
||||
parents=[self.common_providers_parser],
|
||||
help="Microsoft365 Provider",
|
||||
)
|
||||
# Authentication Modes
|
||||
microsoft365_auth_subparser = microsoft365_parser.add_argument_group(
|
||||
"Authentication Modes"
|
||||
)
|
||||
microsoft365_auth_modes_group = (
|
||||
microsoft365_auth_subparser.add_mutually_exclusive_group()
|
||||
)
|
||||
microsoft365_auth_modes_group.add_argument(
|
||||
"--app-env-auth",
|
||||
action="store_true",
|
||||
help="Use application environment variables authentication to log in against Microsoft 365",
|
||||
)
|
||||
# Regions
|
||||
microsoft365_regions_subparser = microsoft365_parser.add_argument_group("Regions")
|
||||
microsoft365_regions_subparser.add_argument(
|
||||
"--microsoft365-region",
|
||||
nargs="?",
|
||||
default="AzureCloud",
|
||||
type=validate_microsoft365_region,
|
||||
help="microsoft365 region from `az cloud list --output table`, by default AzureCloud",
|
||||
)
|
||||
|
||||
|
||||
def validate_microsoft365_region(region):
|
||||
"""validate_microsoft365_region validates if the region passed as argument is valid"""
|
||||
regions_allowed = [
|
||||
"AzureChinaCloud",
|
||||
"AzureUSGovernment",
|
||||
"AzureCloud",
|
||||
]
|
||||
if region not in regions_allowed:
|
||||
raise ArgumentTypeError(
|
||||
f"Region {region} not allowed, allowed regions are {' '.join(regions_allowed)}"
|
||||
)
|
||||
return region
|
||||
@@ -0,0 +1,17 @@
|
||||
from prowler.lib.check.models import Check_Report_Microsoft365
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
|
||||
|
||||
class Microsoft365Mutelist(Mutelist):
|
||||
def is_finding_muted(
|
||||
self,
|
||||
finding: Check_Report_Microsoft365,
|
||||
cluster: str,
|
||||
) -> bool:
|
||||
return self.is_muted(
|
||||
cluster,
|
||||
finding.check_metadata.CheckID,
|
||||
finding.resource_name,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
from azure.identity import AzureAuthorityHosts
|
||||
|
||||
AZURE_CHINA_CLOUD = "https://management.chinacloudapi.cn"
|
||||
AZURE_US_GOV_CLOUD = "https://management.usgovcloudapi.net"
|
||||
AZURE_GENERIC_CLOUD = "https://management.azure.com"
|
||||
|
||||
|
||||
def get_regions_config(region):
|
||||
allowed_regions = {
|
||||
"AzureCloud": {
|
||||
"authority": None,
|
||||
"base_url": AZURE_GENERIC_CLOUD,
|
||||
"credential_scopes": [AZURE_GENERIC_CLOUD + "/.default"],
|
||||
},
|
||||
"AzureChinaCloud": {
|
||||
"authority": AzureAuthorityHosts.AZURE_CHINA,
|
||||
"base_url": AZURE_CHINA_CLOUD,
|
||||
"credential_scopes": [AZURE_CHINA_CLOUD + "/.default"],
|
||||
},
|
||||
"AzureUSGovernment": {
|
||||
"authority": AzureAuthorityHosts.AZURE_GOVERNMENT,
|
||||
"base_url": AZURE_US_GOV_CLOUD,
|
||||
"credential_scopes": [AZURE_US_GOV_CLOUD + "/.default"],
|
||||
},
|
||||
}
|
||||
return allowed_regions[region]
|
||||
@@ -0,0 +1,33 @@
|
||||
from msgraph import GraphServiceClient
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider
|
||||
|
||||
|
||||
class Microsoft365Service:
|
||||
def __init__(
|
||||
self,
|
||||
provider: Microsoft365Provider,
|
||||
):
|
||||
self.clients = self.__set_clients__(
|
||||
provider.identity,
|
||||
provider.session,
|
||||
provider.region_config,
|
||||
)
|
||||
|
||||
self.locations = provider.locations
|
||||
self.audit_config = provider.audit_config
|
||||
self.fixer_config = provider.fixer_config
|
||||
|
||||
def __set_clients__(self, identity, session, region_config):
|
||||
clients = {}
|
||||
try:
|
||||
clients.update(
|
||||
{identity.tenant_domain: GraphServiceClient(credentials=session)}
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
else:
|
||||
return clients
|
||||
@@ -0,0 +1,455 @@
|
||||
import asyncio
|
||||
import os
|
||||
from argparse import ArgumentTypeError
|
||||
from os import getenv
|
||||
|
||||
import requests
|
||||
from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
|
||||
from azure.identity import ClientSecretCredential, DefaultAzureCredential
|
||||
from colorama import Fore, Style
|
||||
from msgraph import GraphServiceClient
|
||||
|
||||
from prowler.config.config import (
|
||||
default_config_file_path,
|
||||
get_default_mute_file_path,
|
||||
load_and_validate_config_file,
|
||||
)
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import print_boxes
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.microsoft365.exceptions.exceptions import (
|
||||
Microsoft365ArgumentTypeValidationError,
|
||||
Microsoft365CredentialsUnavailableError,
|
||||
Microsoft365EnvironmentVariableError,
|
||||
Microsoft365GetTokenIdentityError,
|
||||
Microsoft365HTTPResponseError,
|
||||
Microsoft365SetUpRegionConfigError,
|
||||
)
|
||||
from prowler.providers.microsoft365.lib.arguments.arguments import (
|
||||
validate_microsoft365_region,
|
||||
)
|
||||
from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist
|
||||
from prowler.providers.microsoft365.lib.regions.regions import get_regions_config
|
||||
from prowler.providers.microsoft365.models import (
|
||||
Microsoft365IdentityInfo,
|
||||
Microsoft365RegionConfig,
|
||||
)
|
||||
|
||||
|
||||
class Microsoft365Provider(Provider):
|
||||
"""
|
||||
Represents an Microsoft365 provider.
|
||||
|
||||
This class provides functionality to interact with the Microsoft365 resources.
|
||||
It handles authentication, region configuration, and provides access to various properties and methods
|
||||
related to the Microsoft365 provider.
|
||||
|
||||
Attributes:
|
||||
_type (str): The type of the provider, which is set to "microsoft365".
|
||||
_session (DefaultMicrosoft365Credential): The session object associated with the Microsoft365 provider.
|
||||
_identity (Microsoft365IdentityInfo): The identity information for the Microsoft365 provider.
|
||||
_audit_config (dict): The audit configuration for the Microsoft365 provider.
|
||||
_region_config (Microsoft365RegionConfig): The region configuration for the Microsoft365 provider.
|
||||
_locations (dict): A dictionary containing the available locations for the Microsoft365 provider.
|
||||
_mutelist (Microsoft365Mutelist): The mutelist object associated with the Microsoft365 provider.
|
||||
audit_metadata (Audit_Metadata): The audit metadata for the Microsoft365 provider.
|
||||
|
||||
Methods:
|
||||
__init__ -> Initializes the Microsoft365 provider.
|
||||
identity(self): Returns the identity of the Microsoft365 provider.
|
||||
type(self): Returns the type of the Microsoft365 provider.
|
||||
session(self): Returns the session object associated with the Microsoft365 provider.
|
||||
region_config(self): Returns the region configuration for the Microsoft365 provider.
|
||||
locations(self): Returns a list of available locations for the Microsoft365 provider.
|
||||
audit_config(self): Returns the audit configuration for the Microsoft365 provider.
|
||||
fixer_config(self): Returns the fixer configuration.
|
||||
output_options(self, options: tuple): Sets the output options for the Microsoft365 provider.
|
||||
mutelist(self) -> Microsoft365Mutelist: Returns the mutelist object associated with the Microsoft365 provider.
|
||||
validate_arguments(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id): Validates the authentication arguments for the Microsoft365 provider.
|
||||
setup_region_config(cls, region): Sets up the region configuration for the Microsoft365 provider.
|
||||
print_credentials(self): Prints the Microsoft365 credentials information.
|
||||
setup_session(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id, region_config): Set up the Microsoft365 session with the specified authentication method.
|
||||
"""
|
||||
|
||||
_type: str = "microsoft365"
|
||||
_session: DefaultAzureCredential
|
||||
_identity: Microsoft365IdentityInfo
|
||||
_audit_config: dict
|
||||
_region_config: Microsoft365RegionConfig
|
||||
_locations: dict
|
||||
_mutelist: Microsoft365Mutelist
|
||||
# TODO: this is not optional, enforce for all providers
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app_env_auth: bool = False,
|
||||
tenant_id: str = None,
|
||||
region: str = "AzureCloud",
|
||||
client_id: str = None,
|
||||
client_secret: str = None,
|
||||
config_content: dict = None,
|
||||
config_path: str = None,
|
||||
mutelist_path: str = None,
|
||||
mutelist_content: dict = None,
|
||||
fixer_config: dict = {},
|
||||
):
|
||||
"""
|
||||
Initializes the Microsoft365 provider.
|
||||
|
||||
Args:
|
||||
app_env_auth (bool): Flag indicating whether to use application authentication with environment variables.
|
||||
tenant_id (str): The Microsoft365 Active Directory tenant ID.
|
||||
region (str): The Microsoft365 region.
|
||||
client_id (str): The Microsoft365 client ID.
|
||||
client_secret (str): The Microsoft365 client secret.
|
||||
config_path (str): The path to the configuration file.
|
||||
config_content (dict): The configuration content.
|
||||
fixer_config (dict): The fixer configuration.
|
||||
mutelist_path (str): The path to the mutelist file.
|
||||
mutelist_content (dict): The mutelist content.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation.
|
||||
Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration.
|
||||
Microsoft365DefaultMicrosoft365CredentialError: If there is an error in retrieving the Microsoft365 credentials.
|
||||
Microsoft365InteractiveBrowserCredentialError: If there is an error in retrieving the Microsoft365 credentials using browser authentication.
|
||||
Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary.
|
||||
Microsoft365GetTokenIdentityError: If there is an error in getting the token from the Microsoft365 identity.
|
||||
Microsoft365HTTPResponseError: If there is an HTTP response error.
|
||||
"""
|
||||
logger.info("Setting Microsoft365 provider ...")
|
||||
|
||||
logger.info("Checking if any credentials mode is set ...")
|
||||
|
||||
logger.info("Checking if region is different than default one")
|
||||
self._region_config = self.setup_region_config(region)
|
||||
|
||||
# Set up the Microsoft365 session
|
||||
self._session = self.setup_session(
|
||||
app_env_auth,
|
||||
)
|
||||
|
||||
# Set up the identity
|
||||
self._identity = self.setup_identity(
|
||||
app_env_auth,
|
||||
)
|
||||
|
||||
# TODO: should we keep this here or within the identity?
|
||||
self._locations = self.get_locations(self.session)
|
||||
|
||||
# Audit Config
|
||||
if config_content:
|
||||
self._audit_config = config_content
|
||||
else:
|
||||
if not config_path:
|
||||
config_path = default_config_file_path
|
||||
self._audit_config = load_and_validate_config_file(self._type, config_path)
|
||||
|
||||
# Fixer Config
|
||||
self._fixer_config = fixer_config
|
||||
|
||||
# Mutelist
|
||||
if mutelist_content:
|
||||
self._mutelist = Microsoft365Mutelist(
|
||||
mutelist_content=mutelist_content,
|
||||
)
|
||||
else:
|
||||
if not mutelist_path:
|
||||
mutelist_path = get_default_mute_file_path(self.type)
|
||||
self._mutelist = Microsoft365Mutelist(
|
||||
mutelist_path=mutelist_path,
|
||||
)
|
||||
|
||||
Provider.set_global_provider(self)
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
"""Returns the identity of the Microsoft365 provider."""
|
||||
return self._identity
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Returns the type of the Microsoft365 provider."""
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""Returns the session object associated with the Microsoft365 provider."""
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def region_config(self):
|
||||
"""Returns the region configuration for the Microsoft365 provider."""
|
||||
return self._region_config
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
"""Returns a list of available locations for the Microsoft365 provider."""
|
||||
return self._locations
|
||||
|
||||
@property
|
||||
def audit_config(self):
|
||||
"""Returns the audit configuration for the Microsoft365 provider."""
|
||||
return self._audit_config
|
||||
|
||||
@property
|
||||
def fixer_config(self):
|
||||
"""Returns the fixer configuration."""
|
||||
return self._fixer_config
|
||||
|
||||
@property
|
||||
def mutelist(self) -> Microsoft365Mutelist:
|
||||
"""Mutelist object associated with this Microsoft365 provider."""
|
||||
return self._mutelist
|
||||
|
||||
@staticmethod
|
||||
def setup_region_config(region):
|
||||
"""
|
||||
Sets up the region configuration for the Microsoft365 provider.
|
||||
|
||||
Args:
|
||||
region (str): The name of the region.
|
||||
|
||||
Returns:
|
||||
Microsoft365RegionConfig: The region configuration object.
|
||||
|
||||
"""
|
||||
try:
|
||||
validate_microsoft365_region(region)
|
||||
config = get_regions_config(region)
|
||||
|
||||
return Microsoft365RegionConfig(
|
||||
name=region,
|
||||
authority=config["authority"],
|
||||
base_url=config["base_url"],
|
||||
credential_scopes=config["credential_scopes"],
|
||||
)
|
||||
except ArgumentTypeError as validation_error:
|
||||
logger.error(
|
||||
f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error}"
|
||||
)
|
||||
raise Microsoft365ArgumentTypeValidationError(
|
||||
file=os.path.basename(__file__),
|
||||
original_exception=validation_error,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise Microsoft365SetUpRegionConfigError(
|
||||
file=os.path.basename(__file__),
|
||||
original_exception=error,
|
||||
)
|
||||
|
||||
def print_credentials(self):
|
||||
"""Microsoft365 credentials information.
|
||||
|
||||
This method prints the Microsoft365 Tenant Domain, Microsoft365 Tenant ID, Microsoft365 Region,
|
||||
Microsoft365 Subscriptions, Microsoft365 Identity Type, and Microsoft365 Identity ID.
|
||||
|
||||
Args:
|
||||
None
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
printed_subscriptions = []
|
||||
for key, value in self._identity.subscriptions.items():
|
||||
intermediate = key + ": " + value
|
||||
printed_subscriptions.append(intermediate)
|
||||
report_lines = [
|
||||
f"Microsoft365 Region: {Fore.YELLOW}{self.region_config.name}{Style.RESET_ALL}",
|
||||
f"Microsoft365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Microsoft365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}",
|
||||
]
|
||||
report_title = (
|
||||
f"{Style.BRIGHT}Using the Azure credentials below:{Style.RESET_ALL}"
|
||||
)
|
||||
print_boxes(report_lines, report_title)
|
||||
|
||||
# TODO: setup_session or setup_credentials?
|
||||
# This should be setup_credentials, since it is setting up the credentials for the provider
|
||||
@staticmethod
|
||||
def setup_session(
|
||||
app_env_auth: bool,
|
||||
):
|
||||
"""Returns the Microsoft365 credentials object.
|
||||
|
||||
Set up the Microsoft365 session with the specified authentication method.
|
||||
|
||||
Args:
|
||||
app_env_auth (bool): Flag indicating whether to use application authentication with environment variables.
|
||||
|
||||
Returns:
|
||||
credentials: The Microsoft365 credentials object.
|
||||
|
||||
Raises:
|
||||
Exception: If failed to retrieve Microsoft365 credentials.
|
||||
|
||||
"""
|
||||
# Browser auth creds cannot be set with DefaultMicrosoft365Credentials()
|
||||
if app_env_auth:
|
||||
try:
|
||||
Microsoft365Provider.check_application_creds_env_vars()
|
||||
credentials = ClientSecretCredential(
|
||||
client_id=getenv("APP_CLIENT_ID"),
|
||||
tenant_id=getenv("APP_TENANT_ID"),
|
||||
client_secret=getenv("APP_CLIENT_SECRET"),
|
||||
)
|
||||
except (
|
||||
Microsoft365EnvironmentVariableError
|
||||
) as environment_credentials_error:
|
||||
logger.critical(
|
||||
f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}"
|
||||
)
|
||||
raise environment_credentials_error
|
||||
if not credentials:
|
||||
raise Microsoft365CredentialsUnavailableError(
|
||||
file=os.path.basename(__file__),
|
||||
message="Failed to retrieve Microsoft365 credentials.",
|
||||
)
|
||||
return credentials
|
||||
|
||||
@staticmethod
|
||||
def check_application_creds_env_vars():
|
||||
"""
|
||||
Checks the presence of required environment variables for application authentication against Azure.
|
||||
|
||||
This method checks for the presence of the following environment variables:
|
||||
- APP_CLIENT_ID: Microsoft365 client ID
|
||||
- APP_TENANT_ID: Microsoft365 tenant ID
|
||||
- APP_CLIENT_SECRET: Microsoft365 client secret
|
||||
|
||||
If any of the environment variables is missing, it logs a critical error and exits the program.
|
||||
"""
|
||||
logger.info(
|
||||
"Microsoft365 provider: checking service principal environment variables ..."
|
||||
)
|
||||
for env_var in ["APP_CLIENT_ID", "APP_TENANT_ID", "APP_CLIENT_SECRET"]:
|
||||
if not getenv(env_var):
|
||||
logger.critical(
|
||||
f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365"
|
||||
)
|
||||
raise Microsoft365EnvironmentVariableError(
|
||||
file=os.path.basename(__file__),
|
||||
message=f"Missing environment variable {env_var} required to authenticate.",
|
||||
)
|
||||
|
||||
def setup_identity(
|
||||
self,
|
||||
app_env_auth,
|
||||
):
|
||||
"""
|
||||
Sets up the identity for the Microsoft365 provider.
|
||||
|
||||
Args:
|
||||
app_env_auth (bool): Flag indicating if Service Principal environment authentication is used.
|
||||
|
||||
Returns:
|
||||
Microsoft365IdentityInfo: An instance of Microsoft365IdentityInfo containing the identity information.
|
||||
"""
|
||||
credentials = self.session
|
||||
# TODO: fill this object with real values not default and set to none
|
||||
identity = Microsoft365IdentityInfo()
|
||||
|
||||
# If credentials comes from service principal or browser, if the required permissions are assigned
|
||||
# the identity can access AAD and retrieve the tenant domain name.
|
||||
# With cli also should be possible but right now it does not work, microsoft365 python package issue is coming
|
||||
# At the time of writting this with az cli creds is not working, despite that is included
|
||||
if app_env_auth:
|
||||
|
||||
async def get_microsoft365_identity():
|
||||
# Trying to recover tenant domain info
|
||||
try:
|
||||
logger.info(
|
||||
"Trying to retrieve tenant domain from AAD to populate identity structure ..."
|
||||
)
|
||||
client = GraphServiceClient(credentials=credentials)
|
||||
|
||||
domain_result = await client.domains.get()
|
||||
if getattr(domain_result, "value"):
|
||||
if getattr(domain_result.value[0], "id"):
|
||||
identity.tenant_domain = domain_result.value[0].id
|
||||
|
||||
except HttpResponseError as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
raise Microsoft365HTTPResponseError(
|
||||
file=os.path.basename(__file__),
|
||||
original_exception=error,
|
||||
)
|
||||
except ClientAuthenticationError as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
raise Microsoft365GetTokenIdentityError(
|
||||
file=os.path.basename(__file__),
|
||||
original_exception=error,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
# since that exception is not considered as critical, we keep filling another identity fields
|
||||
if app_env_auth:
|
||||
# The id of the sp can be retrieved from environment variables
|
||||
identity.identity_id = getenv("APP_CLIENT_ID")
|
||||
identity.identity_type = "Application"
|
||||
# Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli
|
||||
# should work but it doesn't, pending issue
|
||||
else:
|
||||
identity.identity_id = "Unknown user id (Missing AAD permissions)"
|
||||
identity.identity_type = "User"
|
||||
try:
|
||||
logger.info(
|
||||
"Trying to retrieve user information from AAD to populate identity structure ..."
|
||||
)
|
||||
client = GraphServiceClient(credentials=credentials)
|
||||
|
||||
me = await client.me.get()
|
||||
if me:
|
||||
if getattr(me, "user_principal_name"):
|
||||
identity.identity_id = me.user_principal_name
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(get_microsoft365_identity())
|
||||
|
||||
return identity
|
||||
|
||||
def get_locations(self, credentials) -> dict[str, list[str]]:
|
||||
"""
|
||||
Retrieves the locations available for each subscription using the provided credentials.
|
||||
|
||||
Args:
|
||||
credentials: The credentials object used to authenticate the request.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the locations available for each subscription. The dictionary
|
||||
has subscription display names as keys and lists of location names as values.
|
||||
"""
|
||||
locations = None
|
||||
if credentials:
|
||||
locations = {}
|
||||
token = credentials.get_token("https://management.azure.com/.default").token
|
||||
for display_name, subscription_id in self._identity.subscriptions.items():
|
||||
locations.update({display_name: []})
|
||||
url = f"https://management.azure.com/subscriptions/{subscription_id}/locations?api-version=2022-12-01"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
for location in data["value"]:
|
||||
locations[display_name].append(location["name"])
|
||||
return locations
|
||||
@@ -0,0 +1,50 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.config.config import output_file_timestamp
|
||||
from prowler.providers.common.models import ProviderOutputOptions
|
||||
|
||||
|
||||
class Microsoft365IdentityInfo(BaseModel):
|
||||
identity_id: str = ""
|
||||
identity_type: str = ""
|
||||
tenant_ids: list[str] = []
|
||||
tenant_domain: str = "Unknown tenant domain (missing AAD permissions)"
|
||||
subscriptions: dict = {}
|
||||
locations: dict = {}
|
||||
|
||||
|
||||
class Microsoft365RegionConfig(BaseModel):
|
||||
name: str = ""
|
||||
authority: str = None
|
||||
base_url: str = ""
|
||||
credential_scopes: list = []
|
||||
|
||||
|
||||
class Microsoft365Subscription(BaseModel):
|
||||
id: str
|
||||
subscription_id: str
|
||||
display_name: str
|
||||
state: str
|
||||
|
||||
|
||||
class Microsoft365OutputOptions(ProviderOutputOptions):
|
||||
def __init__(self, arguments, bulk_checks_metadata, identity):
|
||||
# First call Provider_Output_Options init
|
||||
super().__init__(arguments, bulk_checks_metadata)
|
||||
|
||||
# Check if custom output filename was input, if not, set the default
|
||||
if (
|
||||
not hasattr(arguments, "output_filename")
|
||||
or arguments.output_filename is None
|
||||
):
|
||||
if (
|
||||
identity.tenant_domain
|
||||
!= "Unknown tenant domain (missing AAD permissions)"
|
||||
):
|
||||
self.output_filename = (
|
||||
f"prowler-output-{identity.tenant_domain}-{output_file_timestamp}"
|
||||
)
|
||||
else:
|
||||
self.output_filename = f"prowler-output-{'-'.join(identity.tenant_ids)}-{output_file_timestamp}"
|
||||
else:
|
||||
self.output_filename = arguments.output_filename
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Provider": "microsoft365",
|
||||
"CheckID": "users_administrative_accounts_cloud_only",
|
||||
"CheckTitle": "Ensure Administrative accounts are cloud-only",
|
||||
"CheckType": [],
|
||||
"ServiceName": "users",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AdministrativeAccount",
|
||||
"Description": "Administrative accounts must be cloud-only and separated from on-premises accounts. These accounts should not have applications assigned to them and should be used exclusively for administrative tasks.",
|
||||
"Risk": "Failing to separate administrative accounts can lead to compromised security in hybrid environments. A breach in the cloud could potentially impact the on-premises environment and vice versa.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "Get-MsolUser -Admin | Where-Object {$_.ImmutableId -ne $null} | Remove-MsolUser",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Create cloud-only administrative accounts and ensure they are not synchronized from on-premises directories. Remove any unnecessary application assignments.",
|
||||
"Url": "https://learn.microsoft.com/en-us/azure/active-directory/roles/security-design-administrative-accounts"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Administrative accounts should be strictly cloud-only and dedicated to admin tasks. Migrate all necessary permissions, including M365 and Azure RBAC roles, to these accounts."
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_Microsoft365
|
||||
from prowler.providers.microsoft365.services.users.users_client import users_client
|
||||
|
||||
|
||||
class users_administrative_accounts_cloud_only(Check):
|
||||
def execute(self) -> Check_Report_Microsoft365:
|
||||
findings = []
|
||||
|
||||
for tenant_domain, directory_roles in users_client.directory_roles.items():
|
||||
for role_name, directory_role in directory_roles.items():
|
||||
report = Check_Report_Microsoft365(self.metadata())
|
||||
report.subscription = f"Tenant: {tenant_domain}"
|
||||
report.resource_name = role_name
|
||||
report.resource_id = directory_role.id
|
||||
report.status = "PASS"
|
||||
|
||||
non_compliant_members = [
|
||||
member
|
||||
for member in directory_roles.members
|
||||
if member.on_premises_sync_enabled
|
||||
]
|
||||
|
||||
if non_compliant_members:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"The following administrators in role '{role_name}' "
|
||||
f"are synchronized with on-premises: "
|
||||
f"{', '.join([member.name for member in non_compliant_members])}."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
f"All administrators in role '{role_name}' are cloud-only."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.microsoft365.services.users.users_service import Users
|
||||
|
||||
users_client = Users(Provider.get_global_provider())
|
||||
@@ -0,0 +1,107 @@
|
||||
from asyncio import gather, get_event_loop
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.microsoft365.lib.service.service import Microsoft365Service
|
||||
from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider
|
||||
|
||||
|
||||
class Users(Microsoft365Service):
|
||||
def __init__(self, provider: Microsoft365Provider):
|
||||
super().__init__(provider)
|
||||
|
||||
loop = get_event_loop()
|
||||
|
||||
# Get users first alone because it is a dependency for other attributes
|
||||
self.users = loop.run_until_complete(self._get_users())
|
||||
|
||||
attributes = loop.run_until_complete(
|
||||
gather(
|
||||
self._get_directory_roles(),
|
||||
)
|
||||
)
|
||||
|
||||
self.directory_roles = attributes[0]
|
||||
|
||||
async def _get_users(self):
|
||||
logger.info("Entra - Getting users...")
|
||||
users = {}
|
||||
try:
|
||||
for tenant, client in self.clients.items():
|
||||
users_list = await client.users.get(
|
||||
params={
|
||||
"$select": "id,displayName,userPrincipalName,onPremisesSyncEnabled"
|
||||
}
|
||||
)
|
||||
users.update({tenant: {}})
|
||||
for user in users_list.value:
|
||||
users[tenant].update(
|
||||
{
|
||||
user.user_principal_name: User(
|
||||
id=user.id,
|
||||
name=user.display_name,
|
||||
on_premises_sync_enabled=user.on_premises_sync_enabled,
|
||||
)
|
||||
}
|
||||
)
|
||||
except Exception as error:
|
||||
if (
|
||||
error.__class__.__name__ == "ODataError"
|
||||
and error.__dict__.get("response_status_code", None) == 403
|
||||
):
|
||||
logger.error(
|
||||
"You need 'UserAuthenticationMethod.Read.All' permission to access this information. It only can be granted through Service Principal authentication."
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
return users
|
||||
|
||||
async def _get_directory_roles(self):
|
||||
logger.info("Entra - Getting directory roles...")
|
||||
directory_roles_with_members = {}
|
||||
try:
|
||||
for tenant, client in self.clients.items():
|
||||
directory_roles_with_members.update({tenant: {}})
|
||||
directory_roles = await client.directory_roles.get()
|
||||
for directory_role in directory_roles.value:
|
||||
directory_role_members = (
|
||||
await client.directory_roles.by_directory_role_id(
|
||||
directory_role.id
|
||||
).members.get()
|
||||
)
|
||||
directory_roles_with_members[tenant].update(
|
||||
{
|
||||
directory_role.display_name: DirectoryRole(
|
||||
id=directory_role.id,
|
||||
members=[
|
||||
self.users[tenant][member.user_principal_name]
|
||||
for member in directory_role_members.value
|
||||
if self.users[tenant].get(
|
||||
member.user_principal_name, None
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return directory_roles_with_members
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
on_premises_sync_enabled: Optional[bool] = None
|
||||
|
||||
|
||||
class DirectoryRole(BaseModel):
|
||||
id: str
|
||||
members: List[User]
|
||||
+20
-20
@@ -23,43 +23,43 @@ packages = [
|
||||
{include = "dashboard"}
|
||||
]
|
||||
readme = "README.md"
|
||||
version = "4.5.0"
|
||||
version = "4.6.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
alive-progress = "3.1.5"
|
||||
alive-progress = "3.2.0"
|
||||
awsipranges = "0.3.3"
|
||||
azure-identity = "1.19.0"
|
||||
azure-keyvault-keys = "4.9.0"
|
||||
azure-keyvault-keys = "4.10.0"
|
||||
azure-mgmt-applicationinsights = "4.0.0"
|
||||
azure-mgmt-authorization = "4.0.0"
|
||||
azure-mgmt-compute = "33.0.0"
|
||||
azure-mgmt-containerregistry = "10.3.0"
|
||||
azure-mgmt-containerservice = "32.0.0"
|
||||
azure-mgmt-containerservice = "32.1.0"
|
||||
azure-mgmt-cosmosdb = "9.6.0"
|
||||
azure-mgmt-keyvault = "10.3.1"
|
||||
azure-mgmt-monitor = "6.0.2"
|
||||
azure-mgmt-network = "27.0.0"
|
||||
azure-mgmt-rdbms = "10.1.0"
|
||||
azure-mgmt-resource = "23.1.1"
|
||||
azure-mgmt-resource = "23.2.0"
|
||||
azure-mgmt-security = "7.0.0"
|
||||
azure-mgmt-sql = "3.0.1"
|
||||
azure-mgmt-storage = "21.2.1"
|
||||
azure-mgmt-subscription = "3.1.1"
|
||||
azure-mgmt-web = "7.3.1"
|
||||
azure-storage-blob = "12.23.1"
|
||||
boto3 = "1.35.29"
|
||||
botocore = "1.35.29"
|
||||
boto3 = "1.35.57"
|
||||
botocore = "1.35.58"
|
||||
colorama = "0.4.6"
|
||||
cryptography = "43.0.1"
|
||||
dash = "2.18.1"
|
||||
dash = "2.18.2"
|
||||
dash-bootstrap-components = "1.6.0"
|
||||
detect-secrets = "1.5.0"
|
||||
google-api-python-client = "2.147.0"
|
||||
google-api-python-client = "2.151.0"
|
||||
google-auth-httplib2 = ">=0.1,<0.3"
|
||||
jsonschema = "4.23.0"
|
||||
kubernetes = "31.0.0"
|
||||
microsoft-kiota-abstractions = "1.3.3"
|
||||
msgraph-sdk = "1.8.0"
|
||||
microsoft-kiota-abstractions = "1.6.0"
|
||||
msgraph-sdk = "1.11.0"
|
||||
numpy = "2.0.2"
|
||||
pandas = "2.2.3"
|
||||
presidio-analyzer = "2.2.355"
|
||||
@@ -70,14 +70,14 @@ python-dateutil = "^2.9.0.post0"
|
||||
pytz = "2024.2"
|
||||
schema = "0.7.7"
|
||||
shodan = "1.31.0"
|
||||
slack-sdk = "3.33.1"
|
||||
slack-sdk = "3.33.3"
|
||||
tabulate = "0.9.0"
|
||||
tzlocal = "5.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
bandit = "1.7.10"
|
||||
black = "24.8.0"
|
||||
coverage = "7.6.1"
|
||||
black = "24.10.0"
|
||||
coverage = "7.6.4"
|
||||
docker = "7.1.0"
|
||||
flake8 = "7.1.1"
|
||||
freezegun = "1.5.1"
|
||||
@@ -87,20 +87,20 @@ openapi-schema-validator = "0.6.2"
|
||||
openapi-spec-validator = "0.7.1"
|
||||
pylint = "3.3.1"
|
||||
pytest = "8.3.3"
|
||||
pytest-cov = "5.0.0"
|
||||
pytest-cov = "6.0.0"
|
||||
pytest-env = "1.1.5"
|
||||
pytest-randomly = "3.15.0"
|
||||
pytest-randomly = "3.16.0"
|
||||
pytest-xdist = "3.6.1"
|
||||
safety = "3.2.8"
|
||||
vulture = "2.12"
|
||||
safety = "3.2.9"
|
||||
vulture = "2.13"
|
||||
|
||||
[tool.poetry.group.docs]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs = "1.6.1"
|
||||
mkdocs-git-revision-date-localized-plugin = "1.2.9"
|
||||
mkdocs-material = "9.5.39"
|
||||
mkdocs-git-revision-date-localized-plugin = "1.3.0"
|
||||
mkdocs-material = "9.5.44"
|
||||
mkdocs-material-extensions = "1.3.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -176,7 +176,7 @@ class TestSlackIntegration:
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
"url": "https://goto.prowler.com/slack",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -305,7 +305,7 @@ class TestSlackIntegration:
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
"url": "https://goto.prowler.com/slack",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -432,7 +432,7 @@ class TestSlackIntegration:
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
"url": "https://goto.prowler.com/slack",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1443,6 +1443,18 @@ aws:
|
||||
)
|
||||
assert connection.error.code == 1015
|
||||
|
||||
@mock_aws
|
||||
def test_test_connection_generic_exception(self):
|
||||
with patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.setup_session",
|
||||
side_effect=Exception(),
|
||||
):
|
||||
connection = AwsProvider.test_connection(raise_on_exception=False)
|
||||
|
||||
assert isinstance(connection, Connection)
|
||||
assert not connection.is_connected
|
||||
assert isinstance(connection.error, Exception)
|
||||
|
||||
@mock_aws
|
||||
def test_create_sts_session(self):
|
||||
current_session = session.Session()
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
from unittest import mock
|
||||
|
||||
import botocore
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from prowler.providers.aws.services.appsync.appsync_service import AppSync
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
orig = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_make_api_call(self, operation_name, kwarg):
|
||||
if operation_name == "ListGraphqlApis":
|
||||
return {
|
||||
"graphqlApis": [
|
||||
{
|
||||
"name": "test-log-level",
|
||||
"apiId": "idididid",
|
||||
"apiType": "MERGED",
|
||||
"arn": f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-log-level",
|
||||
"authenticationType": "API_KEY",
|
||||
"logConfig": {"fieldLogLevel": "ALL"},
|
||||
"region": AWS_REGION_US_EAST_1,
|
||||
"tags": {"test": "test", "test2": "test2"},
|
||||
},
|
||||
]
|
||||
}
|
||||
return orig(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_v2(self, operation_name, kwarg):
|
||||
if operation_name == "ListGraphqlApis":
|
||||
return {
|
||||
"graphqlApis": [
|
||||
{
|
||||
"name": "test-none-log-level",
|
||||
"apiId": "idididid",
|
||||
"apiType": "GRAPHQL",
|
||||
"arn": f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-none-log-level",
|
||||
"authenticationType": "AWS_IAM",
|
||||
"logConfig": {"fieldLogLevel": "NONE"},
|
||||
"region": AWS_REGION_US_EAST_1,
|
||||
"tags": {"test": "test", "test2": "test2"},
|
||||
},
|
||||
]
|
||||
}
|
||||
return orig(self, operation_name, kwarg)
|
||||
|
||||
|
||||
class Test_appsync_field_level_logging_enabled:
|
||||
@mock_aws
|
||||
def test_no_apis(self):
|
||||
client("appsync", region_name=AWS_REGION_US_EAST_1)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.appsync.appsync_field_level_logging_enabled.appsync_field_level_logging_enabled.appsync_client",
|
||||
new=AppSync(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.appsync.appsync_field_level_logging_enabled.appsync_field_level_logging_enabled import (
|
||||
appsync_field_level_logging_enabled,
|
||||
)
|
||||
|
||||
check = appsync_field_level_logging_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
|
||||
def test_graphql_no_api_key(self):
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.appsync.appsync_field_level_logging_enabled.appsync_field_level_logging_enabled.appsync_client",
|
||||
new=AppSync(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.appsync.appsync_field_level_logging_enabled.appsync_field_level_logging_enabled import (
|
||||
appsync_field_level_logging_enabled,
|
||||
)
|
||||
|
||||
check = appsync_field_level_logging_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-log-level"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "idididid"
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "AppSync API test-log-level has field log level enabled."
|
||||
)
|
||||
assert result[0].resource_tags == [{"test": "test", "test2": "test2"}]
|
||||
|
||||
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_v2)
|
||||
def test_graphql_api_key(self):
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.appsync.appsync_field_level_logging_enabled.appsync_field_level_logging_enabled.appsync_client",
|
||||
new=AppSync(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.appsync.appsync_field_level_logging_enabled.appsync_field_level_logging_enabled import (
|
||||
appsync_field_level_logging_enabled,
|
||||
)
|
||||
|
||||
check = appsync_field_level_logging_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-none-log-level"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "idididid"
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "AppSync API test-none-log-level does not have field log level enabled."
|
||||
)
|
||||
assert result[0].resource_tags == [{"test": "test", "test2": "test2"}]
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
from unittest import mock
|
||||
|
||||
import botocore
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from prowler.providers.aws.services.appsync.appsync_service import AppSync
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
orig = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_make_api_call(self, operation_name, kwarg):
|
||||
if operation_name == "ListGraphqlApis":
|
||||
return {
|
||||
"graphqlApis": [
|
||||
{
|
||||
"name": "test-merged-api",
|
||||
"apiId": "api_id",
|
||||
"apiType": "MERGED",
|
||||
"arn": f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-merged-api",
|
||||
"authenticationType": "API_KEY",
|
||||
"region": AWS_REGION_US_EAST_1,
|
||||
"tags": {"test": "test", "test2": "test2"},
|
||||
},
|
||||
]
|
||||
}
|
||||
return orig(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_v2(self, operation_name, kwarg):
|
||||
if operation_name == "ListGraphqlApis":
|
||||
return {
|
||||
"graphqlApis": [
|
||||
{
|
||||
"name": "test-graphql-no-api-key",
|
||||
"apiId": "api_id",
|
||||
"apiType": "GRAPHQL",
|
||||
"arn": f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-graphql-no-api-key",
|
||||
"authenticationType": "AWS_IAM",
|
||||
"region": AWS_REGION_US_EAST_1,
|
||||
"tags": {"test": "test", "test2": "test2"},
|
||||
},
|
||||
]
|
||||
}
|
||||
return orig(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_v3(self, operation_name, kwarg):
|
||||
if operation_name == "ListGraphqlApis":
|
||||
return {
|
||||
"graphqlApis": [
|
||||
{
|
||||
"name": "test-graphql-api-key",
|
||||
"apiId": "api_id",
|
||||
"apiType": "GRAPHQL",
|
||||
"arn": f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-graphql-api-key",
|
||||
"authenticationType": "API_KEY",
|
||||
"region": AWS_REGION_US_EAST_1,
|
||||
"tags": {"test": "test", "test2": "test2"},
|
||||
},
|
||||
]
|
||||
}
|
||||
return orig(self, operation_name, kwarg)
|
||||
|
||||
|
||||
class Test_appsync_graphql_api_no_api_key_authentication:
|
||||
@mock_aws
|
||||
def test_no_apis(self):
|
||||
client("appsync", region_name=AWS_REGION_US_EAST_1)
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication.appsync_client",
|
||||
new=AppSync(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication import (
|
||||
appsync_graphql_api_no_api_key_authentication,
|
||||
)
|
||||
|
||||
check = appsync_graphql_api_no_api_key_authentication()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
|
||||
def test_merged_api(self):
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication.appsync_client",
|
||||
new=AppSync(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication import (
|
||||
appsync_graphql_api_no_api_key_authentication,
|
||||
)
|
||||
|
||||
check = appsync_graphql_api_no_api_key_authentication()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_v2)
|
||||
def test_graphql_no_api_key(self):
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication.appsync_client",
|
||||
new=AppSync(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication import (
|
||||
appsync_graphql_api_no_api_key_authentication,
|
||||
)
|
||||
|
||||
check = appsync_graphql_api_no_api_key_authentication()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-graphql-no-api-key"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "api_id"
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "AppSync GraphQL API test-graphql-no-api-key is not using an API KEY for authentication."
|
||||
)
|
||||
assert result[0].resource_tags == [{"test": "test", "test2": "test2"}]
|
||||
|
||||
@mock_aws
|
||||
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_v3)
|
||||
def test_graphql_api_key(self):
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication.appsync_client",
|
||||
new=AppSync(aws_provider),
|
||||
):
|
||||
# Test Check
|
||||
from prowler.providers.aws.services.appsync.appsync_graphql_api_no_api_key_authentication.appsync_graphql_api_no_api_key_authentication import (
|
||||
appsync_graphql_api_no_api_key_authentication,
|
||||
)
|
||||
|
||||
check = appsync_graphql_api_no_api_key_authentication()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== f"arn:aws:appsync:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:graphqlapi/test-graphql-api-key"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "api_id"
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "AppSync GraphQL API test-graphql-api-key is using an API KEY for authentication."
|
||||
)
|
||||
assert result[0].resource_tags == [{"test": "test", "test2": "test2"}]
|
||||
@@ -0,0 +1,66 @@
|
||||
from boto3 import client
|
||||
from mock import patch
|
||||
from moto import mock_aws
|
||||
|
||||
from prowler.providers.aws.services.appsync.appsync_service import AppSync
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
|
||||
def mock_generate_regional_clients(provider, service):
|
||||
regional_client = provider._session.current_session.client(
|
||||
service, region_name=AWS_REGION_US_EAST_1
|
||||
)
|
||||
regional_client.region = AWS_REGION_US_EAST_1
|
||||
return {AWS_REGION_US_EAST_1: regional_client}
|
||||
|
||||
|
||||
@patch(
|
||||
"prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients",
|
||||
new=mock_generate_regional_clients,
|
||||
)
|
||||
class Test_AppSync_Service:
|
||||
# Test AppSync Service
|
||||
def test_service(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
appsync = AppSync(aws_provider)
|
||||
assert appsync.service == "appsync"
|
||||
|
||||
# Test AppSync Client
|
||||
def test_client(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
appsync = AppSync(aws_provider)
|
||||
assert appsync.client.__class__.__name__ == "AppSync"
|
||||
|
||||
# Test AppSync Session
|
||||
def test__get_session__(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
appsync = AppSync(aws_provider)
|
||||
assert appsync.session.__class__.__name__ == "Session"
|
||||
|
||||
# Test AppSync Session
|
||||
def test_audited_account(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
appsync = AppSync(aws_provider)
|
||||
assert appsync.audited_account == AWS_ACCOUNT_NUMBER
|
||||
|
||||
# Test AppSync Describe File Systems
|
||||
@mock_aws
|
||||
def test_list_graphql_apis(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
appsync = client("appsync", region_name=AWS_REGION_US_EAST_1)
|
||||
api = appsync.create_graphql_api(
|
||||
name="test-api",
|
||||
authenticationType="API_KEY",
|
||||
logConfig={"fieldLogLevel": "ALL", "cloudWatchLogsRoleArn": "test"},
|
||||
)
|
||||
api_arn = api["graphqlApi"]["arn"]
|
||||
appsync_client = AppSync(aws_provider)
|
||||
|
||||
assert appsync_client.graphql_apis[api_arn].name == "test-api"
|
||||
assert appsync_client.graphql_apis[api_arn].field_log_level == "ALL"
|
||||
assert appsync_client.graphql_apis[api_arn].authentication_type == "API_KEY"
|
||||
assert appsync_client.graphql_apis[api_arn].tags == [{}]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user