mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-21 13:20:57 +00:00
Compare commits
173
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a3c9b8dc3 | ||
|
|
4fb010847f | ||
|
|
8eac17d826 | ||
|
|
8a1abb60c9 | ||
|
|
74776bed06 | ||
|
|
3373a8e6c8 | ||
|
|
7db74516b3 | ||
|
|
3ae2f9e0bf | ||
|
|
91a8a13dc6 | ||
|
|
700ed5ecec | ||
|
|
50aebcaf88 | ||
|
|
ad96963706 | ||
|
|
95832bdb12 | ||
|
|
d7bdc0aaef | ||
|
|
1c0c836286 | ||
|
|
822252d03b | ||
|
|
a20fdb30f9 | ||
|
|
2e339f964a | ||
|
|
bb4498a3d5 | ||
|
|
dae4392efc | ||
|
|
8b3a1608c2 | ||
|
|
148bdacebe | ||
|
|
b60edc1f6b | ||
|
|
d28462b642 | ||
|
|
ef9b37d5c0 | ||
|
|
764f260cea | ||
|
|
e771218bba | ||
|
|
c627a3e9aa | ||
|
|
1c58644118 | ||
|
|
398fa37a23 | ||
|
|
412d948585 | ||
|
|
6bc0432ed5 | ||
|
|
b2ebc8410a | ||
|
|
94b6fbda91 | ||
|
|
3ce785a7b4 | ||
|
|
e4ecc98aae | ||
|
|
53a4befb01 | ||
|
|
fee0bf3ea1 | ||
|
|
6811a22651 | ||
|
|
fe2dd69b08 | ||
|
|
26a9748700 | ||
|
|
cef0a54bc3 | ||
|
|
553a51ddc2 | ||
|
|
61dc09d15d | ||
|
|
9b91ba2b91 | ||
|
|
18d60c98d7 | ||
|
|
00054b5cd9 | ||
|
|
24fc86cbb3 | ||
|
|
861fb22257 | ||
|
|
7e14204be8 | ||
|
|
09ea6ba6c4 | ||
|
|
a83725fbed | ||
|
|
34210cfc06 | ||
|
|
2e20d52030 | ||
|
|
9b0b61ef02 | ||
|
|
0203aec9e0 | ||
|
|
862a4ad76c | ||
|
|
4b7883c464 | ||
|
|
2bf835d3d2 | ||
|
|
09733eb298 | ||
|
|
096749a455 | ||
|
|
b06e549d81 | ||
|
|
a5f5967bb2 | ||
|
|
7a4f5f34f7 | ||
|
|
e33b081dc6 | ||
|
|
c8fdaa3923 | ||
|
|
8a491bcf7d | ||
|
|
f5e71db5e0 | ||
|
|
456f79d80c | ||
|
|
9f728833a7 | ||
|
|
f01ce849dc | ||
|
|
572d5a1f2e | ||
|
|
c69571abcd | ||
|
|
8ddb9fbb84 | ||
|
|
193b79c221 | ||
|
|
a25a6148f2 | ||
|
|
0a63e707c2 | ||
|
|
f53a887291 | ||
|
|
ca35510d74 | ||
|
|
5d7c8d9cd2 | ||
|
|
0d01790b22 | ||
|
|
78b518e22b | ||
|
|
17af724995 | ||
|
|
c9898d6d01 | ||
|
|
259538d5e4 | ||
|
|
4785feae0e | ||
|
|
8be83fc632 | ||
|
|
005d251106 | ||
|
|
b6c8adfc64 | ||
|
|
7a711095cd | ||
|
|
b0bb348480 | ||
|
|
c1b050b8b9 | ||
|
|
28c7e803ac | ||
|
|
919acfd548 | ||
|
|
1586cdae5e | ||
|
|
cb74dae296 | ||
|
|
3608aa3536 | ||
|
|
c59b08c40b | ||
|
|
73361a1cea | ||
|
|
794268cec5 | ||
|
|
06b41cf8e6 | ||
|
|
a419b4b898 | ||
|
|
622bc48688 | ||
|
|
c0659f712a | ||
|
|
796983a530 | ||
|
|
8dc042e594 | ||
|
|
e881a0f274 | ||
|
|
5641160177 | ||
|
|
dca49b1972 | ||
|
|
b8b60e6bc5 | ||
|
|
9d65b49cb4 | ||
|
|
f1334190d8 | ||
|
|
c434181dfd | ||
|
|
f3cfacae9a | ||
|
|
77c7986797 | ||
|
|
2ac716d6db | ||
|
|
daee5fb4d2 | ||
|
|
d587d40451 | ||
|
|
f0cd88bd0e | ||
|
|
aca17904fa | ||
|
|
0157802ac1 | ||
|
|
10766d708d | ||
|
|
f231d8b080 | ||
|
|
590a7b2697 | ||
|
|
3c3421644f | ||
|
|
f1f68da25d | ||
|
|
48df7fdebf | ||
|
|
f2e8691bf4 | ||
|
|
344d54155a | ||
|
|
8ecffa3039 | ||
|
|
efbbfc1c68 | ||
|
|
dc68c1b955 | ||
|
|
5de13bdd8a | ||
|
|
5d0f498425 | ||
|
|
716558ffcb | ||
|
|
23929b3e68 | ||
|
|
a5612abc8c | ||
|
|
78dddc1e03 | ||
|
|
76020d4d47 | ||
|
|
b0af1390b5 | ||
|
|
bc3cd43126 | ||
|
|
087dae07d8 | ||
|
|
0baf4fb224 | ||
|
|
0f8ea48f2f | ||
|
|
ec207c50ce | ||
|
|
b59b40b822 | ||
|
|
aa51045329 | ||
|
|
1a9f854063 | ||
|
|
6bdcb509e1 | ||
|
|
ce1e9de104 | ||
|
|
2471bc569a | ||
|
|
d0ef75d8d9 | ||
|
|
aa79a289ce | ||
|
|
0340ab9570 | ||
|
|
a2929f2efb | ||
|
|
bf4db86dec | ||
|
|
a339dafcc6 | ||
|
|
f376516aad | ||
|
|
816b49fac5 | ||
|
|
6851350093 | ||
|
|
d5873c0437 | ||
|
|
a2dba30869 | ||
|
|
0662dff13f | ||
|
|
0ae26bddfc | ||
|
|
43efabef6c | ||
|
|
e73fc14f62 | ||
|
|
89fe8fa8e2 | ||
|
|
634ef2e599 | ||
|
|
4efb70a508 | ||
|
|
c3ae0aa873 | ||
|
|
a109cd2816 | ||
|
|
78fb540bbb | ||
|
|
5b543bf058 |
+39
-2
@@ -5,6 +5,7 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# v5
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -14,6 +15,7 @@ updates:
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -24,20 +26,55 @@ updates:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "npm"
|
||||
|
||||
# v4.6
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v4.6
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- "v4"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v4.6
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
- "v4"
|
||||
|
||||
# v3
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- "v3"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
|
||||
@@ -79,3 +79,10 @@ output/csv:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/csv/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/csv/**"
|
||||
|
||||
compliance:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/compliance/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/compliance/**"
|
||||
- any-glob-to-any-file: "prowler/compliance/**"
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ name: build-lint-push-containers
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# For `v3-latest`
|
||||
- "v3"
|
||||
# For `v4-latest`
|
||||
- "v4.6"
|
||||
# For `latest`
|
||||
- "master"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
@@ -80,8 +84,8 @@ jobs:
|
||||
;;
|
||||
|
||||
4)
|
||||
echo "LATEST_TAG=latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
|
||||
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.83.2
|
||||
uses: trufflesecurity/trufflehog@v3.85.0
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -91,6 +91,6 @@ jobs:
|
||||
poetry run pytest -n auto --cov=./prowler --cov-report=xml tests
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -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>
|
||||
@@ -64,8 +64,8 @@ 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 | 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 | 138 | 17 -> `prowler azure --list-services` | 3 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| GCP | 77 | 13 -> `prowler gcp --list-services` | 3 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
|
||||
| Azure | 138 | 17 -> `prowler azure --list-services` | 4 -> `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
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_ens
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
# append the requirements_description to idgrupocontrol
|
||||
data["REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL"] = (
|
||||
data["REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL"]
|
||||
+ " - "
|
||||
+ data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ATTRIBUTES_MARCO",
|
||||
"REQUIREMENTS_ATTRIBUTES_CATEGORIA",
|
||||
"REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL",
|
||||
"REQUIREMENTS_ATTRIBUTES_TIPO",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_ens(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_MARCO",
|
||||
"REQUIREMENTS_ATTRIBUTES_CATEGORIA",
|
||||
"REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL",
|
||||
"REQUIREMENTS_ATTRIBUTES_TIPO",
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_ens
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
# append the requirements_description to idgrupocontrol
|
||||
data["REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL"] = (
|
||||
data["REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL"]
|
||||
+ " - "
|
||||
+ data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_ATTRIBUTES_MARCO",
|
||||
"REQUIREMENTS_ATTRIBUTES_CATEGORIA",
|
||||
"REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL",
|
||||
"REQUIREMENTS_ATTRIBUTES_TIPO",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_ens(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_MARCO",
|
||||
"REQUIREMENTS_ATTRIBUTES_CATEGORIA",
|
||||
"REQUIREMENTS_ATTRIBUTES_IDGRUPOCONTROL",
|
||||
"REQUIREMENTS_ATTRIBUTES_TIPO",
|
||||
)
|
||||
@@ -148,6 +148,7 @@ else:
|
||||
select_account_dropdown_list = ["All"]
|
||||
# Append to the list the unique values of the columns ACCOUNTID, PROJECTID and SUBSCRIPTIONID if they exist
|
||||
if "ACCOUNTID" in data.columns:
|
||||
data["ACCOUNTID"] = data["ACCOUNTID"].astype(str)
|
||||
select_account_dropdown_list = select_account_dropdown_list + list(
|
||||
data["ACCOUNTID"].unique()
|
||||
)
|
||||
@@ -246,9 +247,11 @@ def display_data(
|
||||
dfs = []
|
||||
for file in files:
|
||||
df = pd.read_csv(
|
||||
file, sep=";", on_bad_lines="skip", encoding=encoding_format
|
||||
file, sep=";", on_bad_lines="skip", encoding=encoding_format, dtype=str
|
||||
)
|
||||
dfs.append(df.astype(str))
|
||||
df = df.astype(str).fillna("nan")
|
||||
df.columns = df.columns.astype(str)
|
||||
dfs.append(df)
|
||||
return pd.concat(dfs, ignore_index=True)
|
||||
|
||||
data = load_csv_files(files)
|
||||
@@ -274,17 +277,24 @@ def display_data(
|
||||
data.rename(columns={"PROJECTID": "ACCOUNTID"}, inplace=True)
|
||||
data["REGION"] = "-"
|
||||
# Rename the column SUBSCRIPTIONID to ACCOUNTID for Azure
|
||||
if data.columns.str.contains("SUBSCRIPTIONID").any():
|
||||
if (
|
||||
data.columns.str.contains("SUBSCRIPTIONID").any()
|
||||
and not data.columns.str.contains("ACCOUNTID").any()
|
||||
):
|
||||
data.rename(columns={"SUBSCRIPTIONID": "ACCOUNTID"}, inplace=True)
|
||||
data["REGION"] = "-"
|
||||
# Handle v3 azure cis compliance
|
||||
if data.columns.str.contains("SUBSCRIPTION").any():
|
||||
if (
|
||||
data.columns.str.contains("SUBSCRIPTION").any()
|
||||
and not data.columns.str.contains("ACCOUNTID").any()
|
||||
):
|
||||
data.rename(columns={"SUBSCRIPTION": "ACCOUNTID"}, inplace=True)
|
||||
data["REGION"] = "-"
|
||||
|
||||
# Filter ACCOUNT
|
||||
if account_filter == ["All"]:
|
||||
updated_cloud_account_values = data["ACCOUNTID"].unique()
|
||||
|
||||
elif "All" in account_filter and len(account_filter) > 1:
|
||||
# Remove 'All' from the list
|
||||
account_filter.remove("All")
|
||||
@@ -299,9 +309,11 @@ def display_data(
|
||||
|
||||
account_filter_options = list(data["ACCOUNTID"].unique())
|
||||
account_filter_options = account_filter_options + ["All"]
|
||||
for item in account_filter_options:
|
||||
if "nan" in item or item.__class__.__name__ != "str" or item is None:
|
||||
account_filter_options.remove(item)
|
||||
account_filter_options = [
|
||||
item
|
||||
for item in account_filter_options
|
||||
if isinstance(item, str) and item.lower() != "nan"
|
||||
]
|
||||
|
||||
# Filter REGION
|
||||
if region_filter_analytics == ["All"]:
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
For technical support or any type of inquiries, you are very welcome to:
|
||||
|
||||
- Reach out to community members on the [**Prowler Slack channel**](https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog)
|
||||
- Reach out to community members on the [**Prowler Slack channel**](https://goto.prowler.com/slack)
|
||||
|
||||
- Open an Issue or a Pull Request in our [**GitHub repository**](https://github.com/prowler-cloud/prowler).
|
||||
|
||||
|
||||
@@ -160,14 +160,20 @@ else:
|
||||
All the checks MUST fill the `report.resource_id` and `report.resource_arn` with the following criteria:
|
||||
|
||||
- AWS
|
||||
- Resource ID -- `report.resource_id`
|
||||
- AWS Account --> Account Number `123456789012`
|
||||
- AWS Resource --> Resource ID / Name
|
||||
- Root resource --> `<root_account>`
|
||||
- Resource ARN -- `report.resource_arn`
|
||||
- AWS Account --> Root ARN `arn:aws:iam::123456789012:root`
|
||||
- AWS Resource --> Resource ARN
|
||||
- Root resource --> Resource Type ARN `f"arn:{service_client.audited_partition}:<service_name>:{service_client.region}:{service_client.audited_account}:<resource_type>"`
|
||||
- Resouce ID and resource ARN:
|
||||
- If the resource audited is the AWS account:
|
||||
- `resource_id` -> AWS Account Number
|
||||
- `resource_arn` -> AWS Account Root ARN
|
||||
- If we can’t get the ARN from the resource audited, we create a valid ARN with the `resource_id` part as the resource audited. Examples:
|
||||
- Bedrock -> `arn:<partition>:bedrock:<region>:<account-id>:model-invocation-logging`
|
||||
- DirectConnect -> `arn:<partition>:directconnect:<region>:<account-id>:dxcon`
|
||||
- If there is no real resource to audit we do the following:
|
||||
- resource_id -> `resource_type/unknown`
|
||||
- resource_arn -> `arn:<partition>:<service>:<region>:<account-id>:<resource_type>/unknown`
|
||||
- Examples:
|
||||
- AWS Security Hub -> `arn:<partition>:security-hub:<region>:<account-id>:hub/unknown`
|
||||
- Access Analyzer -> `arn:<partition>:access-analyzer:<region>:<account-id>:analyzer/unknown`
|
||||
- GuardDuty -> `arn:<partition>:guardduty:<region>:<account-id>:detector/unknown`
|
||||
- GCP
|
||||
- Resource ID -- `report.resource_id`
|
||||
- GCP Resource --> Resource ID
|
||||
|
||||
@@ -67,4 +67,4 @@ If you create or review a PR in https://github.com/prowler-cloud/prowler please
|
||||
|
||||
## Want some swag as appreciation for your contribution?
|
||||
|
||||
If you are like us and you love swag, we are happy to thank you for your contribution with some laptop stickers or whatever other swag we may have at that time. Please, tell us more details and your pull request link in our [Slack workspace here](https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog). You can also reach out to Toni de la Fuente on Twitter [here](https://twitter.com/ToniBlyx), his DMs are open.
|
||||
If you are like us and you love swag, we are happy to thank you for your contribution with some laptop stickers or whatever other swag we may have at that time. Please, tell us more details and your pull request link in our [Slack workspace here](https://goto.prowler.com/slack). You can also reach out to Toni de la Fuente on Twitter [here](https://twitter.com/ToniBlyx), his DMs are open.
|
||||
|
||||
@@ -190,18 +190,18 @@ from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.<new_provider_name>.models import (
|
||||
# All providers models needed
|
||||
ProvierSessionModel,
|
||||
ProvierIdentityModel,
|
||||
ProvierOutputOptionsModel
|
||||
ProviderSessionModel,
|
||||
ProviderIdentityModel,
|
||||
ProviderOutputOptionsModel
|
||||
)
|
||||
|
||||
class NewProvider(Provider):
|
||||
# All properties from the class, some of this are properties in the base class
|
||||
_type: str = "<provider_name>"
|
||||
_session: <ProvierSessionModel>
|
||||
_identity: <ProvierIdentityModel>
|
||||
_session: <ProviderSessionModel>
|
||||
_identity: <ProviderIdentityModel>
|
||||
_audit_config: dict
|
||||
_output_options: ProvierOutputOptionsModel
|
||||
_output_options: ProviderOutputOptionsModel
|
||||
_mutelist: dict
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
@@ -212,13 +212,13 @@ class NewProvider(Provider):
|
||||
arguments (dict): A dictionary containing configuration arguments.
|
||||
"""
|
||||
logger.info("Setting <NewProviderName> provider ...")
|
||||
# First get from arguments the necesary from the cloud acount (subscriptions or projects or whatever the provider use for storing services)
|
||||
# First get from arguments the necessary from the cloud account (subscriptions or projects or whatever the provider use for storing services)
|
||||
|
||||
# Set the session with the method enforced by parent class
|
||||
self._session = self.setup_session(credentials_file)
|
||||
|
||||
# Set the Identity class normaly the provider class give by Python provider library
|
||||
self._identity = <ProvierIdentityModel>()
|
||||
self._identity = <ProviderIdentityModel>()
|
||||
|
||||
# Set the provider configuration
|
||||
self._audit_config = load_and_validate_config_file(
|
||||
@@ -254,7 +254,7 @@ class NewProvider(Provider):
|
||||
<all_needed_for_auth> Can include all necessary arguments to setup the session
|
||||
|
||||
Returns:
|
||||
Credentials necesary to communicate with the provider.
|
||||
Credentials necessary to communicate with the provider.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -125,5 +125,5 @@ prowler <provider> --list-categories
|
||||
```
|
||||
- Execute specific category(s):
|
||||
```console
|
||||
prowler <provider> --categories
|
||||
prowler <provider> --categories secrets
|
||||
```
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
# Prowler Check Kreator
|
||||
|
||||
???+ note
|
||||
Currently, this tool is only available for creating checks for the AWS provider.
|
||||
|
||||
**Prowler Check Kreator** is a utility designed to streamline the creation of new checks for Prowler. This tool generates all necessary files required to add a new check to the Prowler repository. Specifically, it creates:
|
||||
|
||||
- A dedicated folder for the check.
|
||||
- The main check script.
|
||||
- A metadata file with essential details.
|
||||
- A folder and file structure for testing the check.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the tool, execute the main script with the following command:
|
||||
|
||||
```bash
|
||||
python util/prowler_check_kreator/prowler_check_kreator.py <prowler_provider> <check_name>
|
||||
```
|
||||
Parameters:
|
||||
|
||||
- `<prowler_provider>`: Currently only AWS is supported.
|
||||
- `<check_name>`: The name you wish to assign to the new check.
|
||||
|
||||
## AI integration
|
||||
|
||||
This tool optionally integrates AI to assist in generating the check code and metadata file content. When AI assistance is chosen, the tool uses [Gemini](https://gemini.google.com/) to produce preliminary code and metadata.
|
||||
|
||||
???+ note
|
||||
For this feature to work, you must have the library `google-generativeai` installed in your Python environment.
|
||||
|
||||
???+ warning
|
||||
AI-generated code and metadata might contain errors or require adjustments to align with specific Prowler requirements. Carefully review all AI-generated content before committing.
|
||||
|
||||
To enable AI assistance, simply confirm when prompted by the tool. Additionally, ensure that the `GEMINI_API_KEY` environment variable is set with a valid Gemini API key. For instructions on obtaining your API key, refer to the [Gemini documentation](https://ai.google.dev/gemini-api/docs/api-key).
|
||||
@@ -65,6 +65,7 @@ nav:
|
||||
- Pentesting: tutorials/pentesting.md
|
||||
- Parallel Execution: tutorials/parallel-execution.md
|
||||
- Developer Guide: developer-guide/introduction.md
|
||||
- Prowler Check Kreator: tutorials/prowler-check-kreator.md
|
||||
- AWS:
|
||||
- Authentication: tutorials/aws/authentication.md
|
||||
- Assume Role: tutorials/aws/role-assumption.md
|
||||
|
||||
Generated
+1177
-1055
File diff suppressed because it is too large
Load Diff
+58
-1
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from os import environ
|
||||
|
||||
from colorama import Fore, Style
|
||||
@@ -10,11 +11,16 @@ from colorama import init as colorama_init
|
||||
from prowler.config.config import (
|
||||
csv_file_suffix,
|
||||
get_available_compliance_frameworks,
|
||||
get_available_update,
|
||||
html_file_suffix,
|
||||
json_asff_file_suffix,
|
||||
json_ocsf_file_suffix,
|
||||
)
|
||||
from prowler.lib.banner import print_banner
|
||||
from prowler.lib.banner import (
|
||||
print_banner,
|
||||
print_prowler_cloud_banner,
|
||||
print_update_notice,
|
||||
)
|
||||
from prowler.lib.check.check import (
|
||||
exclude_checks_to_run,
|
||||
exclude_services_to_run,
|
||||
@@ -53,6 +59,8 @@ from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS
|
||||
from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
|
||||
from prowler.lib.outputs.compliance.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS
|
||||
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
|
||||
from prowler.lib.outputs.compliance.iso27001.iso27001_aws import AWSISO27001
|
||||
from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp_aws import AWSKISAISMSP
|
||||
@@ -116,6 +124,18 @@ def prowler():
|
||||
if args.no_color:
|
||||
colorama_init(strip=True)
|
||||
|
||||
# Check in the background whether a newer Prowler release is available;
|
||||
# the result is printed at the end of the scan so the check never adds
|
||||
# latency. Skipped with --no-banner/--only-logs and via
|
||||
# PROWLER_NO_VERSION_CHECK/DO_NOT_TRACK (handled in get_available_update).
|
||||
available_update = {}
|
||||
update_check_thread = threading.Thread(
|
||||
target=lambda: available_update.update(latest=get_available_update()),
|
||||
daemon=True,
|
||||
)
|
||||
if not args.no_banner and not args.only_logs:
|
||||
update_check_thread.start()
|
||||
|
||||
if not args.no_banner:
|
||||
legend = args.verbose or getattr(args, "fixer", None)
|
||||
print_banner(legend)
|
||||
@@ -511,6 +531,20 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(mitre_attack)
|
||||
mitre_attack.batch_write_data_to_file()
|
||||
elif compliance_name.startswith("ens_"):
|
||||
# Generate ENS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
ens = AzureENS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(ens)
|
||||
ens.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -555,6 +589,20 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(mitre_attack)
|
||||
mitre_attack.batch_write_data_to_file()
|
||||
elif compliance_name.startswith("ens_"):
|
||||
# Generate ENS Finding Object
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
ens = GCPENS(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
create_file_descriptor=True,
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(ens)
|
||||
ens.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -689,6 +737,15 @@ def prowler():
|
||||
f"\nDetailed compliance results are in {Fore.YELLOW}{output_options.output_directory}/compliance/{Style.RESET_ALL}\n"
|
||||
)
|
||||
|
||||
# Promote Prowler Cloud as the last thing the user sees after the results,
|
||||
# preceded by an update notice when a newer release is available
|
||||
if not args.no_banner and not args.only_logs:
|
||||
if update_check_thread.is_alive():
|
||||
update_check_thread.join(timeout=2)
|
||||
if available_update.get("latest"):
|
||||
print_update_notice(available_update["latest"])
|
||||
print_prowler_cloud_banner()
|
||||
|
||||
# If custom checks were passed, remove the modules
|
||||
if checks_folder:
|
||||
remove_custom_checks_module(checks_folder, provider)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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.4"
|
||||
prowler_version = "4.6.3"
|
||||
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"
|
||||
@@ -86,23 +86,50 @@ def get_default_mute_file_path(provider: str):
|
||||
return mutelist_path
|
||||
|
||||
|
||||
def check_current_version():
|
||||
def get_latest_release_version():
|
||||
"""Return the latest Prowler release tag name from GitHub, or None if it cannot be retrieved."""
|
||||
try:
|
||||
prowler_version_string = f"Prowler {prowler_version}"
|
||||
release_response = requests.get(
|
||||
"https://api.github.com/repos/prowler-cloud/prowler/tags", timeout=1
|
||||
)
|
||||
latest_version = release_response.json()[0]["name"]
|
||||
return release_response.json()[0]["name"]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_available_update():
|
||||
"""Return the latest Prowler version if it is newer than the running one, None otherwise.
|
||||
|
||||
Honors the PROWLER_NO_VERSION_CHECK and DO_NOT_TRACK environment variables:
|
||||
when either is set, no network call is made and None is returned.
|
||||
"""
|
||||
if os.environ.get("PROWLER_NO_VERSION_CHECK") or os.environ.get("DO_NOT_TRACK"):
|
||||
return None
|
||||
latest_version = get_latest_release_version()
|
||||
try:
|
||||
if latest_version and version.parse(latest_version) > version.parse(
|
||||
prowler_version
|
||||
):
|
||||
return latest_version
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def check_current_version():
|
||||
prowler_version_string = f"Prowler {prowler_version}"
|
||||
latest_version = get_latest_release_version()
|
||||
if not latest_version:
|
||||
return prowler_version_string
|
||||
try:
|
||||
if version.parse(latest_version) > version.parse(prowler_version):
|
||||
return f"{prowler_version_string} (latest is {latest_version}, upgrade for the latest features)"
|
||||
else:
|
||||
return (
|
||||
f"{prowler_version_string} (You are running the latest version, yay!)"
|
||||
)
|
||||
except requests.RequestException:
|
||||
return f"{prowler_version_string}"
|
||||
except Exception:
|
||||
return f"{prowler_version_string}"
|
||||
return prowler_version_string
|
||||
|
||||
|
||||
def load_and_validate_config_file(provider: str, config_file_path: str) -> dict:
|
||||
|
||||
@@ -368,6 +368,11 @@ aws:
|
||||
# Maximum number of days a secret should be rotated
|
||||
max_days_secret_unrotated: 90
|
||||
|
||||
# AWS Kinesis Configuration
|
||||
# Minimum retention period in hours for Kinesis streams
|
||||
min_kinesis_stream_retention_hours: 168 # 7 days
|
||||
|
||||
|
||||
# Azure Configuration
|
||||
azure:
|
||||
# Azure Network Configuration
|
||||
|
||||
@@ -2,6 +2,75 @@ from colorama import Fore, Style
|
||||
|
||||
from prowler.config.config import banner_color, orange_color, prowler_version, timestamp
|
||||
|
||||
# Prowler Cloud landing URL used by the CLI banner. The visible text stays
|
||||
# "cloud.prowler.com" while the clickable target carries the UTM parameters so
|
||||
# terminals that support OSC 8 hyperlinks attribute the visit to the v4 CLI.
|
||||
CLOUD_DISPLAY_TEXT = "cloud.prowler.com"
|
||||
CLOUD_BANNER_URL = (
|
||||
"https://cloud.prowler.com/sign-up?utm_source=prowler-cli&utm_content=v4"
|
||||
)
|
||||
|
||||
|
||||
def _hyperlink(url: str, text: str) -> str:
|
||||
"""Wrap ``text`` in an OSC 8 terminal hyperlink pointing to ``url``.
|
||||
|
||||
Terminals that support OSC 8 render ``text`` as a clickable link to ``url``;
|
||||
those that do not simply display ``text`` unchanged.
|
||||
"""
|
||||
return f"\033]8;;{url}\033\\{text}\033]8;;\033\\"
|
||||
|
||||
|
||||
def print_update_notice(latest_version: str):
|
||||
"""
|
||||
Prints a notice that a newer Prowler version is available.
|
||||
|
||||
Parameters:
|
||||
- latest_version (str): The latest released Prowler version.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
print(
|
||||
f"\n{Fore.YELLOW}A new version of Prowler is available: {prowler_version} → {latest_version}{Style.RESET_ALL}\n"
|
||||
f"Upgrading from Prowler v4 is a major version upgrade — see the release notes at\n"
|
||||
f"https://github.com/prowler-cloud/prowler/releases before upgrading.\n"
|
||||
f"Upgrade with: {Style.BRIGHT}pipx upgrade prowler{Style.RESET_ALL} "
|
||||
f"(disable this check with PROWLER_NO_VERSION_CHECK=1)"
|
||||
)
|
||||
|
||||
|
||||
def print_prowler_cloud_banner():
|
||||
"""
|
||||
Prints a promotional banner highlighting what Prowler Cloud adds on top of
|
||||
the open-source CLI.
|
||||
|
||||
Shown at the end of a scan to let users know about the managed platform
|
||||
capabilities they are missing.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
check = f"{Fore.GREEN}✓{Style.RESET_ALL}"
|
||||
bar = f"{banner_color}│{Style.RESET_ALL}"
|
||||
print(
|
||||
f"""
|
||||
{bar} {Style.BRIGHT}You're getting a snapshot 📸. Prowler Cloud gives you the full picture:{Style.RESET_ALL}
|
||||
{bar}
|
||||
{bar} {check} {Style.BRIGHT}Send your findings{Style.RESET_ALL} - directly from the Prowler CLI to Prowler Cloud.
|
||||
{bar} {check} {Style.BRIGHT}Continuous Security Monitoring{Style.RESET_ALL} - custom scheduling and scan configuration with history, trends and alerts.
|
||||
{bar} {check} {Style.BRIGHT}Triage{Style.RESET_ALL} - review findings, flag false positives and track accepted risk with your team.
|
||||
{bar} {check} {Style.BRIGHT}Lighthouse AI + MCP{Style.RESET_ALL} - autonomous triage, custom dashboards, prioritization with prevention and remediation.
|
||||
{bar} {check} {Style.BRIGHT}Alerts{Style.RESET_ALL} - get notified when anything you want is happening.
|
||||
{bar} {check} {Style.BRIGHT}Live Compliance{Style.RESET_ALL} - dashboards for 50+ frameworks, always up to date.
|
||||
{bar} {check} {Style.BRIGHT}Remediation{Style.RESET_ALL} - complete guided remediation including Autonomous remediation with Lighthouse AI.
|
||||
{bar} {check} {Style.BRIGHT}Attack Path Visualization{Style.RESET_ALL} - see how attackers chain risks to reach your crown jewels.
|
||||
{bar} {check} {Style.BRIGHT}Bulk Provisioning{Style.RESET_ALL} - add your entire AWS Organization in seconds.
|
||||
{bar} {check} {Style.BRIGHT}Integrations{Style.RESET_ALL} - Anything with our MCP + Jira, Slack, AWS Security Hub, Amazon S3, SSO and RBAC.
|
||||
{bar}
|
||||
{bar} {banner_color}Start free at 👉 {_hyperlink(CLOUD_BANNER_URL, CLOUD_DISPLAY_TEXT)}{Style.RESET_ALL}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def print_banner(legend: bool = False):
|
||||
"""
|
||||
|
||||
@@ -111,7 +111,7 @@ def load_checks_to_execute(
|
||||
):
|
||||
checks_to_execute.add(check_name)
|
||||
# Only execute threat detection checks if threat-detection category is set
|
||||
if categories and categories != [] and "threat-detection" not in categories:
|
||||
if not categories or "threat-detection" not in categories:
|
||||
for threat_detection_check in check_categories.get("threat-detection", []):
|
||||
checks_to_execute.discard(threat_detection_check)
|
||||
|
||||
|
||||
@@ -322,8 +322,9 @@ class CheckMetadata(BaseModel):
|
||||
checks = set()
|
||||
|
||||
if service:
|
||||
if service == "lambda":
|
||||
service = "awslambda"
|
||||
# This is a special case for the AWS provider since `lambda` is a reserved keyword in Python
|
||||
if service == "awslambda":
|
||||
service = "lambda"
|
||||
checks = {
|
||||
check_name
|
||||
for check_name, check_metadata in bulk_checks_metadata.items()
|
||||
|
||||
@@ -94,11 +94,12 @@ def get_cis_table(
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -30,7 +30,7 @@ def get_ens_table(
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "ENS" and compliance.Provider == "AWS":
|
||||
if compliance.Framework == "ENS":
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
marco_categoria = f"{attribute.Marco}/{attribute.Categoria}"
|
||||
@@ -95,11 +95,12 @@ def get_ens_table(
|
||||
print(
|
||||
f"\nEstado de Cumplimiento de {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) CUMPLE{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) CUMPLE{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.ens.models import AzureENSModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class AzureENS(ComplianceOutput):
|
||||
"""
|
||||
This class represents the Azure ENS compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into Azure ENS compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into AWS ENS compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
# Get the compliance requirements for the finding
|
||||
finding_requirements = finding.compliance.get(compliance_name, [])
|
||||
for requirement in compliance.Requirements:
|
||||
if requirement.Id in finding_requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AzureENSModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
SubscriptionId=finding.account_name,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_IdGrupoControl=attribute.IdGrupoControl,
|
||||
Requirements_Attributes_Marco=attribute.Marco,
|
||||
Requirements_Attributes_Categoria=attribute.Categoria,
|
||||
Requirements_Attributes_DescripcionControl=attribute.DescripcionControl,
|
||||
Requirements_Attributes_Nivel=attribute.Nivel,
|
||||
Requirements_Attributes_Tipo=attribute.Tipo,
|
||||
Requirements_Attributes_Dimensiones=",".join(
|
||||
attribute.Dimensiones
|
||||
),
|
||||
Requirements_Attributes_ModoEjecucion=attribute.ModoEjecucion,
|
||||
Requirements_Attributes_Dependencias=",".join(
|
||||
attribute.Dependencias
|
||||
),
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AzureENSModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
SubscriptionId="",
|
||||
Location="",
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_IdGrupoControl=attribute.IdGrupoControl,
|
||||
Requirements_Attributes_Marco=attribute.Marco,
|
||||
Requirements_Attributes_Categoria=attribute.Categoria,
|
||||
Requirements_Attributes_DescripcionControl=attribute.DescripcionControl,
|
||||
Requirements_Attributes_Nivel=attribute.Nivel,
|
||||
Requirements_Attributes_Tipo=attribute.Tipo,
|
||||
Requirements_Attributes_Dimensiones=",".join(
|
||||
attribute.Dimensiones
|
||||
),
|
||||
Requirements_Attributes_ModoEjecucion=attribute.ModoEjecucion,
|
||||
Requirements_Attributes_Dependencias=",".join(
|
||||
attribute.Dependencias
|
||||
),
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -0,0 +1,103 @@
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.ens.models import GCPENSModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class GCPENS(ComplianceOutput):
|
||||
"""
|
||||
This class represents the GCP ENS compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into GCP ENS compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into AWS ENS compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
# Get the compliance requirements for the finding
|
||||
finding_requirements = finding.compliance.get(compliance_name, [])
|
||||
for requirement in compliance.Requirements:
|
||||
if requirement.Id in finding_requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = GCPENSModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
ProjectId=finding.account_uid,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_IdGrupoControl=attribute.IdGrupoControl,
|
||||
Requirements_Attributes_Marco=attribute.Marco,
|
||||
Requirements_Attributes_Categoria=attribute.Categoria,
|
||||
Requirements_Attributes_DescripcionControl=attribute.DescripcionControl,
|
||||
Requirements_Attributes_Nivel=attribute.Nivel,
|
||||
Requirements_Attributes_Tipo=attribute.Tipo,
|
||||
Requirements_Attributes_Dimensiones=",".join(
|
||||
attribute.Dimensiones
|
||||
),
|
||||
Requirements_Attributes_ModoEjecucion=attribute.ModoEjecucion,
|
||||
Requirements_Attributes_Dependencias=",".join(
|
||||
attribute.Dependencias
|
||||
),
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = GCPENSModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
ProjectId="",
|
||||
Location="",
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_IdGrupoControl=attribute.IdGrupoControl,
|
||||
Requirements_Attributes_Marco=attribute.Marco,
|
||||
Requirements_Attributes_Categoria=attribute.Categoria,
|
||||
Requirements_Attributes_DescripcionControl=attribute.DescripcionControl,
|
||||
Requirements_Attributes_Nivel=attribute.Nivel,
|
||||
Requirements_Attributes_Tipo=attribute.Tipo,
|
||||
Requirements_Attributes_Dimensiones=",".join(
|
||||
attribute.Dimensiones
|
||||
),
|
||||
Requirements_Attributes_ModoEjecucion=attribute.ModoEjecucion,
|
||||
Requirements_Attributes_Dependencias=",".join(
|
||||
attribute.Dependencias
|
||||
),
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -28,3 +28,61 @@ class AWSENSModel(BaseModel):
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
|
||||
|
||||
class AzureENSModel(BaseModel):
|
||||
"""
|
||||
AzureENSModel generates a finding's output in CSV ENS format for Azure.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
SubscriptionId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Attributes_IdGrupoControl: str
|
||||
Requirements_Attributes_Marco: str
|
||||
Requirements_Attributes_Categoria: str
|
||||
Requirements_Attributes_DescripcionControl: str
|
||||
Requirements_Attributes_Nivel: str
|
||||
Requirements_Attributes_Tipo: str
|
||||
Requirements_Attributes_Dimensiones: str
|
||||
Requirements_Attributes_ModoEjecucion: str
|
||||
Requirements_Attributes_Dependencias: str
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
|
||||
|
||||
class GCPENSModel(BaseModel):
|
||||
"""
|
||||
GCPENSModel generates a finding's output in CSV ENS format for GCP.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
ProjectId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Attributes_IdGrupoControl: str
|
||||
Requirements_Attributes_Marco: str
|
||||
Requirements_Attributes_Categoria: str
|
||||
Requirements_Attributes_DescripcionControl: str
|
||||
Requirements_Attributes_Nivel: str
|
||||
Requirements_Attributes_Tipo: str
|
||||
Requirements_Attributes_Dimensiones: str
|
||||
Requirements_Attributes_ModoEjecucion: str
|
||||
Requirements_Attributes_Dependencias: str
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
|
||||
@@ -39,11 +39,12 @@ def get_generic_compliance_table(
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -61,11 +61,12 @@ def get_kisa_ismsp_table(
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -69,11 +69,12 @@ def get_mitre_attack_table(
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 9000 to 9999 are reserved for Jira exceptions
|
||||
class JiraBaseException(ProwlerException):
|
||||
"""Base class for Jira exceptions."""
|
||||
|
||||
JIRA_ERROR_CODES = {
|
||||
(9000, "JiraNoProjectsError"): {
|
||||
"message": "No projects were found in Jira.",
|
||||
"remediation": "Please create a project in Jira.",
|
||||
},
|
||||
(9001, "JiraAuthenticationError"): {
|
||||
"message": "Failed to authenticate with Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again. Needed scopes are: read:jira-user read:jira-work write:jira-work",
|
||||
},
|
||||
(9002, "JiraTestConnectionError"): {
|
||||
"message": "Failed to connect to Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9003, "JiraCreateIssueError"): {
|
||||
"message": "Failed to create an issue in Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9004, "JiraGetProjectsError"): {
|
||||
"message": "Failed to get projects from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9005, "JiraGetCloudIDError"): {
|
||||
"message": "Failed to get the cloud ID from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9006, "JiraGetCloudIDNoResourcesError"): {
|
||||
"message": "No resources were found in Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9007, "JiraGetCloudIDResponseError"): {
|
||||
"message": "Failed to get the cloud ID from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9008, "JiraRefreshTokenResponseError"): {
|
||||
"message": "Failed to refresh the access token, response code did not match 200.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9009, "JiraRefreshTokenError"): {
|
||||
"message": "Failed to refresh the access token.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9010, "JiraGetAccessTokenError"): {
|
||||
"message": "Failed to get the access token.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9011, "JiraGetAuthResponseError"): {
|
||||
"message": "Failed to authenticate with Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9012, "JiraGetProjectsResponseError"): {
|
||||
"message": "Failed to get projects from Jira, response code did not match 200.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9013, "JiraSendFindingsResponseError"): {
|
||||
"message": "Failed to send findings to Jira, response code did not match 201.",
|
||||
"remediation": "Please check the finding format and try again.",
|
||||
},
|
||||
(9014, "JiraGetAvailableIssueTypesError"): {
|
||||
"message": "Failed to get available issue types from Jira.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9015, "JiraGetAvailableIssueTypesResponseError"): {
|
||||
"message": "Failed to get available issue types from Jira, response code did not match 200.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(9016, "JiraInvalidIssueTypeError"): {
|
||||
"message": "The issue type is invalid.",
|
||||
"remediation": "Please check the issue type and try again.",
|
||||
},
|
||||
(9017, "JiraNoTokenError"): {
|
||||
"message": "No token was found.",
|
||||
"remediation": "Make sure the token is set when using the Jira integration.",
|
||||
},
|
||||
(9018, "JiraInvalidProjectKeyError"): {
|
||||
"message": "The project key is invalid.",
|
||||
"remediation": "Please check the project key and try again.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
module = "Jira"
|
||||
error_info = self.JIRA_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code=code,
|
||||
source=module,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
)
|
||||
|
||||
|
||||
class JiraNoProjectsError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraAuthenticationError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraTestConnectionError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9002, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraCreateIssueError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9003, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetProjectsError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9004, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetCloudIDError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9005, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetCloudIDNoResourcesError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9006, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetCloudIDResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9007, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraRefreshTokenResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9008, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraRefreshTokenError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9009, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAccessTokenError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9010, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAuthResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9011, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetProjectsResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9012, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraSendFindingsResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9013, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAvailableIssueTypesError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9014, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraGetAvailableIssueTypesResponseError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9015, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraInvalidIssueTypeError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9016, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraNoTokenError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9017, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class JiraInvalidProjectKeyError(JiraBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
9018, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -185,7 +185,7 @@ class Slack:
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
"url": "https://goto.prowler.com/slack",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ from prowler.providers.aws.exceptions.exceptions import (
|
||||
AWSIAMRoleARNPartitionEmptyError,
|
||||
AWSIAMRoleARNRegionNotEmtpyError,
|
||||
AWSIAMRoleARNServiceNotIAMnorSTSError,
|
||||
AWSInvalidPartitionError,
|
||||
AWSInvalidProviderIdError,
|
||||
AWSNoCredentialsError,
|
||||
AWSProfileNotFoundError,
|
||||
@@ -62,12 +63,32 @@ from prowler.providers.aws.models import (
|
||||
AWSMFAInfo,
|
||||
AWSOrganizationsInfo,
|
||||
AWSSession,
|
||||
Partition,
|
||||
)
|
||||
from prowler.providers.common.models import Audit_Metadata, Connection
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
class AwsProvider(Provider):
|
||||
"""
|
||||
AwsProvider class is the main class for the AWS provider.
|
||||
|
||||
This class is responsible for initializing the AWS provider, setting up the AWS session, validating the AWS
|
||||
credentials, assuming an IAM role, getting the AWS Organizations metadata, and setting the AWS identity.
|
||||
|
||||
Attributes:
|
||||
_type (str): The provider type.
|
||||
_identity (AWSIdentityInfo): The AWS provider identity information.
|
||||
_session (AWSSession): The AWS provider session.
|
||||
_organizations_metadata (AWSOrganizationsInfo): The AWS Organizations metadata.
|
||||
_audit_resources (list): The list of resources to audit.
|
||||
_audit_config (dict): The audit configuration.
|
||||
_scan_unused_services (bool): A boolean indicating whether to scan unused services.
|
||||
_enabled_regions (set): The set of enabled regions.
|
||||
_mutelist (AWSMutelist): The AWS provider mutelist.
|
||||
audit_metadata (Audit_Metadata): The audit metadata.
|
||||
"""
|
||||
|
||||
_type: str = "aws"
|
||||
_identity: AWSIdentityInfo
|
||||
_session: AWSSession
|
||||
@@ -106,10 +127,10 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
Initializes the AWS provider.
|
||||
|
||||
Arguments:
|
||||
Args:
|
||||
- retries_max_attempts: The maximum number of retries for the AWS client.
|
||||
- role_arn: The ARN of the IAM role to assume.
|
||||
- session_duration: The duration of the session in seconds.
|
||||
- session_duration: The duration of the session in seconds, between 900 and 43200.
|
||||
- external_id: The external ID to use when assuming the IAM role.
|
||||
- role_session_name: The name of the session when assuming the IAM role.
|
||||
- mfa: A boolean indicating whether MFA is enabled.
|
||||
@@ -134,6 +155,37 @@ class AwsProvider(Provider):
|
||||
- ArgumentTypeError: If the input external ID is invalid.
|
||||
- ArgumentTypeError: If the input role session name is invalid.
|
||||
|
||||
Usage:
|
||||
- Boto3 is used so we follow their credential setup process:
|
||||
- Authentication: Make sure you have properly configured your AWS CLI with a valid Access Key and Region or declare the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.
|
||||
- aws configure
|
||||
or
|
||||
- export AWS_ACCESS_KEY_ID="ASXXXXXXX"
|
||||
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
|
||||
export AWS_SESSION_TOKEN="XXXXXXXXX"
|
||||
- To create a new aws object you can use:
|
||||
- aws = AwsProvider()
|
||||
- aws = AwsProvider(aws_access_key_id="ASXXXXXXX", aws_secret_access_key="XXXXXXXXX", aws_session_token="XXXXXXXXX")
|
||||
- Profile: If you have multiple profiles in your AWS CLI configuration, you can specify the profile to use:
|
||||
- aws = AwsProvider(profile="profile_name")
|
||||
- MFA: If you have MFA enabled you can specify it:
|
||||
- aws = AwsProvider(mfa=True)
|
||||
* Note: If you have MFA enabled you will be prompted to enter the MFA ARN and the MFA TOTP code.
|
||||
* Note: Take into account that you can use static credentials or a profile, with the combination of MFA.
|
||||
|
||||
- Assume Role: *Requires authentication.* Prowler can be used against multiple accounts using IAM Assume Role features depending on each use case:
|
||||
- Set up a custom profile inside your AWS CLI configuration file:
|
||||
- [profile profile_name]
|
||||
role_arn = arn:aws:iam::123456789012:role/role_name
|
||||
- aws = AwsProvider(profile="profile_name")
|
||||
- Use role_arn directly:
|
||||
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name")
|
||||
- Use role_arn with session duration(in seconds, by default 3600) and external ID:
|
||||
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name", session_duration=3600, external_id="external_id")
|
||||
- Use custom role session name:
|
||||
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name", role_session_name="custom_session_name")
|
||||
* Note: You can use the combination of MFA with Assume Role.
|
||||
- aws = AwsProvider(role_arn="arn:aws:iam::123456789012:role/role_name", mfa=True)
|
||||
"""
|
||||
|
||||
logger.info("Initializing AWS provider ...")
|
||||
@@ -367,7 +419,7 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
get_organizations_info returns a AWSOrganizationsInfo object if the account to be audited is a delegated administrator for AWS Organizations or if the AWS Organizations Role ARN (--organizations-role) is passed.
|
||||
|
||||
Arguments:
|
||||
Args:
|
||||
- organizations_session: needs to be a Session object with permissions to do organizations:DescribeAccount and organizations:ListTagsForResource.
|
||||
- aws_account_id: is the AWS Account ID from which we want to get the AWS Organizations account metadata
|
||||
|
||||
@@ -424,6 +476,21 @@ class AwsProvider(Provider):
|
||||
regions: set,
|
||||
profile_region: str,
|
||||
) -> AWSIdentityInfo:
|
||||
"""
|
||||
set_identity sets the AWS provider identity information.
|
||||
|
||||
Args:
|
||||
- caller_identity: The AWS caller identity information.
|
||||
- profile: The AWS CLI profile name.
|
||||
- regions: A set of regions to audit.
|
||||
- profile_region: The AWS CLI profile region.
|
||||
|
||||
Returns:
|
||||
- AWSIdentityInfo: The AWS provider identity information.
|
||||
|
||||
Raises:
|
||||
- AWSInvalidProviderIdError: If the AWS provider ID is invalid.
|
||||
"""
|
||||
logger.info(f"Original AWS Caller Identity UserId: {caller_identity.user_id}")
|
||||
logger.info(f"Original AWS Caller Identity ARN: {caller_identity.arn}")
|
||||
|
||||
@@ -447,6 +514,22 @@ class AwsProvider(Provider):
|
||||
aws_secret_access_key: str = None,
|
||||
aws_session_token: Optional[str] = None,
|
||||
) -> Session:
|
||||
"""
|
||||
setup_session sets up an AWS session using the provided credentials.
|
||||
|
||||
Args:
|
||||
- mfa: A boolean indicating whether MFA is enabled.
|
||||
- profile: The name of the AWS CLI profile to use.
|
||||
- aws_access_key_id: The AWS access key ID.
|
||||
- aws_secret_access_key: The AWS secret access key.
|
||||
- aws_session_token: The AWS session token, optional.
|
||||
|
||||
Returns:
|
||||
- Session: The AWS session.
|
||||
|
||||
Raises:
|
||||
- AWSSetUpSessionError: If an error occurs during the setup process.
|
||||
"""
|
||||
try:
|
||||
logger.debug("Creating original session ...")
|
||||
|
||||
@@ -588,6 +671,21 @@ class AwsProvider(Provider):
|
||||
return refreshed_credentials
|
||||
|
||||
def print_credentials(self):
|
||||
"""
|
||||
Print the AWS credentials.
|
||||
|
||||
This method prints the AWS credentials used by the provider.
|
||||
|
||||
Example output:
|
||||
```
|
||||
Using the AWS credentials below:
|
||||
AWS-CLI Profile: default
|
||||
AWS Regions: all
|
||||
AWS Account: 123456789012
|
||||
User Id: AIDAJDPLRKLG7EXAMPLE
|
||||
Caller Identity ARN: arn:aws:iam::123456789012:user/prowler
|
||||
```
|
||||
"""
|
||||
# Beautify audited regions, set "all" if there is no filter region
|
||||
regions = (
|
||||
", ".join(self._identity.audited_regions)
|
||||
@@ -659,7 +757,7 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
get_available_aws_service_regions returns the available regions for the given service and partition.
|
||||
|
||||
Arguments:
|
||||
Args:
|
||||
- service: The AWS service name.
|
||||
- partition: The AWS partition name. Default is "aws".
|
||||
- audited_regions: A set of regions to audit. Default is None.
|
||||
@@ -679,6 +777,12 @@ class AwsProvider(Provider):
|
||||
def get_checks_from_input_arn(self) -> set:
|
||||
"""
|
||||
get_checks_from_input_arn gets the list of checks from the input arns
|
||||
|
||||
Returns:
|
||||
- set: set of strings representing the checks from the input arns
|
||||
|
||||
Example:
|
||||
checks = get_checks_from_input_arn()
|
||||
"""
|
||||
checks_from_arn = set()
|
||||
is_subservice_in_checks = False
|
||||
@@ -745,7 +849,18 @@ class AwsProvider(Provider):
|
||||
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def get_regions_from_audit_resources(self, audit_resources: list) -> set:
|
||||
"""get_regions_from_audit_resources gets the regions from the audit resources arns"""
|
||||
"""get_regions_from_audit_resources gets the regions from the audit resources arns
|
||||
|
||||
Args:
|
||||
- audit_resources: list of ARNs of the resources to audit
|
||||
|
||||
Returns:
|
||||
- set: set of strings representing the regions from the audit resources arns
|
||||
|
||||
Example:
|
||||
audit_resources = ["arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0"]
|
||||
regions = get_regions_from_audit_resources(audit_resources)
|
||||
"""
|
||||
audited_regions = set()
|
||||
for resource in audit_resources:
|
||||
region = resource.split(":")[3]
|
||||
@@ -805,7 +920,18 @@ class AwsProvider(Provider):
|
||||
raise error
|
||||
|
||||
def get_default_region(self, service: str) -> str:
|
||||
"""get_default_region returns the default region based on the profile and audited service regions"""
|
||||
"""get_default_region returns the default region based on the profile and audited service regions
|
||||
|
||||
Args:
|
||||
- service: The AWS service name
|
||||
|
||||
Returns:
|
||||
- str: The default region for the given service
|
||||
|
||||
Example:
|
||||
service = "ec2"
|
||||
default_region = get_default_region(service)
|
||||
"""
|
||||
try:
|
||||
service_regions = AwsProvider.get_available_aws_service_regions(
|
||||
service, self._identity.partition, self._identity.audited_regions
|
||||
@@ -826,7 +952,14 @@ class AwsProvider(Provider):
|
||||
raise error
|
||||
|
||||
def get_global_region(self) -> str:
|
||||
"""get_global_region returns the global region based on the audited partition"""
|
||||
"""get_global_region returns the global region based on the audited partition
|
||||
|
||||
Returns:
|
||||
- str: The global region for the audited partition
|
||||
|
||||
Example:
|
||||
global_region = get_global_region()a
|
||||
"""
|
||||
global_region = "us-east-1"
|
||||
if self._identity.partition == "aws-cn":
|
||||
global_region = "cn-north-1"
|
||||
@@ -838,7 +971,14 @@ class AwsProvider(Provider):
|
||||
|
||||
@staticmethod
|
||||
def input_role_mfa_token_and_code() -> AWSMFAInfo:
|
||||
"""input_role_mfa_token_and_code ask for the AWS MFA ARN and TOTP and returns it."""
|
||||
"""input_role_mfa_token_and_code ask for the AWS MFA ARN and TOTP and returns it.
|
||||
|
||||
Returns:
|
||||
- AWSMFAInfo: An object containing the MFA ARN and TOTP code
|
||||
|
||||
Example:
|
||||
mfa_info = input_role_mfa_token_and_code()
|
||||
"""
|
||||
mfa_ARN = input("Enter ARN of MFA: ")
|
||||
mfa_TOTP = input("Enter MFA code: ")
|
||||
return AWSMFAInfo(arn=mfa_ARN, totp=mfa_TOTP)
|
||||
@@ -846,6 +986,12 @@ class AwsProvider(Provider):
|
||||
def set_session_config(self, retries_max_attempts: int) -> Config:
|
||||
"""
|
||||
set_session_config returns a botocore Config object with the Prowler user agent and the default retrier configuration if nothing is passed as argument
|
||||
|
||||
Args:
|
||||
- retries_max_attempts: The maximum number of retries for the standard retrier config
|
||||
|
||||
Returns:
|
||||
- Config: The botocore Config object
|
||||
"""
|
||||
# Set the maximum retries for the standard retrier config
|
||||
default_session_config = Config(
|
||||
@@ -872,6 +1018,13 @@ class AwsProvider(Provider):
|
||||
) -> AWSCredentials:
|
||||
"""
|
||||
assume_role assumes the IAM roles passed with the given session and returns AWSCredentials
|
||||
|
||||
Args:
|
||||
- session: The AWS session object
|
||||
- assumed_role_info: The AWSAssumeRoleInfo object
|
||||
|
||||
Returns:
|
||||
- AWSCredentials: The AWS credentials for the assumed role
|
||||
"""
|
||||
try:
|
||||
role_session_name = (
|
||||
@@ -923,7 +1076,14 @@ class AwsProvider(Provider):
|
||||
)
|
||||
|
||||
def get_aws_enabled_regions(self, current_session: Session) -> set:
|
||||
"""get_aws_enabled_regions returns a set of enabled AWS regions"""
|
||||
"""get_aws_enabled_regions returns a set of enabled AWS regions
|
||||
|
||||
Args:
|
||||
- current_session: The AWS session object
|
||||
|
||||
Returns:
|
||||
- set: set of strings representing the enabled AWS regions
|
||||
"""
|
||||
try:
|
||||
# EC2 Client to check enabled regions
|
||||
service = "ec2"
|
||||
@@ -947,6 +1107,12 @@ class AwsProvider(Provider):
|
||||
# TODO: review this function
|
||||
# Maybe this should be done within the AwsProvider and not in __main__.py
|
||||
def get_checks_to_execute_by_audit_resources(self) -> set[str]:
|
||||
"""
|
||||
get_checks_to_execute_by_audit_resources gets the checks to execute based on the audit resources
|
||||
|
||||
Returns:
|
||||
- set: set of strings representing the checks to execute
|
||||
"""
|
||||
# Once the provider is set and we have the eventual checks from arn, it is time to exclude the others
|
||||
try:
|
||||
checks = set()
|
||||
@@ -1273,7 +1439,7 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
Create an STS session client.
|
||||
|
||||
Parameters:
|
||||
Args:
|
||||
- session (session.Session): The AWS session object.
|
||||
- aws_region (str): The AWS region to use for the session.
|
||||
|
||||
@@ -1297,6 +1463,59 @@ class AwsProvider(Provider):
|
||||
)
|
||||
raise error
|
||||
|
||||
@staticmethod
|
||||
def get_regions(partition: Partition = Partition.aws) -> set:
|
||||
"""
|
||||
Get the available AWS regions from the AWS services JSON file with the ability of filtering by partition.
|
||||
|
||||
Args:
|
||||
partition (str): The AWS partition to retrieve regions for. Defaults to "aws".
|
||||
|
||||
Returns:
|
||||
set: A set of region names.
|
||||
|
||||
Raises:
|
||||
AWSInvalidPartitionError: If the provided partition name is invalid.
|
||||
|
||||
Example:
|
||||
>>> AwsProvider.get_regions("aws")
|
||||
{"af-south-1"}
|
||||
"""
|
||||
|
||||
try:
|
||||
regions = set()
|
||||
data = read_aws_regions_file()
|
||||
|
||||
if partition is None:
|
||||
for service in data["services"].values():
|
||||
for partition in service["regions"]:
|
||||
regions.update(service["regions"][partition])
|
||||
else:
|
||||
partition = Partition(partition)
|
||||
for service in data["services"].values():
|
||||
regions.update(service["regions"][partition.value])
|
||||
|
||||
return regions
|
||||
except ValueError as value_error:
|
||||
logger.error(
|
||||
f"{value_error.__class__.__name__}[{value_error.__traceback__.tb_lineno}]: {value_error}"
|
||||
)
|
||||
raise AWSInvalidPartitionError(
|
||||
message=f"Invalid partition: {partition}",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
except KeyError as key_error:
|
||||
logger.error(
|
||||
f"{key_error.__class__.__name__}[{key_error.__traceback__.tb_lineno}]: {key_error}"
|
||||
)
|
||||
raise AWSInvalidPartitionError(
|
||||
message=f"Invalid partition: {partition}",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(f"{error.__class__.__name__}: {error}")
|
||||
raise error
|
||||
|
||||
|
||||
def read_aws_regions_file() -> dict:
|
||||
"""
|
||||
@@ -1313,29 +1532,21 @@ def read_aws_regions_file() -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def get_aws_available_regions() -> set:
|
||||
"""
|
||||
Get the available AWS regions from the AWS services JSON file.
|
||||
|
||||
Returns:
|
||||
set: A set of available AWS regions.
|
||||
"""
|
||||
try:
|
||||
data = read_aws_regions_file()
|
||||
|
||||
regions = set()
|
||||
for service in data["services"].values():
|
||||
for partition in service["regions"]:
|
||||
for item in service["regions"][partition]:
|
||||
regions.add(item)
|
||||
return regions
|
||||
except Exception as error:
|
||||
logger.error(f"{error.__class__.__name__}: {error}")
|
||||
return set()
|
||||
|
||||
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def get_aws_region_for_sts(session_region: str, regions: set[str]) -> str:
|
||||
"""
|
||||
Get the AWS region for the STS Assume Role operation.
|
||||
|
||||
Args:
|
||||
- session_region (str): The region configured in the AWS session.
|
||||
- regions (set[str]): The regions passed with the -f/--region/--filter-region option.
|
||||
|
||||
Returns:
|
||||
str: The AWS region for the STS Assume Role operation
|
||||
|
||||
Example:
|
||||
aws_region = get_aws_region_for_sts(session_region, regions)
|
||||
"""
|
||||
# If there is no region passed with -f/--region/--filter-region
|
||||
if regions is None or len(regions) == 0:
|
||||
# If you have a region configured in your AWS config or credentials file
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -2621,6 +2627,7 @@
|
||||
"connectcampaigns": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
@@ -3288,6 +3295,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3972,6 +3980,7 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
@@ -3984,6 +3993,7 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
@@ -4390,6 +4400,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4831,6 +4842,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5541,6 +5553,7 @@
|
||||
"iotfleetwise": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-south-1",
|
||||
"eu-central-1",
|
||||
"us-east-1"
|
||||
],
|
||||
@@ -5847,6 +5860,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5963,6 +5977,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -6235,6 +6250,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7152,6 +7168,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7615,15 +7632,9 @@
|
||||
"opsworkscm": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
"us-east-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
@@ -9201,6 +9212,7 @@
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
@@ -9255,7 +9267,6 @@
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -9263,10 +9274,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"
|
||||
@@ -9848,6 +9856,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10646,6 +10655,7 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
@@ -10680,7 +10690,10 @@
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-cn": [
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
@@ -10688,6 +10701,7 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
@@ -10766,6 +10780,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -11082,15 +11097,19 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -11337,6 +11356,7 @@
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
|
||||
@@ -74,6 +74,10 @@ class AWSBaseException(ProwlerException):
|
||||
"message": "The provided AWS Session Token is expired",
|
||||
"remediation": "Get a new AWS Session Token and configure it for the provider.",
|
||||
},
|
||||
(1917, "AWSInvalidPartitionError"): {
|
||||
"message": "The provided AWS partition is invalid",
|
||||
"remediation": "Check the provided AWS partition and ensure it is valid.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
@@ -220,3 +224,10 @@ class AWSSessionTokenExpiredError(AWSCredentialsError):
|
||||
super().__init__(
|
||||
1016, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSInvalidPartitionError(AWSBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1917, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from argparse import ArgumentTypeError, Namespace
|
||||
from re import fullmatch, search
|
||||
|
||||
from prowler.providers.aws.aws_provider import get_aws_available_regions
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.config import ROLE_SESSION_NAME
|
||||
from prowler.providers.aws.lib.arn.arn import arn_type
|
||||
|
||||
@@ -64,7 +64,7 @@ def init_parser(self):
|
||||
"-f",
|
||||
nargs="+",
|
||||
help="AWS region names to run Prowler against",
|
||||
choices=get_aws_available_regions(),
|
||||
choices=AwsProvider.get_regions(partition=None),
|
||||
)
|
||||
# AWS Organizations
|
||||
aws_orgs_subparser = aws_parser.add_argument_group("AWS Organizations")
|
||||
|
||||
@@ -101,3 +101,17 @@ class AWSService:
|
||||
except Exception:
|
||||
# Handle exceptions if necessary
|
||||
pass # Replace 'pass' with any additional exception handling logic. Currently handled within the called function
|
||||
|
||||
def get_unknown_arn(self, resource_type: str = None, region: str = None) -> str:
|
||||
"""
|
||||
Generate an unknown ARN for the service
|
||||
Args:
|
||||
region (str): The region to get the unknown ARN for.
|
||||
resource_type (str): The resource type to get the unknown ARN for
|
||||
Returns:
|
||||
str: The unknown ARN for the region.
|
||||
Examples:
|
||||
>>> service.get_unknown_arn(resource_type="bucket", region="us-east-1")
|
||||
arn:aws:s3:us-east-1:123456789012:bucket/unknown
|
||||
"""
|
||||
return f"arn:{self.audited_partition}:{self.service}:{f'{region}' if region else ''}:{self.audited_account}:{f'{resource_type}/' if resource_type else ''}unknown"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from boto3.session import Session
|
||||
from botocore.config import Config
|
||||
@@ -82,6 +83,29 @@ class AWSMFAInfo:
|
||||
totp: str
|
||||
|
||||
|
||||
class Partition(str, Enum):
|
||||
"""
|
||||
Enum class representing different AWS partitions.
|
||||
|
||||
Attributes:
|
||||
aws (str): Represents the standard AWS commercial regions.
|
||||
aws_cn (str): Represents the AWS China regions.
|
||||
aws_us_gov (str): Represents the AWS GovCloud (US) Regions.
|
||||
aws_iso (str): Represents the AWS ISO (US) Regions.
|
||||
aws_iso_b (str): Represents the AWS ISOB (US) Regions.
|
||||
aws_iso_e (str): Represents the AWS ISOE (Europe) Regions.
|
||||
aws_iso_f (str): Represents the AWS ISOF Regions.
|
||||
"""
|
||||
|
||||
aws = "aws"
|
||||
aws_cn = "aws-cn"
|
||||
aws_us_gov = "aws-us-gov"
|
||||
aws_iso = "aws-iso"
|
||||
aws_iso_b = "aws-iso-b"
|
||||
aws_iso_e = "aws-iso-e"
|
||||
aws_iso_f = "aws-iso-f"
|
||||
|
||||
|
||||
class AWSOutputOptions(ProviderOutputOptions):
|
||||
security_hub_enabled: bool
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ class accessanalyzer_enabled(Check):
|
||||
else:
|
||||
if analyzer.status == "NOT_AVAILABLE":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"IAM Access Analyzer in account {analyzer.name} is not enabled."
|
||||
report.status_extended = f"IAM Access Analyzer in account {accessanalyzer_client.audited_account} is not enabled."
|
||||
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ from prowler.providers.aws.services.accessanalyzer.accessanalyzer_client import
|
||||
|
||||
def fixer(region):
|
||||
"""
|
||||
Enable Access Analyzer in a region. Requires the access-analyzer:CreateAnalyzer permission:
|
||||
Enable Access Analyzer in a region. Requires the access-analyzer:CreateAnalyzer permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
|
||||
@@ -43,8 +43,10 @@ class AccessAnalyzer(AWSService):
|
||||
if analyzer_count == 0:
|
||||
self.analyzers.append(
|
||||
Analyzer(
|
||||
arn=self.audited_account_arn,
|
||||
name=self.audited_account,
|
||||
arn=self.get_unknown_arn(
|
||||
region=regional_client.region, resource_type="analyzer"
|
||||
),
|
||||
name="analyzer/unknown",
|
||||
status="NOT_AVAILABLE",
|
||||
tags=[],
|
||||
type="",
|
||||
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
"Url": "https://aws.amazon.com/blogs/security/how-to-get-ready-for-certificate-transparency/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+3
-1
@@ -28,7 +28,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/apigateway/latest/developerguide/security-monitoring.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.aws.services.appsync.appsync_service import AppSync
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
appsync_client = AppSync(Provider.get_global_provider())
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "appsync_field_level_logging_enabled",
|
||||
"CheckTitle": "AWS AppSync should have field-level logging enabled",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "appsync",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:appsync:{region}:{account-id}:apis/{api-id}",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsAppSyncGraphQLApi",
|
||||
"Description": "This control checks whether an AWS AppSync API (only GraphQL APIs since boto3 doesnt have a method to return other APIs) field-level logging turned on. The control fails if the field resolver log level is set to None.",
|
||||
"Risk": "Without field-level logging enabled, it's difficult to monitor, troubleshoot, and optimize GraphQL API queries effectively.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/appsync-logging-enabled.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws appsync update-graphql-api --api-id <api-id> --log-config fieldLogLevel=<fieldLoggingLevel>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/appsync-controls.html#appsync-2",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable field-level logging for your AWS AppSync API to monitor and troubleshoot GraphQL queries effectively.",
|
||||
"Url": "https://docs.aws.amazon.com/appsync/latest/devguide/monitoring.html#setup-and-configuration"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.appsync.appsync_client import appsync_client
|
||||
|
||||
|
||||
class appsync_field_level_logging_enabled(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
# Check only GraphQL APIs because boto3 does not have a method to get other types of AppSync APIs (list_apis is not working)
|
||||
for api in appsync_client.graphql_apis.values():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = api.region
|
||||
report.resource_id = api.id
|
||||
report.resource_arn = api.arn
|
||||
report.resource_tags = api.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"AppSync API {api.name} has field log level enabled."
|
||||
)
|
||||
if api.field_log_level != "ALL" and api.field_log_level != "ERROR":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"AppSync API {api.name} does not have field log level enabled."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "appsync_graphql_api_no_api_key_authentication",
|
||||
"CheckTitle": "AWS AppSync GraphQL APIs should not be authenticated with API keys",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "appsync",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:appsync:{region}:{account-id}:apis/{api-id}",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsAppSyncGraphQLApi",
|
||||
"Description": "This control checks whether your application uses an API key to interact with an AWS AppSync GraphQL API. The control fails if an AWS AppSync GraphQL API is authenticated with an API key.",
|
||||
"Risk": "API keys in AppSync can expose applications to unauthorized access if compromised. Avoiding API keys helps reduce the risk of unintended access.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/appsync-authorization-check.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws appsync update-graphql-api --api-id <api-id> --authentication-type <authentication-type>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/appsync-controls.html#appsync-5",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use authentication methods other than API keys for AWS AppSync GraphQL APIs, such as AWS_IAM or Amazon Cognito.",
|
||||
"Url": "https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"trustboundaries"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.appsync.appsync_client import appsync_client
|
||||
|
||||
|
||||
class appsync_graphql_api_no_api_key_authentication(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for api in appsync_client.graphql_apis.values():
|
||||
if api.type == "GRAPHQL":
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = api.region
|
||||
report.resource_id = api.id
|
||||
report.resource_arn = api.arn
|
||||
report.resource_tags = api.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"AppSync GraphQL API {api.name} is not using an API KEY for authentication."
|
||||
if api.authentication_type == "API_KEY":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"AppSync GraphQL API {api.name} is using an API KEY for authentication."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
|
||||
class AppSync(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.graphql_apis = {}
|
||||
self.__threading_call__(self._list_graphql_apis)
|
||||
|
||||
def _list_graphql_apis(self, regional_client):
|
||||
logger.info("AppSync - Describing APIs...")
|
||||
try:
|
||||
list_graphql_apis_paginator = regional_client.get_paginator(
|
||||
"list_graphql_apis"
|
||||
)
|
||||
for page in list_graphql_apis_paginator.paginate():
|
||||
for api in page["graphqlApis"]:
|
||||
api_arn = api["arn"]
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(
|
||||
api_arn,
|
||||
self.audit_resources,
|
||||
)
|
||||
):
|
||||
self.graphql_apis[api_arn] = GraphqlApi(
|
||||
id=api["apiId"],
|
||||
name=api["name"],
|
||||
arn=api_arn,
|
||||
region=regional_client.region,
|
||||
type=api.get("apiType", "GRAPHQL"),
|
||||
field_log_level=api.get("logConfig", {}).get(
|
||||
"fieldLogLevel", ""
|
||||
),
|
||||
authentication_type=api.get(
|
||||
"authenticationType", "API_KEY"
|
||||
),
|
||||
tags=[api.get("tags", {})],
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class GraphqlApi(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
arn: str
|
||||
region: str
|
||||
type: str
|
||||
field_log_level: str
|
||||
authentication_type: str
|
||||
tags: Optional[list] = []
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
@@ -183,21 +183,26 @@ class Backup(AWSService):
|
||||
def _list_recovery_points(self, regional_client):
|
||||
logger.info("Backup - Listing Recovery Points...")
|
||||
try:
|
||||
for backup_vault in self.backup_vaults:
|
||||
paginator = regional_client.get_paginator(
|
||||
"list_recovery_points_by_backup_vault"
|
||||
)
|
||||
for page in paginator.paginate(BackupVaultName=backup_vault.name):
|
||||
for recovery_point in page.get("RecoveryPoints", []):
|
||||
self.recovery_points.append(
|
||||
RecoveryPoint(
|
||||
arn=recovery_point.get("RecoveryPointArn"),
|
||||
backup_vault_name=backup_vault.name,
|
||||
encrypted=recovery_point.get("IsEncrypted", False),
|
||||
backup_vault_region=backup_vault.region,
|
||||
tags=[],
|
||||
)
|
||||
)
|
||||
if self.backup_vaults:
|
||||
for backup_vault in self.backup_vaults:
|
||||
paginator = regional_client.get_paginator(
|
||||
"list_recovery_points_by_backup_vault"
|
||||
)
|
||||
for page in paginator.paginate(BackupVaultName=backup_vault.name):
|
||||
for recovery_point in page.get("RecoveryPoints", []):
|
||||
arn = recovery_point.get("RecoveryPointArn")
|
||||
if arn:
|
||||
self.recovery_points.append(
|
||||
RecoveryPoint(
|
||||
arn=arn,
|
||||
backup_vault_name=backup_vault.name,
|
||||
encrypted=recovery_point.get(
|
||||
"IsEncrypted", False
|
||||
),
|
||||
backup_vault_region=backup_vault.region,
|
||||
tags=[],
|
||||
)
|
||||
)
|
||||
except ClientError as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
+4
-2
@@ -8,8 +8,10 @@ class bedrock_model_invocation_logging_enabled(Check):
|
||||
for region, logging in bedrock_client.logging_configurations.items():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = region
|
||||
report.resource_id = bedrock_client.audited_account
|
||||
report.resource_arn = bedrock_client.audited_account_arn
|
||||
report.resource_id = "model-invocation-logging"
|
||||
report.resource_arn = (
|
||||
bedrock_client._get_model_invocation_logging_arn_template(region)
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "Bedrock Model Invocation Logging is disabled."
|
||||
if logging.enabled:
|
||||
|
||||
+4
-2
@@ -13,8 +13,10 @@ class bedrock_model_invocation_logs_encryption_enabled(Check):
|
||||
cloudwatch_encryption = True
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = region
|
||||
report.resource_id = bedrock_client.audited_account
|
||||
report.resource_arn = bedrock_client.audited_account_arn
|
||||
report.resource_id = "model-invocation-logging"
|
||||
report.resource_arn = (
|
||||
bedrock_client._get_model_invocation_logging_arn_template(region)
|
||||
)
|
||||
report.status = "PASS"
|
||||
report.status_extended = "Bedrock Model Invocation logs are encrypted."
|
||||
if logging.s3_bucket:
|
||||
|
||||
@@ -18,6 +18,13 @@ class Bedrock(AWSService):
|
||||
self.__threading_call__(self._get_guardrail, self.guardrails.values())
|
||||
self.__threading_call__(self._list_tags_for_resource, self.guardrails.values())
|
||||
|
||||
def _get_model_invocation_logging_arn_template(self, region):
|
||||
return (
|
||||
f"arn:{self.audited_partition}:bedrock:{region}:{self.audited_account}:model-invocation-logging"
|
||||
if region
|
||||
else f"arn:{self.audited_partition}:bedrock:{self.region}:{self.audited_account}:model-invocation-logging"
|
||||
)
|
||||
|
||||
def _get_model_invocation_logging_configuration(self, regional_client):
|
||||
logger.info("Bedrock - Getting Model Invocation Logging Configuration...")
|
||||
try:
|
||||
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
|
||||
def fixer(region):
|
||||
"""
|
||||
NOTE: Define the S3 bucket name in the fixer_config.yaml file.
|
||||
Enable CloudTrail in a region. Requires the cloudtrail:CreateTrail permission:
|
||||
Enable CloudTrail in a region. Requires the cloudtrail:CreateTrail permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
|
||||
+2
-4
@@ -67,10 +67,8 @@ class cloudtrail_threat_detection_enumeration(Check):
|
||||
found_potential_enumeration = True
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = cloudtrail_client.region
|
||||
report.resource_id = cloudtrail_client.audited_account
|
||||
report.resource_arn = cloudtrail_client._get_trail_arn_template(
|
||||
cloudtrail_client.region
|
||||
)
|
||||
report.resource_id = aws_identity_arn.split("/")[-1]
|
||||
report.resource_arn = aws_identity_arn
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Potential enumeration attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}."
|
||||
findings.append(report)
|
||||
|
||||
+2
-4
@@ -67,10 +67,8 @@ class cloudtrail_threat_detection_llm_jacking(Check):
|
||||
found_potential_llm_jacking = True
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = cloudtrail_client.region
|
||||
report.resource_id = cloudtrail_client.audited_account
|
||||
report.resource_arn = cloudtrail_client._get_trail_arn_template(
|
||||
cloudtrail_client.region
|
||||
)
|
||||
report.resource_id = aws_identity_arn.split("/")[-1]
|
||||
report.resource_arn = aws_identity_arn
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Potential LLM Jacking attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}."
|
||||
findings.append(report)
|
||||
|
||||
+2
-4
@@ -69,10 +69,8 @@ class cloudtrail_threat_detection_privilege_escalation(Check):
|
||||
found_potential_privilege_escalation = True
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = cloudtrail_client.region
|
||||
report.resource_id = cloudtrail_client.audited_account
|
||||
report.resource_arn = cloudtrail_client._get_trail_arn_template(
|
||||
cloudtrail_client.region
|
||||
)
|
||||
report.resource_id = aws_identity_arn.split("/")[-1]
|
||||
report.resource_arn = aws_identity_arn
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Potential privilege escalation attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}."
|
||||
findings.append(report)
|
||||
|
||||
+4
-2
@@ -20,8 +20,10 @@ class directconnect_connection_redundancy(Check):
|
||||
for region, connections in regions.items():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = region
|
||||
report.resource_arn = directconnect_client.audited_account_arn
|
||||
report.resource_id = directconnect_client.audited_account
|
||||
report.resource_arn = directconnect_client._get_connection_arn_template(
|
||||
region
|
||||
)
|
||||
report.resource_id = "unknown"
|
||||
if connections["Connections"] == 1:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
|
||||
@@ -18,6 +18,13 @@ class DirectConnect(AWSService):
|
||||
self.__threading_call__(self._describe_connections)
|
||||
self.__threading_call__(self._describe_vifs)
|
||||
|
||||
def _get_connection_arn_template(self, region):
|
||||
return (
|
||||
f"arn:{self.audited_partition}:directconnect:{region}:{self.audited_account}:dxcon"
|
||||
if region
|
||||
else f"arn:{self.audited_partition}:directconnect:{self.region}:{self.audited_account}:dxcon"
|
||||
)
|
||||
|
||||
def _describe_connections(self, regional_client):
|
||||
"""List DirectConnect(s) in the given region.
|
||||
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/directoryservice/latest/admin-guide/incident-response.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "dms_endpoint_redis_in_transit_encryption_enabled",
|
||||
"CheckTitle": "Check if DMS endpoints for Redis OSS are encrypted in transit.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "dms",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:dms:region:account-id:endpoint/endpoint-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsDmsEndpoint",
|
||||
"Description": "This control checks whether an AWS DMS endpoint for Redis OSS is configured with a TLS connection. The control fails if the endpoint doesn't have TLS enabled.",
|
||||
"Risk": "Without TLS, data transmitted between databases may be vulnerable to interception or eavesdropping, increasing the risk of data breaches and other security incidents.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Source.Redis.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws dms modify-endpoint --endpoint-arn <endpoint-arn> --redis-settings '{'SslSecurityProtocol': 'ssl-encryption'}'",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-12",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable TLS for DMS endpoints for Redis OSS to ensure encrypted communication during data migration.",
|
||||
"Url": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Target.Redis.html#CHAP_Target.Redis.EndpointSettings"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.dms.dms_client import dms_client
|
||||
|
||||
|
||||
class dms_endpoint_redis_in_transit_encryption_enabled(Check):
|
||||
"""
|
||||
Check if AWS DMS Endpoints for Redis OSS have TLS enabled.
|
||||
|
||||
This class verifies whether each AWS DMS Endpoint configured for Redis OSS is encrypted in transit
|
||||
by checking the `TlsEnabled` property in the endpoint's configuration. The check ensures that
|
||||
TLS is enabled to secure data in transit, preventing unauthorized access and ensuring data integrity.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""
|
||||
Execute the DMS Redis TLS enabled check.
|
||||
|
||||
Iterates over all DMS Endpoints and generates a report indicating whether
|
||||
each Redis OSS endpoint is encrypted in transit.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of report objects with the results of the check.
|
||||
"""
|
||||
findings = []
|
||||
for endpoint_arn, endpoint in dms_client.endpoints.items():
|
||||
if endpoint.engine_name == "redis":
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = endpoint.id
|
||||
report.resource_arn = endpoint_arn
|
||||
report.region = endpoint.region
|
||||
report.resource_tags = endpoint.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Endpoint {endpoint.id} for Redis OSS is not encrypted in transit."
|
||||
if endpoint.redis_ssl_protocol == "ssl-encryption":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"DMS Endpoint {endpoint.id} for Redis OSS is encrypted in transit."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "dms_replication_task_source_logging_enabled",
|
||||
"CheckTitle": "Check if DMS replication tasks for the source database have logging enabled.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "dms",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:dms:region:account-id:task/task-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsDmsReplicationTask",
|
||||
"Description": "This control checks whether logging is enabled with the minimum severity level of LOGGER_SEVERITY_DEFAULT for DMS replication tasks SOURCE_CAPTURE and SOURCE_UNLOAD. The control fails if logging isn't enabled for these tasks or if the minimum severity level is less than LOGGER_SEVERITY_DEFAULT.",
|
||||
"Risk": "Without logging enabled, issues in data migration may go undetected, affecting the integrity and compliance of replicated data.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Monitoring.html#CHAP_Monitoring.ManagingLogs",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws dms modify-replication-task --replication-task-arn <task-arn> --task-settings '{\"Logging\":{\"EnableLogging\":true,\"LogComponents\":[{\"Id\":\"SOURCE_CAPTURE\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"},{\"Id\":\"SOURCE_UNLOAD\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"}]}}'",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-8",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable logging for source database DMS replication tasks with a minimum severity level of LOGGER_SEVERITY_DEFAULT.",
|
||||
"Url": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Tasks.CustomizingTasks.TaskSettings.Logging.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.dms.dms_client import dms_client
|
||||
|
||||
|
||||
class dms_replication_task_source_logging_enabled(Check):
|
||||
"""
|
||||
Check if AWS DMS replication tasks have logging enabled with the required
|
||||
logging components and severity levels.
|
||||
|
||||
This class verifies that each DMS replication task has logging enabled
|
||||
and that the components SOURCE_CAPTURE and SOURCE_UNLOAD are configured with
|
||||
at least LOGGER_SEVERITY_DEFAULT severity level. If either component is missing
|
||||
or does not meet the minimum severity requirement, the check will fail.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""
|
||||
Execute the DMS replication task logging requirements check.
|
||||
|
||||
Iterates over all DMS replication tasks and generates a report indicating
|
||||
whether each task has logging enabled and meets the logging requirements
|
||||
for SOURCE_CAPTURE and SOURCE_UNLOAD components.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of report objects with the results of the check.
|
||||
"""
|
||||
MINIMUM_SEVERITY_LEVELS = [
|
||||
"LOGGER_SEVERITY_DEFAULT",
|
||||
"LOGGER_SEVERITY_DEBUG",
|
||||
"LOGGER_SEVERITY_DETAILED_DEBUG",
|
||||
]
|
||||
findings = []
|
||||
for (
|
||||
replication_task_arn,
|
||||
replication_task,
|
||||
) in dms_client.replication_tasks.items():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = replication_task.id
|
||||
report.resource_arn = replication_task_arn
|
||||
report.region = replication_task.region
|
||||
report.resource_tags = replication_task.tags
|
||||
|
||||
if not replication_task.logging_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not have logging enabled for source events."
|
||||
else:
|
||||
missing_components = []
|
||||
source_capture_compliant = False
|
||||
source_unload_compliant = False
|
||||
|
||||
for component in replication_task.log_components:
|
||||
if (
|
||||
component["Id"] == "SOURCE_CAPTURE"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_capture_compliant = True
|
||||
elif (
|
||||
component["Id"] == "SOURCE_UNLOAD"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_unload_compliant = True
|
||||
|
||||
if not source_capture_compliant:
|
||||
missing_components.append("Source Capture")
|
||||
if not source_unload_compliant:
|
||||
missing_components.append("Source Unload")
|
||||
|
||||
if source_capture_compliant and source_unload_compliant:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} has logging enabled with the minimum severity level in source events."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not meet the minimum severity level of logging in {' and '.join(missing_components)} events."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "dms_replication_task_target_logging_enabled",
|
||||
"CheckTitle": "Check if DMS replication tasks for the target database have logging enabled.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "dms",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:dms:region:account-id:task/task-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsDmsReplicationTask",
|
||||
"Description": "This control checks whether logging is enabled with the minimum severity level of LOGGER_SEVERITY_DEFAULT for DMS replication tasks TARGET_APPLY and TARGET_LOAD. The control fails if logging isn't enabled for these tasks or if the minimum severity level is less than LOGGER_SEVERITY_DEFAULT.",
|
||||
"Risk": "Without logging enabled, issues in data migration may go undetected, affecting the integrity and compliance of replicated data.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Monitoring.html#CHAP_Monitoring.ManagingLogs",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws dms modify-replication-task --replication-task-arn <task-arn> --task-settings '{\"Logging\":{\"EnableLogging\":true,\"LogComponents\":[{\"Id\":\"TARGET_APPLY\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"},{\"Id\":\"TARGET_LOAD\",\"Severity\":\"LOGGER_SEVERITY_DEFAULT\"}]}}'",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-7",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable logging for target database DMS replication tasks with a minimum severity level of LOGGER_SEVERITY_DEFAULT.",
|
||||
"Url": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Tasks.CustomizingTasks.TaskSettings.Logging.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.dms.dms_client import dms_client
|
||||
|
||||
|
||||
class dms_replication_task_target_logging_enabled(Check):
|
||||
"""
|
||||
Check if AWS DMS replication tasks have logging enabled with the required
|
||||
logging components and severity levels.
|
||||
|
||||
This class verifies that each DMS replication task has logging enabled
|
||||
and that the components TARGET_APPLY and TARGET_LOAD are configured with
|
||||
at least LOGGER_SEVERITY_DEFAULT severity level. If either component is missing
|
||||
or does not meet the minimum severity requirement, the check will fail.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""
|
||||
Execute the DMS replication task logging requirements check.
|
||||
|
||||
Iterates over all DMS replication tasks and generates a report indicating
|
||||
whether each task has logging enabled and meets the logging requirements
|
||||
for TARGET_APPLY and TARGET_LOAD components.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of report objects with the results of the check.
|
||||
"""
|
||||
MINIMUM_SEVERITY_LEVELS = [
|
||||
"LOGGER_SEVERITY_DEFAULT",
|
||||
"LOGGER_SEVERITY_DEBUG",
|
||||
"LOGGER_SEVERITY_DETAILED_DEBUG",
|
||||
]
|
||||
findings = []
|
||||
for (
|
||||
replication_task_arn,
|
||||
replication_task,
|
||||
) in dms_client.replication_tasks.items():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = replication_task.id
|
||||
report.resource_arn = replication_task_arn
|
||||
report.region = replication_task.region
|
||||
report.resource_tags = replication_task.tags
|
||||
|
||||
if not replication_task.logging_enabled:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not have logging enabled for target events."
|
||||
else:
|
||||
missing_components = []
|
||||
source_capture_compliant = False
|
||||
source_unload_compliant = False
|
||||
|
||||
for component in replication_task.log_components:
|
||||
if (
|
||||
component["Id"] == "TARGET_APPLY"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_capture_compliant = True
|
||||
elif (
|
||||
component["Id"] == "TARGET_LOAD"
|
||||
and component["Severity"] in MINIMUM_SEVERITY_LEVELS
|
||||
):
|
||||
source_unload_compliant = True
|
||||
|
||||
if not source_capture_compliant:
|
||||
missing_components.append("Target Apply")
|
||||
if not source_unload_compliant:
|
||||
missing_components.append("Target Load")
|
||||
|
||||
if source_capture_compliant and source_unload_compliant:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} has logging enabled with the minimum severity level in target events."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"DMS Replication Task {replication_task.id} does not meet the minimum severity level of logging in {' and '.join(missing_components)} events."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -13,10 +14,14 @@ class DMS(AWSService):
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.instances = []
|
||||
self.endpoints = {}
|
||||
self.replication_tasks = {}
|
||||
self.__threading_call__(self._describe_replication_instances)
|
||||
self.__threading_call__(self._list_tags, self.instances)
|
||||
self.__threading_call__(self._describe_endpoints)
|
||||
self.__threading_call__(self._describe_replication_tasks)
|
||||
self.__threading_call__(self._list_tags, self.endpoints.values())
|
||||
self.__threading_call__(self._describe_replication_tasks)
|
||||
self.__threading_call__(self._list_tags, self.replication_tasks.values())
|
||||
|
||||
def _describe_replication_instances(self, regional_client):
|
||||
logger.info("DMS - Describing DMS Replication Instances...")
|
||||
@@ -71,6 +76,9 @@ class DMS(AWSService):
|
||||
id=endpoint["EndpointIdentifier"],
|
||||
region=regional_client.region,
|
||||
ssl_mode=endpoint.get("SslMode", False),
|
||||
redis_ssl_protocol=endpoint.get("RedisSettings", {}).get(
|
||||
"SslSecurityProtocol", "plaintext"
|
||||
),
|
||||
mongodb_auth_type=endpoint.get("MongoDbSettings", {}).get(
|
||||
"AuthType", "no"
|
||||
),
|
||||
@@ -84,6 +92,37 @@ class DMS(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_replication_tasks(self, regional_client):
|
||||
logger.info("DMS - Describing DMS Replication Tasks for Logging Settings...")
|
||||
try:
|
||||
paginator = regional_client.get_paginator("describe_replication_tasks")
|
||||
for page in paginator.paginate():
|
||||
for task in page["ReplicationTasks"]:
|
||||
arn = task["ReplicationTaskArn"]
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(arn, self.audit_resources)
|
||||
):
|
||||
task_settings = json.loads(
|
||||
task.get("ReplicationTaskSettings", "")
|
||||
)
|
||||
self.replication_tasks[arn] = ReplicationTasks(
|
||||
arn=arn,
|
||||
id=task["ReplicationTaskIdentifier"],
|
||||
region=regional_client.region,
|
||||
source_endpoint_arn=task["SourceEndpointArn"],
|
||||
target_endpoint_arn=task["TargetEndpointArn"],
|
||||
logging_enabled=task_settings.get("Logging", {}).get(
|
||||
"EnableLogging", False
|
||||
),
|
||||
log_components=task_settings.get("Logging", {}).get(
|
||||
"LogComponents", []
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags(self, resource: any):
|
||||
try:
|
||||
resource.tags = self.regional_clients[
|
||||
@@ -100,10 +139,11 @@ class Endpoint(BaseModel):
|
||||
id: str
|
||||
region: str
|
||||
ssl_mode: str
|
||||
tags: Optional[list]
|
||||
redis_ssl_protocol: str
|
||||
mongodb_auth_type: str
|
||||
neptune_iam_auth_enabled: bool = False
|
||||
engine_name: str
|
||||
tags: Optional[list]
|
||||
|
||||
|
||||
class RepInstance(BaseModel):
|
||||
@@ -117,3 +157,14 @@ class RepInstance(BaseModel):
|
||||
multi_az: bool
|
||||
region: str
|
||||
tags: Optional[list] = []
|
||||
|
||||
|
||||
class ReplicationTasks(BaseModel):
|
||||
arn: str
|
||||
id: str
|
||||
region: str
|
||||
source_endpoint_arn: str
|
||||
target_endpoint_arn: str
|
||||
logging_enabled: bool = False
|
||||
log_components: list[dict] = []
|
||||
tags: Optional[list] = []
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.documentdb.documentdb_client import (
|
||||
documentdb_client,
|
||||
)
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the attributes of a DocumentDB cluster snapshot to remove public access.
|
||||
Specifically, this fixer removes the 'all' value from the 'restore' attribute to
|
||||
prevent the snapshot from being publicly accessible. Requires the rds:ModifyDBClusterSnapshotAttribute permissions.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "rds:ModifyDBClusterSnapshotAttribute",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The DB cluster snapshot identifier.
|
||||
region (str): AWS region where the snapshot exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (public access is removed), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = documentdb_client.regional_clients[region]
|
||||
regional_client.modify_db_cluster_snapshot_attribute(
|
||||
DBClusterSnapshotIdentifier=resource_id,
|
||||
AttributeName="restore",
|
||||
ValuesToRemove=["all"],
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+2
-1
@@ -5,7 +5,8 @@ from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
def fixer(region):
|
||||
"""
|
||||
Enable EBS encryption by default in a region. NOTE: Custom KMS keys for EBS Default Encryption may be overwritten.
|
||||
Requires the ec2:EnableEbsEncryptionByDefault permission:
|
||||
Requires the ec2:EnableEbsEncryptionByDefault permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
|
||||
def fixer(resource_id: str, region: str) -> bool:
|
||||
"""
|
||||
Modify the attributes of an EBS snapshot to remove public access.
|
||||
Specifically, this fixer removes the 'all' value from the 'createVolumePermission' attribute to
|
||||
prevent the snapshot from being publicly accessible. Requires the ec2:ModifySnapshotAttribute permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "ec2:ModifySnapshotAttribute",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
resource_id (str): The snapshot identifier.
|
||||
region (str): AWS region where the snapshot exists.
|
||||
Returns:
|
||||
bool: True if the operation is successful (public access is removed), False otherwise.
|
||||
"""
|
||||
try:
|
||||
regional_client = ec2_client.regional_clients[region]
|
||||
regional_client.modify_snapshot_attribute(
|
||||
SnapshotId=resource_id,
|
||||
Attribute="createVolumePermission",
|
||||
OperationType="remove",
|
||||
GroupNames=["all"],
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
+2
-1
@@ -5,7 +5,8 @@ from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
def fixer(region):
|
||||
"""
|
||||
Enable EBS snapshot block public access in a region.
|
||||
Requires the ec2:EnableSnapshotBlockPublicAccess permission:
|
||||
Requires the ec2:EnableSnapshotBlockPublicAccess permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"ServiceName": "ec2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id",
|
||||
"Severity": "medium",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsEc2Instance",
|
||||
"Description": "Ensure Instance Metadata Service Version 2 (IMDSv2) is enforced for EC2 instances at the account level to protect against SSRF vulnerabilities.",
|
||||
"Risk": "EC2 instances that use IMDSv1 are vulnerable to SSRF attacks.",
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@ from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
def fixer(region):
|
||||
"""
|
||||
Enable IMDSv2 for EC2 instances in the specified region.
|
||||
Requires the ec2:ModifyInstanceMetadataDefaults permission:
|
||||
Requires the ec2:ModifyInstanceMetadataDefaults permission.
|
||||
Permissions:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"ServiceName": "ec2",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "medium",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsEc2Instance",
|
||||
"Description": "Check if EC2 Instance Metadata Service Version 2 (IMDSv2) is Enabled and Required.",
|
||||
"Risk": "Using IMDSv2 will protect from misconfiguration and SSRF vulnerabilities. IMDSv1 will not.",
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"forensics-ready"
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
|
||||
+4
-1
@@ -25,7 +25,10 @@
|
||||
"Url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"forensics-ready",
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from prowler.providers.aws.services.firehose.firehose_service import Firehose
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
firehose_client = Firehose(Provider.get_global_provider())
|
||||
@@ -0,0 +1,96 @@
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from botocore.client import ClientError
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
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 Firehose(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.delivery_streams = {}
|
||||
self.__threading_call__(self._list_delivery_streams)
|
||||
self.__threading_call__(
|
||||
self._list_tags_for_delivery_stream, self.delivery_streams.values()
|
||||
)
|
||||
self.__threading_call__(
|
||||
self._describe_delivery_stream, self.delivery_streams.values()
|
||||
)
|
||||
|
||||
def _list_delivery_streams(self, regional_client):
|
||||
logger.info("Firehose - Listing delivery streams...")
|
||||
try:
|
||||
for stream_name in regional_client.list_delivery_streams()[
|
||||
"DeliveryStreamNames"
|
||||
]:
|
||||
stream_arn = f"arn:{self.audited_partition}:firehose:{regional_client.region}:{self.audited_account}:deliverystream/{stream_name}"
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(stream_arn, self.audit_resources)
|
||||
):
|
||||
self.delivery_streams[stream_arn] = DeliveryStream(
|
||||
arn=stream_arn,
|
||||
name=stream_name,
|
||||
region=regional_client.region,
|
||||
)
|
||||
except ClientError as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags_for_delivery_stream(self, stream):
|
||||
logger.info(f"Firehose - Listing tags for stream {stream.name}...")
|
||||
try:
|
||||
stream.tags = (
|
||||
self.regional_clients[stream.region]
|
||||
.list_tags_for_delivery_stream(DeliveryStreamName=stream.name)
|
||||
.get("Tags", [])
|
||||
)
|
||||
except ClientError as error:
|
||||
logger.error(
|
||||
f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_delivery_stream(self, stream):
|
||||
logger.info(f"Firehose - Describing stream {stream.name}...")
|
||||
try:
|
||||
describe_stream = self.regional_clients[
|
||||
stream.region
|
||||
].describe_delivery_stream(DeliveryStreamName=stream.name)
|
||||
encryption_config = describe_stream.get(
|
||||
"DeliveryStreamDescription", {}
|
||||
).get("DeliveryStreamEncryptionConfiguration", {})
|
||||
stream.kms_encryption = EncryptionStatus(
|
||||
encryption_config.get("Status", "DISABLED")
|
||||
)
|
||||
stream.kms_key_arn = encryption_config.get("KeyARN", "")
|
||||
except ClientError as error:
|
||||
logger.error(
|
||||
f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class EncryptionStatus(Enum):
|
||||
"""Possible values for the status of the encryption of a Firehose stream"""
|
||||
|
||||
ENABLED = "ENABLED"
|
||||
DISABLED = "DISABLED"
|
||||
ENABLING = "ENABLING"
|
||||
DISABLING = "DISABLING"
|
||||
ENABLING_FAILED = "ENABLING_FAILED"
|
||||
DISABLING_FAILED = "DISABLING_FAILED"
|
||||
|
||||
|
||||
class DeliveryStream(BaseModel):
|
||||
"""Model for a Firehose Delivery Stream"""
|
||||
|
||||
arn: str
|
||||
name: str
|
||||
region: str
|
||||
kms_key_arn: Optional[str] = Field(default_factory=str)
|
||||
kms_encryption: Optional[str] = Field(default_factory=str)
|
||||
tags: Optional[List[Dict[str, str]]] = Field(default_factory=list)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "firehose_stream_encrypted_at_rest",
|
||||
"CheckTitle": "DataFirehose delivery streams should be encrypted at rest.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
|
||||
],
|
||||
"ServiceName": "firehose",
|
||||
"SubServiceName": "Ensure DataFirehose delivery streams are encrypted at rest.",
|
||||
"ResourceIdTemplate": "arn:partition:firehose:region:account-id:deliverystream/delivery-stream-id",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsKinesisFirehoseDeliveryStream",
|
||||
"Description": "",
|
||||
"Risk": "Without encryption at rest, data in Amazon Kinesis Data Firehose delivery streams is vulnerable to unauthorized access if the storage layer is compromised. This increases the risk of sensitive information exposure, potentially leading to data breaches or non-compliance with security regulations.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws firehose update-delivery-stream --delivery-stream-name <delivery-stream-name> --delivery-stream-encryption-configuration-input '{ \"KeyType\": \"CUSTOMER_MANAGED_CMK\", \"KeyARN\": \"<kms-key-arn>\" }'",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-kinesis-firehoses-delivery-stream-is-encrypted/",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/datafirehose-controls.html#datafirehose-1",
|
||||
"Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Firehose/delivery-stream-encrypted-with-kms-customer-master-keys.html"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable server-side encryption for Kinesis Firehose delivery streams using AWS Key Management Service (KMS). This encrypts data at rest, ensuring that sensitive information remains secure and compliant with regulatory standards.",
|
||||
"Url": "https://docs.aws.amazon.com/firehose/latest/dev/encryption.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.firehose.firehose_client import firehose_client
|
||||
from prowler.providers.aws.services.firehose.firehose_service import EncryptionStatus
|
||||
|
||||
|
||||
class firehose_stream_encrypted_at_rest(Check):
|
||||
"""Check if Firehose Streams are encrypted at rest.
|
||||
|
||||
This class verifies that all Firehose Streams have at rest encryption enabled by checking if KMS encryption is active and a KMS Key is configured.
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""Execute the Firehose Stream Encrypted at Rest check.
|
||||
|
||||
Iterates over all Firehose Streams and checks if KMS encryption is enabled and a KMS Key is configured.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of reports for each Firehose Stream.
|
||||
"""
|
||||
findings = []
|
||||
for stream in firehose_client.delivery_streams.values():
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = stream.region
|
||||
report.resource_id = stream.name
|
||||
report.resource_arn = stream.arn
|
||||
report.resource_tags = stream.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Firehose Stream {stream.name} does have at rest encryption enabled."
|
||||
)
|
||||
|
||||
if stream.kms_encryption != EncryptionStatus.ENABLED:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
+3
-1
@@ -25,7 +25,9 @@
|
||||
"Url": "https://docs.aws.amazon.com/glue/latest/dg/monitor-continuous-logging-enable.html"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "This check is being removed since logs for all AWS Glue jobs are now always sent to Amazon CloudWatch."
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user