Compare commits

...
Author SHA1 Message Date
MarioRgzLpz a27029cd95 feat(microsoft365): Add microsoft365 as a new provider. Add a service and a check to test if things are working properly 2024-11-20 08:34:57 +01:00
MarioRgzLpz 9a9cc9a17a feat(microsoft365): Add microsoft365 to provider and summary table 2024-11-20 08:31:38 +01:00
MarioRgzLpz 9bb4329b7f feat(microsoft365): Add microsoft365 CheckReport model 2024-11-20 08:29:33 +01:00
MarioRgzLpz 210207a5fa feat(microsoft365): Add new provider microsoft 365 to prowler main 2024-11-20 08:26:49 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d587d40451 chore(deps): bump botocore from 1.35.57 to 1.35.58 (#5721)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 19:32:42 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f0cd88bd0e chore(deps): bump trufflesecurity/trufflehog from 3.83.5 to 3.83.6 (#5723)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 19:32:13 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> aca17904fa chore(deps-dev): bump mkdocs-git-revision-date-localized-plugin from 1.2.9 to 1.3.0 (#5704)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 14:24:40 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0157802ac1 chore(deps-dev): bump pytest-randomly from 3.15.0 to 3.16.0 (#5705)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 12:12:44 -05:00
sansns-awsandSergio 10766d708d feat(mq): add mq_broker_not_publicly_accessible check (#5604)
Co-authored-by: Sergio <sergio@prowler.com>
2024-11-11 12:12:21 -05:00
Mario Rodriguez LopezandSergio Garcia f231d8b080 feat(appsync): add new check appsync_field_level_logging_enabled (#5602)
Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
2024-11-11 10:23:13 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 590a7b2697 chore(deps): bump boto3 from 1.35.55 to 1.35.57 (#5719)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 10:04:32 -05:00
Hugo Pereira Brito 3c3421644f fix(docs): provider typo (#5713) 2024-11-11 09:21:54 -05:00
Pedro MartínandPepe Fagoaga f1f68da25d feat(jira): add jira integration (#5629)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2024-11-11 15:00:31 +01:00
Prowler Botandsergargar 48df7fdebf chore(regions_update): Changes in regions for AWS services (#5709)
Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com>
2024-11-11 08:51:17 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f2e8691bf4 chore(deps): bump botocore from 1.35.56 to 1.35.57 (#5702)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 08:50:28 -05:00
Matt Johnson 344d54155a docs: Update contact.md with new Slack join URL (#5671) 2024-11-11 12:06:16 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8ecffa3039 chore(deps): bump trufflesecurity/trufflehog from 3.83.4 to 3.83.5 (#5708)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-11 10:11:40 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> efbbfc1c68 chore(deps): bump azure-mgmt-resource from 23.1.1 to 23.2.0 (#5684)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 15:18:01 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dc68c1b955 chore(deps): bump msgraph-sdk from 1.8.0 to 1.11.0 (#5687)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 14:09:14 -05:00
Mario Rodriguez Lopez 5de13bdd8a fix(ec2): unique finding per Security Group in high risk ports check (#5697) 2024-11-08 14:08:27 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5d0f498425 chore(deps): bump botocore from 1.35.55 to 1.35.56 (#5683)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 12:41:05 -05:00
Mario Rodriguez LopezandSergio Garcia 716558ffcb feat(servicecatalog): Add new check servicecatalog_portfolio_shared_within_organization_only (#5632)
Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
2024-11-08 12:22:13 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 23929b3e68 chore(deps): bump dash from 2.18.1 to 2.18.2 (#5682)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 11:50:17 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a5612abc8c chore(deps-dev): bump safety from 3.2.8 to 3.2.9 (#5681)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 10:54:18 -05:00
Prowler Botandsergargar 78dddc1e03 chore(regions_update): Changes in regions for AWS services (#5694)
Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com>
2024-11-08 10:53:57 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 76020d4d47 chore(deps): bump alive-progress from 3.1.5 to 3.2.0 (#5689)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 09:54:57 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b0af1390b5 chore(deps): bump trufflesecurity/trufflehog from 3.83.3 to 3.83.4 (#5692)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 11:48:59 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bc3cd43126 chore(deps): bump slack-sdk from 3.33.1 to 3.33.3 (#5688)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 11:19:02 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 087dae07d8 chore(deps-dev): bump coverage from 7.6.1 to 7.6.4 (#5686)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 09:04:20 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0baf4fb224 chore(deps): bump boto3 from 1.35.29 to 1.35.55 (#5685)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-08 08:15:43 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0f8ea48f2f chore(deps): bump azure-mgmt-containerservice from 32.0.0 to 32.1.0 (#5664)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 15:49:57 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ec207c50ce chore(deps): bump microsoft-kiota-abstractions from 1.3.3 to 1.6.0 (#5662)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 14:54:13 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b59b40b822 chore(deps): bump azure-keyvault-keys from 4.9.0 to 4.10.0 (#5660)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 13:26:51 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> aa51045329 chore(deps-dev): bump mkdocs-material from 9.5.39 to 9.5.44 (#5659)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 12:36:15 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1a9f854063 chore(deps): bump google-api-python-client from 2.147.0 to 2.151.0 (#5661)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 11:42:31 -05:00
Mario Rodriguez LopezandSergio Garcia 6bdcb509e1 feat(appsync): add new check appsync_graphql_apis_no_api_key_authentication (#5591)
Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
2024-11-07 11:42:07 -05:00
Sergio Garcia ce1e9de104 chore(aws): deprecate glue_etl_jobs_logging_enabled check (#5670) 2024-11-07 10:25:32 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2471bc569a chore(deps): bump botocore from 1.35.29 to 1.35.55 (#5663)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 10:22:14 -05:00
Daniel BarranqueroandSergio d0ef75d8d9 feat(dms): add new check dms_replication_task_target_logging_enabled (#5631)
Co-authored-by: Sergio <sergio@prowler.com>
2024-11-07 10:19:44 -05:00
Sergio Garcia aa79a289ce fix(aws): update EKS check in compliance frameworks (#5672) 2024-11-07 15:56:55 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0340ab9570 chore(deps-dev): bump pytest-cov from 5.0.0 to 6.0.0 (#5666)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 09:17:18 -05:00
thomscodeandPepe Fagoaga a2929f2efb fix(mutelist): set arguments while loading providers (#5653)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2024-11-07 09:12:29 -05:00
Prowler Botandsergargar bf4db86dec chore(regions_update): Changes in regions for AWS services (#5655)
Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com>
2024-11-07 08:22:22 -05:00
Daniel Barranquero a339dafcc6 fix(guardduty): fix guardduty_is_enabled_fixer test (#5668) 2024-11-07 08:21:49 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f376516aad chore(deps-dev): bump vulture from 2.12 to 2.13 (#5665)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 08:20:54 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 816b49fac5 chore(deps-dev): bump black from 24.8.0 to 24.10.0 (#5667)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 12:55:16 +01:00
Pepe Fagoaga 6851350093 fix(lock): Use detect-secrets from package not repo (#5656) 2024-11-07 11:30:46 +01:00
Daniel BarranqueroandSergio d5873c0437 feat(dms): add new check dms_replication_task_source_logging_enabled (#5627)
Co-authored-by: Sergio <sergio@prowler.com>
2024-11-06 15:50:48 -05:00
Mario Rodriguez LopezandSergio a2dba30869 feat(servicecatalog): Add new service servicecatalog (#5618)
Co-authored-by: Sergio <sergio@prowler.com>
2024-11-06 12:02:14 -05:00
Mario Rodriguez LopezandSergio 0662dff13f feat(appsync): Add new service AppSync (#5589)
Co-authored-by: Sergio <sergio@prowler.com>
2024-11-06 11:50:27 -05:00
Daniel BarranqueroandSergio Garcia 0ae26bddfc feat(dms): add new check dms_endpoint_redis_tls_enabled (#5583)
Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
2024-11-06 11:03:13 -05:00
Sergio Garcia 43efabef6c fix(docker): add g++ to Dockerfile for presidio-analyzer compatibility (#5645) 2024-11-06 10:45:16 -05:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e73fc14f62 chore(deps): bump trufflesecurity/trufflehog from 3.83.2 to 3.83.3 (#5647)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-06 10:13:09 +01:00
Sergio Garcia 89fe8fa8e2 chore(version): update Prowler version (#5642) 2024-11-06 08:11:13 +01:00
Drew Kerrigan 634ef2e599 fix(docs): Update misc tutorial categories example (#5644) 2024-11-05 15:37:20 -05:00
Sergio Garcia 4efb70a508 chore(README): update summary table (#5633) 2024-11-05 13:24:46 -05:00
Pepe Fagoaga c3ae0aa873 fix(connection): return Connection on generic exception (#5636) 2024-11-05 12:24:18 -05:00
Sergio Garcia a109cd2816 fix(gcp): do not require organization id to get projects (#5637) 2024-11-05 12:24:07 -05:00
sansns-aws 78fb540bbb feat(rds): add rds_cluster_protected_by_backup_plan check (#5638) 2024-11-05 11:30:45 -05:00
sansns-aws 5b543bf058 feat(aws): Update check metadata with redudancy category (#5640) 2024-11-05 11:27:24 -05:00
112 changed files with 8411 additions and 1249 deletions
+1 -1
View File
@@ -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
View File
@@ -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 && \
+5 -5
View File
@@ -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
View File
@@ -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).
+1 -1
View File
@@ -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.
+9 -9
View File
@@ -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
+1 -1
View File
@@ -125,5 +125,5 @@ prowler <provider> --list-categories
```
- Execute specific category(s):
```console
prowler <provider> --categories
prowler <provider> --categories secrets
```
Generated
+1150 -1045
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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",
+1 -1
View File
@@ -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",
+2 -6
View File
@@ -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": ""
}
]
}
]
}
+1 -1
View File
@@ -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"
+18
View File
@@ -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
+1 -1
View File
@@ -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",
},
},
{
+3
View File
@@ -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):
+3 -1
View File
@@ -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())
@@ -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": ""
}
@@ -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
@@ -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": ""
}
@@ -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] = []
@@ -23,7 +23,9 @@
"Url": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-add-availability-zone.html"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -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": ""
}
@@ -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
@@ -23,7 +23,9 @@
"Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/DMS/multi-az.html#"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -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": ""
}
@@ -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
@@ -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": ""
}
@@ -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] = []
@@ -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
@@ -23,7 +23,9 @@
"Url": "https://redis.io/blog/highly-available-in-memory-cloud-datastores/"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -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": ""
@@ -23,7 +23,9 @@
"Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-disable-crosszone-lb.html"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -23,7 +23,9 @@
"Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-subnets.html"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -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."
}
@@ -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": ""
@@ -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": ""
}
@@ -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
@@ -23,7 +23,9 @@
"Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/neptune-controls.html#neptune-9"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -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": ""
@@ -23,7 +23,9 @@
"Url": "https://aws.amazon.com/rds/features/multi-az/"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -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": ""
}
@@ -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
@@ -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())
@@ -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": ""
}
@@ -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] = []
@@ -25,7 +25,9 @@
"Url": "https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html"
}
},
"Categories": [],
"Categories": [
"redundancy"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
+43 -36
View File
@@ -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,
)
+1 -1
View File
@@ -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
+50
View File
@@ -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
@@ -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."
}
@@ -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
View File
@@ -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]
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -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",
},
},
{
+12
View File
@@ -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()
@@ -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"}]
@@ -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