mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-23 06:12:00 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
+6 |
f1f0609697 |
+2
-43
@@ -5,7 +5,6 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# v5
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -15,69 +14,29 @@ updates:
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
- package-ecosystem: "pip"
|
||||
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: "monthly"
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
- "v3"
|
||||
|
||||
@@ -79,10 +79,3 @@ 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,11 +3,7 @@ name: build-lint-push-containers
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# For `v3-latest`
|
||||
- "v3"
|
||||
# For `v4-latest`
|
||||
- "v4.6"
|
||||
# For `latest`
|
||||
- "master"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
@@ -84,8 +80,8 @@ jobs:
|
||||
;;
|
||||
|
||||
4)
|
||||
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
|
||||
echo "LATEST_TAG=latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.85.0
|
||||
uses: trufflesecurity/trufflehog@v3.82.6
|
||||
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@v5
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -85,7 +85,7 @@ repos:
|
||||
# For running trufflehog in docker, use the following entry instead:
|
||||
# entry: bash -c 'docker run -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --only-verified --fail'
|
||||
language: system
|
||||
stages: ["pre-commit", "pre-push"]
|
||||
stages: ["commit", "push"]
|
||||
|
||||
- id: bandit
|
||||
name: bandit
|
||||
|
||||
+7
-6
@@ -2,28 +2,29 @@ FROM python:3.12-alpine
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
# Update system dependencies and install essential tools
|
||||
# Update system dependencies
|
||||
#hadolint ignore=DL3018
|
||||
RUN apk --no-cache upgrade && apk --no-cache add curl git
|
||||
RUN apk --no-cache upgrade && apk --no-cache add curl
|
||||
|
||||
# Create non-root user
|
||||
# Create nonroot user
|
||||
RUN mkdir -p /home/prowler && \
|
||||
echo 'prowler:x:1000:1000:prowler:/home/prowler:' > /etc/passwd && \
|
||||
echo 'prowler:x:1000:' > /etc/group && \
|
||||
chown -R prowler:prowler /home/prowler
|
||||
USER prowler
|
||||
|
||||
# Copy necessary files
|
||||
# Copy necessary files
|
||||
WORKDIR /home/prowler
|
||||
COPY prowler/ /home/prowler/prowler/
|
||||
COPY dashboard/ /home/prowler/dashboard/
|
||||
COPY pyproject.toml /home/prowler
|
||||
COPY README.md /home/prowler
|
||||
|
||||
# Install Python dependencies
|
||||
# Install dependencies
|
||||
ENV HOME='/home/prowler'
|
||||
ENV PATH="$HOME/.local/bin:$PATH"
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
#hadolint ignore=DL3013
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir .
|
||||
|
||||
# Remove deprecated dash dependencies
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
<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>
|
||||
<br>
|
||||
<a href="https://goto.prowler.com/slack">Join our Prowler community!</a>
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-2oinmgmw6-cl7gOrljSEqo_aoripVPFA">Join our Prowler community!</a>
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center">
|
||||
<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://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://pypi.org/project/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/v/prowler.svg"></a>
|
||||
<a href="https://pypi.python.org/pypi/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/pyversions/prowler.svg"></a>
|
||||
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Prowler Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=prowler%20downloads"></a>
|
||||
@@ -63,9 +63,9 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 553 | 77 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --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` |
|
||||
| AWS | 457 | 67 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
|
||||
| GCP | 77 | 13 -> `prowler gcp --list-services` | 2 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
|
||||
| Azure | 136 | 17 -> `prowler azure --list-services` | 3 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
|
||||
|
||||
# 💻 Installation
|
||||
|
||||
@@ -12,11 +12,7 @@ Originally based on [org-multi-account](https://github.com/prowler-cloud/prowler
|
||||
|
||||
## Architecture Explanation
|
||||
|
||||
The solution is designed to be very simple. Prowler is run via an ECS Task definition that launches a single Fargate container. This Task Definition is executed on a schedule using an EventBridge Rule.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This solution assumes that you have a VPC architecture with two redundant subnets that can reach the AWS API endpoints (e.g. PrivateLink, NAT Gateway, etc.).
|
||||
The solution is designed to be very simple. Prowler is run via an ECS Task definition that launches a single Fargate container. This Task Definition is executed on a schedule using an EventBridge Rule.
|
||||
|
||||
## CloudFormation Templates
|
||||
|
||||
@@ -63,9 +59,9 @@ The logs that are generated and sent to Cloudwatch are error logs, and assessmen
|
||||
|
||||
## Instructions
|
||||
1. Create a Private ECR Repository in the account that will host the Prowler container. The Audit account is recommended, but any account can be used.
|
||||
2. Configure the .awsvariables file. Note the ROLE name chosen as it will be the CrossAccountRole.
|
||||
3. Follow the steps from "View Push Commands" to build and upload the container image. Substitute step 2 with the build command provided in the Dockerfile. You need to have Docker and AWS CLI installed, and use the cli to login to the account first. After upload note the Image URI, as it is required for the CF-Prowler-ECS template. Ensure that you pay attention to the architecture while performing the docker build command. A common mistake is not specifying the architecture and then building on Apple silicon. Your task will fail with *exec /home/prowler/.local/bin/prowler: exec format error*.
|
||||
4. Make sure SecurityHub is enabled in every account in AWS Organizations, and that the SecurityHub integration is enabled as explained in [Prowler - Security Hub Integration](https://github.com/prowler-cloud/prowler#security-hub-integration)
|
||||
2. Configure the .awsvariables file. Note the ROLE name chosen as it will be the CrossAccountRole.
|
||||
3. Follow the steps from "View Push Commands" to build and upload the container image. You need to have Docker and AWS CLI installed, and use the cli to login to the account first. After upload note the Image URI, as it is required for the CF-Prowler-ECS template.
|
||||
4. Make sure SecurityHub is enabled in every account in AWS Organizations, and that the SecurityHub integration is enabled as explained in [Prowler - Security Hub Integration](https://github.com/prowler-cloud/prowler#security-hub-integration)
|
||||
5. Deploy **CF-Prowler-CrossAccountRole.yml** in the Master Account as a single stack. You will have to choose the CrossAccountRole name (ProwlerXA-Role by default) and the ProwlerTaskRoleName (ProwlerECSTask-Role by default)
|
||||
6. Deploy **CF-Prowler-CrossAccountRole.yml** in every Member Account as a StackSet. Choose the same CrossAccountName and ProwlerTaskRoleName as the previous step.
|
||||
7. Deploy **CF-Prowler-IAM.yml** in the account that will host the Prowler container (the same from step 1). The following template parameters must be provided:
|
||||
@@ -95,4 +91,4 @@ If you permission find errors in the CloudWatch logs, the culprit might be a [Se
|
||||
## Upgrading Prowler
|
||||
|
||||
Prowler version is controlled by the PROWLERVER argument in the Dockerfile, change it to the desired version and follow the ECR Push Commands to update the container image.
|
||||
Old images can be deleted from the ECR Repository after the new image is confirmed to work. They will show as "untagged" as only one image can hold the "latest" tag.
|
||||
Old images can be deleted from the ECR Repository after the new image is confirmed to work. They will show as "untagged" as only one image can hold the "latest" tag.
|
||||
|
||||
@@ -68,7 +68,7 @@ for accountId in ${ACCOUNTS_IN_ORGS}; do
|
||||
# Run Prowler
|
||||
echo -e "Assessing AWS Account: ${accountId}, using Role: ${ROLE} on $(date)"
|
||||
# Pipe stdout to /dev/null to reduce unnecessary Cloudwatch logs
|
||||
prowler aws -R arn:"${PARTITION}":iam::"${accountId}":role/"${ROLE}" --security-hub --send-sh-only-fails -f "${REGION}" > /dev/null
|
||||
prowler aws -R arn:"${PARTITION}":iam::"${accountId}":role/"${ROLE}" -q -S -f "${REGION}" > /dev/null
|
||||
TOTAL_SEC=$((SECONDS - START_TIME))
|
||||
printf "Completed AWS Account: ${accountId} in %02dh:%02dm:%02ds" $((TOTAL_SEC / 3600)) $((TOTAL_SEC % 3600 / 60)) $((TOTAL_SEC % 60))
|
||||
echo ""
|
||||
|
||||
@@ -60,42 +60,24 @@ Resources:
|
||||
Effect: Allow
|
||||
Resource: "*"
|
||||
Action:
|
||||
- account:Get*
|
||||
- appstream:Describe*
|
||||
- appstream:List*
|
||||
- backup:List*
|
||||
- cloudtrail:GetInsightSelectors
|
||||
- codeartifact:List*
|
||||
- codebuild:BatchGet*
|
||||
- cognito-idp:GetUserPoolMfaConfig
|
||||
- dlm:Get*
|
||||
- drs:Describe*
|
||||
- ds:Describe*
|
||||
- ds:Get*
|
||||
- ds:List*
|
||||
- dynamodb:GetResourcePolicy
|
||||
- ds:ListAuthorizedApplications
|
||||
- ec2:GetEbsEncryptionByDefault
|
||||
- ec2:GetSnapshotBlockPublicAccessState
|
||||
- ec2:GetInstanceMetadataDefaults
|
||||
- ecr:Describe*
|
||||
- ecr:GetRegistryScanningConfiguration
|
||||
- elasticfilesystem:DescribeBackupPolicy
|
||||
- glue:GetConnections
|
||||
- glue:GetSecurityConfiguration*
|
||||
- glue:GetSecurityConfiguration
|
||||
- glue:SearchTables
|
||||
- lambda:GetFunction*
|
||||
- logs:FilterLogEvents
|
||||
- lightsail:GetRelationalDatabases
|
||||
- macie2:GetMacieSession
|
||||
- lambda:GetFunction
|
||||
- s3:GetAccountPublicAccessBlock
|
||||
- shield:DescribeProtection
|
||||
- shield:GetSubscriptionState
|
||||
- ssm:GetDocument
|
||||
- ssm-incidents:List*
|
||||
- support:Describe*
|
||||
- tag:GetTagKeys
|
||||
- wellarchitected:List*
|
||||
|
||||
- PolicyName: Prowler-Security-Hub
|
||||
PolicyDocument:
|
||||
Version: 2012-10-17
|
||||
Statement:
|
||||
- Sid: AllowProwlerSecurityHub
|
||||
Effect: Allow
|
||||
Resource: "*"
|
||||
|
||||
@@ -62,7 +62,7 @@ Resources:
|
||||
awslogs-stream-prefix: ecs
|
||||
Cpu: 1024
|
||||
ExecutionRoleArn: !Ref ECSExecutionRole
|
||||
Memory: 8192
|
||||
Memory: 2048
|
||||
NetworkMode: awsvpc
|
||||
TaskRoleArn: !Ref ProwlerTaskRole
|
||||
Family: SecurityHubProwlerTask
|
||||
|
||||
@@ -97,15 +97,9 @@ Outputs:
|
||||
ECSExecutionRoleARN:
|
||||
Description: ARN of the ECS Task Execution Role
|
||||
Value: !GetAtt ECSExecutionRole.Arn
|
||||
Export:
|
||||
Name: ECSExecutionRoleArn
|
||||
ProwlerTaskRoleARN:
|
||||
Description: ARN of the ECS Prowler Task Role
|
||||
Value: !GetAtt ProwlerTaskRole.Arn
|
||||
Export:
|
||||
Name: ProwlerTaskRoleArn
|
||||
ECSEventRoleARN:
|
||||
Description: ARN of the Eventbridge Task Role
|
||||
Value: !GetAtt ECSEventRole.Arn
|
||||
Export:
|
||||
Name: ECSEventRoleARN
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
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",
|
||||
)
|
||||
@@ -1,36 +0,0 @@
|
||||
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,7 +148,6 @@ 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()
|
||||
)
|
||||
@@ -247,11 +246,9 @@ def display_data(
|
||||
dfs = []
|
||||
for file in files:
|
||||
df = pd.read_csv(
|
||||
file, sep=";", on_bad_lines="skip", encoding=encoding_format, dtype=str
|
||||
file, sep=";", on_bad_lines="skip", encoding=encoding_format
|
||||
)
|
||||
df = df.astype(str).fillna("nan")
|
||||
df.columns = df.columns.astype(str)
|
||||
dfs.append(df)
|
||||
dfs.append(df.astype(str))
|
||||
return pd.concat(dfs, ignore_index=True)
|
||||
|
||||
data = load_csv_files(files)
|
||||
@@ -277,24 +274,17 @@ 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()
|
||||
and not data.columns.str.contains("ACCOUNTID").any()
|
||||
):
|
||||
if data.columns.str.contains("SUBSCRIPTIONID").any():
|
||||
data.rename(columns={"SUBSCRIPTIONID": "ACCOUNTID"}, inplace=True)
|
||||
data["REGION"] = "-"
|
||||
# Handle v3 azure cis compliance
|
||||
if (
|
||||
data.columns.str.contains("SUBSCRIPTION").any()
|
||||
and not data.columns.str.contains("ACCOUNTID").any()
|
||||
):
|
||||
if data.columns.str.contains("SUBSCRIPTION").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")
|
||||
@@ -309,11 +299,9 @@ def display_data(
|
||||
|
||||
account_filter_options = list(data["ACCOUNTID"].unique())
|
||||
account_filter_options = account_filter_options + ["All"]
|
||||
account_filter_options = [
|
||||
item
|
||||
for item in account_filter_options
|
||||
if isinstance(item, str) and item.lower() != "nan"
|
||||
]
|
||||
for item in account_filter_options:
|
||||
if "nan" in item or item.__class__.__name__ != "str" or item is None:
|
||||
account_filter_options.remove(item)
|
||||
|
||||
# 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://goto.prowler.com/slack)
|
||||
- Reach out to community members on the [**Prowler Slack channel**](https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog)
|
||||
|
||||
- Open an Issue or a Pull Request in our [**GitHub repository**](https://github.com/prowler-cloud/prowler).
|
||||
|
||||
|
||||
@@ -160,20 +160,14 @@ else:
|
||||
All the checks MUST fill the `report.resource_id` and `report.resource_arn` with the following criteria:
|
||||
|
||||
- AWS
|
||||
- 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`
|
||||
- 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>"`
|
||||
- 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://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.
|
||||
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.
|
||||
|
||||
@@ -44,6 +44,7 @@ class Provider(ABC):
|
||||
Methods:
|
||||
print_credentials(): Displays the provider's credentials used for auditing in the command-line interface.
|
||||
setup_session(): Sets up the session for the provider.
|
||||
get_output_mapping(): Returns the output mapping between the provider and the generic model.
|
||||
validate_arguments(): Validates the arguments for the provider.
|
||||
get_checks_to_execute_by_audit_resources(): Returns a set of checks based on the input resources to scan.
|
||||
|
||||
@@ -130,6 +131,15 @@ class Provider(ABC):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_output_mapping(self) -> dict:
|
||||
"""
|
||||
get_output_mapping returns the output mapping between the provider and the generic model.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_arguments(self) -> None:
|
||||
"""
|
||||
validate_arguments validates the arguments for the provider.
|
||||
@@ -190,18 +200,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
|
||||
ProviderSessionModel,
|
||||
ProviderIdentityModel,
|
||||
ProviderOutputOptionsModel
|
||||
ProvierSessionModel,
|
||||
ProvierIdentityModel,
|
||||
ProvierOutputOptionsModel
|
||||
)
|
||||
|
||||
class NewProvider(Provider):
|
||||
# All properties from the class, some of this are properties in the base class
|
||||
_type: str = "<provider_name>"
|
||||
_session: <ProviderSessionModel>
|
||||
_identity: <ProviderIdentityModel>
|
||||
_session: <ProvierSessionModel>
|
||||
_identity: <ProvierIdentityModel>
|
||||
_audit_config: dict
|
||||
_output_options: ProviderOutputOptionsModel
|
||||
_output_options: ProvierOutputOptionsModel
|
||||
_mutelist: dict
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
@@ -212,13 +222,13 @@ class NewProvider(Provider):
|
||||
arguments (dict): A dictionary containing configuration arguments.
|
||||
"""
|
||||
logger.info("Setting <NewProviderName> provider ...")
|
||||
# First get from arguments the necessary from the cloud account (subscriptions or projects or whatever the provider use for storing services)
|
||||
# First get from arguments the necesary from the cloud acount (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 = <ProviderIdentityModel>()
|
||||
self._identity = <ProvierIdentityModel>()
|
||||
|
||||
# Set the provider configuration
|
||||
self._audit_config = load_and_validate_config_file(
|
||||
@@ -254,7 +264,7 @@ class NewProvider(Provider):
|
||||
<all_needed_for_auth> Can include all necessary arguments to setup the session
|
||||
|
||||
Returns:
|
||||
Credentials necessary to communicate with the provider.
|
||||
Credentials necesary to communicate with the provider.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -4,25 +4,21 @@ Prowler allows you to do threat detection in AWS based on the CloudTrail log rec
|
||||
```
|
||||
prowler aws --category threat-detection
|
||||
```
|
||||
This command will run these checks:
|
||||
This comand will run these checks:
|
||||
|
||||
* `cloudtrail_threat_detection_privilege_escalation` -> Detects privilege escalation attacks.
|
||||
* `cloudtrail_threat_detection_enumeration` -> Detects enumeration attacks.
|
||||
* `cloudtrail_threat_detection_llm_jacking` -> Detects LLM Jacking attacks.
|
||||
* `cloudtrail_threat_detection_privilege_escalation`
|
||||
* `cloudtrail_threat_detection_enumeration`
|
||||
|
||||
???+ note
|
||||
Threat Detection checks will be only executed using `--category threat-detection` flag due to performance.
|
||||
Threat Detection checks will be only executed using `--category threat-detection` flag due to preformance.
|
||||
|
||||
## Config File
|
||||
|
||||
If you want to manage the behavior of the Threat Detection checks you can edit `config.yaml` file from `/prowler/config`. In this file you can edit the following attributes related with Threat Detection:
|
||||
|
||||
* `threat_detection_privilege_escalation_threshold`: determines the percentage of actions found to decide if it is an privilege_scalation attack event, by default is 0.2 (20%)
|
||||
* `threat_detection_privilege_escalation_threshold`: determines the percentage of actions found to decide if it is an privilege_scalation attack event, by default is 0.1 (10%)
|
||||
* `threat_detection_privilege_escalation_minutes`: it is the past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
|
||||
* `threat_detection_privilege_escalation_actions`: these are the default actions related with privilege escalation.
|
||||
* `threat_detection_enumeration_threshold`: determines the percentage of actions found to decide if it is an enumeration attack event, by default is 0.3 (30%)
|
||||
* `threat_detection_privilege_escalation_actions`: these are the default actions related with priviledge scalation.
|
||||
* `threat_detection_enumeration_threshold`: determines the percentage of actions found to decide if it is an enumeration attack event, by default is 0.1 (10%)
|
||||
* `threat_detection_enumeration_minutes`: it is the past minutes to search from now for enumeration attacks, by default is 1440 minutes (24 hours)
|
||||
* `threat_detection_enumeration_actions`: these are the default actions related with enumeration attacks.
|
||||
* `threat_detection_llm_jacking_threshold`: determines the percentage of actions found to decide if it is an LLM Jacking attack event, by default is 0.4 (40%)
|
||||
* `threat_detection_llm_jacking_minutes`: it is the past minutes to search from now for LLM Jacking attacks, by default is 1440 minutes (24 hours)
|
||||
* `threat_detection_llm_jacking_actions`: these are the default actions related with LLM Jacking attacks.
|
||||
|
||||
@@ -7,6 +7,7 @@ At the time of writing this documentation the available Azure Clouds from differ
|
||||
- AzureCloud
|
||||
- AzureChinaCloud
|
||||
- AzureUSGovernment
|
||||
- AzureGermanCloud
|
||||
|
||||
If you want to change the default one you must include the flag `--azure-region`, i.e.:
|
||||
|
||||
|
||||
@@ -56,8 +56,6 @@ The following list includes all the AWS checks with configurable variables that
|
||||
| `organizations_scp_check_deny_regions` | `organizations_enabled_regions` | List of Strings |
|
||||
| `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean |
|
||||
| `securityhub_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `secretsmanager_secret_unused` | `max_days_secret_unused` | Integer |
|
||||
| `secretsmanager_secret_rotated_periodically` | `max_days_secret_unrotated` | Integer |
|
||||
| `ssm_document_secrets` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean |
|
||||
| `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings |
|
||||
@@ -228,7 +226,7 @@ aws:
|
||||
|
||||
# AWS CloudTrail Configuration
|
||||
# aws.cloudtrail_threat_detection_privilege_escalation
|
||||
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.2 (20%)
|
||||
threat_detection_privilege_escalation_threshold: 0.1 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.1 (10%)
|
||||
threat_detection_privilege_escalation_minutes: 1440 # Past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_privilege_escalation_actions:
|
||||
[
|
||||
@@ -285,7 +283,7 @@ aws:
|
||||
"UpdateLoginProfile",
|
||||
]
|
||||
# aws.cloudtrail_threat_detection_enumeration
|
||||
threat_detection_enumeration_threshold: 0.3 # Percentage of actions found to decide if it is an enumeration attack event, by default is 0.3 (30%)
|
||||
threat_detection_enumeration_threshold: 0.1 # Percentage of actions found to decide if it is an enumeration attack event, by default is 0.1 (10%)
|
||||
threat_detection_enumeration_minutes: 1440 # Past minutes to search from now for enumeration attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_enumeration_actions:
|
||||
[
|
||||
@@ -380,24 +378,6 @@ aws:
|
||||
"LookupEvents",
|
||||
"Search",
|
||||
]
|
||||
# aws.cloudtrail_threat_detection_llm_jacking
|
||||
threat_detection_llm_jacking_threshold: 0.4 # Percentage of actions found to decide if it is an LLM Jacking attack event, by default is 0.4 (40%)
|
||||
threat_detection_llm_jacking_minutes: 1440 # Past minutes to search from now for LLM Jacking attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_llm_jacking_actions:
|
||||
[
|
||||
"PutUseCaseForModelAccess", # Submits a use case for model access, providing justification (Write).
|
||||
"PutFoundationModelEntitlement", # Grants entitlement for accessing a foundation model (Write).
|
||||
"PutModelInvocationLoggingConfiguration", # Configures logging for model invocations (Write).
|
||||
"CreateFoundationModelAgreement", # Creates a new agreement to use a foundation model (Write).
|
||||
"InvokeModel", # Invokes a specified Bedrock model for inference using provided prompt and parameters (Read).
|
||||
"InvokeModelWithResponseStream", # Invokes a Bedrock model for inference with real-time token streaming (Read).
|
||||
"GetUseCaseForModelAccess", # Retrieves an existing use case for model access (Read).
|
||||
"GetModelInvocationLoggingConfiguration", # Fetches the logging configuration for model invocations (Read).
|
||||
"GetFoundationModelAvailability", # Checks the availability of a foundation model for use (Read).
|
||||
"ListFoundationModelAgreementOffers", # Lists available agreement offers for accessing foundation models (List).
|
||||
"ListFoundationModels", # Lists the available foundation models in Bedrock (List).
|
||||
"ListProvisionedModelThroughputs", # Lists the provisioned throughput for previously created models (List).
|
||||
]
|
||||
|
||||
# AWS RDS Configuration
|
||||
# aws.rds_instance_backup_enabled
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# GCP Organization
|
||||
|
||||
By default, Prowler scans all Google Cloud projects accessible to the authenticated user.
|
||||
|
||||
To limit the scan to projects within a specific Google Cloud organization, use the `--organization-id` option with the GCP organization ID:
|
||||
|
||||
```console
|
||||
prowler gcp --organization-id organization-id
|
||||
```
|
||||
|
||||
???+ warning
|
||||
Make sure that the used credentials have the role Cloud Asset Viewer (`roles/cloudasset.viewer`) or Cloud Asset Owner (`roles/cloudasset.owner`) on the organization level.
|
||||
|
||||
???+ note
|
||||
With this option, Prowler retrieves all projects within the specified organization, including those organized in folders and nested subfolders. This ensures that every project under the organization’s hierarchy is scanned, providing full visibility across the entire organization.
|
||||
|
||||
???+ note
|
||||
To find the organization ID, use the following command:
|
||||
|
||||
```console
|
||||
gcloud organizations list
|
||||
```
|
||||
@@ -24,11 +24,6 @@ Prowler can run without showing its banner:
|
||||
```console
|
||||
prowler <provider> -b/--no-banner
|
||||
```
|
||||
## Disable Colors
|
||||
Prowler can run without showing colors:
|
||||
```console
|
||||
prowler <provider> --no-color
|
||||
```
|
||||
## Checks
|
||||
Prowler has checks per provider, there are options related with them:
|
||||
|
||||
@@ -125,5 +120,5 @@ prowler <provider> --list-categories
|
||||
```
|
||||
- Execute specific category(s):
|
||||
```console
|
||||
prowler <provider> --categories secrets
|
||||
prowler <provider> --categories
|
||||
```
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
# 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).
|
||||
@@ -142,8 +142,7 @@ The JSON-OCSF output format implements the [Detection Finding](https://schema.oc
|
||||
"desc": "Ensure CloudTrail is enabled in all regions",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure CloudTrail is enabled in all regions",
|
||||
"uid": "prowler-aws-cloudtrail_multi_region_enabled-123456789012-ap-northeast-1-123456789012",
|
||||
"types": ["Software and Configuration Checks","Industry and Regulatory Standards","CIS AWS Foundations Benchmark"],
|
||||
"uid": "prowler-aws-cloudtrail_multi_region_enabled-123456789012-ap-northeast-1-123456789012"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
@@ -190,10 +189,11 @@ The JSON-OCSF output format implements the [Detection Finding](https://schema.oc
|
||||
"type_uid": 200401,
|
||||
"type_name": "Create",
|
||||
"unmapped": {
|
||||
"check_type": "Software and Configuration Checks,Industry and Regulatory Standards,CIS AWS Foundations Benchmark",
|
||||
"related_url": "",
|
||||
"categories": ["forensics-ready"],
|
||||
"depends_on": [],
|
||||
"related_to": [],
|
||||
"categories": "forensics-ready",
|
||||
"depends_on": "",
|
||||
"related_to": "",
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CISA": [
|
||||
@@ -336,7 +336,7 @@ The following is the mapping between the native JSON and the Detection Finding f
|
||||
| Provider | cloud.provider |
|
||||
| CheckID | metadata.event_code |
|
||||
| CheckTitle | finding_info.title |
|
||||
| CheckType | finding_info.types |
|
||||
| CheckType | unmapped.check_type |
|
||||
| ServiceName | resources.group.name |
|
||||
| SubServiceName | _Not mapped yet_ |
|
||||
| Status | status_code |
|
||||
|
||||
@@ -65,7 +65,6 @@ 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
|
||||
@@ -88,7 +87,6 @@ nav:
|
||||
- Google Cloud:
|
||||
- Authentication: tutorials/gcp/authentication.md
|
||||
- Projects: tutorials/gcp/projects.md
|
||||
- Organization: tutorials/gcp/organization.md
|
||||
- Kubernetes:
|
||||
- In-Cluster Execution: tutorials/kubernetes/in-cluster.md
|
||||
- Non In-Cluster Execution: tutorials/kubernetes/outside-cluster.md
|
||||
|
||||
@@ -59,12 +59,9 @@ Resources:
|
||||
- 'appstream:Describe*'
|
||||
- 'appstream:List*'
|
||||
- 'backup:List*'
|
||||
- 'bedrock:List*'
|
||||
- 'bedrock:Get*'
|
||||
- 'cloudtrail:GetInsightSelectors'
|
||||
- 'codeartifact:List*'
|
||||
- 'codebuild:BatchGet*'
|
||||
- 'codebuild:ListReportGroups'
|
||||
- 'cognito-idp:GetUserPoolMfaConfig'
|
||||
- 'dlm:Get*'
|
||||
- 'drs:Describe*'
|
||||
@@ -85,14 +82,11 @@ Resources:
|
||||
- 'logs:FilterLogEvents'
|
||||
- 'lightsail:GetRelationalDatabases'
|
||||
- 'macie2:GetMacieSession'
|
||||
- 'macie2:GetAutomatedDiscoveryConfiguration'
|
||||
- 's3:GetAccountPublicAccessBlock'
|
||||
- 'shield:DescribeProtection'
|
||||
- 'shield:GetSubscriptionState'
|
||||
- 'securityhub:BatchImportFindings'
|
||||
- 'securityhub:GetFindings'
|
||||
- 'servicecatalog:Describe*'
|
||||
- 'servicecatalog:List*'
|
||||
- 'ssm:GetDocument'
|
||||
- 'ssm-incidents:List*'
|
||||
- 'support:Describe*'
|
||||
|
||||
@@ -7,12 +7,9 @@
|
||||
"appstream:Describe*",
|
||||
"appstream:List*",
|
||||
"backup:List*",
|
||||
"bedrock:List*",
|
||||
"bedrock:Get*",
|
||||
"cloudtrail:GetInsightSelectors",
|
||||
"codeartifact:List*",
|
||||
"codebuild:BatchGet*",
|
||||
"codebuild:ListReportGroups",
|
||||
"cognito-idp:GetUserPoolMfaConfig",
|
||||
"dlm:Get*",
|
||||
"drs:Describe*",
|
||||
@@ -33,14 +30,11 @@
|
||||
"logs:FilterLogEvents",
|
||||
"lightsail:GetRelationalDatabases",
|
||||
"macie2:GetMacieSession",
|
||||
"macie2:GetAutomatedDiscoveryConfiguration",
|
||||
"s3:GetAccountPublicAccessBlock",
|
||||
"shield:DescribeProtection",
|
||||
"shield:GetSubscriptionState",
|
||||
"securityhub:BatchImportFindings",
|
||||
"securityhub:GetFindings",
|
||||
"servicecatalog:Describe*",
|
||||
"servicecatalog:List*",
|
||||
"ssm:GetDocument",
|
||||
"ssm-incidents:List*",
|
||||
"support:Describe*",
|
||||
|
||||
Generated
+1099
-1233
File diff suppressed because it is too large
Load Diff
+14
-76
@@ -2,25 +2,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from os import environ
|
||||
|
||||
from colorama import Fore, Style
|
||||
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,
|
||||
print_prowler_cloud_banner,
|
||||
print_update_notice,
|
||||
)
|
||||
from prowler.lib.banner import print_banner
|
||||
from prowler.lib.check.check import (
|
||||
exclude_checks_to_run,
|
||||
exclude_services_to_run,
|
||||
@@ -59,8 +52,6 @@ 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
|
||||
@@ -121,21 +112,6 @@ def prowler():
|
||||
and not checks_folder
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -197,15 +173,15 @@ def prowler():
|
||||
|
||||
# Load checks to execute
|
||||
checks_to_execute = load_checks_to_execute(
|
||||
bulk_checks_metadata=bulk_checks_metadata,
|
||||
bulk_compliance_frameworks=bulk_compliance_frameworks,
|
||||
checks_file=checks_file,
|
||||
check_list=checks,
|
||||
service_list=services,
|
||||
severities=severities,
|
||||
compliance_frameworks=compliance_framework,
|
||||
categories=categories,
|
||||
provider=provider,
|
||||
bulk_checks_metadata,
|
||||
bulk_compliance_frameworks,
|
||||
checks_file,
|
||||
checks,
|
||||
services,
|
||||
severities,
|
||||
compliance_framework,
|
||||
categories,
|
||||
provider,
|
||||
)
|
||||
|
||||
# if --list-checks-json, dump a json file and exit
|
||||
@@ -260,6 +236,9 @@ def prowler():
|
||||
# Sort final check list
|
||||
checks_to_execute = sorted(checks_to_execute)
|
||||
|
||||
# Setup Mutelist
|
||||
global_provider.mutelist = args.mutelist_file
|
||||
|
||||
# Setup Output Options
|
||||
if provider == "aws":
|
||||
output_options = AWSOutputOptions(
|
||||
@@ -531,20 +510,6 @@ 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/"
|
||||
@@ -589,20 +554,6 @@ 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/"
|
||||
@@ -669,11 +620,7 @@ def prowler():
|
||||
)
|
||||
|
||||
security_hub_regions = (
|
||||
global_provider.get_available_aws_service_regions(
|
||||
"securityhub",
|
||||
global_provider.identity.partition,
|
||||
global_provider.identity.audited_regions,
|
||||
)
|
||||
global_provider.get_available_aws_service_regions("securityhub")
|
||||
if not global_provider.identity.audited_regions
|
||||
else global_provider.identity.audited_regions
|
||||
)
|
||||
@@ -737,15 +684,6 @@ 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)
|
||||
|
||||
@@ -557,7 +557,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"inspector2_is_enabled"
|
||||
"inspector2_findings_exist"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -587,8 +587,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"inspector2_active_findings_exist",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_findings_exist",
|
||||
"ecr_registry_scan_images_on_push_enabled",
|
||||
"ecr_repositories_scan_vulnerabilities_in_latest_image",
|
||||
"ecr_repositories_scan_images_on_push_enabled"
|
||||
|
||||
@@ -485,7 +485,7 @@
|
||||
"codeartifact_packages_external_public_publishing_disabled",
|
||||
"ecr_repositories_not_publicly_accessible",
|
||||
"efs_not_publicly_accessible",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"elb_internet_facing",
|
||||
"elbv2_internet_facing",
|
||||
"s3_account_level_public_access_blocks",
|
||||
@@ -664,7 +664,7 @@
|
||||
"awslambda_function_not_publicly_accessible",
|
||||
"apigateway_restapi_waf_acl_attached",
|
||||
"cloudfront_distributions_using_waf",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_control_plane_endpoint_access_restricted",
|
||||
"sagemaker_models_network_isolation_enabled",
|
||||
"sagemaker_models_vpc_settings_configured",
|
||||
"sagemaker_notebook_instance_vpc_settings_configured",
|
||||
|
||||
@@ -645,7 +645,7 @@
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2.4 Elastic File System (EFS)",
|
||||
"Section": "2.4 Relational Database Service (RDS)",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Manual",
|
||||
"Description": "EFS data should be encrypted at rest using AWS KMS (Key Management Service).",
|
||||
|
||||
@@ -643,7 +643,7 @@
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2.4 Elastic File System (EFS)",
|
||||
"Section": "2.4 Relational Database Service (RDS)",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Manual",
|
||||
"Description": "EFS data should be encrypted at rest using AWS KMS (Key Management Service).",
|
||||
|
||||
@@ -643,7 +643,7 @@
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2.4 Elastic File System (EFS)",
|
||||
"Section": "2.4 Relational Database Service (RDS)",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Automated",
|
||||
"Description": "EFS data should be encrypted at rest using AWS KMS (Key Management Service).",
|
||||
|
||||
@@ -1192,6 +1192,7 @@
|
||||
"Checks": [
|
||||
"organizations_scp_check_deny_regions",
|
||||
"cognito_user_pool_self_registration_disabled",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"iam_user_administrator_access_policy",
|
||||
"iam_customer_unattached_policy_no_administrative_privileges",
|
||||
"iam_inline_policy_allows_privilege_escalation",
|
||||
@@ -1509,9 +1510,9 @@
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"networkfirewall_in_all_vpc",
|
||||
"eks_cluster_network_policy_enabled",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_control_plane_endpoint_access_restricted",
|
||||
"eks_cluster_private_nodes_enabled",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"kafka_cluster_is_public",
|
||||
"kafka_cluster_unrestricted_access_disabled",
|
||||
"vpc_peering_routing_tables_with_least_privilege",
|
||||
@@ -2653,6 +2654,7 @@
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"cloudtrail_insights_exist",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"trustedadvisor_errors_and_warnings"
|
||||
],
|
||||
|
||||
@@ -1192,6 +1192,7 @@
|
||||
"Checks": [
|
||||
"organizations_scp_check_deny_regions",
|
||||
"cognito_user_pool_self_registration_disabled",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"iam_user_administrator_access_policy",
|
||||
"iam_customer_unattached_policy_no_administrative_privileges",
|
||||
"iam_inline_policy_allows_privilege_escalation",
|
||||
@@ -1509,9 +1510,9 @@
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"networkfirewall_in_all_vpc",
|
||||
"eks_cluster_network_policy_enabled",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_control_plane_endpoint_access_restricted",
|
||||
"eks_cluster_private_nodes_enabled",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"kafka_cluster_is_public",
|
||||
"kafka_cluster_unrestricted_access_disabled",
|
||||
"vpc_peering_routing_tables_with_least_privilege",
|
||||
@@ -2653,6 +2654,7 @@
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"cloudtrail_insights_exist",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"trustedadvisor_errors_and_warnings"
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_profile_attached",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"iam_aws_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
@@ -61,7 +61,7 @@
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_profile_attached",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"iam_aws_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
@@ -102,7 +102,7 @@
|
||||
"Checks": [
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"awslambda_function_not_publicly_accessible",
|
||||
"awslambda_function_url_public",
|
||||
|
||||
@@ -971,7 +971,7 @@
|
||||
"Checks": [
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_instance_public_ip",
|
||||
"eks_cluster_not_publicly_accessible",
|
||||
"eks_endpoints_not_publicly_accessible",
|
||||
"emr_cluster_master_nodes_no_public_ip",
|
||||
"awslambda_function_url_public",
|
||||
"rds_instance_no_public_access",
|
||||
|
||||
@@ -3043,7 +3043,9 @@
|
||||
{
|
||||
"Id": "9.4",
|
||||
"Description": "Ensure that Register with Entra ID is enabled on App Service",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
""
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "9. AppService",
|
||||
@@ -3173,7 +3175,9 @@
|
||||
{
|
||||
"Id": "9.10",
|
||||
"Description": "Ensure Azure Key Vaults are Used to Store Secrets",
|
||||
"Checks": [],
|
||||
"Checks": [
|
||||
""
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "9. AppService",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+10
-43
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import pathlib
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from os import getcwd
|
||||
|
||||
import requests
|
||||
@@ -12,7 +11,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "4.6.3"
|
||||
prowler_version = "4.4.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
@@ -22,13 +21,8 @@ gcp_logo = "https://user-images.githubusercontent.com/38561120/235928332-eb4accd
|
||||
orange_color = "\033[38;5;208m"
|
||||
banner_color = "\033[1;92m"
|
||||
|
||||
|
||||
class Provider(str, Enum):
|
||||
AWS = "aws"
|
||||
GCP = "gcp"
|
||||
AZURE = "azure"
|
||||
KUBERNETES = "kubernetes"
|
||||
|
||||
finding_statuses = ["PASS", "FAIL", "MANUAL"]
|
||||
valid_severities = ["critical", "high", "medium", "low", "informational"]
|
||||
|
||||
# Compliance
|
||||
actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
@@ -36,7 +30,7 @@ actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
def get_available_compliance_frameworks(provider=None):
|
||||
available_compliance_frameworks = []
|
||||
providers = [p.value for p in Provider]
|
||||
providers = ["aws", "gcp", "azure", "kubernetes"]
|
||||
if provider:
|
||||
providers = [provider]
|
||||
for provider in providers:
|
||||
@@ -86,50 +80,23 @@ def get_default_mute_file_path(provider: str):
|
||||
return mutelist_path
|
||||
|
||||
|
||||
def get_latest_release_version():
|
||||
"""Return the latest Prowler release tag name from GitHub, or None if it cannot be retrieved."""
|
||||
def check_current_version():
|
||||
try:
|
||||
prowler_version_string = f"Prowler {prowler_version}"
|
||||
release_response = requests.get(
|
||||
"https://api.github.com/repos/prowler-cloud/prowler/tags", timeout=1
|
||||
)
|
||||
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:
|
||||
latest_version = release_response.json()[0]["name"]
|
||||
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 prowler_version_string
|
||||
return f"{prowler_version_string}"
|
||||
|
||||
|
||||
def load_and_validate_config_file(provider: str, config_file_path: str) -> dict:
|
||||
|
||||
@@ -57,11 +57,6 @@ aws:
|
||||
8088,
|
||||
]
|
||||
|
||||
# AWS ECS Configuration
|
||||
# aws.ecs_service_fargate_latest_platform_version
|
||||
fargate_linux_latest_version: "1.4.0"
|
||||
fargate_windows_latest_version: "1.0.0"
|
||||
|
||||
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
|
||||
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
|
||||
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
|
||||
@@ -99,9 +94,7 @@ aws:
|
||||
"nodejs10.x",
|
||||
"nodejs12.x",
|
||||
"nodejs14.x",
|
||||
"nodejs16.x",
|
||||
"dotnet5.0",
|
||||
"dotnet7",
|
||||
"dotnetcore1.0",
|
||||
"dotnetcore2.0",
|
||||
"dotnetcore2.1",
|
||||
@@ -135,7 +128,7 @@ aws:
|
||||
|
||||
# AWS CloudTrail Configuration
|
||||
# aws.cloudtrail_threat_detection_privilege_escalation
|
||||
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.2 (20%)
|
||||
threat_detection_privilege_escalation_threshold: 0.1 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.1 (10%)
|
||||
threat_detection_privilege_escalation_minutes: 1440 # Past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_privilege_escalation_actions:
|
||||
[
|
||||
@@ -192,7 +185,7 @@ aws:
|
||||
"UpdateLoginProfile",
|
||||
]
|
||||
# aws.cloudtrail_threat_detection_enumeration
|
||||
threat_detection_enumeration_threshold: 0.3 # Percentage of actions found to decide if it is an enumeration attack event, by default is 0.3 (30%)
|
||||
threat_detection_enumeration_threshold: 0.1 # Percentage of actions found to decide if it is an enumeration attack event, by default is 0.1 (10%)
|
||||
threat_detection_enumeration_minutes: 1440 # Past minutes to search from now for enumeration attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_enumeration_actions:
|
||||
[
|
||||
@@ -287,24 +280,6 @@ aws:
|
||||
"LookupEvents",
|
||||
"Search",
|
||||
]
|
||||
# aws.cloudtrail_threat_detection_llm_jacking
|
||||
threat_detection_llm_jacking_threshold: 0.4 # Percentage of actions found to decide if it is an LLM Jacking attack event, by default is 0.4 (40%)
|
||||
threat_detection_llm_jacking_minutes: 1440 # Past minutes to search from now for LLM Jacking attacks, by default is 1440 minutes (24 hours)
|
||||
threat_detection_llm_jacking_actions:
|
||||
[
|
||||
"PutUseCaseForModelAccess", # Submits a use case for model access, providing justification (Write).
|
||||
"PutFoundationModelEntitlement", # Grants entitlement for accessing a foundation model (Write).
|
||||
"PutModelInvocationLoggingConfiguration", # Configures logging for model invocations (Write).
|
||||
"CreateFoundationModelAgreement", # Creates a new agreement to use a foundation model (Write).
|
||||
"InvokeModel", # Invokes a specified Bedrock model for inference using provided prompt and parameters (Read).
|
||||
"InvokeModelWithResponseStream", # Invokes a Bedrock model for inference with real-time token streaming (Read).
|
||||
"GetUseCaseForModelAccess", # Retrieves an existing use case for model access (Read).
|
||||
"GetModelInvocationLoggingConfiguration", # Fetches the logging configuration for model invocations (Read).
|
||||
"GetFoundationModelAvailability", # Checks the availability of a foundation model for use (Read).
|
||||
"ListFoundationModelAgreementOffers", # Lists available agreement offers for accessing foundation models (List).
|
||||
"ListFoundationModels", # Lists the available foundation models in Bedrock (List).
|
||||
"ListProvisionedModelThroughputs", # Lists the provisioned throughput for previously created models (List).
|
||||
]
|
||||
|
||||
# AWS RDS Configuration
|
||||
# aws.rds_instance_backup_enabled
|
||||
@@ -355,24 +330,10 @@ aws:
|
||||
elbv2_min_azs: 2
|
||||
|
||||
|
||||
# AWS Secrets Configuration
|
||||
# Patterns to ignore in the secrets checks
|
||||
# Known secrets to ignore on detection
|
||||
# this will include a list of regex patterns to ignore on detection
|
||||
secrets_ignore_patterns: []
|
||||
|
||||
# AWS Secrets Manager Configuration
|
||||
# aws.secretsmanager_secret_unused
|
||||
# Maximum number of days a secret can be unused
|
||||
max_days_secret_unused: 90
|
||||
|
||||
# aws.secretsmanager_secret_rotated_periodically
|
||||
# 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
|
||||
|
||||
@@ -9,14 +9,14 @@ class ProwlerException(Exception):
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, code, source=None, file=None, original_exception=None, error_info=None
|
||||
self, code, provider=None, file=None, original_exception=None, error_info=None
|
||||
):
|
||||
"""
|
||||
Initialize the ProwlerException class.
|
||||
|
||||
Args:
|
||||
code (int): The error code.
|
||||
source (str): The source name. This can be the provider name, module name, service name, etc.
|
||||
provider (str): The provider name.
|
||||
file (str): The file name.
|
||||
original_exception (Exception): The original exception.
|
||||
error_info (dict): The error information.
|
||||
@@ -28,7 +28,7 @@ class ProwlerException(Exception):
|
||||
>>> [1901] Unexpected error occurred. - Exception: Error occurred.
|
||||
"""
|
||||
self.code = code
|
||||
self.source = source
|
||||
self.provider = provider
|
||||
self.file = file
|
||||
if error_info is None:
|
||||
error_info = self.ERROR_CODES.get((code, self.__class__.__name__))
|
||||
@@ -45,12 +45,9 @@ class ProwlerException(Exception):
|
||||
|
||||
def __str__(self):
|
||||
"""Overriding the __str__ method"""
|
||||
default_str = f"{self.__class__.__name__}[{self.code}]: {self.message}"
|
||||
if self.original_exception:
|
||||
default_str += f" - {self.original_exception}"
|
||||
return default_str
|
||||
return f"{self.__class__.__name__}[{self.code}]: {self.message} - {self.original_exception}"
|
||||
|
||||
|
||||
class UnexpectedError(ProwlerException):
|
||||
def __init__(self, source, file, original_exception=None):
|
||||
super().__init__(1901, source, file, original_exception)
|
||||
def __init__(self, provider, file, original_exception=None):
|
||||
super().__init__(1901, provider, file, original_exception)
|
||||
|
||||
@@ -2,75 +2,6 @@ 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):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
@@ -273,7 +274,7 @@ def print_checks(
|
||||
for check in check_list:
|
||||
try:
|
||||
print(
|
||||
f"[{bulk_checks_metadata[check].CheckID}] {bulk_checks_metadata[check].CheckTitle} - {Fore.MAGENTA}{bulk_checks_metadata[check].ServiceName} {Fore.YELLOW}[{bulk_checks_metadata[check].Severity.value}]{Style.RESET_ALL}"
|
||||
f"[{bulk_checks_metadata[check].CheckID}] {bulk_checks_metadata[check].CheckTitle} - {Fore.MAGENTA}{bulk_checks_metadata[check].ServiceName} {Fore.YELLOW}[{bulk_checks_metadata[check].Severity}]{Style.RESET_ALL}"
|
||||
)
|
||||
except KeyError as error:
|
||||
logger.error(
|
||||
@@ -292,6 +293,32 @@ def print_checks(
|
||||
print(message)
|
||||
|
||||
|
||||
# Parse checks from compliance frameworks specification
|
||||
def parse_checks_from_compliance_framework(
|
||||
compliance_frameworks: list, bulk_compliance_frameworks: dict
|
||||
) -> list:
|
||||
"""parse_checks_from_compliance_framework returns a set of checks from the given compliance_frameworks"""
|
||||
checks_to_execute = set()
|
||||
try:
|
||||
for framework in compliance_frameworks:
|
||||
# compliance_framework_json["Requirements"][*]["Checks"]
|
||||
compliance_framework_checks_list = [
|
||||
requirement.Checks
|
||||
for requirement in bulk_compliance_frameworks[framework].Requirements
|
||||
]
|
||||
# Reduce nested list into a list
|
||||
# Pythonic functional magic
|
||||
compliance_framework_checks = functools.reduce(
|
||||
lambda x, y: x + y, compliance_framework_checks_list
|
||||
)
|
||||
# Then union this list of checks with the initial one
|
||||
checks_to_execute = checks_to_execute.union(compliance_framework_checks)
|
||||
except Exception as e:
|
||||
logger.error(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}] -- {e}")
|
||||
|
||||
return checks_to_execute
|
||||
|
||||
|
||||
# Import an input check using its path
|
||||
def import_check(check_path: str) -> ModuleType:
|
||||
lib = importlib.import_module(f"{check_path}")
|
||||
@@ -442,7 +469,7 @@ def execute_checks(
|
||||
continue
|
||||
if verbose:
|
||||
print(
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity.value}]{Style.RESET_ALL}"
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}"
|
||||
)
|
||||
check_findings = execute(
|
||||
check,
|
||||
@@ -522,7 +549,7 @@ def execute_checks(
|
||||
continue
|
||||
if verbose:
|
||||
print(
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity.value}]{Style.RESET_ALL}"
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}"
|
||||
)
|
||||
check_findings = execute(
|
||||
check,
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
from colorama import Fore, Style
|
||||
|
||||
from prowler.lib.check.check import parse_checks_from_file
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.models import CheckMetadata, Severity
|
||||
from prowler.config.config import valid_severities
|
||||
from prowler.lib.check.check import (
|
||||
parse_checks_from_compliance_framework,
|
||||
parse_checks_from_file,
|
||||
)
|
||||
from prowler.lib.check.utils import (
|
||||
recover_checks_from_provider,
|
||||
recover_checks_from_service,
|
||||
)
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
# Generate the list of checks to execute
|
||||
def load_checks_to_execute(
|
||||
bulk_checks_metadata: dict,
|
||||
bulk_compliance_frameworks: dict,
|
||||
checks_file: str,
|
||||
check_list: list,
|
||||
service_list: list,
|
||||
severities: list,
|
||||
compliance_frameworks: list,
|
||||
categories: set,
|
||||
provider: str,
|
||||
bulk_checks_metadata: dict = None,
|
||||
bulk_compliance_frameworks: dict = None,
|
||||
checks_file: str = None,
|
||||
check_list: list = None,
|
||||
service_list: list = None,
|
||||
severities: list = None,
|
||||
compliance_frameworks: list = None,
|
||||
categories: set = None,
|
||||
) -> set:
|
||||
"""Generate the list of checks to execute based on the cloud provider and the input arguments given"""
|
||||
try:
|
||||
# Local subsets
|
||||
checks_to_execute = set()
|
||||
check_aliases = {}
|
||||
check_severities = {key: [] for key in valid_severities}
|
||||
check_categories = {}
|
||||
check_severities = {severity.value: [] for severity in Severity}
|
||||
|
||||
if not bulk_checks_metadata:
|
||||
bulk_checks_metadata = CheckMetadata.get_bulk(provider=provider)
|
||||
# First, loop over the bulk_checks_metadata to extract the needed subsets
|
||||
for check, metadata in bulk_checks_metadata.items():
|
||||
try:
|
||||
@@ -62,41 +66,24 @@ def load_checks_to_execute(
|
||||
checks_to_execute.update(check_severities[severity])
|
||||
|
||||
if service_list:
|
||||
for service in service_list:
|
||||
checks_to_execute = (
|
||||
set(
|
||||
CheckMetadata.list(
|
||||
bulk_checks_metadata=bulk_checks_metadata,
|
||||
service=service,
|
||||
)
|
||||
)
|
||||
& checks_to_execute
|
||||
)
|
||||
checks_to_execute = (
|
||||
recover_checks_from_service(service_list, provider)
|
||||
& checks_to_execute
|
||||
)
|
||||
|
||||
# Handle if there are checks passed using -C/--checks-file
|
||||
elif checks_file:
|
||||
checks_to_execute = parse_checks_from_file(checks_file, provider)
|
||||
|
||||
# Handle if there are services passed using -s/--services
|
||||
elif service_list:
|
||||
for service in service_list:
|
||||
checks_to_execute.update(
|
||||
CheckMetadata.list(
|
||||
bulk_checks_metadata=bulk_checks_metadata,
|
||||
service=service,
|
||||
)
|
||||
)
|
||||
checks_to_execute = recover_checks_from_service(service_list, provider)
|
||||
|
||||
# Handle if there are compliance frameworks passed using --compliance
|
||||
elif compliance_frameworks:
|
||||
if not bulk_compliance_frameworks:
|
||||
bulk_compliance_frameworks = Compliance.get_bulk(provider=provider)
|
||||
for compliance_framework in compliance_frameworks:
|
||||
checks_to_execute.update(
|
||||
CheckMetadata.list(
|
||||
bulk_compliance_frameworks=bulk_compliance_frameworks,
|
||||
compliance_framework=compliance_framework,
|
||||
)
|
||||
)
|
||||
checks_to_execute = parse_checks_from_compliance_framework(
|
||||
compliance_frameworks, bulk_compliance_frameworks
|
||||
)
|
||||
|
||||
# Handle if there are categories passed using --categories
|
||||
elif categories:
|
||||
@@ -105,13 +92,17 @@ def load_checks_to_execute(
|
||||
|
||||
# If there are no checks passed as argument
|
||||
else:
|
||||
# get all checks
|
||||
for check_name in CheckMetadata.list(
|
||||
bulk_checks_metadata=bulk_checks_metadata
|
||||
):
|
||||
# Get all check modules to run with the specific provider
|
||||
checks = recover_checks_from_provider(provider)
|
||||
|
||||
for check_info in checks:
|
||||
# Recover check name from import path (last part)
|
||||
# Format: "providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
check_name = check_info[0]
|
||||
checks_to_execute.add(check_name)
|
||||
|
||||
# Only execute threat detection checks if threat-detection category is set
|
||||
if not categories or "threat-detection" not in categories:
|
||||
if "threat-detection" not in categories:
|
||||
for threat_detection_check in check_categories.get("threat-detection", []):
|
||||
checks_to_execute.discard(threat_detection_check)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
import yaml
|
||||
from jsonschema import validate
|
||||
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.config.config import valid_severities
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
custom_checks_metadata_schema = {
|
||||
@@ -17,7 +17,7 @@ custom_checks_metadata_schema = {
|
||||
"properties": {
|
||||
"Severity": {
|
||||
"type": "string",
|
||||
"enum": [severity.value for severity in Severity],
|
||||
"enum": valid_severities,
|
||||
},
|
||||
"CheckTitle": {
|
||||
"type": "string",
|
||||
|
||||
+10
-223
@@ -1,16 +1,12 @@
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Set
|
||||
|
||||
from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
from prowler.config.config import Provider
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.config.config import valid_severities
|
||||
from prowler.lib.check.utils import recover_checks_from_provider
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
@@ -58,14 +54,6 @@ class Remediation(BaseModel):
|
||||
Recommendation: Recommendation
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
critical = "critical"
|
||||
high = "high"
|
||||
medium = "medium"
|
||||
low = "low"
|
||||
informational = "informational"
|
||||
|
||||
|
||||
class CheckMetadata(BaseModel):
|
||||
"""
|
||||
Model representing the metadata of a check.
|
||||
@@ -105,7 +93,7 @@ class CheckMetadata(BaseModel):
|
||||
ServiceName: str
|
||||
SubServiceName: str
|
||||
ResourceIdTemplate: str
|
||||
Severity: Severity
|
||||
Severity: str
|
||||
ResourceType: str
|
||||
Description: str
|
||||
Risk: str
|
||||
@@ -134,6 +122,14 @@ class CheckMetadata(BaseModel):
|
||||
def severity_to_lower(severity):
|
||||
return severity.lower()
|
||||
|
||||
@validator("Severity")
|
||||
def valid_severity(severity):
|
||||
if severity not in valid_severities:
|
||||
raise ValueError(
|
||||
f"Invalid severity: {severity}. Severity must be one of {', '.join(valid_severities)}"
|
||||
)
|
||||
return severity
|
||||
|
||||
@staticmethod
|
||||
def get_bulk(provider: str) -> dict[str, "CheckMetadata"]:
|
||||
"""
|
||||
@@ -162,215 +158,6 @@ class CheckMetadata(BaseModel):
|
||||
|
||||
return bulk_check_metadata
|
||||
|
||||
@staticmethod
|
||||
def list(
|
||||
bulk_checks_metadata: dict = None,
|
||||
bulk_compliance_frameworks: dict = None,
|
||||
provider: str = None,
|
||||
severity: str = None,
|
||||
category: str = None,
|
||||
service: str = None,
|
||||
compliance_framework: str = None,
|
||||
) -> Set["CheckMetadata"]:
|
||||
"""
|
||||
Returns a set of checks from the bulk checks metadata.
|
||||
|
||||
Args:
|
||||
provider (str): The provider of the checks.
|
||||
bulk_checks_metadata (dict): The bulk checks metadata.
|
||||
bulk_compliance_frameworks (dict): The bulk compliance frameworks.
|
||||
severity (str): The severity of the checks.
|
||||
category (str): The category of the checks.
|
||||
service (str): The service of the checks.
|
||||
compliance_framework (str): The compliance framework of the checks.
|
||||
|
||||
Returns:
|
||||
set: A set of checks.
|
||||
"""
|
||||
checks_from_provider = set()
|
||||
checks_from_severity = set()
|
||||
checks_from_category = set()
|
||||
checks_from_service = set()
|
||||
checks_from_compliance_framework = set()
|
||||
# If the bulk checks metadata is not provided, get it
|
||||
if not bulk_checks_metadata:
|
||||
bulk_checks_metadata = {}
|
||||
available_providers = [p.value for p in Provider]
|
||||
for provider_name in available_providers:
|
||||
bulk_checks_metadata.update(CheckMetadata.get_bulk(provider_name))
|
||||
if provider:
|
||||
checks_from_provider = {
|
||||
check_name
|
||||
for check_name, check_metadata in bulk_checks_metadata.items()
|
||||
if check_metadata.Provider == provider
|
||||
}
|
||||
if severity:
|
||||
checks_from_severity = CheckMetadata.list_by_severity(
|
||||
bulk_checks_metadata=bulk_checks_metadata, severity=severity
|
||||
)
|
||||
if category:
|
||||
checks_from_category = CheckMetadata.list_by_category(
|
||||
bulk_checks_metadata=bulk_checks_metadata, category=category
|
||||
)
|
||||
if service:
|
||||
checks_from_service = CheckMetadata.list_by_service(
|
||||
bulk_checks_metadata=bulk_checks_metadata, service=service
|
||||
)
|
||||
if compliance_framework:
|
||||
# Loaded here, as it is not always needed
|
||||
if not bulk_compliance_frameworks:
|
||||
bulk_compliance_frameworks = {}
|
||||
available_providers = [p.value for p in Provider]
|
||||
for provider in available_providers:
|
||||
bulk_compliance_frameworks = Compliance.get_bulk(provider=provider)
|
||||
checks_from_compliance_framework = (
|
||||
CheckMetadata.list_by_compliance_framework(
|
||||
bulk_compliance_frameworks=bulk_compliance_frameworks,
|
||||
compliance_framework=compliance_framework,
|
||||
)
|
||||
)
|
||||
|
||||
# Get all the checks:
|
||||
checks = set(bulk_checks_metadata.keys())
|
||||
# Get the intersection of the checks
|
||||
if len(checks_from_provider) > 0 or provider:
|
||||
checks = checks & checks_from_provider
|
||||
if len(checks_from_severity) > 0 or severity:
|
||||
checks = checks & checks_from_severity
|
||||
if len(checks_from_category) > 0 or category:
|
||||
checks = checks & checks_from_category
|
||||
if len(checks_from_service) > 0 or service:
|
||||
checks = checks & checks_from_service
|
||||
if len(checks_from_compliance_framework) > 0 or compliance_framework:
|
||||
checks = checks & checks_from_compliance_framework
|
||||
|
||||
return checks
|
||||
|
||||
@staticmethod
|
||||
def get(bulk_checks_metadata: dict, check_id: str) -> "CheckMetadata":
|
||||
"""
|
||||
Returns the check metadata from the bulk checks metadata.
|
||||
|
||||
Args:
|
||||
bulk_checks_metadata (dict): The bulk checks metadata.
|
||||
check_id (str): The check ID.
|
||||
|
||||
Returns:
|
||||
CheckMetadata: The check metadata.
|
||||
"""
|
||||
|
||||
return bulk_checks_metadata.get(check_id, None)
|
||||
|
||||
@staticmethod
|
||||
def list_by_severity(bulk_checks_metadata: dict, severity: str = None) -> set:
|
||||
"""
|
||||
Returns a set of checks by severity from the bulk checks metadata.
|
||||
|
||||
Args:
|
||||
bulk_checks_metadata (dict): The bulk checks metadata.
|
||||
severity (str): The severity.
|
||||
|
||||
Returns:
|
||||
set: A set of checks by severity.
|
||||
"""
|
||||
checks = set()
|
||||
|
||||
if severity:
|
||||
checks = {
|
||||
check_name
|
||||
for check_name, check_metadata in bulk_checks_metadata.items()
|
||||
if check_metadata.Severity == severity
|
||||
}
|
||||
|
||||
return checks
|
||||
|
||||
@staticmethod
|
||||
def list_by_category(bulk_checks_metadata: dict, category: str = None) -> set:
|
||||
"""
|
||||
Returns a set of checks by category from the bulk checks metadata.
|
||||
|
||||
Args:
|
||||
bulk_checks_metadata (dict): The bulk checks metadata.
|
||||
category (str): The category.
|
||||
|
||||
Returns:
|
||||
set: A set of checks by category.
|
||||
"""
|
||||
checks = set()
|
||||
|
||||
if category:
|
||||
checks = {
|
||||
check_name
|
||||
for check_name, check_metadata in bulk_checks_metadata.items()
|
||||
if category in check_metadata.Categories
|
||||
}
|
||||
|
||||
return checks
|
||||
|
||||
@staticmethod
|
||||
def list_by_service(bulk_checks_metadata: dict, service: str = None) -> set:
|
||||
"""
|
||||
Returns a set of checks by service from the bulk checks metadata.
|
||||
|
||||
Args:
|
||||
bulk_checks_metadata (dict): The bulk checks metadata.
|
||||
service (str): The service.
|
||||
|
||||
Returns:
|
||||
set: A set of checks by service.
|
||||
"""
|
||||
checks = set()
|
||||
|
||||
if service:
|
||||
# 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()
|
||||
if check_metadata.ServiceName == service
|
||||
}
|
||||
|
||||
return checks
|
||||
|
||||
@staticmethod
|
||||
def list_by_compliance_framework(
|
||||
bulk_compliance_frameworks: dict, compliance_framework: str = None
|
||||
) -> set:
|
||||
"""
|
||||
Returns a set of checks by compliance framework from the bulk compliance frameworks.
|
||||
|
||||
Args:
|
||||
bulk_compliance_frameworks (dict): The bulk compliance frameworks.
|
||||
compliance_framework (str): The compliance framework.
|
||||
|
||||
Returns:
|
||||
set: A set of checks by compliance framework.
|
||||
"""
|
||||
checks = set()
|
||||
|
||||
if compliance_framework:
|
||||
try:
|
||||
checks_from_framework_list = [
|
||||
requirement.Checks
|
||||
for requirement in bulk_compliance_frameworks[
|
||||
compliance_framework
|
||||
].Requirements
|
||||
]
|
||||
# Reduce nested list into a list
|
||||
# Pythonic functional magic
|
||||
checks_from_framework = functools.reduce(
|
||||
lambda x, y: x + y, checks_from_framework_list
|
||||
)
|
||||
# Then union this list of checks with the initial one
|
||||
checks = checks.union(checks_from_framework)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}] -- {e}"
|
||||
)
|
||||
|
||||
return checks
|
||||
|
||||
|
||||
class Check(ABC, CheckMetadata):
|
||||
"""Prowler Check"""
|
||||
|
||||
@@ -10,9 +10,9 @@ from prowler.config.config import (
|
||||
default_config_file_path,
|
||||
default_fixer_config_file_path,
|
||||
default_output_directory,
|
||||
finding_statuses,
|
||||
valid_severities,
|
||||
)
|
||||
from prowler.lib.check.models import Severity
|
||||
from prowler.lib.outputs.common import Status
|
||||
from prowler.providers.common.arguments import (
|
||||
init_providers_parser,
|
||||
validate_provider_arguments,
|
||||
@@ -138,8 +138,8 @@ Detailed documentation at https://docs.prowler.com
|
||||
common_outputs_parser.add_argument(
|
||||
"--status",
|
||||
nargs="+",
|
||||
help=f"Filter by the status of the findings {[status.value for status in Status]}",
|
||||
choices=[status.value for status in Status],
|
||||
help=f"Filter by the status of the findings {finding_statuses}",
|
||||
choices=finding_statuses,
|
||||
)
|
||||
common_outputs_parser.add_argument(
|
||||
"--output-formats",
|
||||
@@ -177,12 +177,6 @@ Detailed documentation at https://docs.prowler.com
|
||||
common_outputs_parser.add_argument(
|
||||
"--no-banner", "-b", action="store_true", help="Hide Prowler banner"
|
||||
)
|
||||
common_outputs_parser.add_argument(
|
||||
"--no-color",
|
||||
action="store_true",
|
||||
help="Disable color codes in output",
|
||||
)
|
||||
|
||||
common_outputs_parser.add_argument(
|
||||
"--unix-timestamp",
|
||||
action="store_true",
|
||||
@@ -263,8 +257,8 @@ Detailed documentation at https://docs.prowler.com
|
||||
"--severity",
|
||||
"--severities",
|
||||
nargs="+",
|
||||
help=f"Severities to be executed {[severity.value for severity in Severity]}",
|
||||
choices=[severity.value for severity in Severity],
|
||||
help=f"Severities to be executed {valid_severities}",
|
||||
choices=valid_severities,
|
||||
)
|
||||
group.add_argument(
|
||||
"--compliance",
|
||||
|
||||
@@ -5,8 +5,6 @@ import yaml
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.mutelist.models import mutelist_schema
|
||||
from prowler.lib.outputs.common import Status
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
|
||||
|
||||
class Mutelist(ABC):
|
||||
@@ -239,35 +237,6 @@ class Mutelist(ABC):
|
||||
)
|
||||
return False
|
||||
|
||||
def mute_finding(self, finding):
|
||||
"""
|
||||
Check if the provided finding is muted
|
||||
|
||||
Args:
|
||||
finding (Finding): The finding to be evaluated for muting.
|
||||
|
||||
Returns:
|
||||
Finding: The finding with the status updated if it is muted, otherwise the finding is returned
|
||||
|
||||
"""
|
||||
try:
|
||||
if self.is_muted(
|
||||
finding.account_uid,
|
||||
finding.metadata.CheckID,
|
||||
finding.region,
|
||||
finding.resource_uid,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
):
|
||||
finding.raw["status"] = finding.status
|
||||
finding.status = Status.MUTED
|
||||
finding.muted = True
|
||||
return finding
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
)
|
||||
return finding
|
||||
|
||||
def is_excepted(
|
||||
self,
|
||||
exceptions,
|
||||
|
||||
@@ -72,23 +72,23 @@ class ASFF(Output):
|
||||
AWSSecurityFindingFormat(
|
||||
# The following line cannot be changed because it is the format we use to generate unique findings for AWS Security Hub
|
||||
# If changed some findings could be lost because the unique identifier will be different
|
||||
Id=f"prowler-{finding.metadata.CheckID}-{finding.account_uid}-{finding.region}-{hash_sha512(finding.resource_uid)}",
|
||||
Id=f"prowler-{finding.check_id}-{finding.account_uid}-{finding.region}-{hash_sha512(finding.resource_uid)}",
|
||||
ProductArn=f"arn:{finding.partition}:securityhub:{finding.region}::product/prowler/prowler",
|
||||
ProductFields=ProductFields(
|
||||
ProwlerResourceName=finding.resource_uid,
|
||||
),
|
||||
GeneratorId="prowler-" + finding.metadata.CheckID,
|
||||
GeneratorId="prowler-" + finding.check_id,
|
||||
AwsAccountId=finding.account_uid,
|
||||
Types=(
|
||||
finding.metadata.CheckType
|
||||
if finding.metadata.CheckType
|
||||
finding.check_type.split(",")
|
||||
if finding.check_type
|
||||
else ["Software and Configuration Checks"]
|
||||
),
|
||||
FirstObservedAt=timestamp,
|
||||
UpdatedAt=timestamp,
|
||||
CreatedAt=timestamp,
|
||||
Severity=Severity(Label=finding.metadata.Severity.value),
|
||||
Title=finding.metadata.CheckTitle,
|
||||
Severity=Severity(Label=finding.severity.value),
|
||||
Title=finding.check_title,
|
||||
Description=(
|
||||
(finding.status_extended[:1000] + "...")
|
||||
if len(finding.status_extended) > 1000
|
||||
@@ -97,7 +97,7 @@ class ASFF(Output):
|
||||
Resources=[
|
||||
Resource(
|
||||
Id=finding.resource_uid,
|
||||
Type=finding.metadata.ResourceType,
|
||||
Type=finding.resource_type,
|
||||
Partition=finding.partition,
|
||||
Region=finding.region,
|
||||
Tags=finding.resource_tags,
|
||||
@@ -110,8 +110,8 @@ class ASFF(Output):
|
||||
),
|
||||
Remediation=Remediation(
|
||||
Recommendation=Recommendation(
|
||||
Text=finding.metadata.Remediation.Recommendation.Text,
|
||||
Url=finding.metadata.Remediation.Recommendation.Url,
|
||||
Text=finding.remediation_recommendation_text,
|
||||
Url=finding.remediation_recommendation_url,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,26 +1,62 @@
|
||||
from enum import Enum
|
||||
from operator import attrgetter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.utils import unroll_tags
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.utils import unroll_list, unroll_tags
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def get_provider_data_mapping(provider) -> dict:
|
||||
data = {}
|
||||
for generic_field, provider_field in provider.get_output_mapping.items():
|
||||
try:
|
||||
provider_value = attrgetter(provider_field)(provider)
|
||||
data[generic_field] = provider_value
|
||||
except AttributeError:
|
||||
data[generic_field] = ""
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
# TODO: add test for outputs_unix_timestamp
|
||||
def fill_common_finding_data(finding: dict, unix_timestamp: bool) -> dict:
|
||||
finding_data = {
|
||||
"metadata": finding.check_metadata,
|
||||
"timestamp": outputs_unix_timestamp(unix_timestamp, timestamp),
|
||||
"check_id": finding.check_metadata.CheckID,
|
||||
"check_title": finding.check_metadata.CheckTitle,
|
||||
"check_type": ",".join(finding.check_metadata.CheckType),
|
||||
"status": finding.status,
|
||||
"status_extended": finding.status_extended,
|
||||
"muted": finding.muted,
|
||||
"service_name": finding.check_metadata.ServiceName,
|
||||
"subservice_name": finding.check_metadata.SubServiceName,
|
||||
"severity": finding.check_metadata.Severity,
|
||||
"resource_type": finding.check_metadata.ResourceType,
|
||||
"resource_details": finding.resource_details,
|
||||
"resource_tags": unroll_tags(finding.resource_tags),
|
||||
"description": finding.check_metadata.Description,
|
||||
"risk": finding.check_metadata.Risk,
|
||||
"related_url": finding.check_metadata.RelatedUrl,
|
||||
"remediation_recommendation_text": (
|
||||
finding.check_metadata.Remediation.Recommendation.Text
|
||||
),
|
||||
"remediation_recommendation_url": (
|
||||
finding.check_metadata.Remediation.Recommendation.Url
|
||||
),
|
||||
"remediation_code_nativeiac": (
|
||||
finding.check_metadata.Remediation.Code.NativeIaC
|
||||
),
|
||||
"remediation_code_terraform": (
|
||||
finding.check_metadata.Remediation.Code.Terraform
|
||||
),
|
||||
"remediation_code_cli": (finding.check_metadata.Remediation.Code.CLI),
|
||||
"remediation_code_other": (finding.check_metadata.Remediation.Code.Other),
|
||||
"categories": unroll_list(finding.check_metadata.Categories),
|
||||
"depends_on": unroll_list(finding.check_metadata.DependsOn),
|
||||
"related_to": unroll_list(finding.check_metadata.RelatedTo),
|
||||
"notes": finding.check_metadata.Notes,
|
||||
}
|
||||
return finding_data
|
||||
|
||||
|
||||
class Status(str, Enum):
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
MANUAL = "MANUAL"
|
||||
MUTED = "MUTED"
|
||||
|
||||
@@ -94,12 +94,11 @@ 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) / 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}",
|
||||
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}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -42,7 +42,7 @@ class AzureCIS(ComplianceOutput):
|
||||
compliance_row = AzureCISModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
SubscriptionId=finding.account_uid,
|
||||
Subscription=finding.account_name,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
@@ -73,7 +73,7 @@ class AzureCIS(ComplianceOutput):
|
||||
compliance_row = AzureCISModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
SubscriptionId="",
|
||||
Subscription="",
|
||||
Location="",
|
||||
AssessmentDate=str(finding.timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
|
||||
@@ -38,7 +38,7 @@ class AzureCISModel(BaseModel):
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
SubscriptionId: str
|
||||
Subscription: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
|
||||
@@ -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":
|
||||
if compliance.Framework == "ENS" and compliance.Provider == "AWS":
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
marco_categoria = f"{attribute.Marco}/{attribute.Categoria}"
|
||||
@@ -95,12 +95,11 @@ 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) / 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}",
|
||||
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}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
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)
|
||||
@@ -1,103 +0,0 @@
|
||||
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,61 +28,3 @@ 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,12 +39,11 @@ 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) / 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}",
|
||||
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}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -61,12 +61,11 @@ 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) / 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}",
|
||||
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}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -69,12 +69,11 @@ 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) / 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}",
|
||||
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}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
from csv import DictWriter
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.lib.outputs.output import Output
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_list
|
||||
from prowler.lib.outputs.utils import unroll_dict
|
||||
|
||||
|
||||
class CSV(Output):
|
||||
def transform(self, findings: List[Finding]) -> None:
|
||||
def transform(self, findings: list[Finding]) -> None:
|
||||
"""Transforms the findings into the CSV format.
|
||||
|
||||
Args:
|
||||
@@ -17,68 +16,16 @@ class CSV(Output):
|
||||
"""
|
||||
try:
|
||||
for finding in findings:
|
||||
finding_dict = {}
|
||||
finding_dict["AUTH_METHOD"] = finding.auth_method
|
||||
finding_dict["TIMESTAMP"] = finding.timestamp
|
||||
finding_dict["ACCOUNT_UID"] = finding.account_uid
|
||||
finding_dict["ACCOUNT_NAME"] = finding.account_name
|
||||
finding_dict["ACCOUNT_EMAIL"] = finding.account_email
|
||||
finding_dict["ACCOUNT_ORGANIZATION_UID"] = (
|
||||
finding.account_organization_uid
|
||||
)
|
||||
finding_dict["ACCOUNT_ORGANIZATION_NAME"] = (
|
||||
finding.account_organization_name
|
||||
finding_dict = {k.upper(): v for k, v in finding.dict().items()}
|
||||
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
|
||||
finding_dict["COMPLIANCE"] = unroll_dict(
|
||||
finding.compliance, separator=": "
|
||||
)
|
||||
finding_dict["ACCOUNT_TAGS"] = unroll_dict(
|
||||
finding.account_tags, separator=":"
|
||||
)
|
||||
finding_dict["FINDING_UID"] = finding.uid
|
||||
finding_dict["PROVIDER"] = finding.metadata.Provider
|
||||
finding_dict["CHECK_ID"] = finding.metadata.CheckID
|
||||
finding_dict["CHECK_TITLE"] = finding.metadata.CheckTitle
|
||||
finding_dict["CHECK_TYPE"] = unroll_list(finding.metadata.CheckType)
|
||||
finding_dict["STATUS"] = finding.status.value
|
||||
finding_dict["STATUS_EXTENDED"] = finding.status_extended
|
||||
finding_dict["MUTED"] = finding.muted
|
||||
finding_dict["SERVICE_NAME"] = finding.metadata.ServiceName
|
||||
finding_dict["SUBSERVICE_NAME"] = finding.metadata.SubServiceName
|
||||
finding_dict["SEVERITY"] = finding.metadata.Severity.value
|
||||
finding_dict["RESOURCE_TYPE"] = finding.metadata.ResourceType
|
||||
finding_dict["RESOURCE_UID"] = finding.resource_uid
|
||||
finding_dict["RESOURCE_NAME"] = finding.resource_name
|
||||
finding_dict["RESOURCE_DETAILS"] = finding.resource_details
|
||||
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
|
||||
finding_dict["PARTITION"] = finding.partition
|
||||
finding_dict["REGION"] = finding.region
|
||||
finding_dict["DESCRIPTION"] = finding.metadata.Description
|
||||
finding_dict["RISK"] = finding.metadata.Risk
|
||||
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
|
||||
finding.metadata.Remediation.Recommendation.Text
|
||||
)
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_URL"] = (
|
||||
finding.metadata.Remediation.Recommendation.Url
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_NATIVEIAC"] = (
|
||||
finding.metadata.Remediation.Code.NativeIaC
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_TERRAFORM"] = (
|
||||
finding.metadata.Remediation.Code.Terraform
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_CLI"] = (
|
||||
finding.metadata.Remediation.Code.CLI
|
||||
)
|
||||
finding_dict["REMEDIATION_CODE_OTHER"] = (
|
||||
finding.metadata.Remediation.Code.Other
|
||||
)
|
||||
finding_dict["COMPLIANCE"] = unroll_dict(
|
||||
finding.compliance, separator=": "
|
||||
)
|
||||
finding_dict["CATEGORIES"] = unroll_list(finding.metadata.Categories)
|
||||
finding_dict["DEPENDS_ON"] = unroll_list(finding.metadata.DependsOn)
|
||||
finding_dict["RELATED_TO"] = unroll_list(finding.metadata.RelatedTo)
|
||||
finding_dict["NOTES"] = finding.metadata.Notes
|
||||
finding_dict["PROWLER_VERSION"] = finding.prowler_version
|
||||
finding_dict["SEVERITY"] = finding.severity.value
|
||||
self._data.append(finding_dict)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
|
||||
+66
-111
@@ -1,17 +1,34 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.config.config import prowler_version
|
||||
from prowler.lib.check.models import Check_Report, CheckMetadata
|
||||
from prowler.lib.check.models import Check_Report
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.common import Status, fill_common_finding_data
|
||||
from prowler.lib.outputs.common import (
|
||||
fill_common_finding_data,
|
||||
get_provider_data_mapping,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.compliance import get_check_compliance
|
||||
from prowler.lib.utils.utils import dict_to_lowercase, get_nested_attribute
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
class Status(str, Enum):
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
MANUAL = "MANUAL"
|
||||
|
||||
|
||||
class Severity(str, Enum):
|
||||
critical = "critical"
|
||||
high = "high"
|
||||
medium = "medium"
|
||||
low = "low"
|
||||
informational = "informational"
|
||||
|
||||
|
||||
class Finding(BaseModel):
|
||||
"""
|
||||
Represents the output model for a finding across different providers.
|
||||
@@ -24,69 +41,50 @@ class Finding(BaseModel):
|
||||
auth_method: str
|
||||
timestamp: Union[int, datetime]
|
||||
account_uid: str
|
||||
account_name: Optional[str] = None
|
||||
account_email: Optional[str] = None
|
||||
account_organization_uid: Optional[str] = None
|
||||
account_organization_name: Optional[str] = None
|
||||
metadata: CheckMetadata
|
||||
# Optional since it depends on permissions
|
||||
account_name: Optional[str]
|
||||
# Optional since it depends on permissions
|
||||
account_email: Optional[str]
|
||||
# Optional since it depends on permissions
|
||||
account_organization_uid: Optional[str]
|
||||
# Optional since it depends on permissions
|
||||
account_organization_name: Optional[str]
|
||||
# Optional since it depends on permissions
|
||||
account_tags: dict = {}
|
||||
uid: str
|
||||
finding_uid: str
|
||||
provider: str
|
||||
check_id: str
|
||||
check_title: str
|
||||
check_type: str
|
||||
status: Status
|
||||
status_extended: str
|
||||
muted: bool = False
|
||||
service_name: str
|
||||
subservice_name: str
|
||||
severity: Severity
|
||||
resource_type: str
|
||||
resource_uid: str
|
||||
resource_name: str
|
||||
resource_details: str
|
||||
resource_tags: dict = Field(default_factory=dict)
|
||||
partition: Optional[str] = None
|
||||
resource_tags: dict = {}
|
||||
# Only present for AWS and Azure
|
||||
partition: Optional[str]
|
||||
region: str
|
||||
description: str
|
||||
risk: str
|
||||
related_url: str
|
||||
remediation_recommendation_text: str
|
||||
remediation_recommendation_url: str
|
||||
remediation_code_nativeiac: str
|
||||
remediation_code_terraform: str
|
||||
remediation_code_cli: str
|
||||
remediation_code_other: str
|
||||
compliance: dict
|
||||
categories: str
|
||||
depends_on: str
|
||||
related_to: str
|
||||
notes: str
|
||||
prowler_version: str = prowler_version
|
||||
raw: dict = Field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
"""
|
||||
Returns the provider from the finding check's metadata.
|
||||
"""
|
||||
return self.metadata.Provider
|
||||
|
||||
@property
|
||||
def check_id(self) -> str:
|
||||
"""
|
||||
Returns the ID from the finding check's metadata.
|
||||
"""
|
||||
return self.metadata.CheckID
|
||||
|
||||
@property
|
||||
def severity(self) -> str:
|
||||
"""
|
||||
Returns the severity from the finding check's metadata.
|
||||
"""
|
||||
return self.metadata.Severity
|
||||
|
||||
@property
|
||||
def resource_type(self) -> str:
|
||||
"""
|
||||
Returns the resource type from the finding check's metadata.
|
||||
"""
|
||||
return self.metadata.ResourceType
|
||||
|
||||
@property
|
||||
def service_name(self) -> str:
|
||||
"""
|
||||
Returns the service name from the finding check's metadata.
|
||||
"""
|
||||
return self.metadata.ServiceName
|
||||
|
||||
def get_metadata(self) -> dict:
|
||||
"""
|
||||
Retrieves the metadata of the object and returns it as a dictionary with all keys in lowercase.
|
||||
Returns:
|
||||
dict: A dictionary containing the metadata with keys converted to lowercase.
|
||||
"""
|
||||
|
||||
return dict_to_lowercase(self.metadata.dict())
|
||||
|
||||
@classmethod
|
||||
def generate_output(
|
||||
@@ -102,6 +100,9 @@ class Finding(BaseModel):
|
||||
finding_output (Finding): the finding output object
|
||||
|
||||
"""
|
||||
# TODO: think about get_provider_data_mapping
|
||||
provider_data_mapping = get_provider_data_mapping(provider)
|
||||
|
||||
# TODO: move fill_common_finding_data
|
||||
unix_timestamp = False
|
||||
if hasattr(output_options, "unix_timestamp"):
|
||||
@@ -109,6 +110,7 @@ class Finding(BaseModel):
|
||||
|
||||
common_finding_data = fill_common_finding_data(check_output, unix_timestamp)
|
||||
output_data = {}
|
||||
output_data.update(provider_data_mapping)
|
||||
output_data.update(common_finding_data)
|
||||
|
||||
bulk_checks_metadata = {}
|
||||
@@ -119,35 +121,9 @@ class Finding(BaseModel):
|
||||
check_output, provider.type, bulk_checks_metadata
|
||||
)
|
||||
try:
|
||||
output_data["provider"] = provider.type
|
||||
|
||||
if provider.type == "aws":
|
||||
output_data["account_uid"] = get_nested_attribute(
|
||||
provider, "identity.account"
|
||||
)
|
||||
output_data["account_name"] = get_nested_attribute(
|
||||
provider, "organizations_metadata.account_name"
|
||||
)
|
||||
output_data["account_email"] = get_nested_attribute(
|
||||
provider, "organizations_metadata.account_email"
|
||||
)
|
||||
output_data["account_organization_uid"] = get_nested_attribute(
|
||||
provider, "organizations_metadata.organization_arn"
|
||||
)
|
||||
output_data["account_organization_name"] = get_nested_attribute(
|
||||
provider, "organizations_metadata.organization_id"
|
||||
)
|
||||
output_data["account_tags"] = get_nested_attribute(
|
||||
provider, "organizations_metadata.account_tags"
|
||||
)
|
||||
output_data["partition"] = get_nested_attribute(
|
||||
provider, "identity.partition"
|
||||
)
|
||||
|
||||
# TODO: probably Organization UID is without the account id
|
||||
output_data["auth_method"] = (
|
||||
f"profile: {get_nested_attribute(provider, 'identity.profile')}"
|
||||
)
|
||||
output_data["auth_method"] = f"profile: {output_data['auth_method']}"
|
||||
output_data["resource_name"] = check_output.resource_id
|
||||
output_data["resource_uid"] = check_output.resource_arn
|
||||
output_data["region"] = check_output.region
|
||||
@@ -158,9 +134,9 @@ class Finding(BaseModel):
|
||||
f"{provider.identity.identity_type}: {provider.identity.identity_id}"
|
||||
)
|
||||
# Get the first tenant domain ID, just in case
|
||||
output_data["account_organization_uid"] = get_nested_attribute(
|
||||
provider, "identity.tenant_ids"
|
||||
)[0]
|
||||
output_data["account_organization_uid"] = output_data[
|
||||
"account_organization_uid"
|
||||
][0]
|
||||
output_data["account_uid"] = (
|
||||
output_data["account_organization_uid"]
|
||||
if "Tenant:" in check_output.subscription
|
||||
@@ -170,33 +146,15 @@ class Finding(BaseModel):
|
||||
output_data["resource_name"] = check_output.resource_name
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["region"] = check_output.location
|
||||
# TODO: check the tenant_ids
|
||||
# TODO: we have to get the account organization, the tenant is not that
|
||||
output_data["account_organization_name"] = get_nested_attribute(
|
||||
provider, "identity.tenant_domain"
|
||||
)
|
||||
|
||||
output_data["partition"] = get_nested_attribute(
|
||||
provider, "region_config.name"
|
||||
)
|
||||
# TODO: pending to get the subscription tags
|
||||
# "account_tags": "organizations_metadata.account_details_tags",
|
||||
# TODO: store subscription_name + id pairs
|
||||
# "account_name": "organizations_metadata.account_details_name",
|
||||
# "account_email": "organizations_metadata.account_details_email",
|
||||
|
||||
elif provider.type == "gcp":
|
||||
output_data["auth_method"] = (
|
||||
f"Principal: {get_nested_attribute(provider, 'identity.profile')}"
|
||||
)
|
||||
output_data["auth_method"] = f"Principal: {output_data['auth_method']}"
|
||||
output_data["account_uid"] = provider.projects[
|
||||
check_output.project_id
|
||||
].id
|
||||
output_data["account_name"] = provider.projects[
|
||||
check_output.project_id
|
||||
].name
|
||||
# There is no concept as project email in GCP
|
||||
# "account_email": "organizations_metadata.account_details_email",
|
||||
output_data["account_tags"] = provider.projects[
|
||||
check_output.project_id
|
||||
].labels
|
||||
@@ -215,7 +173,7 @@ class Finding(BaseModel):
|
||||
check_output.project_id
|
||||
].organization.id
|
||||
# TODO: for now is None since we don't retrieve that data
|
||||
output_data["account_organization_name"] = provider.projects[
|
||||
output_data["account_organization"] = provider.projects[
|
||||
check_output.project_id
|
||||
].organization.display_name
|
||||
|
||||
@@ -227,15 +185,12 @@ class Finding(BaseModel):
|
||||
output_data["resource_name"] = check_output.resource_name
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["account_name"] = f"context: {provider.identity.context}"
|
||||
output_data["account_uid"] = get_nested_attribute(
|
||||
provider, "identity.cluster"
|
||||
)
|
||||
output_data["region"] = f"namespace: {check_output.namespace}"
|
||||
|
||||
# check_output Unique ID
|
||||
# TODO: move this to a function
|
||||
# TODO: in Azure, GCP and K8s there are fidings without resource_name
|
||||
output_data["uid"] = (
|
||||
output_data["finding_uid"] = (
|
||||
f"prowler-{provider.type}-{check_output.check_metadata.CheckID}-{output_data['account_uid']}-"
|
||||
f"{output_data['region']}-{output_data['resource_name']}"
|
||||
)
|
||||
|
||||
@@ -39,16 +39,16 @@ class HTML(Output):
|
||||
f"""
|
||||
<tr class="{row_class}">
|
||||
<td>{finding_status}</td>
|
||||
<td>{finding.metadata.Severity.value}</td>
|
||||
<td>{finding.metadata.ServiceName}</td>
|
||||
<td>{finding.severity.value}</td>
|
||||
<td>{finding.service_name}</td>
|
||||
<td>{finding.region.lower()}</td>
|
||||
<td>{finding.metadata.CheckID.replace("_", "<wbr />_")}</td>
|
||||
<td>{finding.metadata.CheckTitle}</td>
|
||||
<td>{finding.check_id.replace("_", "<wbr />_")}</td>
|
||||
<td>{finding.check_title}</td>
|
||||
<td>{finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "<wbr />_")}</td>
|
||||
<td>{parse_html_string(unroll_dict(finding.resource_tags))}</td>
|
||||
<td>{finding.status_extended.replace("<", "<").replace(">", ">").replace("_", "<wbr />_")}</td>
|
||||
<td><p class="show-read-more">{html.escape(finding.metadata.Risk)}</p></td>
|
||||
<td><p class="show-read-more">{html.escape(finding.metadata.Remediation.Recommendation.Text)}</p> <a class="read-more" href="{finding.metadata.Remediation.Recommendation.Url}"><i class="fas fa-external-link-alt"></i></a></td>
|
||||
<td><p class="show-read-more">{html.escape(finding.risk)}</p></td>
|
||||
<td><p class="show-read-more">{html.escape(finding.remediation_recommendation_text)}</p> <a class="read-more" href="{finding.remediation_recommendation_url}"><i class="fas fa-external-link-alt"></i></a></td>
|
||||
<td><p class="show-read-more">{parse_html_string(unroll_dict(finding.compliance, separator=": "))}</p></td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
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
@@ -9,6 +9,7 @@ from py_ocsf_models.events.findings.detection_finding import (
|
||||
from py_ocsf_models.events.findings.finding import ActivityID, FindingInformation
|
||||
from py_ocsf_models.objects.account import Account, TypeID
|
||||
from py_ocsf_models.objects.cloud import Cloud
|
||||
from py_ocsf_models.objects.container import Container
|
||||
from py_ocsf_models.objects.group import Group
|
||||
from py_ocsf_models.objects.metadata import Metadata
|
||||
from py_ocsf_models.objects.organization import Organization
|
||||
@@ -36,7 +37,7 @@ class OCSF(Output):
|
||||
- transform(findings: List[Finding]) -> None: Transforms the findings into the OCSF Detection Finding format.
|
||||
- batch_write_data_to_file() -> None: Writes the findings to a file using the OCSF Detection Finding format using the `Output._file_descriptor`.
|
||||
- get_account_type_id_by_provider(provider: str) -> TypeID: Returns the TypeID based on the provider.
|
||||
- get_finding_status_id(muted: bool) -> StatusID: Returns the StatusID based on the muted value.
|
||||
- get_finding_status_id(status: str, muted: bool) -> StatusID: Returns the StatusID based on the status and muted values.
|
||||
|
||||
References:
|
||||
- OCSF: https://schema.ocsf.io/1.2.0/classes/detection_finding
|
||||
@@ -53,42 +54,37 @@ class OCSF(Output):
|
||||
for finding in findings:
|
||||
finding_activity = ActivityID.Create
|
||||
cloud_account_type = self.get_account_type_id_by_provider(
|
||||
finding.metadata.Provider
|
||||
finding.provider
|
||||
)
|
||||
finding_severity = getattr(
|
||||
SeverityID,
|
||||
finding.metadata.Severity.capitalize(),
|
||||
SeverityID.Unknown,
|
||||
SeverityID, finding.severity.capitalize(), SeverityID.Unknown
|
||||
)
|
||||
finding_status = self.get_finding_status_id(
|
||||
finding.status, finding.muted
|
||||
)
|
||||
finding_status = self.get_finding_status_id(finding.muted)
|
||||
|
||||
detection_finding = DetectionFinding(
|
||||
message=finding.status_extended,
|
||||
activity_id=finding_activity.value,
|
||||
activity_name=finding_activity.name,
|
||||
finding_info=FindingInformation(
|
||||
created_time_dt=finding.timestamp,
|
||||
created_time=int(finding.timestamp.timestamp()),
|
||||
desc=finding.metadata.Description,
|
||||
title=finding.metadata.CheckTitle,
|
||||
uid=finding.uid,
|
||||
name=finding.resource_name,
|
||||
created_time=finding.timestamp,
|
||||
desc=finding.description,
|
||||
title=finding.check_title,
|
||||
uid=finding.finding_uid,
|
||||
product_uid="prowler",
|
||||
types=finding.metadata.CheckType,
|
||||
),
|
||||
time_dt=finding.timestamp,
|
||||
time=int(finding.timestamp.timestamp()),
|
||||
event_time=finding.timestamp,
|
||||
remediation=Remediation(
|
||||
desc=finding.metadata.Remediation.Recommendation.Text,
|
||||
desc=finding.remediation_recommendation_text,
|
||||
references=list(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
finding.metadata.Remediation.Code.NativeIaC,
|
||||
finding.metadata.Remediation.Code.Terraform,
|
||||
finding.metadata.Remediation.Code.CLI,
|
||||
finding.metadata.Remediation.Code.Other,
|
||||
finding.metadata.Remediation.Recommendation.Url,
|
||||
finding.remediation_code_nativeiac,
|
||||
finding.remediation_code_terraform,
|
||||
finding.remediation_code_cli,
|
||||
finding.remediation_code_other,
|
||||
finding.remediation_recommendation_url,
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -99,73 +95,60 @@ class OCSF(Output):
|
||||
status=finding_status.name,
|
||||
status_code=finding.status,
|
||||
status_detail=finding.status_extended,
|
||||
risk_details=finding.metadata.Risk,
|
||||
resources=(
|
||||
[
|
||||
ResourceDetails(
|
||||
labels=unroll_dict_to_list(finding.resource_tags),
|
||||
name=finding.resource_name,
|
||||
uid=finding.resource_uid,
|
||||
group=Group(name=finding.metadata.ServiceName),
|
||||
type=finding.metadata.ResourceType,
|
||||
# TODO: this should be included only if using the Cloud profile
|
||||
cloud_partition=finding.partition,
|
||||
region=finding.region,
|
||||
data={"details": finding.resource_details},
|
||||
)
|
||||
]
|
||||
if finding.metadata.Provider != "kubernetes"
|
||||
else [
|
||||
ResourceDetails(
|
||||
labels=unroll_dict_to_list(finding.resource_tags),
|
||||
name=finding.resource_name,
|
||||
uid=finding.resource_uid,
|
||||
group=Group(name=finding.metadata.ServiceName),
|
||||
type=finding.metadata.ResourceType,
|
||||
data={"details": finding.resource_details},
|
||||
namespace=finding.region.replace("namespace: ", ""),
|
||||
)
|
||||
]
|
||||
),
|
||||
risk_details=finding.risk,
|
||||
resources=[
|
||||
ResourceDetails(
|
||||
labels=unroll_dict_to_list(finding.resource_tags),
|
||||
name=finding.resource_name,
|
||||
uid=finding.resource_uid,
|
||||
group=Group(name=finding.service_name),
|
||||
type=finding.resource_type,
|
||||
# TODO: this should be included only if using the Cloud profile
|
||||
cloud_partition=finding.partition,
|
||||
region=finding.region,
|
||||
data={"details": finding.resource_details},
|
||||
)
|
||||
],
|
||||
metadata=Metadata(
|
||||
event_code=finding.metadata.CheckID,
|
||||
event_code=finding.check_id,
|
||||
product=Product(
|
||||
uid="prowler",
|
||||
name="Prowler",
|
||||
vendor_name="Prowler",
|
||||
version=finding.prowler_version,
|
||||
),
|
||||
profiles=(
|
||||
["cloud", "datetime"]
|
||||
if finding.metadata.Provider != "kubernetes"
|
||||
else ["container", "datetime"]
|
||||
),
|
||||
tenant_uid=finding.account_organization_uid,
|
||||
),
|
||||
type_uid=DetectionFindingTypeID.Create,
|
||||
type_name=f"Detection Finding: {DetectionFindingTypeID.Create.name}",
|
||||
type_name=DetectionFindingTypeID.Create.name,
|
||||
unmapped={
|
||||
"related_url": finding.metadata.RelatedUrl,
|
||||
"categories": finding.metadata.Categories,
|
||||
"depends_on": finding.metadata.DependsOn,
|
||||
"related_to": finding.metadata.RelatedTo,
|
||||
"notes": finding.metadata.Notes,
|
||||
"check_type": finding.check_type,
|
||||
"related_url": finding.related_url,
|
||||
"categories": finding.categories,
|
||||
"depends_on": finding.depends_on,
|
||||
"related_to": finding.related_to,
|
||||
"notes": finding.notes,
|
||||
"compliance": finding.compliance,
|
||||
},
|
||||
)
|
||||
if finding.provider != "kubernetes":
|
||||
|
||||
if finding.provider == "kubernetes":
|
||||
detection_finding.container = Container(
|
||||
name=finding.resource_name,
|
||||
uid=finding.resource_uid,
|
||||
)
|
||||
# TODO: Get the PID of the namespace (we only have the name of the namespace)
|
||||
# detection_finding.namespace_pid=,
|
||||
else:
|
||||
detection_finding.cloud = Cloud(
|
||||
account=Account(
|
||||
name=finding.account_name,
|
||||
type_id=cloud_account_type.value,
|
||||
type=cloud_account_type.name.replace("_", " "),
|
||||
type=cloud_account_type.name,
|
||||
uid=finding.account_uid,
|
||||
labels=unroll_dict_to_list(finding.account_tags),
|
||||
),
|
||||
org=Organization(
|
||||
uid=finding.account_organization_uid,
|
||||
name=finding.account_organization_name,
|
||||
# TODO: add the org unit id and name
|
||||
),
|
||||
provider=finding.provider,
|
||||
region=finding.region,
|
||||
@@ -225,17 +208,20 @@ class OCSF(Output):
|
||||
return type_id
|
||||
|
||||
@staticmethod
|
||||
def get_finding_status_id(muted: bool) -> StatusID:
|
||||
def get_finding_status_id(status: str, muted: bool) -> StatusID:
|
||||
"""
|
||||
Returns the StatusID based on the muted value.
|
||||
Returns the StatusID based on the status and muted values.
|
||||
|
||||
Args:
|
||||
status (str): The status value
|
||||
muted (bool): The muted value
|
||||
|
||||
Returns:
|
||||
StatusID: The StatusID based on the muted value
|
||||
StatusID: The StatusID based on the status and muted values
|
||||
"""
|
||||
status_id = StatusID.New
|
||||
status_id = StatusID.Other
|
||||
if status == "FAIL":
|
||||
status_id = StatusID.New
|
||||
if muted:
|
||||
status_id = StatusID.Suppressed
|
||||
return status_id
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 8000 to 8999 are reserved for Slack exceptions
|
||||
class SlackBaseException(ProwlerException):
|
||||
"""Base class for Slack errors."""
|
||||
|
||||
SLACK_ERROR_CODES = {
|
||||
(8000, "SlackClientError"): {
|
||||
"message": "Slack ClientError occurred",
|
||||
"remediation": "Check your Slack client configuration and permissions.",
|
||||
},
|
||||
(8001, "SlackNoCredentialsError"): {
|
||||
"message": "Invalid Slack credentials found",
|
||||
"remediation": "Some aspect of authentication cannot be validated. Either the provided token is invalid or the request originates from an IP address disallowed from making the request.",
|
||||
},
|
||||
(8002, "SlackChannelNotFound"): {
|
||||
"message": "Slack channel not found",
|
||||
"remediation": "Check the channel name and ensure it exists.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
error_info = self.SLACK_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code,
|
||||
source="Slack",
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
)
|
||||
|
||||
|
||||
class SlackCredentialsError(SlackBaseException):
|
||||
"""Base class for Slack credentials errors."""
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
super().__init__(code, file, original_exception, message)
|
||||
|
||||
|
||||
class SlackClientError(SlackCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
8000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class SlackNoCredentialsError(SlackCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
8001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class SlackChannelNotFound(SlackCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
8002, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from slack_sdk import WebClient
|
||||
@@ -6,12 +5,6 @@ from slack_sdk.web.base_client import SlackResponse
|
||||
|
||||
from prowler.config.config import aws_logo, azure_logo, gcp_logo, square_logo_img
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.slack.exceptions.exceptions import (
|
||||
SlackChannelNotFound,
|
||||
SlackClientError,
|
||||
SlackNoCredentialsError,
|
||||
)
|
||||
from prowler.providers.common.models import Connection
|
||||
|
||||
|
||||
class Slack:
|
||||
@@ -50,7 +43,6 @@ class Slack:
|
||||
username="Prowler",
|
||||
icon_url=square_logo_img,
|
||||
channel=f"#{self.channel}",
|
||||
text="Prowler Scan Summary",
|
||||
blocks=self.__create_message_blocks__(identity, logo, stats, args),
|
||||
)
|
||||
return response
|
||||
@@ -58,6 +50,7 @@ class Slack:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return error
|
||||
|
||||
def __create_message_identity__(self, provider: Any):
|
||||
"""
|
||||
@@ -185,7 +178,7 @@ class Slack:
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://goto.prowler.com/slack",
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -237,66 +230,3 @@ class Slack:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
token: str,
|
||||
channel: str,
|
||||
raise_on_exception: bool = True,
|
||||
) -> Connection:
|
||||
"""
|
||||
Test the Slack connection by validating the provided token and channel.
|
||||
|
||||
Args:
|
||||
token (str): The Slack token to be tested.
|
||||
channel (str): The Slack channel to be validated.
|
||||
|
||||
Returns:
|
||||
Connection: A Connection object.
|
||||
"""
|
||||
try:
|
||||
client = WebClient(token=token)
|
||||
# Test if the token is valid
|
||||
auth_response = client.auth_test()
|
||||
if auth_response["ok"]:
|
||||
# Test if the channel is accessible
|
||||
channels_response = client.conversations_info(
|
||||
token=token, channel=channel
|
||||
)
|
||||
if channels_response["ok"]:
|
||||
return Connection(is_connected=True)
|
||||
else:
|
||||
exception = SlackChannelNotFound(
|
||||
file=os.path.basename(__file__),
|
||||
message=(
|
||||
channels_response["error"]
|
||||
if "error" in channels_response
|
||||
else "Unknown error"
|
||||
),
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise exception
|
||||
return Connection(error=exception)
|
||||
else:
|
||||
exception = SlackNoCredentialsError(
|
||||
file=os.path.basename(__file__),
|
||||
message=(
|
||||
auth_response["error"]
|
||||
if "error" in auth_response
|
||||
else "Unknown error"
|
||||
),
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise exception
|
||||
return Connection(error=exception)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise SlackClientError(
|
||||
file=os.path.basename(__file__),
|
||||
original_exception=error,
|
||||
) from error
|
||||
return Connection(error=error)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 5000 to 5999 are reserved for Scan exceptions
|
||||
class ScanBaseException(ProwlerException):
|
||||
"""Base class for Scan errors."""
|
||||
|
||||
SCAN_ERROR_CODES = {
|
||||
(5000, "ScanInvalidSeverityError"): {
|
||||
"message": "Invalid severity level provided.",
|
||||
"remediation": "Please provide a valid severity level. Valid severities are: critical, high, medium, low, informational.",
|
||||
},
|
||||
(5001, "ScanInvalidCheckError"): {
|
||||
"message": "Invalid check provided.",
|
||||
"remediation": "Please provide a valid check name.",
|
||||
},
|
||||
(5002, "ScanInvalidServiceError"): {
|
||||
"message": "Invalid service provided.",
|
||||
"remediation": "Please provide a valid service name.",
|
||||
},
|
||||
(5003, "ScanInvalidComplianceFrameworkError"): {
|
||||
"message": "Invalid compliance framework provided.",
|
||||
"remediation": "Please provide a valid compliance framework name for the chosen provider.",
|
||||
},
|
||||
(5004, "ScanInvalidCategoryError"): {
|
||||
"message": "Invalid category provided.",
|
||||
"remediation": "Please provide a valid category name.",
|
||||
},
|
||||
(5005, "ScanInvalidStatusError"): {
|
||||
"message": "Invalid status provided.",
|
||||
"remediation": "Please provide a valid status: FAIL, PASS, MANUAL.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
module = "Scan"
|
||||
error_info = self.SCAN_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 ScanInvalidSeverityError(ScanBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
5000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class ScanInvalidCheckError(ScanBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
5001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class ScanInvalidServiceError(ScanBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
5002, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class ScanInvalidComplianceFrameworkError(ScanBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
5003, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class ScanInvalidCategoryError(ScanBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
5004, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class ScanInvalidStatusError(ScanBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
5005, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
+9
-173
@@ -1,27 +1,8 @@
|
||||
import datetime
|
||||
from typing import Generator
|
||||
|
||||
from prowler.lib.check.check import (
|
||||
execute,
|
||||
import_check,
|
||||
list_services,
|
||||
update_audit_metadata,
|
||||
)
|
||||
from prowler.lib.check.checks_loader import load_checks_to_execute
|
||||
from prowler.lib.check.compliance import update_checks_metadata_with_compliance
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.models import CheckMetadata, Severity
|
||||
from prowler.lib.check.check import execute, import_check, update_audit_metadata
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.common import Status
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.lib.scan.exceptions.exceptions import (
|
||||
ScanInvalidCategoryError,
|
||||
ScanInvalidCheckError,
|
||||
ScanInvalidComplianceFrameworkError,
|
||||
ScanInvalidServiceError,
|
||||
ScanInvalidSeverityError,
|
||||
ScanInvalidStatusError,
|
||||
)
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
@@ -37,161 +18,29 @@ class Scan:
|
||||
_service_checks_completed: dict[str, set[str]]
|
||||
_progress: float = 0.0
|
||||
_findings: list = []
|
||||
_duration: int = 0
|
||||
_status: list[str] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider: Provider,
|
||||
checks: list[str] = None,
|
||||
services: list[str] = None,
|
||||
compliances: list[str] = None,
|
||||
categories: list[str] = None,
|
||||
severities: list[str] = None,
|
||||
excluded_checks: list[str] = None,
|
||||
excluded_services: list[str] = None,
|
||||
status: list[str] = None,
|
||||
):
|
||||
def __init__(self, provider: Provider, checks_to_execute: list[str]):
|
||||
"""
|
||||
Scan is the class that executes the checks and yields the progress and the findings.
|
||||
|
||||
Params:
|
||||
provider: Provider -> The provider to scan
|
||||
checks: list[str] -> The checks to execute
|
||||
services: list[str] -> The services to scan
|
||||
compliances: list[str] -> The compliances to check
|
||||
categories: list[str] -> The categories of the checks
|
||||
severities: list[str] -> The severities of the checks
|
||||
excluded_checks: list[str] -> The checks to exclude
|
||||
excluded_services: list[str] -> The services to exclude
|
||||
status: list[str] -> The status of the checks
|
||||
|
||||
Raises:
|
||||
ScanInvalidCheckError: If the check does not exist in the provider or is from another provider.
|
||||
ScanInvalidServiceError: If the service does not exist in the provider.
|
||||
ScanInvalidComplianceFrameworkError: If the compliance framework does not exist in the provider.
|
||||
ScanInvalidCategoryError: If the category does not exist in the provider.
|
||||
ScanInvalidSeverityError: If the severity does not exist in the provider.
|
||||
ScanInvalidStatusError: If the status does not exist in the provider.
|
||||
checks_to_execute: list[str] -> The checks to execute
|
||||
"""
|
||||
self._provider = provider
|
||||
# Remove duplicated checks and sort them
|
||||
self._checks_to_execute = sorted(list(set(checks_to_execute)))
|
||||
|
||||
# Validate the status
|
||||
if status:
|
||||
try:
|
||||
for s in status:
|
||||
Status(s)
|
||||
if not self._status:
|
||||
self._status = []
|
||||
if s not in self._status:
|
||||
self._status.append(s)
|
||||
except ValueError:
|
||||
raise ScanInvalidStatusError(f"Invalid status provided: {s}.")
|
||||
self._number_of_checks_to_execute = len(checks_to_execute)
|
||||
|
||||
# Load bulk compliance frameworks
|
||||
bulk_compliance_frameworks = Compliance.get_bulk(provider.type)
|
||||
|
||||
# Get bulk checks metadata for the provider
|
||||
bulk_checks_metadata = CheckMetadata.get_bulk(provider.type)
|
||||
# Complete checks metadata with the compliance framework specification
|
||||
bulk_checks_metadata = update_checks_metadata_with_compliance(
|
||||
bulk_compliance_frameworks, bulk_checks_metadata
|
||||
)
|
||||
|
||||
# Create a list of valid categories
|
||||
valid_categories = set()
|
||||
for check, metadata in bulk_checks_metadata.items():
|
||||
for category in metadata.Categories:
|
||||
if category not in valid_categories:
|
||||
valid_categories.add(category)
|
||||
|
||||
# Validate checks
|
||||
if checks:
|
||||
for check in checks:
|
||||
if check not in bulk_checks_metadata.keys():
|
||||
raise ScanInvalidCheckError(f"Invalid check provided: {check}.")
|
||||
|
||||
# Validate services
|
||||
if services:
|
||||
for service in services:
|
||||
if service not in list_services(provider.type):
|
||||
raise ScanInvalidServiceError(
|
||||
f"Invalid service provided: {service}."
|
||||
)
|
||||
|
||||
# Validate compliances
|
||||
if compliances:
|
||||
for compliance in compliances:
|
||||
if compliance not in bulk_compliance_frameworks.keys():
|
||||
raise ScanInvalidComplianceFrameworkError(
|
||||
f"Invalid compliance provided: {compliance}."
|
||||
)
|
||||
|
||||
# Validate categories
|
||||
if categories:
|
||||
for category in categories:
|
||||
if category not in valid_categories:
|
||||
raise ScanInvalidCategoryError(
|
||||
f"Invalid category provided: {category}."
|
||||
)
|
||||
|
||||
# Validate severity
|
||||
if severities:
|
||||
for severity in severities:
|
||||
try:
|
||||
Severity(severity)
|
||||
except ValueError:
|
||||
raise ScanInvalidSeverityError(
|
||||
f"Invalid severity provided: {severity}."
|
||||
)
|
||||
|
||||
# Load checks to execute
|
||||
self._checks_to_execute = sorted(
|
||||
load_checks_to_execute(
|
||||
bulk_checks_metadata=bulk_checks_metadata,
|
||||
bulk_compliance_frameworks=bulk_compliance_frameworks,
|
||||
check_list=checks,
|
||||
service_list=services,
|
||||
compliance_frameworks=compliances,
|
||||
categories=categories,
|
||||
severities=severities,
|
||||
provider=provider.type,
|
||||
checks_file=None,
|
||||
)
|
||||
)
|
||||
|
||||
# Exclude checks
|
||||
if excluded_checks:
|
||||
for check in excluded_checks:
|
||||
if check in self._checks_to_execute:
|
||||
self._checks_to_execute.remove(check)
|
||||
else:
|
||||
raise ScanInvalidCheckError(
|
||||
f"Invalid check provided: {check}. Check does not exist in the provider."
|
||||
)
|
||||
|
||||
# Exclude services
|
||||
if excluded_services:
|
||||
for check in self._checks_to_execute:
|
||||
if get_service_name_from_check_name(check) in excluded_services:
|
||||
self._checks_to_execute.remove(check)
|
||||
else:
|
||||
raise ScanInvalidServiceError(
|
||||
f"Invalid service provided: {check}. Service does not exist in the provider."
|
||||
)
|
||||
|
||||
self._number_of_checks_to_execute = len(self._checks_to_execute)
|
||||
|
||||
service_checks_to_execute = get_service_checks_to_execute(
|
||||
self._checks_to_execute
|
||||
)
|
||||
service_checks_to_execute = get_service_checks_to_execute(checks_to_execute)
|
||||
service_checks_completed = dict()
|
||||
|
||||
self._service_checks_to_execute = service_checks_to_execute
|
||||
self._service_checks_completed = service_checks_completed
|
||||
|
||||
@property
|
||||
def checks_to_execute(self) -> list[str]:
|
||||
def checks_to_execute(self) -> set[str]:
|
||||
return self._checks_to_execute
|
||||
|
||||
@property
|
||||
@@ -212,10 +61,6 @@ class Scan:
|
||||
self._number_of_checks_completed / self._number_of_checks_to_execute * 100
|
||||
)
|
||||
|
||||
@property
|
||||
def duration(self) -> int:
|
||||
return self._duration
|
||||
|
||||
@property
|
||||
def findings(self) -> list:
|
||||
return self._findings
|
||||
@@ -250,8 +95,6 @@ class Scan:
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
start_time = datetime.datetime.now()
|
||||
|
||||
for check_name in checks_to_execute:
|
||||
try:
|
||||
# Recover service from check name
|
||||
@@ -276,12 +119,6 @@ class Scan:
|
||||
output_options=None,
|
||||
)
|
||||
|
||||
# Filter the findings by the status
|
||||
if self._status:
|
||||
for finding in check_findings:
|
||||
if finding.status not in self._status:
|
||||
check_findings.remove(finding)
|
||||
|
||||
# Store findings
|
||||
self._findings.extend(check_findings)
|
||||
|
||||
@@ -312,6 +149,7 @@ class Scan:
|
||||
]
|
||||
|
||||
yield self.progress, findings
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
@@ -321,8 +159,6 @@ class Scan:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
# Update the scan duration when all checks are completed
|
||||
self._duration = int((datetime.datetime.now() - start_time).total_seconds())
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
from operator import attrgetter
|
||||
|
||||
try:
|
||||
import grp
|
||||
@@ -17,7 +16,7 @@ from io import TextIOWrapper
|
||||
from ipaddress import ip_address
|
||||
from os.path import exists
|
||||
from time import mktime
|
||||
from typing import Any, Optional
|
||||
from typing import Optional
|
||||
|
||||
from colorama import Style
|
||||
from detect_secrets import SecretsCollection
|
||||
@@ -121,7 +120,7 @@ def detect_secrets_scan(
|
||||
{"name": "HexHighEntropyString", "limit": 3.0},
|
||||
{"name": "IbmCloudIamDetector"},
|
||||
{"name": "IbmCosHmacDetector"},
|
||||
# {"name": "IPPublicDetector"}, https://github.com/Yelp/detect-secrets/pull/885
|
||||
{"name": "IPPublicDetector"},
|
||||
{"name": "JwtTokenDetector"},
|
||||
{"name": "KeywordDetector"},
|
||||
{"name": "MailchimpDetector"},
|
||||
@@ -273,44 +272,3 @@ def print_boxes(messages: list, report_title: str):
|
||||
f"{Style.BRIGHT}{Style.RESET_ALL} · {message}{Style.BRIGHT}{Style.RESET_ALL}"
|
||||
)
|
||||
print()
|
||||
|
||||
|
||||
def dict_to_lowercase(d):
|
||||
"""
|
||||
Convert all keys in a dictionary to lowercase.
|
||||
This function takes a dictionary and returns a new dictionary
|
||||
with all the keys converted to lowercase. If a value in the
|
||||
dictionary is another dictionary, the function will recursively
|
||||
convert the keys of that dictionary to lowercase as well.
|
||||
Args:
|
||||
d (dict): The dictionary to convert.
|
||||
Returns:
|
||||
dict: A new dictionary with all keys in lowercase.
|
||||
"""
|
||||
|
||||
new_dict = {}
|
||||
for k, v in d.items():
|
||||
if isinstance(v, dict):
|
||||
v = dict_to_lowercase(v)
|
||||
new_dict[k.lower()] = v
|
||||
return new_dict
|
||||
|
||||
|
||||
def get_nested_attribute(obj: Any, attr: str) -> Any:
|
||||
"""
|
||||
Get a nested attribute from an object.
|
||||
Args:
|
||||
obj (Any): The object to get the attribute from.
|
||||
attr (str): The attribute to get.
|
||||
Returns:
|
||||
Any: The attribute value if present, otherwise "".
|
||||
"""
|
||||
try:
|
||||
return attrgetter(attr)(obj)
|
||||
except AttributeError:
|
||||
return ""
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return ""
|
||||
|
||||
@@ -2,8 +2,8 @@ import os
|
||||
import pathlib
|
||||
from datetime import datetime
|
||||
from re import fullmatch
|
||||
from typing import Optional
|
||||
|
||||
from boto3 import client
|
||||
from boto3.session import Session
|
||||
from botocore.config import Config
|
||||
from botocore.credentials import RefreshableCredentials
|
||||
@@ -13,12 +13,7 @@ from colorama import Fore, Style
|
||||
from pytz import utc
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from prowler.config.config import (
|
||||
aws_services_json_file,
|
||||
default_config_file_path,
|
||||
get_default_mute_file_path,
|
||||
load_and_validate_config_file,
|
||||
)
|
||||
from prowler.config.config import aws_services_json_file, get_default_mute_file_path
|
||||
from prowler.lib.check.utils import list_modules, recover_checks_from_service
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
|
||||
@@ -29,22 +24,17 @@ from prowler.providers.aws.config import (
|
||||
ROLE_SESSION_NAME,
|
||||
)
|
||||
from prowler.providers.aws.exceptions.exceptions import (
|
||||
AWSAccessKeyIDInvalidError,
|
||||
AWSArgumentTypeValidationError,
|
||||
AWSAssumeRoleError,
|
||||
AWSClientError,
|
||||
AWSIAMRoleARNEmptyResourceError,
|
||||
AWSIAMRoleARNInvalidAccountIDError,
|
||||
AWSIAMRoleARNInvalidResourceTypeError,
|
||||
AWSIAMRoleARNPartitionEmptyError,
|
||||
AWSIAMRoleARNRegionNotEmtpyError,
|
||||
AWSIAMRoleARNServiceNotIAMnorSTSError,
|
||||
AWSInvalidPartitionError,
|
||||
AWSInvalidProviderIdError,
|
||||
AWSIAMRoleARNEmptyResource,
|
||||
AWSIAMRoleARNInvalidAccountID,
|
||||
AWSIAMRoleARNInvalidResourceType,
|
||||
AWSIAMRoleARNPartitionEmpty,
|
||||
AWSIAMRoleARNRegionNotEmtpy,
|
||||
AWSIAMRoleARNServiceNotIAMnorSTS,
|
||||
AWSNoCredentialsError,
|
||||
AWSProfileNotFoundError,
|
||||
AWSSecretAccessKeyInvalidError,
|
||||
AWSSessionTokenExpiredError,
|
||||
AWSSetUpSessionError,
|
||||
)
|
||||
from prowler.providers.aws.lib.arn.arn import parse_iam_credentials_arn
|
||||
@@ -63,32 +53,12 @@ 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
|
||||
@@ -97,7 +67,6 @@ class AwsProvider(Provider):
|
||||
_audit_config: dict
|
||||
_scan_unused_services: bool = False
|
||||
_enabled_regions: set = set()
|
||||
_mutelist: AWSMutelist
|
||||
# TODO: this is not optional, enforce for all providers
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
@@ -112,42 +81,30 @@ class AwsProvider(Provider):
|
||||
profile: str = None,
|
||||
regions: set = set(),
|
||||
organizations_role_arn: str = None,
|
||||
scan_unused_services: bool = False,
|
||||
scan_unused_services: bool = None,
|
||||
resource_tags: list[str] = [],
|
||||
resource_arn: list[str] = [],
|
||||
config_path: str = None,
|
||||
config_content: dict = None,
|
||||
audit_config: dict = {},
|
||||
fixer_config: dict = {},
|
||||
mutelist_path: str = None,
|
||||
mutelist_content: dict = None,
|
||||
aws_access_key_id: str = None,
|
||||
aws_secret_access_key: str = None,
|
||||
aws_session_token: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initializes the AWS provider.
|
||||
|
||||
Args:
|
||||
Arguments:
|
||||
- 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, between 900 and 43200.
|
||||
- session_duration: The duration of the session in seconds.
|
||||
- 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.
|
||||
- profile: The name of the AWS CLI profile to use.
|
||||
- regions: A set of regions to audit.
|
||||
- organizations_role_arn: The ARN of the AWS Organizations IAM role to assume.
|
||||
- scan_unused_services: A boolean indicating whether to scan unused services. False by default.
|
||||
- scan_unused_services: A boolean indicating whether to scan unused services.
|
||||
- resource_tags: A list of tags to filter the resources to audit.
|
||||
- resource_arn: A list of ARNs of the resources to audit.
|
||||
- config_path: The path to the configuration file.
|
||||
- config_content: The content of the configuration file.
|
||||
- audit_config: The audit configuration.
|
||||
- fixer_config: The fixer configuration.
|
||||
- mutelist_path: The path to the mutelist file.
|
||||
- mutelist_content: The content of the mutelist file.
|
||||
- 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.
|
||||
|
||||
Raises:
|
||||
- ArgumentTypeError: If the input MFA ARN is invalid.
|
||||
@@ -155,52 +112,14 @@ 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 ...")
|
||||
|
||||
######## AWS Session
|
||||
logger.info("Generating original session ...")
|
||||
|
||||
# Configure the initial AWS Session using the local credentials: profile or environment variables
|
||||
aws_session = self.setup_session(
|
||||
mfa=mfa,
|
||||
profile=profile,
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
aws_session_token=aws_session_token,
|
||||
)
|
||||
aws_session = self.setup_session(mfa, profile)
|
||||
session_config = self.set_session_config(retries_max_attempts)
|
||||
# Current session and the original session points to the same session object until we get a new one, if needed
|
||||
self._session = AWSSession(
|
||||
@@ -345,32 +264,10 @@ class AwsProvider(Provider):
|
||||
self._scan_unused_services = scan_unused_services
|
||||
|
||||
# Audit Config
|
||||
if config_content:
|
||||
self._audit_config = config_content
|
||||
else:
|
||||
if not config_path:
|
||||
config_path = default_config_file_path
|
||||
self._audit_config = load_and_validate_config_file(self._type, config_path)
|
||||
|
||||
self._audit_config = audit_config
|
||||
# Fixer Config
|
||||
self._fixer_config = fixer_config
|
||||
|
||||
# Mutelist
|
||||
if mutelist_content:
|
||||
self._mutelist = AWSMutelist(
|
||||
mutelist_content=mutelist_content,
|
||||
session=self._session.current_session,
|
||||
aws_account_id=self._identity.account,
|
||||
)
|
||||
else:
|
||||
if not mutelist_path:
|
||||
mutelist_path = get_default_mute_file_path(self.type)
|
||||
self._mutelist = AWSMutelist(
|
||||
mutelist_path=mutelist_path,
|
||||
session=self._session.current_session,
|
||||
aws_account_id=self._identity.account,
|
||||
)
|
||||
|
||||
Provider.set_global_provider(self)
|
||||
|
||||
@property
|
||||
@@ -412,6 +309,37 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
return self._mutelist
|
||||
|
||||
# TODO: this is going to be called from another place soon, since the provider
|
||||
# shouldn't hold the mutelist
|
||||
@mutelist.setter
|
||||
def mutelist(self, mutelist_path):
|
||||
"""
|
||||
mutelist.setter sets the provider's mutelist.
|
||||
"""
|
||||
# Set default mutelist path if none is set
|
||||
if not mutelist_path:
|
||||
mutelist_path = get_default_mute_file_path(self.type)
|
||||
|
||||
self._mutelist = AWSMutelist(
|
||||
mutelist_path=mutelist_path,
|
||||
session=self._session.current_session,
|
||||
aws_account_id=self._identity.account,
|
||||
)
|
||||
|
||||
@property
|
||||
def get_output_mapping(self):
|
||||
return {
|
||||
"auth_method": "identity.profile",
|
||||
"provider": "type",
|
||||
"account_uid": "identity.account",
|
||||
"account_name": "organizations_metadata.account_name",
|
||||
"account_email": "organizations_metadata.account_email",
|
||||
"account_organization_uid": "organizations_metadata.organization_arn",
|
||||
"account_organization_name": "organizations_metadata.organization_id",
|
||||
"account_tags": "organizations_metadata.account_tags",
|
||||
"partition": "identity.partition",
|
||||
}
|
||||
|
||||
# TODO: This can be moved to another class since it doesn't need self
|
||||
def get_organizations_info(
|
||||
self, organizations_session: Session, aws_account_id: str
|
||||
@@ -419,7 +347,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.
|
||||
|
||||
Args:
|
||||
Arguments:
|
||||
- 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
|
||||
|
||||
@@ -476,21 +404,6 @@ 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}")
|
||||
|
||||
@@ -510,49 +423,17 @@ class AwsProvider(Provider):
|
||||
def setup_session(
|
||||
mfa: bool = False,
|
||||
profile: str = None,
|
||||
aws_access_key_id: str = None,
|
||||
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 ...")
|
||||
|
||||
session_arguments = {}
|
||||
if profile:
|
||||
session_arguments["profile_name"] = profile
|
||||
elif aws_access_key_id and aws_secret_access_key:
|
||||
session_arguments["aws_access_key_id"] = aws_access_key_id
|
||||
session_arguments["aws_secret_access_key"] = aws_secret_access_key
|
||||
if aws_session_token:
|
||||
session_arguments["aws_session_token"] = aws_session_token
|
||||
|
||||
logger.info("Creating original session ...")
|
||||
if mfa:
|
||||
session = Session(**session_arguments)
|
||||
sts_client = session.client("sts")
|
||||
|
||||
# TODO: pass values from the input
|
||||
mfa_info = AwsProvider.input_role_mfa_token_and_code()
|
||||
# TODO: validate MFA ARN here
|
||||
get_session_token_arguments = {
|
||||
"SerialNumber": mfa_info.arn,
|
||||
"TokenCode": mfa_info.totp,
|
||||
}
|
||||
sts_client = client("sts")
|
||||
session_credentials = sts_client.get_session_token(
|
||||
**get_session_token_arguments
|
||||
)
|
||||
@@ -564,9 +445,12 @@ class AwsProvider(Provider):
|
||||
aws_session_token=session_credentials["Credentials"][
|
||||
"SessionToken"
|
||||
],
|
||||
profile_name=profile,
|
||||
)
|
||||
else:
|
||||
return Session(**session_arguments)
|
||||
return Session(
|
||||
profile_name=profile,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"AWSSetUpSessionError[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -671,21 +555,6 @@ 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)
|
||||
@@ -727,9 +596,7 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
try:
|
||||
regional_clients = {}
|
||||
service_regions = AwsProvider.get_available_aws_service_regions(
|
||||
service, self._identity.partition, self._identity.audited_regions
|
||||
)
|
||||
service_regions = self.get_available_aws_service_regions(service)
|
||||
|
||||
# Get the regions enabled for the account and get the intersection with the service available regions
|
||||
if self._enabled_regions:
|
||||
@@ -750,26 +617,14 @@ class AwsProvider(Provider):
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_available_aws_service_regions(
|
||||
service: str, partition: str = "aws", audited_regions: set = None
|
||||
) -> set:
|
||||
"""
|
||||
get_available_aws_service_regions returns the available regions for the given service and partition.
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
- A set of strings representing the available regions for the given service and partition.
|
||||
"""
|
||||
def get_available_aws_service_regions(self, service: str) -> set:
|
||||
data = read_aws_regions_file()
|
||||
json_regions = set(data["services"][service]["regions"][partition])
|
||||
if audited_regions:
|
||||
json_regions = set(
|
||||
data["services"][service]["regions"][self._identity.partition]
|
||||
)
|
||||
if self._identity.audited_regions:
|
||||
# Get common regions between input and json
|
||||
regions = json_regions.intersection(audited_regions)
|
||||
regions = json_regions.intersection(self._identity.audited_regions)
|
||||
else: # Get all regions from json of the service and partition
|
||||
regions = json_regions
|
||||
return regions
|
||||
@@ -777,12 +632,6 @@ 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
|
||||
@@ -849,18 +698,7 @@ 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
|
||||
|
||||
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)
|
||||
"""
|
||||
"""get_regions_from_audit_resources gets the regions from the audit resources arns"""
|
||||
audited_regions = set()
|
||||
for resource in audit_resources:
|
||||
region = resource.split(":")[3]
|
||||
@@ -920,22 +758,9 @@ 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
|
||||
|
||||
Args:
|
||||
- service: The AWS service name
|
||||
|
||||
Returns:
|
||||
- str: The default region for the given service
|
||||
|
||||
Example:
|
||||
service = "ec2"
|
||||
default_region = get_default_region(service)
|
||||
"""
|
||||
"""get_default_region returns the default region based on the profile and audited service regions"""
|
||||
try:
|
||||
service_regions = AwsProvider.get_available_aws_service_regions(
|
||||
service, self._identity.partition, self._identity.audited_regions
|
||||
)
|
||||
service_regions = self.get_available_aws_service_regions(service)
|
||||
default_region = self.get_global_region()
|
||||
# global region of the partition when all regions are audited and there is no profile region
|
||||
if self._identity.profile_region in service_regions:
|
||||
@@ -952,14 +777,7 @@ class AwsProvider(Provider):
|
||||
raise error
|
||||
|
||||
def get_global_region(self) -> str:
|
||||
"""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
|
||||
"""
|
||||
"""get_global_region returns the global region based on the audited partition"""
|
||||
global_region = "us-east-1"
|
||||
if self._identity.partition == "aws-cn":
|
||||
global_region = "cn-north-1"
|
||||
@@ -971,14 +789,7 @@ 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.
|
||||
|
||||
Returns:
|
||||
- AWSMFAInfo: An object containing the MFA ARN and TOTP code
|
||||
|
||||
Example:
|
||||
mfa_info = input_role_mfa_token_and_code()
|
||||
"""
|
||||
"""input_role_mfa_token_and_code ask for the AWS MFA ARN and TOTP and returns it."""
|
||||
mfa_ARN = input("Enter ARN of MFA: ")
|
||||
mfa_TOTP = input("Enter MFA code: ")
|
||||
return AWSMFAInfo(arn=mfa_ARN, totp=mfa_TOTP)
|
||||
@@ -986,12 +797,6 @@ 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(
|
||||
@@ -1018,13 +823,6 @@ 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 = (
|
||||
@@ -1076,14 +874,7 @@ class AwsProvider(Provider):
|
||||
)
|
||||
|
||||
def get_aws_enabled_regions(self, current_session: Session) -> set:
|
||||
"""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
|
||||
"""
|
||||
"""get_aws_enabled_regions returns a set of enabled AWS regions"""
|
||||
try:
|
||||
# EC2 Client to check enabled regions
|
||||
service = "ec2"
|
||||
@@ -1107,12 +898,6 @@ 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()
|
||||
@@ -1154,31 +939,6 @@ class AwsProvider(Provider):
|
||||
arn=ARN(caller_identity.get("Arn")),
|
||||
region=aws_region,
|
||||
)
|
||||
except ClientError as client_error:
|
||||
logger.error(
|
||||
f"{client_error.__class__.__name__}[{client_error.__traceback__.tb_lineno}]: {client_error}"
|
||||
)
|
||||
if client_error.response["Error"]["Code"] == "InvalidClientTokenId":
|
||||
raise AWSAccessKeyIDInvalidError(
|
||||
original_exception=client_error,
|
||||
file=pathlib.Path(__file__).name,
|
||||
)
|
||||
elif client_error.response["Error"]["Code"] == "SignatureDoesNotMatch":
|
||||
raise AWSSecretAccessKeyInvalidError(
|
||||
original_exception=client_error,
|
||||
file=pathlib.Path(__file__).name,
|
||||
)
|
||||
elif client_error.response["Error"]["Code"] == "ExpiredToken":
|
||||
raise AWSSessionTokenExpiredError(
|
||||
original_exception=client_error,
|
||||
file=pathlib.Path(__file__).name,
|
||||
)
|
||||
else:
|
||||
raise AWSClientError(
|
||||
original_exception=client_error,
|
||||
file=pathlib.Path(__file__).name,
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -1195,10 +955,6 @@ class AwsProvider(Provider):
|
||||
external_id: str = None,
|
||||
mfa_enabled: bool = False,
|
||||
raise_on_exception: bool = True,
|
||||
aws_access_key_id: str = None,
|
||||
aws_secret_access_key: str = None,
|
||||
aws_session_token: Optional[str] = None,
|
||||
provider_id: Optional[str] = None,
|
||||
) -> Connection:
|
||||
"""
|
||||
Test the connection to AWS with one of the Boto3 credentials methods.
|
||||
@@ -1212,10 +968,6 @@ class AwsProvider(Provider):
|
||||
external_id (str): The external ID to use when assuming the role.
|
||||
mfa_enabled (bool): Whether MFA (Multi-Factor Authentication) is enabled.
|
||||
raise_on_exception (bool): Whether to raise an exception if an error occurs.
|
||||
aws_access_key_id (str): The AWS access key ID to use for the session.
|
||||
aws_secret_access_key (str): The AWS secret access key to use for the session.
|
||||
aws_session_token (str): The AWS session token to use for the session. Optional.
|
||||
provider_id (str): The AWS account ID to validate that the provided credentials belongs to it.
|
||||
|
||||
Returns:
|
||||
Connection: An object tha contains the result of the test connection operation.
|
||||
@@ -1242,19 +994,9 @@ class AwsProvider(Provider):
|
||||
Connection(is_connected=False, Error=ProfileNotFound('The config profile (not-found) could not be found'))
|
||||
>>> AwsProvider.test_connection(raise_on_exception=False))
|
||||
Connection(is_connected=False, Error=NoCredentialsError('Unable to locate credentials'))
|
||||
>>> AwsProvider.test_connection(aws_access_key_id="XXXXXXXX", aws_secret_access_key="XXXXXXXX", raise_on_exception=False))
|
||||
Connection(is_connected=True, Error=None))
|
||||
>>> AwsProvider.test_connection(aws_access_key_id="XXXXXXXX", aws_secret_access_key="XXXXXXXX", provider_id="111122223333", raise_on_exception=False))
|
||||
Connection(is_connected=True, Error=None))
|
||||
"""
|
||||
try:
|
||||
session = AwsProvider.setup_session(
|
||||
mfa=mfa_enabled,
|
||||
profile=profile,
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
aws_session_token=aws_session_token,
|
||||
)
|
||||
session = AwsProvider.setup_session(mfa_enabled, profile)
|
||||
|
||||
if role_arn:
|
||||
session_duration = validate_session_duration(session_duration)
|
||||
@@ -1279,83 +1021,62 @@ class AwsProvider(Provider):
|
||||
profile_name=profile,
|
||||
)
|
||||
|
||||
caller_identity = AwsProvider.validate_credentials(session, aws_region)
|
||||
# Do an extra validation if the AWS account ID is provided
|
||||
if provider_id and caller_identity.account != provider_id:
|
||||
raise AWSInvalidProviderIdError(file=pathlib.Path(__file__).name)
|
||||
|
||||
sts_client = AwsProvider.create_sts_session(session, aws_region)
|
||||
_ = sts_client.get_caller_identity()
|
||||
return Connection(
|
||||
is_connected=True,
|
||||
)
|
||||
|
||||
except AWSSetUpSessionError as setup_session_error:
|
||||
logger.error(
|
||||
f"{setup_session_error.__class__.__name__}[{setup_session_error.__traceback__.tb_lineno}]: {setup_session_error}"
|
||||
)
|
||||
logger.error(str(setup_session_error))
|
||||
if raise_on_exception:
|
||||
raise setup_session_error
|
||||
return Connection(error=setup_session_error)
|
||||
|
||||
except AWSArgumentTypeValidationError as validation_error:
|
||||
logger.error(
|
||||
f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error}"
|
||||
)
|
||||
logger.error(str(validation_error))
|
||||
if raise_on_exception:
|
||||
raise validation_error
|
||||
return Connection(error=validation_error)
|
||||
|
||||
except AWSIAMRoleARNRegionNotEmtpyError as arn_region_not_empty_error:
|
||||
logger.error(
|
||||
f"{arn_region_not_empty_error.__class__.__name__}[{arn_region_not_empty_error.__traceback__.tb_lineno}]: {arn_region_not_empty_error}"
|
||||
)
|
||||
except AWSIAMRoleARNRegionNotEmtpy as arn_region_not_empty_error:
|
||||
logger.error(str(arn_region_not_empty_error))
|
||||
if raise_on_exception:
|
||||
raise arn_region_not_empty_error
|
||||
return Connection(error=arn_region_not_empty_error)
|
||||
|
||||
except AWSIAMRoleARNPartitionEmptyError as arn_partition_empty_error:
|
||||
logger.error(
|
||||
f"{arn_partition_empty_error.__class__.__name__}[{arn_partition_empty_error.__traceback__.tb_lineno}]: {arn_partition_empty_error}"
|
||||
)
|
||||
except AWSIAMRoleARNPartitionEmpty as arn_partition_empty_error:
|
||||
logger.error(str(arn_partition_empty_error))
|
||||
if raise_on_exception:
|
||||
raise arn_partition_empty_error
|
||||
return Connection(error=arn_partition_empty_error)
|
||||
|
||||
except AWSIAMRoleARNServiceNotIAMnorSTSError as arn_service_not_iam_sts_error:
|
||||
logger.error(
|
||||
f"{arn_service_not_iam_sts_error.__class__.__name__}[{arn_service_not_iam_sts_error.__traceback__.tb_lineno}]: {arn_service_not_iam_sts_error}"
|
||||
)
|
||||
except AWSIAMRoleARNServiceNotIAMnorSTS as arn_service_not_iam_sts_error:
|
||||
logger.error(str(arn_service_not_iam_sts_error))
|
||||
if raise_on_exception:
|
||||
raise arn_service_not_iam_sts_error
|
||||
return Connection(error=arn_service_not_iam_sts_error)
|
||||
|
||||
except AWSIAMRoleARNInvalidAccountIDError as arn_invalid_account_id_error:
|
||||
logger.error(
|
||||
f"{arn_invalid_account_id_error.__class__.__name__}[{arn_invalid_account_id_error.__traceback__.tb_lineno}]: {arn_invalid_account_id_error}"
|
||||
)
|
||||
except AWSIAMRoleARNInvalidAccountID as arn_invalid_account_id_error:
|
||||
logger.error(str(arn_invalid_account_id_error))
|
||||
if raise_on_exception:
|
||||
raise arn_invalid_account_id_error
|
||||
return Connection(error=arn_invalid_account_id_error)
|
||||
|
||||
except AWSIAMRoleARNInvalidResourceTypeError as arn_invalid_resource_type_error:
|
||||
logger.error(
|
||||
f"{arn_invalid_resource_type_error.__class__.__name__}[{arn_invalid_resource_type_error.__traceback__.tb_lineno}]: {arn_invalid_resource_type_error}"
|
||||
)
|
||||
except AWSIAMRoleARNInvalidResourceType as arn_invalid_resource_type_error:
|
||||
logger.error(str(arn_invalid_resource_type_error))
|
||||
if raise_on_exception:
|
||||
raise arn_invalid_resource_type_error
|
||||
return Connection(error=arn_invalid_resource_type_error)
|
||||
|
||||
except AWSIAMRoleARNEmptyResourceError as arn_empty_resource_error:
|
||||
logger.error(
|
||||
f"{arn_empty_resource_error.__class__.__name__}[{arn_empty_resource_error.__traceback__.tb_lineno}]: {arn_empty_resource_error}"
|
||||
)
|
||||
except AWSIAMRoleARNEmptyResource as arn_empty_resource_error:
|
||||
logger.error(str(arn_empty_resource_error))
|
||||
if raise_on_exception:
|
||||
raise arn_empty_resource_error
|
||||
return Connection(error=arn_empty_resource_error)
|
||||
|
||||
except AWSAssumeRoleError as assume_role_error:
|
||||
logger.error(
|
||||
f"{assume_role_error.__class__.__name__}[{assume_role_error.__traceback__.tb_lineno}]: {assume_role_error}"
|
||||
)
|
||||
logger.error(str(assume_role_error))
|
||||
if raise_on_exception:
|
||||
raise assume_role_error
|
||||
return Connection(error=assume_role_error)
|
||||
@@ -1392,45 +1113,11 @@ class AwsProvider(Provider):
|
||||
) from no_credentials_error
|
||||
return Connection(error=no_credentials_error)
|
||||
|
||||
except AWSAccessKeyIDInvalidError as access_key_id_invalid_error:
|
||||
logger.error(
|
||||
f"{access_key_id_invalid_error.__class__.__name__}[{access_key_id_invalid_error.__traceback__.tb_lineno}]: {access_key_id_invalid_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise access_key_id_invalid_error
|
||||
return Connection(error=access_key_id_invalid_error)
|
||||
|
||||
except AWSSecretAccessKeyInvalidError as secret_access_key_invalid_error:
|
||||
logger.error(
|
||||
f"{secret_access_key_invalid_error.__class__.__name__}[{secret_access_key_invalid_error.__traceback__.tb_lineno}]: {secret_access_key_invalid_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise secret_access_key_invalid_error
|
||||
return Connection(error=secret_access_key_invalid_error)
|
||||
|
||||
except AWSInvalidProviderIdError as invalid_account_credentials_error:
|
||||
logger.error(
|
||||
f"{invalid_account_credentials_error.__class__.__name__}[{invalid_account_credentials_error.__traceback__.tb_lineno}]: {invalid_account_credentials_error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise invalid_account_credentials_error
|
||||
return Connection(error=invalid_account_credentials_error)
|
||||
|
||||
except AWSSessionTokenExpiredError as session_token_expired:
|
||||
logger.error(
|
||||
f"{session_token_expired.__class__.__name__}[{session_token_expired.__traceback__.tb_lineno}]: {session_token_expired}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise session_token_expired
|
||||
return Connection(error=session_token_expired)
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise error
|
||||
return Connection(error=error)
|
||||
raise error
|
||||
|
||||
@staticmethod
|
||||
def create_sts_session(
|
||||
@@ -1439,7 +1126,7 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
Create an STS session client.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
- session (session.Session): The AWS session object.
|
||||
- aws_region (str): The AWS region to use for the session.
|
||||
|
||||
@@ -1463,59 +1150,6 @@ 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:
|
||||
"""
|
||||
@@ -1532,21 +1166,29 @@ def read_aws_regions_file() -> dict:
|
||||
return data
|
||||
|
||||
|
||||
# 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:
|
||||
def get_aws_available_regions() -> set:
|
||||
"""
|
||||
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.
|
||||
Get the available AWS regions from the AWS services JSON file.
|
||||
|
||||
Returns:
|
||||
str: The AWS region for the STS Assume Role operation
|
||||
|
||||
Example:
|
||||
aws_region = get_aws_region_for_sts(session_region, regions)
|
||||
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:
|
||||
# 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
|
||||
|
||||
@@ -223,7 +223,6 @@
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"sa-east-1",
|
||||
@@ -1145,7 +1144,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -1264,50 +1262,20 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bedrock-agent": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -1316,24 +1284,20 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -2627,7 +2591,6 @@
|
||||
"connectcampaigns": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
@@ -2690,7 +2653,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -2731,7 +2693,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3295,7 +3256,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -3980,7 +3940,6 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
@@ -3993,7 +3952,6 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
@@ -4400,7 +4358,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -4842,7 +4799,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5553,7 +5509,6 @@
|
||||
"iotfleetwise": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-south-1",
|
||||
"eu-central-1",
|
||||
"us-east-1"
|
||||
],
|
||||
@@ -5860,7 +5815,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -5977,7 +5931,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -6250,7 +6203,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -6840,7 +6792,6 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-4",
|
||||
@@ -6850,7 +6801,6 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -6975,7 +6925,6 @@
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -6989,10 +6938,7 @@
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"meteringmarketplace": {
|
||||
@@ -7168,7 +7114,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7567,13 +7512,11 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -7584,7 +7527,6 @@
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -7632,9 +7574,15 @@
|
||||
"opsworkscm": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"us-east-1"
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
@@ -7740,7 +7688,6 @@
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -8513,10 +8460,7 @@
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"rekognition": {
|
||||
@@ -8529,7 +8473,6 @@
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"il-central-1",
|
||||
@@ -9212,7 +9155,6 @@
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
@@ -9267,6 +9209,7 @@
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -9274,7 +9217,10 @@
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-cn": [
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
@@ -9436,10 +9382,7 @@
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"sdb": {
|
||||
@@ -9809,39 +9752,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"sesv2": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"shield": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -9856,7 +9766,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10225,30 +10134,6 @@
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"ssm-quicksetup": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"ssm-sap": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -10386,7 +10271,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -10620,7 +10504,6 @@
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -10655,7 +10538,6 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
@@ -10690,10 +10572,7 @@
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
@@ -10701,7 +10580,6 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
@@ -10780,7 +10658,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -11095,21 +10972,16 @@
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"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",
|
||||
@@ -11176,7 +11048,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -11262,7 +11133,6 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -11356,10 +11226,8 @@
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-west-2",
|
||||
"us-east-1"
|
||||
|
||||
@@ -1,83 +1,62 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 1000 to 1999 are reserved for AWS exceptions
|
||||
class AWSBaseException(ProwlerException):
|
||||
"""Base class for AWS errors."""
|
||||
|
||||
AWS_ERROR_CODES = {
|
||||
(1000, "AWSClientError"): {
|
||||
(1902, "AWSClientError"): {
|
||||
"message": "AWS ClientError occurred",
|
||||
"remediation": "Check your AWS client configuration and permissions.",
|
||||
},
|
||||
(1001, "AWSProfileNotFoundError"): {
|
||||
(1903, "AWSProfileNotFoundError"): {
|
||||
"message": "AWS Profile not found",
|
||||
"remediation": "Ensure the AWS profile is correctly configured, please visit https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html",
|
||||
},
|
||||
(1002, "AWSNoCredentialsError"): {
|
||||
(1904, "AWSNoCredentialsError"): {
|
||||
"message": "No AWS credentials found",
|
||||
"remediation": "Verify that AWS credentials are properly set up, please visit https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/aws/authentication/ and https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-configure.html",
|
||||
},
|
||||
(1003, "AWSArgumentTypeValidationError"): {
|
||||
(1905, "AWSArgumentTypeValidationError"): {
|
||||
"message": "AWS argument type validation error",
|
||||
"remediation": "Check the provided argument types specific to AWS and ensure they meet the required format. For session duration check: https://docs.aws.amazon.com/singlesignon/latest/userguide/howtosessionduration.html and for role session name check: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-session-name",
|
||||
},
|
||||
(1004, "AWSSetUpSessionError"): {
|
||||
(1906, "AWSSetUpSessionError"): {
|
||||
"message": "AWS session setup error",
|
||||
"remediation": "Check the AWS session setup and ensure it is properly configured, please visit https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html and check if the provided profile has the necessary permissions.",
|
||||
},
|
||||
(1005, "AWSIAMRoleARNRegionNotEmtpyError"): {
|
||||
(1907, "AWSIAMRoleARNRegionNotEmtpy"): {
|
||||
"message": "AWS IAM Role ARN region is not empty",
|
||||
"remediation": "Check the AWS IAM Role ARN region and ensure it is empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1006, "AWSIAMRoleARNPartitionEmptyError"): {
|
||||
(1908, "AWSIAMRoleARNPartitionEmpty"): {
|
||||
"message": "AWS IAM Role ARN partition is empty",
|
||||
"remediation": "Check the AWS IAM Role ARN partition and ensure it is not empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1007, "AWSIAMRoleARNMissingFieldsError"): {
|
||||
(1909, "AWSIAMRoleARNMissingFields"): {
|
||||
"message": "AWS IAM Role ARN missing fields",
|
||||
"remediation": "Check the AWS IAM Role ARN and ensure all required fields are present, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1008, "AWSIAMRoleARNServiceNotIAMnorSTSError"): {
|
||||
(1910, "AWSIAMRoleARNServiceNotIAMnorSTS"): {
|
||||
"message": "AWS IAM Role ARN service is not IAM nor STS",
|
||||
"remediation": "Check the AWS IAM Role ARN service and ensure it is either IAM or STS, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1009, "AWSIAMRoleARNInvalidAccountIDError"): {
|
||||
(1911, "AWSIAMRoleARNInvalidAccountID"): {
|
||||
"message": "AWS IAM Role ARN account ID is invalid",
|
||||
"remediation": "Check the AWS IAM Role ARN account ID and ensure it is a valid 12-digit number, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1010, "AWSIAMRoleARNInvalidResourceTypeError"): {
|
||||
(1912, "AWSIAMRoleARNInvalidResourceType"): {
|
||||
"message": "AWS IAM Role ARN resource type is invalid",
|
||||
"remediation": "Check the AWS IAM Role ARN resource type and ensure it is valid, resources types are: role, user, assumed-role, root, federated-user, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1011, "AWSIAMRoleARNEmptyResourceError"): {
|
||||
(1913, "AWSIAMRoleARNEmptyResource"): {
|
||||
"message": "AWS IAM Role ARN resource is empty",
|
||||
"remediation": "Check the AWS IAM Role ARN resource and ensure it is not empty, visit https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-arns for more information.",
|
||||
},
|
||||
(1012, "AWSAssumeRoleError"): {
|
||||
(1914, "AWSAssumeRoleError"): {
|
||||
"message": "AWS assume role error",
|
||||
"remediation": "Check the AWS assume role configuration and ensure it is properly set up, please visit https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/aws/role-assumption/ and https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-session-name",
|
||||
},
|
||||
(1013, "AWSAccessKeyIDInvalidError"): {
|
||||
"message": "AWS Access Key ID or Session Token is invalid",
|
||||
"remediation": "Check your AWS Access Key ID or Session Token and ensure it is valid.",
|
||||
},
|
||||
(1014, "AWSSecretAccessKeyInvalidError"): {
|
||||
"message": "AWS Secret Access Key is invalid",
|
||||
"remediation": "Check your AWS Secret Access Key and signing method and ensure it is valid.",
|
||||
},
|
||||
(1015, "AWSInvalidProviderIdError"): {
|
||||
"message": "The provided AWS credentials belong to a different account",
|
||||
"remediation": "Check the provided AWS credentials and review if belong to the account you want to use.",
|
||||
},
|
||||
(1016, "AWSSessionTokenExpiredError"): {
|
||||
"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):
|
||||
@@ -86,7 +65,7 @@ class AWSBaseException(ProwlerException):
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code,
|
||||
source="AWS",
|
||||
provider="AWS",
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
@@ -103,35 +82,35 @@ class AWSCredentialsError(AWSBaseException):
|
||||
class AWSClientError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1000, file=file, original_exception=original_exception, message=message
|
||||
1902, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSProfileNotFoundError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1001, file=file, original_exception=original_exception, message=message
|
||||
1903, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSNoCredentialsError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1002, file=file, original_exception=original_exception, message=message
|
||||
1904, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSArgumentTypeValidationError(AWSBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1003, file=file, original_exception=original_exception, message=message
|
||||
1905, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSSetUpSessionError(AWSBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1004, file=file, original_exception=original_exception, message=message
|
||||
1906, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
@@ -142,92 +121,57 @@ class AWSRoleArnError(AWSBaseException):
|
||||
super().__init__(code, file, original_exception, message)
|
||||
|
||||
|
||||
class AWSIAMRoleARNRegionNotEmtpyError(AWSRoleArnError):
|
||||
class AWSIAMRoleARNRegionNotEmtpy(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1005, file=file, original_exception=original_exception, message=message
|
||||
1907, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNPartitionEmptyError(AWSRoleArnError):
|
||||
class AWSIAMRoleARNPartitionEmpty(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1006, file=file, original_exception=original_exception, message=message
|
||||
1908, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNMissingFieldsError(AWSRoleArnError):
|
||||
class AWSIAMRoleARNMissingFields(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1007, file=file, original_exception=original_exception, message=message
|
||||
1909, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNServiceNotIAMnorSTSError(AWSRoleArnError):
|
||||
class AWSIAMRoleARNServiceNotIAMnorSTS(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1008, file=file, original_exception=original_exception, message=message
|
||||
1910, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNInvalidAccountIDError(AWSRoleArnError):
|
||||
class AWSIAMRoleARNInvalidAccountID(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1009, file=file, original_exception=original_exception, message=message
|
||||
1911, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNInvalidResourceTypeError(AWSRoleArnError):
|
||||
class AWSIAMRoleARNInvalidResourceType(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1010, file=file, original_exception=original_exception, message=message
|
||||
1912, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSIAMRoleARNEmptyResourceError(AWSRoleArnError):
|
||||
class AWSIAMRoleARNEmptyResource(AWSRoleArnError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1011, file=file, original_exception=original_exception, message=message
|
||||
1913, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSAssumeRoleError(AWSBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1012, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSAccessKeyIDInvalidError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1013, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSSecretAccessKeyInvalidError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1014, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSInvalidProviderIdError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
1015, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class AWSSessionTokenExpiredError(AWSCredentialsError):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
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
|
||||
1914, 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 AwsProvider
|
||||
from prowler.providers.aws.aws_provider import get_aws_available_regions
|
||||
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=AwsProvider.get_regions(partition=None),
|
||||
choices=get_aws_available_regions(),
|
||||
)
|
||||
# AWS Organizations
|
||||
aws_orgs_subparser = aws_parser.add_argument_group("AWS Organizations")
|
||||
|
||||
@@ -3,12 +3,12 @@ import re
|
||||
from argparse import ArgumentTypeError
|
||||
|
||||
from prowler.providers.aws.exceptions.exceptions import (
|
||||
AWSIAMRoleARNEmptyResourceError,
|
||||
AWSIAMRoleARNInvalidAccountIDError,
|
||||
AWSIAMRoleARNInvalidResourceTypeError,
|
||||
AWSIAMRoleARNPartitionEmptyError,
|
||||
AWSIAMRoleARNRegionNotEmtpyError,
|
||||
AWSIAMRoleARNServiceNotIAMnorSTSError,
|
||||
AWSIAMRoleARNEmptyResource,
|
||||
AWSIAMRoleARNInvalidAccountID,
|
||||
AWSIAMRoleARNInvalidResourceType,
|
||||
AWSIAMRoleARNPartitionEmpty,
|
||||
AWSIAMRoleARNRegionNotEmtpy,
|
||||
AWSIAMRoleARNServiceNotIAMnorSTS,
|
||||
)
|
||||
from prowler.providers.aws.lib.arn.models import ARN
|
||||
|
||||
@@ -25,7 +25,7 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
arn_parsed = ARN(arn)
|
||||
# First check if region is empty (in IAM ARN's region is always empty)
|
||||
if arn_parsed.region:
|
||||
raise AWSIAMRoleARNRegionNotEmtpyError(file=os.path.basename(__file__))
|
||||
raise AWSIAMRoleARNRegionNotEmtpy(file=os.path.basename(__file__))
|
||||
else:
|
||||
# check if needed fields are filled:
|
||||
# - partition
|
||||
@@ -34,15 +34,15 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
# - resource_type
|
||||
# - resource
|
||||
if arn_parsed.partition is None or arn_parsed.partition == "":
|
||||
raise AWSIAMRoleARNPartitionEmptyError(file=os.path.basename(__file__))
|
||||
raise AWSIAMRoleARNPartitionEmpty(file=os.path.basename(__file__))
|
||||
elif arn_parsed.service != "iam" and arn_parsed.service != "sts":
|
||||
raise AWSIAMRoleARNServiceNotIAMnorSTSError(file=os.path.basename(__file__))
|
||||
raise AWSIAMRoleARNServiceNotIAMnorSTS(file=os.path.basename(__file__))
|
||||
elif (
|
||||
arn_parsed.account_id is None
|
||||
or len(arn_parsed.account_id) != 12
|
||||
or not arn_parsed.account_id.isnumeric()
|
||||
):
|
||||
raise AWSIAMRoleARNInvalidAccountIDError(file=os.path.basename(__file__))
|
||||
raise AWSIAMRoleARNInvalidAccountID(file=os.path.basename(__file__))
|
||||
elif (
|
||||
arn_parsed.resource_type != "role"
|
||||
and arn_parsed.resource_type != "user"
|
||||
@@ -50,9 +50,9 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
and arn_parsed.resource_type != "root"
|
||||
and arn_parsed.resource_type != "federated-user"
|
||||
):
|
||||
raise AWSIAMRoleARNInvalidResourceTypeError(file=os.path.basename(__file__))
|
||||
raise AWSIAMRoleARNInvalidResourceType(file=os.path.basename(__file__))
|
||||
elif arn_parsed.resource == "":
|
||||
raise AWSIAMRoleARNEmptyResourceError(file=os.path.basename(__file__))
|
||||
raise AWSIAMRoleARNEmptyResource(file=os.path.basename(__file__))
|
||||
else:
|
||||
return arn_parsed
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.providers.aws.exceptions.exceptions import AWSIAMRoleARNMissingFieldsError
|
||||
from prowler.providers.aws.exceptions.exceptions import AWSIAMRoleARNMissingFields
|
||||
|
||||
|
||||
class ARN(BaseModel):
|
||||
@@ -19,7 +19,7 @@ class ARN(BaseModel):
|
||||
# Validate the ARN
|
||||
## Check that arn starts with arn
|
||||
if not arn.startswith("arn:"):
|
||||
raise AWSIAMRoleARNMissingFieldsError(file=os.path.basename(__file__))
|
||||
raise AWSIAMRoleARNMissingFields(file=os.path.basename(__file__))
|
||||
## Retrieve fields
|
||||
arn_elements = arn.split(":", 5)
|
||||
data = {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 6000 to 6999 are reserved for S3 exceptions
|
||||
class S3BaseException(ProwlerException):
|
||||
"""Base class for S3 exceptions."""
|
||||
|
||||
S3_ERROR_CODES = {
|
||||
(6000, "S3TestConnectionError"): {
|
||||
"message": "Failed to test connection to S3 bucket.",
|
||||
"remediation": "Check the S3 bucket name and permissions.",
|
||||
},
|
||||
(6001, "S3InvalidBucketNameError"): {
|
||||
"message": "The specified S3 bucket name is invalid.",
|
||||
"remediation": "Check the S3 bucket name.",
|
||||
},
|
||||
(6002, "S3BucketAccessDeniedError"): {
|
||||
"message": "Access to the specified S3 bucket is denied.",
|
||||
"remediation": "Check the S3 bucket permissions.",
|
||||
},
|
||||
(6003, "S3ClientError"): {
|
||||
"message": "An error occurred while interacting with the S3 client.",
|
||||
"remediation": "Check the S3 client configuration.",
|
||||
},
|
||||
(6004, "S3IllegalLocationConstraintError"): {
|
||||
"message": "The specified location constraint is not valid.",
|
||||
"remediation": "Check the location constraint.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
module = "S3"
|
||||
error_info = self.S3_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 S3TestConnectionError(S3BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class S3InvalidBucketNameError(S3BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class S3BucketAccessDeniedError(S3BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6002, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class S3ClientError(S3BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6003, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class S3IllegalLocationConstraintError(S3BaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
6004, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
@@ -1,20 +1,10 @@
|
||||
import tempfile
|
||||
from os import path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from boto3 import Session
|
||||
from botocore import exceptions
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.output import Output
|
||||
from prowler.providers.aws.lib.s3.exceptions.exceptions import (
|
||||
S3BucketAccessDeniedError,
|
||||
S3ClientError,
|
||||
S3IllegalLocationConstraintError,
|
||||
S3InvalidBucketNameError,
|
||||
S3TestConnectionError,
|
||||
)
|
||||
from prowler.providers.common.models import Connection
|
||||
|
||||
|
||||
class S3:
|
||||
@@ -157,83 +147,3 @@ class S3:
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
return uploaded_objects
|
||||
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
session, bucket_name: str, raise_on_exception: bool = True
|
||||
) -> Connection:
|
||||
"""
|
||||
Test the connection to the S3 bucket.
|
||||
|
||||
Parameters:
|
||||
- session: An instance of the `Session` class representing the AWS session.
|
||||
- bucket_name: A string representing the name of the S3 bucket.
|
||||
- raise_on_exception: A boolean indicating whether to raise an exception if the connection test fails.
|
||||
|
||||
Returns:
|
||||
- A Connection object indicating the status of the connection test.
|
||||
|
||||
Raises:
|
||||
- Exception: An exception indicating that the connection test failed.
|
||||
"""
|
||||
try:
|
||||
s3_client = session.client(__class__.__name__.lower())
|
||||
if "s3://" in bucket_name:
|
||||
bucket_name = bucket_name.removeprefix("s3://")
|
||||
# Check for the bucket location
|
||||
bucket_location = s3_client.get_bucket_location(Bucket=bucket_name)
|
||||
if bucket_location["LocationConstraint"] == "EU":
|
||||
bucket_location["LocationConstraint"] = "eu-west-1"
|
||||
if (
|
||||
bucket_location["LocationConstraint"] == ""
|
||||
or bucket_location["LocationConstraint"] is None
|
||||
):
|
||||
bucket_location["LocationConstraint"] = "us-east-1"
|
||||
|
||||
# If the bucket location is not the same as the session region, change the session region
|
||||
if (
|
||||
session.region_name != bucket_location["LocationConstraint"]
|
||||
and bucket_location["LocationConstraint"] is not None
|
||||
):
|
||||
s3_client = session.client(
|
||||
__class__.__name__.lower(),
|
||||
region_name=bucket_location["LocationConstraint"],
|
||||
)
|
||||
# Set a Temp file to upload
|
||||
with tempfile.TemporaryFile() as temp_file:
|
||||
temp_file.write(b"Test Prowler Connection")
|
||||
temp_file.seek(0)
|
||||
s3_client.upload_fileobj(
|
||||
temp_file, bucket_name, "test-prowler-connection.txt"
|
||||
)
|
||||
|
||||
# Try to delete the file
|
||||
s3_client.delete_object(
|
||||
Bucket=bucket_name, Key="test-prowler-connection.txt"
|
||||
)
|
||||
return Connection(is_connected=True)
|
||||
|
||||
except exceptions.ClientError as client_error:
|
||||
if raise_on_exception:
|
||||
if (
|
||||
"specified bucket does not exist"
|
||||
in client_error.response["Error"]["Message"]
|
||||
):
|
||||
raise S3InvalidBucketNameError(original_exception=client_error)
|
||||
elif (
|
||||
"IllegalLocationConstraintException"
|
||||
in client_error.response["Error"]["Message"]
|
||||
):
|
||||
raise S3IllegalLocationConstraintError(
|
||||
original_exception=client_error
|
||||
)
|
||||
elif "AccessDenied" in client_error.response["Error"]["Code"]:
|
||||
raise S3BucketAccessDeniedError(original_exception=client_error)
|
||||
else:
|
||||
raise S3ClientError(original_exception=client_error)
|
||||
return Connection(is_connected=False, error=client_error)
|
||||
|
||||
except Exception as error:
|
||||
if raise_on_exception:
|
||||
raise S3TestConnectionError(original_exception=error)
|
||||
return False
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 7000 to 7999 are reserved for Security Hub exceptions
|
||||
class SecurityHubBaseException(ProwlerException):
|
||||
"""Base class for Security Hub exceptions."""
|
||||
|
||||
SECURITYHUB_ERROR_CODES = {
|
||||
(7000, "SecurityHubNoEnabledRegionsError"): {
|
||||
"message": "No regions were found to with the Security Hub integration enabled.",
|
||||
"remediation": "Please check the connection settings and permissions and try again.",
|
||||
},
|
||||
(7001, "SecurityHubInvalidRegionError"): {
|
||||
"message": "Given region has not Security Hub enabled.",
|
||||
"remediation": "Please provide a valid region.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
module = "SecurityHub"
|
||||
error_info = self.SECURITYHUB_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 SecurityHubNoEnabledRegionsError(SecurityHubBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
7000, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
|
||||
|
||||
class SecurityHubInvalidRegionError(SecurityHubBaseException):
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
7001, file=file, original_exception=original_exception, message=message
|
||||
)
|
||||
@@ -1,35 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from boto3 import Session
|
||||
from botocore.client import ClientError
|
||||
|
||||
from prowler.config.config import timestamp_utc
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.asff.asff import AWSSecurityFindingFormat
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.lib.security_hub.exceptions.exceptions import (
|
||||
SecurityHubInvalidRegionError,
|
||||
SecurityHubNoEnabledRegionsError,
|
||||
)
|
||||
from prowler.providers.common.models import Connection
|
||||
|
||||
SECURITY_HUB_INTEGRATION_NAME = "prowler/prowler"
|
||||
SECURITY_HUB_MAX_BATCH = 100
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityHubConnection(Connection):
|
||||
"""
|
||||
Represents a Security Hub connection object.
|
||||
Attributes:
|
||||
enabled_regions (set): Set of regions where Security Hub is enabled.
|
||||
disabled_regions (set): Set of regions where Security Hub is disabled.
|
||||
"""
|
||||
|
||||
enabled_regions: set = None
|
||||
disabled_regions: set = None
|
||||
|
||||
|
||||
class SecurityHub:
|
||||
"""
|
||||
Class representing a SecurityHub object for managing findings and interactions with AWS Security Hub.
|
||||
@@ -74,10 +53,7 @@ class SecurityHub:
|
||||
|
||||
if aws_security_hub_available_regions:
|
||||
self._enabled_regions = self.verify_enabled_per_region(
|
||||
aws_security_hub_available_regions,
|
||||
aws_session,
|
||||
aws_account_id,
|
||||
aws_partition,
|
||||
aws_security_hub_available_regions
|
||||
)
|
||||
if findings and self._enabled_regions:
|
||||
self._findings_per_region = self.filter(findings, send_only_fails)
|
||||
@@ -127,12 +103,9 @@ class SecurityHub:
|
||||
)
|
||||
return findings_per_region
|
||||
|
||||
@staticmethod
|
||||
def verify_enabled_per_region(
|
||||
self,
|
||||
aws_security_hub_available_regions: list[str],
|
||||
session: Session,
|
||||
aws_account_id: str,
|
||||
aws_partition: str,
|
||||
) -> dict[str, Session]:
|
||||
"""
|
||||
Filters the given list of regions where AWS Security Hub is enabled and returns a dictionary containing the region and their boto3 client if the region and the Prowler integration is enabled.
|
||||
@@ -150,11 +123,13 @@ class SecurityHub:
|
||||
f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region."
|
||||
)
|
||||
# Check if security hub is enabled in current region
|
||||
security_hub_client = session.client("securityhub", region_name=region)
|
||||
security_hub_client = self._session.client(
|
||||
"securityhub", region_name=region
|
||||
)
|
||||
security_hub_client.describe_hub()
|
||||
|
||||
# Check if Prowler integration is enabled in Security Hub
|
||||
security_hub_prowler_integration_arn = f"arn:{aws_partition}:securityhub:{region}:{aws_account_id}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}"
|
||||
security_hub_prowler_integration_arn = f"arn:{self._aws_partition}:securityhub:{region}:{self._aws_account_id}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}"
|
||||
if security_hub_prowler_integration_arn not in str(
|
||||
security_hub_client.list_enabled_products_for_import()
|
||||
):
|
||||
@@ -162,7 +137,7 @@ class SecurityHub:
|
||||
f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/"
|
||||
)
|
||||
else:
|
||||
enabled_regions[region] = session.client(
|
||||
enabled_regions[region] = self._session.client(
|
||||
"securityhub", region_name=region
|
||||
)
|
||||
|
||||
@@ -173,7 +148,7 @@ class SecurityHub:
|
||||
error_message = client_error.response["Error"]["Message"]
|
||||
if (
|
||||
error_code == "InvalidAccessException"
|
||||
and f"Account {aws_account_id} is not subscribed to AWS Security Hub"
|
||||
and f"Account {self._aws_account_id} is not subscribed to AWS Security Hub"
|
||||
in error_message
|
||||
):
|
||||
logger.warning(
|
||||
@@ -309,104 +284,3 @@ class SecurityHub:
|
||||
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}"
|
||||
)
|
||||
return success_count
|
||||
|
||||
@staticmethod
|
||||
def test_connection(
|
||||
session: Session,
|
||||
aws_account_id: str,
|
||||
aws_partition: str,
|
||||
regions: set = None,
|
||||
raise_on_exception: bool = True,
|
||||
) -> SecurityHubConnection:
|
||||
"""
|
||||
Test the connection to AWS Security Hub by checking if Security Hub is enabled in the provided region
|
||||
and if the Prowler integration is active.
|
||||
|
||||
Args:
|
||||
session (Session): AWS session to use for authentication.
|
||||
regions (set): Set of regions to check for Security Hub integration.
|
||||
aws_account_id (str): AWS account ID to check for Prowler integration.
|
||||
aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov).
|
||||
raise_on_exception (bool): Whether to raise an exception if an error occurs.
|
||||
|
||||
Returns:
|
||||
Connection: An object that contains the result of the test connection operation.
|
||||
- is_connected (bool): Indicates whether the connection was successful.
|
||||
- error (Exception): An exception object if an error occurs during the connection test.
|
||||
enabled_regions (set): Set of regions where Security Hub is enabled.
|
||||
disabled_regions (set): Set of regions where Security Hub is disabled.
|
||||
"""
|
||||
try:
|
||||
disabled_regions = set()
|
||||
enabled_regions = set()
|
||||
|
||||
all_regions = AwsProvider.get_available_aws_service_regions(
|
||||
service="securityhub", partition=aws_partition
|
||||
)
|
||||
|
||||
enabled_regions = SecurityHub.verify_enabled_per_region(
|
||||
aws_security_hub_available_regions=all_regions,
|
||||
session=session,
|
||||
aws_account_id=aws_account_id,
|
||||
aws_partition=aws_partition,
|
||||
).keys()
|
||||
disabled_regions = all_regions - enabled_regions
|
||||
if regions:
|
||||
if not any(region in enabled_regions for region in regions):
|
||||
logger.warning(
|
||||
f"Prowler integration is not enabled in regions: {regions - enabled_regions}."
|
||||
)
|
||||
invalid_region_error = SecurityHubInvalidRegionError(
|
||||
message="Given regions have not Security Hub enabled."
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise invalid_region_error
|
||||
return SecurityHubConnection(
|
||||
is_connected=False,
|
||||
error=invalid_region_error,
|
||||
enabled_regions=enabled_regions,
|
||||
disabled_regions=disabled_regions,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Prowler integration is enabled in regions: {', '.join(regions)}."
|
||||
)
|
||||
return SecurityHubConnection(
|
||||
is_connected=True,
|
||||
error=None,
|
||||
enabled_regions=enabled_regions,
|
||||
disabled_regions=disabled_regions,
|
||||
)
|
||||
|
||||
if len(enabled_regions) == 0:
|
||||
error_str = (
|
||||
"No regions found with the Security Hub integration enabled."
|
||||
)
|
||||
logger.warning(error_str)
|
||||
no_enabled_regions_error = SecurityHubNoEnabledRegionsError(
|
||||
message=error_str
|
||||
)
|
||||
if raise_on_exception:
|
||||
raise no_enabled_regions_error
|
||||
return SecurityHubConnection(
|
||||
is_connected=False,
|
||||
error=no_enabled_regions_error,
|
||||
enabled_regions=enabled_regions,
|
||||
disabled_regions=disabled_regions,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Security Hub is enabled in the following regions: {', '.join(enabled_regions)}."
|
||||
)
|
||||
return SecurityHubConnection(
|
||||
is_connected=True,
|
||||
error=None,
|
||||
enabled_regions=enabled_regions,
|
||||
disabled_regions=disabled_regions,
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
|
||||
@@ -101,17 +101,3 @@ 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,6 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from boto3.session import Session
|
||||
from botocore.config import Config
|
||||
@@ -83,29 +82,6 @@ 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 {accessanalyzer_client.audited_account} is not enabled."
|
||||
report.status_extended = f"IAM Access Analyzer in account {analyzer.name} is not enabled."
|
||||
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
|
||||
+1
-2
@@ -6,8 +6,7 @@ from prowler.providers.aws.services.accessanalyzer.accessanalyzer_client import
|
||||
|
||||
def fixer(region):
|
||||
"""
|
||||
Enable Access Analyzer in a region. Requires the access-analyzer:CreateAnalyzer permission.
|
||||
Permissions:
|
||||
Enable Access Analyzer in a region. Requires the access-analyzer:CreateAnalyzer permission:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
|
||||
@@ -43,10 +43,8 @@ class AccessAnalyzer(AWSService):
|
||||
if analyzer_count == 0:
|
||||
self.analyzers.append(
|
||||
Analyzer(
|
||||
arn=self.get_unknown_arn(
|
||||
region=regional_client.region, resource_type="analyzer"
|
||||
),
|
||||
name="analyzer/unknown",
|
||||
arn=self.audited_account_arn,
|
||||
name=self.audited_account,
|
||||
status="NOT_AVAILABLE",
|
||||
tags=[],
|
||||
type="",
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS, Severity
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.acm.acm_client import acm_client
|
||||
|
||||
|
||||
class acm_certificates_expiration_check(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for certificate in acm_client.certificates.values():
|
||||
for certificate in acm_client.certificates:
|
||||
if certificate.in_use or acm_client.provider.scan_unused_services:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = certificate.region
|
||||
@@ -22,10 +22,10 @@ class acm_certificates_expiration_check(Check):
|
||||
report.status = "FAIL"
|
||||
if certificate.expiration_days < 0:
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} has expired ({abs(certificate.expiration_days)} days ago)."
|
||||
report.check_metadata.Severity = Severity.high
|
||||
report.check_metadata.Severity = "high"
|
||||
else:
|
||||
report.status_extended = f"ACM Certificate {certificate.id} for {certificate.name} is about to expire in {certificate.expiration_days} days."
|
||||
report.check_metadata.Severity = Severity.medium
|
||||
report.check_metadata.Severity = "medium"
|
||||
|
||||
report.resource_id = certificate.id
|
||||
report.resource_details = certificate.name
|
||||
|
||||
+1
-3
@@ -25,9 +25,7 @@
|
||||
"Url": "https://aws.amazon.com/blogs/security/how-to-get-ready-for-certificate-transparency/"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"Categories": [],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ from prowler.providers.aws.services.acm.acm_client import acm_client
|
||||
class acm_certificates_transparency_logs_enabled(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for certificate in acm_client.certificates.values():
|
||||
for certificate in acm_client.certificates:
|
||||
if certificate.in_use or acm_client.provider.scan_unused_services:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = certificate.region
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ from prowler.providers.aws.services.acm.acm_client import acm_client
|
||||
class acm_certificates_with_secure_key_algorithms(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for certificate in acm_client.certificates.values():
|
||||
for certificate in acm_client.certificates:
|
||||
if certificate.in_use or acm_client.provider.scan_unused_services:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = certificate.region
|
||||
|
||||
@@ -13,12 +13,10 @@ class ACM(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.certificates = {}
|
||||
self.certificates = []
|
||||
self.__threading_call__(self._list_certificates)
|
||||
self.__threading_call__(self._describe_certificates, self.certificates.values())
|
||||
self.__threading_call__(
|
||||
self._list_tags_for_certificate, self.certificates.values()
|
||||
)
|
||||
self._describe_certificates()
|
||||
self._list_tags_for_certificate()
|
||||
|
||||
def _list_certificates(self, regional_client):
|
||||
logger.info("ACM - Listing Certificates...")
|
||||
@@ -56,50 +54,54 @@ class ACM(AWSService):
|
||||
).days
|
||||
else:
|
||||
certificate_expiration_time = 0
|
||||
self.certificates[certificate["CertificateArn"]] = Certificate(
|
||||
arn=certificate["CertificateArn"],
|
||||
name=certificate["DomainName"],
|
||||
id=certificate["CertificateArn"].split("/")[-1],
|
||||
type=certificate["Type"],
|
||||
key_algorithm=certificate["KeyAlgorithm"],
|
||||
expiration_days=certificate_expiration_time,
|
||||
in_use=certificate.get("InUse", False),
|
||||
transparency_logging=False,
|
||||
region=regional_client.region,
|
||||
self.certificates.append(
|
||||
Certificate(
|
||||
arn=certificate["CertificateArn"],
|
||||
name=certificate["DomainName"],
|
||||
id=certificate["CertificateArn"].split("/")[-1],
|
||||
type=certificate["Type"],
|
||||
key_algorithm=certificate["KeyAlgorithm"],
|
||||
expiration_days=certificate_expiration_time,
|
||||
in_use=certificate.get("InUse", False),
|
||||
transparency_logging=False,
|
||||
region=regional_client.region,
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_certificates(self, certificate):
|
||||
def _describe_certificates(self):
|
||||
logger.info("ACM - Describing Certificates...")
|
||||
try:
|
||||
regional_client = self.regional_clients[certificate.region]
|
||||
response = regional_client.describe_certificate(
|
||||
CertificateArn=certificate.arn
|
||||
)["Certificate"]
|
||||
if (
|
||||
response["Options"]["CertificateTransparencyLoggingPreference"]
|
||||
== "ENABLED"
|
||||
):
|
||||
certificate.transparency_logging = True
|
||||
for certificate in self.certificates:
|
||||
regional_client = self.regional_clients[certificate.region]
|
||||
response = regional_client.describe_certificate(
|
||||
CertificateArn=certificate.arn
|
||||
)["Certificate"]
|
||||
if (
|
||||
response["Options"]["CertificateTransparencyLoggingPreference"]
|
||||
== "ENABLED"
|
||||
):
|
||||
certificate.transparency_logging = True
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{certificate.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags_for_certificate(self, certificate):
|
||||
def _list_tags_for_certificate(self):
|
||||
logger.info("ACM - List Tags...")
|
||||
try:
|
||||
regional_client = self.regional_clients[certificate.region]
|
||||
response = regional_client.list_tags_for_certificate(
|
||||
CertificateArn=certificate.arn
|
||||
)["Tags"]
|
||||
certificate.tags = response
|
||||
for certificate in self.certificates:
|
||||
regional_client = self.regional_clients[certificate.region]
|
||||
response = regional_client.list_tags_for_certificate(
|
||||
CertificateArn=certificate.arn
|
||||
)["Tags"]
|
||||
certificate.tags = response
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{certificate.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "apigateway_restapi_cache_encrypted",
|
||||
"CheckTitle": "Check if API Gateway REST API cache data is encrypted at rest.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices"
|
||||
],
|
||||
"ServiceName": "apigateway",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:apigateway:region:account-id:/restapis/restapi-id/stages/stage-name",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsApiGatewayStage",
|
||||
"Description": "This control checks whether all methods in API Gateway REST API stages that have cache enabled are encrypted. The control fails if any method in an API Gateway REST API stage is configured to cache and the cache is not encrypted.",
|
||||
"Risk": "Without encryption, cached data in API Gateway REST APIs may be vulnerable to unauthorized access, potentially exposing sensitive information to users not authenticated to AWS.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-caching.html#enable-api-gateway-caching",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws apigateway update-stage --rest-api-id <restapi-id> --stage-name <stage-name> --patch-operations op=replace,path=/<resourcePath>/<httpMethod>/caching/enabled,value=true op=replace,path=/<resourcePath>/<httpMethod>/caching/dataEncrypted,value=true",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/apigateway-controls.html#apigateway-5",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Ensure that API Gateway REST API cache data is encrypted at rest by enabling the 'Encrypt cache data' setting in the API Gateway stage configuration.",
|
||||
"Url": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-caching.html#enable-api-gateway-caching"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.apigateway.apigateway_client import (
|
||||
apigateway_client,
|
||||
)
|
||||
|
||||
|
||||
class apigateway_restapi_cache_encrypted(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for rest_api in apigateway_client.rest_apis:
|
||||
for stage in rest_api.stages:
|
||||
if stage.cache_enabled:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = rest_api.region
|
||||
report.resource_id = rest_api.name
|
||||
report.resource_arn = stage.arn
|
||||
report.resource_tags = stage.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} in stage {stage.name} has cache encryption enabled."
|
||||
if not stage.cache_data_encrypted:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} in stage {stage.name} has cache enabled but cache data is not encrypted at rest."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user