Merge branch 'master' into prowler-inventory

This commit is contained in:
pedrooot
2024-10-09 17:09:19 +02:00
225 changed files with 8887 additions and 2180 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.82.6
uses: trufflesecurity/trufflehog@v3.82.7
with:
path: ./
base: ${{ github.event.repository.default_branch }}
+5 -6
View File
@@ -2,9 +2,9 @@ FROM python:3.12-alpine
LABEL maintainer="https://github.com/prowler-cloud/prowler"
# Update system dependencies
# Update system dependencies and install essential tools
#hadolint ignore=DL3018
RUN apk --no-cache upgrade && apk --no-cache add curl
RUN apk --no-cache upgrade && apk --no-cache add curl git
# Create nonroot user
RUN mkdir -p /home/prowler && \
@@ -13,18 +13,17 @@ RUN mkdir -p /home/prowler && \
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 dependencies
# Install Python dependencies
ENV HOME='/home/prowler'
ENV PATH="$HOME/.local/bin:$PATH"
#hadolint ignore=DL3013
RUN pip install --no-cache-dir --upgrade pip && \
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
pip install --no-cache-dir .
# Remove deprecated dash dependencies
@@ -12,7 +12,11 @@ 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.
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.).
## CloudFormation Templates
@@ -59,9 +63,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. 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)
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)
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:
@@ -91,4 +95,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}" -q -S -f "${REGION}" > /dev/null
prowler aws -R arn:"${PARTITION}":iam::"${accountId}":role/"${ROLE}" --security-hub --send-sh-only-fails -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,24 +60,42 @@ Resources:
Effect: Allow
Resource: "*"
Action:
- ds:ListAuthorizedApplications
- 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
- ec2:GetEbsEncryptionByDefault
- ec2:GetSnapshotBlockPublicAccessState
- ec2:GetInstanceMetadataDefaults
- ecr:Describe*
- ecr:GetRegistryScanningConfiguration
- elasticfilesystem:DescribeBackupPolicy
- glue:GetConnections
- glue:GetSecurityConfiguration
- glue:GetSecurityConfiguration*
- glue:SearchTables
- lambda:GetFunction
- lambda:GetFunction*
- logs:FilterLogEvents
- lightsail:GetRelationalDatabases
- macie2:GetMacieSession
- s3:GetAccountPublicAccessBlock
- shield:DescribeProtection
- shield:GetSubscriptionState
- ssm:GetDocument
- ssm-incidents:List*
- support:Describe*
- tag:GetTagKeys
- PolicyName: Prowler-Security-Hub
PolicyDocument:
Version: 2012-10-17
Statement:
- wellarchitected:List*
- Sid: AllowProwlerSecurityHub
Effect: Allow
Resource: "*"
@@ -62,7 +62,7 @@ Resources:
awslogs-stream-prefix: ecs
Cpu: 1024
ExecutionRoleArn: !Ref ECSExecutionRole
Memory: 2048
Memory: 8192
NetworkMode: awsvpc
TaskRoleArn: !Ref ProwlerTaskRole
Family: SecurityHubProwlerTask
@@ -97,9 +97,15 @@ 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
Generated
+76 -64
View File
@@ -758,17 +758,17 @@ files = [
[[package]]
name = "boto3"
version = "1.35.28"
version = "1.35.29"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "boto3-1.35.28-py3-none-any.whl", hash = "sha256:dc088b86a14f17d3cd2e96915c6ccfd31bce640dfe9180df579ed311bc6bf0fc"},
{file = "boto3-1.35.28.tar.gz", hash = "sha256:8960fc458b9ba3c8a9890a607c31cee375db821f39aefaec9ff638248e81644a"},
{file = "boto3-1.35.29-py3-none-any.whl", hash = "sha256:2244044cdfa8ac345d7400536dc15a4824835e7ec5c55bc267e118af66bb27db"},
{file = "boto3-1.35.29.tar.gz", hash = "sha256:7bbb1ee649e09e956952285782cfdebd7e81fc78384f48dfab3d66c6eaf3f63f"},
]
[package.dependencies]
botocore = ">=1.35.28,<1.36.0"
botocore = ">=1.35.29,<1.36.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0"
@@ -1302,15 +1302,13 @@ wrapt = ">=1.10,<2"
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
[[package]]
name = "detect-secrets"
name = "detect_secrets"
version = "1.5.0"
description = "Tool for detecting secrets in the codebase"
optional = false
python-versions = "*"
files = [
{file = "detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060"},
{file = "detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a"},
]
files = []
develop = false
[package.dependencies]
pyyaml = "*"
@@ -1320,6 +1318,12 @@ requests = "*"
gibberish = ["gibberish-detector"]
word-list = ["pyahocorasick"]
[package.source]
type = "git"
url = "https://github.com/Yelp/detect-secrets.git"
reference = "master"
resolved_reference = "462720710ec337300fab2b4f2290949c7ee141eb"
[[package]]
name = "dill"
version = "0.3.9"
@@ -1410,13 +1414,13 @@ files = [
[[package]]
name = "email-validator"
version = "2.1.1"
version = "2.2.0"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.8"
files = [
{file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"},
{file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"},
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
]
[package.dependencies]
@@ -2594,13 +2598,13 @@ test = ["pytest", "pytest-cov"]
[[package]]
name = "moto"
version = "5.0.15"
version = "5.0.16"
description = ""
optional = false
python-versions = ">=3.8"
files = [
{file = "moto-5.0.15-py2.py3-none-any.whl", hash = "sha256:fa1e92ffb55dbfb9fa92a2115a88c32481b75aa3fbd24075d1f29af2f9becffa"},
{file = "moto-5.0.15.tar.gz", hash = "sha256:57aa8c2af417cc64a0ddfe63e5bcd1ada90f5079b73cdd1f74c4e9fb30a1a7e6"},
{file = "moto-5.0.16-py2.py3-none-any.whl", hash = "sha256:4ce1f34830307f7b3d553d77a7ef26066ab3b70006203d4226b048c9d11a3be4"},
{file = "moto-5.0.16.tar.gz", hash = "sha256:f4afb176a964cd7a70da9bc5e053d43109614ce3cab26044bcbb53610435dff4"},
]
[package.dependencies]
@@ -3463,18 +3467,19 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "py-ocsf-models"
version = "0.1.1"
version = "0.2.0"
description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/."
optional = false
python-versions = "<3.13,>=3.9"
files = [
{file = "py_ocsf_models-0.1.1-py3-none-any.whl", hash = "sha256:c6ea465fda85470b938a48da65b1f19664f6d83820ebe849ef5551094e6768de"},
{file = "py_ocsf_models-0.1.1.tar.gz", hash = "sha256:b0f2d4495a2596793f75e61a1ba218edea3a2d17a2b9911d46ee0fa623cc657b"},
{file = "py_ocsf_models-0.2.0-py3-none-any.whl", hash = "sha256:ac75fd21077694b343ebaad3479194db113c274879b114277560ff287d5cd7b5"},
{file = "py_ocsf_models-0.2.0.tar.gz", hash = "sha256:3e12648d05329e6776a0e6b1ffea87a3eb60aa7d8cb2c4afd69e5724f443ce03"},
]
[package.dependencies]
email-validator = "2.1.1"
pydantic = "1.10.15"
cryptography = "43.0.1"
email-validator = "2.2.0"
pydantic = "1.10.18"
[[package]]
name = "py-partiql-parser"
@@ -3539,47 +3544,54 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.15"
version = "1.10.18"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"},
{file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"},
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"},
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"},
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"},
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"},
{file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"},
{file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"},
{file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"},
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"},
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"},
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"},
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"},
{file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"},
{file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"},
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"},
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"},
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"},
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"},
{file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"},
{file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"},
{file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"},
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"},
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"},
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"},
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"},
{file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"},
{file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"},
{file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"},
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"},
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"},
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"},
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"},
{file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"},
{file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"},
{file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"},
{file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"},
{file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"},
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"},
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"},
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"},
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"},
{file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"},
{file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"},
{file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"},
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"},
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"},
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"},
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"},
{file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"},
{file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"},
{file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"},
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"},
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"},
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"},
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"},
{file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"},
{file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"},
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"},
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"},
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"},
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"},
{file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"},
{file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"},
{file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"},
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"},
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"},
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"},
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"},
{file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"},
{file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"},
{file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"},
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"},
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"},
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"},
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"},
{file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"},
{file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"},
{file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"},
]
[package.dependencies]
@@ -4302,24 +4314,24 @@ python-versions = ">=3.6"
files = [
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d92f81886165cb14d7b067ef37e142256f1c6a90a65cd156b063a43da1708cfd"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"},
{file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b5edda50e5e9e15e54a6a8a0070302b00c518a9d32accc2346ad6c984aacd279"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"},
{file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:7048c338b6c86627afb27faecf418768acb6331fc24cfa56c93e8c9780f815fa"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"},
{file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"},
@@ -4327,7 +4339,7 @@ files = [
{file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3fcc54cb0c8b811ff66082de1680b4b14cf8a81dce0d4fbf665c2265a81e07a1"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"},
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"},
@@ -4335,7 +4347,7 @@ files = [
{file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:665f58bfd29b167039f714c6998178d27ccd83984084c286110ef26b230f259f"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"},
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"},
@@ -4343,7 +4355,7 @@ files = [
{file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9eb5dee2772b0f704ca2e45b1713e4e5198c18f515b52743576d196348f374d3"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"},
{file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"},
@@ -5060,4 +5072,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.13"
content-hash = "6ab3e45b4316275b0c26103a5dff489527d927bb4d9e4ff6a847849cb106d837"
content-hash = "0b367fa80501022efe43dc1beaa7f3da278fb64ffaddece72a0e88b09a0e53a2"
+1 -1
View File
@@ -11,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.4.0"
prowler_version = "4.5.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"
+7 -2
View File
@@ -57,6 +57,11 @@ 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.
@@ -330,8 +335,8 @@ aws:
elbv2_min_azs: 2
# Known secrets to ignore on detection
# this will include a list of regex patterns to ignore on detection
# AWS Secrets Configuration
# Patterns to ignore in the secrets checks
secrets_ignore_patterns: []
# Azure Configuration
+51 -39
View File
@@ -9,7 +9,6 @@ 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
@@ -37,7 +36,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(status: str, muted: bool) -> StatusID: Returns the StatusID based on the status and muted values.
- get_finding_status_id(muted: bool) -> StatusID: Returns the StatusID based on the muted value.
References:
- OCSF: https://schema.ocsf.io/1.2.0/classes/detection_finding
@@ -59,21 +58,24 @@ class OCSF(Output):
finding_severity = getattr(
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=finding.timestamp,
created_time_dt=finding.timestamp,
created_time=int(finding.timestamp.timestamp()),
desc=finding.description,
title=finding.check_title,
uid=finding.finding_uid,
name=finding.resource_name,
product_uid="prowler",
types=[finding.check_type],
),
event_time=finding.timestamp,
time_dt=finding.timestamp,
time=int(finding.timestamp.timestamp()),
remediation=Remediation(
desc=finding.remediation_recommendation_text,
references=list(
@@ -96,31 +98,51 @@ class OCSF(Output):
status_code=finding.status,
status_detail=finding.status_extended,
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},
)
],
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},
)
]
if finding.provider != "kubernetes"
else [
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,
data={"details": finding.resource_details},
namespace=finding.region.replace("namespace: ", ""),
)
]
),
metadata=Metadata(
event_code=finding.check_id,
product=Product(
uid="prowler",
name="Prowler",
vendor_name="Prowler",
version=finding.prowler_version,
),
profiles=(
["cloud", "datetime"]
if finding.provider != "kubernetes"
else ["container", "datetime"]
),
tenant_uid=finding.account_organization_uid,
),
type_uid=DetectionFindingTypeID.Create,
type_name=DetectionFindingTypeID.Create.name,
type_name=f"Detection Finding: {DetectionFindingTypeID.Create.name}",
unmapped={
"check_type": finding.check_type,
"related_url": finding.related_url,
"categories": finding.categories,
"depends_on": finding.depends_on,
@@ -129,26 +151,19 @@ class OCSF(Output):
"compliance": finding.compliance,
},
)
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:
if finding.provider != "kubernetes":
detection_finding.cloud = Cloud(
account=Account(
name=finding.account_name,
type_id=cloud_account_type.value,
type=cloud_account_type.name,
type=cloud_account_type.name.replace("_", " "),
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,
@@ -208,20 +223,17 @@ class OCSF(Output):
return type_id
@staticmethod
def get_finding_status_id(status: str, muted: bool) -> StatusID:
def get_finding_status_id(muted: bool) -> StatusID:
"""
Returns the StatusID based on the status and muted values.
Returns the StatusID based on the muted value.
Args:
status (str): The status value
muted (bool): The muted value
Returns:
StatusID: The StatusID based on the status and muted values
StatusID: The StatusID based on the muted value
"""
status_id = StatusID.Other
if status == "FAIL":
status_id = StatusID.New
status_id = StatusID.New
if muted:
status_id = StatusID.Suppressed
return status_id
+35 -5
View File
@@ -1,6 +1,8 @@
import datetime
from typing import Generator
from prowler.lib.check.check import execute, import_check, update_audit_metadata
from prowler.lib.check.utils import recover_checks_from_provider
from prowler.lib.logger import logger
from prowler.lib.outputs.finding import Finding
from prowler.providers.common.models import Audit_Metadata
@@ -18,8 +20,9 @@ class Scan:
_service_checks_completed: dict[str, set[str]]
_progress: float = 0.0
_findings: list = []
_duration: int = 0
def __init__(self, provider: Provider, checks_to_execute: list[str]):
def __init__(self, provider: Provider, checks_to_execute: list[str] = None):
"""
Scan is the class that executes the checks and yields the progress and the findings.
@@ -29,11 +32,31 @@ class Scan:
"""
self._provider = provider
# Remove duplicated checks and sort them
self._checks_to_execute = sorted(list(set(checks_to_execute)))
self._checks_to_execute = (
sorted(list(set(checks_to_execute)))
if checks_to_execute
else sorted(
[check[0] for check in recover_checks_from_provider(provider.type)]
)
)
self._number_of_checks_to_execute = len(checks_to_execute)
# TODO This should be done depending on the scan args (future feature)
# Discard threat detection checks
if "cloudtrail_threat_detection_enumeration" in self._checks_to_execute:
self._checks_to_execute.remove("cloudtrail_threat_detection_enumeration")
if (
"cloudtrail_threat_detection_privilege_escalation"
in self._checks_to_execute
):
self._checks_to_execute.remove(
"cloudtrail_threat_detection_privilege_escalation"
)
service_checks_to_execute = get_service_checks_to_execute(checks_to_execute)
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_completed = dict()
self._service_checks_to_execute = service_checks_to_execute
@@ -61,6 +84,10 @@ 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
@@ -95,6 +122,8 @@ class Scan:
audit_progress=0,
)
start_time = datetime.datetime.now()
for check_name in checks_to_execute:
try:
# Recover service from check name
@@ -149,7 +178,6 @@ class Scan:
]
yield self.progress, findings
# If check does not exists in the provider or is from another provider
except ModuleNotFoundError:
logger.error(
@@ -159,6 +187,8 @@ 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 -1
View File
@@ -133,7 +133,7 @@ def detect_secrets_scan(
{"name": "SoftlayerDetector"},
{"name": "SquareOAuthDetector"},
{"name": "StripeDetector"},
# {"name": "TelegramBotTokenDetector"}, https://github.com/Yelp/detect-secrets/pull/878
{"name": "TelegramBotTokenDetector"},
{"name": "TwilioKeyDetector"},
],
"filters_used": [
@@ -1263,6 +1263,7 @@
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
@@ -1273,6 +1274,7 @@
"eu-west-3",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
@@ -7520,6 +7522,7 @@
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-central-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
@@ -7530,6 +7533,7 @@
],
"aws-cn": [],
"aws-us-gov": [
"us-gov-east-1",
"us-gov-west-1"
]
}
@@ -7691,6 +7695,7 @@
"eu-central-1",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
@@ -10975,6 +10980,7 @@
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
@@ -0,0 +1,6 @@
from prowler.providers.aws.services.autoscaling.autoscaling_service import (
ApplicationAutoScaling,
)
from prowler.providers.common.provider import Provider
applicationautoscaling_client = ApplicationAutoScaling(Provider.get_global_provider())
@@ -5,7 +5,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered
from prowler.providers.aws.lib.service.service import AWSService
################## AutoScaling
class AutoScaling(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
@@ -74,6 +73,49 @@ class AutoScaling(AWSService):
)
# Global list for service namespaces needed for Describe Scalable Targets
SERVICE_NAMESPACES = ["dynamodb"]
class ApplicationAutoScaling(AWSService):
def __init__(self, provider):
super().__init__("application-autoscaling", provider)
self.scalable_targets = []
self.__threading_call__(self._describe_scalable_targets)
def _describe_scalable_targets(self, regional_client):
logger.info("ApplicationAutoScaling - Describing Scalable Targets...")
try:
describe_scalable_targets_paginator = regional_client.get_paginator(
"describe_scalable_targets"
)
for service_namespace in SERVICE_NAMESPACES:
logger.info(f"Processing ServiceNamespace: {service_namespace}")
for page in describe_scalable_targets_paginator.paginate(
ServiceNamespace=service_namespace
):
for target in page.get("ScalableTargets", []):
if not self.audit_resources or (
is_resource_filtered(
target["ScalableTargetARN"],
self.audit_resources,
)
):
self.scalable_targets.append(
ScalableTarget(
arn=target.get("ScalableTargetARN", ""),
resource_id=target.get("ResourceId"),
service_namespace=target.get("ServiceNamespace"),
scalable_dimension=target.get("ScalableDimension"),
region=regional_client.region,
)
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class LaunchConfiguration(BaseModel):
arn: str
name: str
@@ -88,3 +130,11 @@ class Group(BaseModel):
region: str
availability_zones: list
tags: list = []
class ScalableTarget(BaseModel):
arn: str
resource_id: str
service_namespace: str
scalable_dimension: str
region: str
@@ -40,21 +40,24 @@ class cloudtrail_threat_detection_enumeration(Check):
):
event_log = json.loads(event_log["CloudTrailEvent"])
if (
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
) not in potential_enumeration:
"arn" in event_log["userIdentity"]
): # Ignore event logs without ARN since they are AWS services
if (
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
) not in potential_enumeration:
potential_enumeration[
(
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
)
] = set()
potential_enumeration[
(
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
)
] = set()
potential_enumeration[
(
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
)
].add(event_name)
].add(event_name)
for aws_identity, actions in potential_enumeration.items():
identity_threshold = round(len(actions) / len(enumeration_actions), 2)
@@ -41,21 +41,24 @@ class cloudtrail_threat_detection_privilege_escalation(Check):
):
event_log = json.loads(event_log["CloudTrailEvent"])
if (
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
) not in potential_privilege_escalation:
"arn" in event_log["userIdentity"]
): # Ignore event logs without ARN since they are AWS services
if (
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
) not in potential_privilege_escalation:
potential_privilege_escalation[
(
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
)
] = set()
potential_privilege_escalation[
(
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
)
] = set()
potential_privilege_escalation[
(
event_log["userIdentity"]["arn"],
event_log["userIdentity"]["type"],
)
].add(event_name)
].add(event_name)
for aws_identity, actions in potential_privilege_escalation.items():
identity_threshold = round(
len(actions) / len(privilege_escalation_actions), 2
@@ -15,24 +15,24 @@ class cloudwatch_changes_to_network_acls_alarm_configured(Check):
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?CreateNetworkAcl.+\$\.eventName\s*=\s*.?CreateNetworkAclEntry.+\$\.eventName\s*=\s*.?DeleteNetworkAcl.+\$\.eventName\s*=\s*.?DeleteNetworkAclEntry.+\$\.eventName\s*=\s*.?ReplaceNetworkAclEntry.+\$\.eventName\s*=\s*.?ReplaceNetworkAclAssociation.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_changes_to_network_gateways_alarm_configured(Check):
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?CreateCustomerGateway.+\$\.eventName\s*=\s*.?DeleteCustomerGateway.+\$\.eventName\s*=\s*.?AttachInternetGateway.+\$\.eventName\s*=\s*.?CreateInternetGateway.+\$\.eventName\s*=\s*.?DeleteInternetGateway.+\$\.eventName\s*=\s*.?DetachInternetGateway.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_changes_to_network_route_tables_alarm_configured(Check):
def execute(self):
pattern = r"\$\.eventSource\s*=\s*.?ec2.amazonaws.com.+\$\.eventName\s*=\s*.?CreateRoute.+\$\.eventName\s*=\s*.?CreateRouteTable.+\$\.eventName\s*=\s*.?ReplaceRoute.+\$\.eventName\s*=\s*.?ReplaceRouteTableAssociation.+\$\.eventName\s*=\s*.?DeleteRouteTable.+\$\.eventName\s*=\s*.?DeleteRoute.+\$\.eventName\s*=\s*.?DisassociateRouteTable.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_changes_to_vpcs_alarm_configured(Check):
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?CreateVpc.+\$\.eventName\s*=\s*.?DeleteVpc.+\$\.eventName\s*=\s*.?ModifyVpcAttribute.+\$\.eventName\s*=\s*.?AcceptVpcPeeringConnection.+\$\.eventName\s*=\s*.?CreateVpcPeeringConnection.+\$\.eventName\s*=\s*.?DeleteVpcPeeringConnection.+\$\.eventName\s*=\s*.?RejectVpcPeeringConnection.+\$\.eventName\s*=\s*.?AttachClassicLinkVpc.+\$\.eventName\s*=\s*.?DetachClassicLinkVpc.+\$\.eventName\s*=\s*.?DisableVpcClassicLink.+\$\.eventName\s*=\s*.?EnableVpcClassicLink.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -16,6 +16,7 @@ class cloudwatch_cross_account_sharing_disabled(Check):
if role.name == "CloudWatch-CrossAccountSharingRole":
report.resource_arn = role.arn
report.resource_id = role.name
report.resource_tags = role.tags
report.status = "FAIL"
report.status_extended = (
"CloudWatch has allowed cross-account sharing."
@@ -24,6 +24,7 @@ class cloudwatch_log_group_no_secrets_in_logs(Check):
report.region = log_group.region
report.resource_id = log_group.name
report.resource_arn = log_group.arn
report.resource_tags = log_group.tags
log_group_secrets = []
if log_group.log_streams:
for log_stream_name in log_group.log_streams:
@@ -17,24 +17,24 @@ class cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_change
def execute(self):
pattern = r"\$\.eventSource\s*=\s*.?config.amazonaws.com.+\$\.eventName\s*=\s*.?StopConfigurationRecorder.+\$\.eventName\s*=\s*.?DeleteDeliveryChannel.+\$\.eventName\s*=\s*.?PutDeliveryChannel.+\$\.eventName\s*=\s*.?PutConfigurationRecorder.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -17,24 +17,24 @@ class cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_change
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?CreateTrail.+\$\.eventName\s*=\s*.?UpdateTrail.+\$\.eventName\s*=\s*.?DeleteTrail.+\$\.eventName\s*=\s*.?StartLogging.+\$\.eventName\s*=\s*.?StopLogging.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_authentication_failures(Check):
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?ConsoleLogin.+\$\.errorMessage\s*=\s*.?Failed authentication.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_aws_organizations_changes(Check):
def execute(self):
pattern = r"\$\.eventSource\s*=\s*.?organizations\.amazonaws\.com.+\$\.eventName\s*=\s*.?AcceptHandshake.+\$\.eventName\s*=\s*.?AttachPolicy.+\$\.eventName\s*=\s*.?CancelHandshake.+\$\.eventName\s*=\s*.?CreateAccount.+\$\.eventName\s*=\s*.?CreateOrganization.+\$\.eventName\s*=\s*.?CreateOrganizationalUnit.+\$\.eventName\s*=\s*.?CreatePolicy.+\$\.eventName\s*=\s*.?DeclineHandshake.+\$\.eventName\s*=\s*.?DeleteOrganization.+\$\.eventName\s*=\s*.?DeleteOrganizationalUnit.+\$\.eventName\s*=\s*.?DeletePolicy.+\$\.eventName\s*=\s*.?EnableAllFeatures.+\$\.eventName\s*=\s*.?EnablePolicyType.+\$\.eventName\s*=\s*.?InviteAccountToOrganization.+\$\.eventName\s*=\s*.?LeaveOrganization.+\$\.eventName\s*=\s*.?DetachPolicy.+\$\.eventName\s*=\s*.?DisablePolicyType.+\$\.eventName\s*=\s*.?MoveAccount.+\$\.eventName\s*=\s*.?RemoveAccountFromOrganization.+\$\.eventName\s*=\s*.?UpdateOrganizationalUnit.+\$\.eventName\s*=\s*.?UpdatePolicy.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk(Chec
def execute(self):
pattern = r"\$\.eventSource\s*=\s*.?kms.amazonaws.com.+\$\.eventName\s*=\s*.?DisableKey.+\$\.eventName\s*=\s*.?ScheduleKeyDeletion.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,25 +15,24 @@ class cloudwatch_log_metric_filter_for_s3_bucket_policy_changes(Check):
def execute(self):
pattern = r"\$\.eventSource\s*=\s*.?s3.amazonaws.com.+\$\.eventName\s*=\s*.?PutBucketAcl.+\$\.eventName\s*=\s*.?PutBucketPolicy.+\$\.eventName\s*=\s*.?PutBucketCors.+\$\.eventName\s*=\s*.?PutBucketLifecycle.+\$\.eventName\s*=\s*.?PutBucketReplication.+\$\.eventName\s*=\s*.?DeleteBucketPolicy.+\$\.eventName\s*=\s*.?DeleteBucketCors.+\$\.eventName\s*=\s*.?DeleteBucketLifecycle.+\$\.eventName\s*=\s*.?DeleteBucketReplication.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_policy_changes(Check):
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?DeleteGroupPolicy.+\$\.eventName\s*=\s*.?DeleteRolePolicy.+\$\.eventName\s*=\s*.?DeleteUserPolicy.+\$\.eventName\s*=\s*.?PutGroupPolicy.+\$\.eventName\s*=\s*.?PutRolePolicy.+\$\.eventName\s*=\s*.?PutUserPolicy.+\$\.eventName\s*=\s*.?CreatePolicy.+\$\.eventName\s*=\s*.?DeletePolicy.+\$\.eventName\s*=\s*.?CreatePolicyVersion.+\$\.eventName\s*=\s*.?DeletePolicyVersion.+\$\.eventName\s*=\s*.?AttachRolePolicy.+\$\.eventName\s*=\s*.?DetachRolePolicy.+\$\.eventName\s*=\s*.?AttachUserPolicy.+\$\.eventName\s*=\s*.?DetachUserPolicy.+\$\.eventName\s*=\s*.?AttachGroupPolicy.+\$\.eventName\s*=\s*.?DetachGroupPolicy.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_root_usage(Check):
def execute(self):
pattern = r"\$\.userIdentity\.type\s*=\s*.?Root.+\$\.userIdentity\.invokedBy NOT EXISTS.+\$\.eventType\s*!=\s*.?AwsServiceEvent.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_security_group_changes(Check):
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?AuthorizeSecurityGroupIngress.+\$\.eventName\s*=\s*.?AuthorizeSecurityGroupEgress.+\$\.eventName\s*=\s*.?RevokeSecurityGroupIngress.+\$\.eventName\s*=\s*.?RevokeSecurityGroupEgress.+\$\.eventName\s*=\s*.?CreateSecurityGroup.+\$\.eventName\s*=\s*.?DeleteSecurityGroup.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_sign_in_without_mfa(Check):
def execute(self):
pattern = r"\$\.eventName\s*=\s*.?ConsoleLogin.+\$\.additionalEventData\.MFAUsed\s*!=\s*.?Yes.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -15,24 +15,24 @@ class cloudwatch_log_metric_filter_unauthorized_api_calls(Check):
def execute(self):
pattern = r"\$\.errorCode\s*=\s*.?\*UnauthorizedOperation.+\$\.errorCode\s*=\s*.?AccessDenied\*.?"
findings = []
if (
cloudtrail_client.trails is not None
and logs_client.metric_filters is not None
and cloudwatch_client.metric_alarms is not None
):
report = Check_Report_AWS(self.metadata())
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
report,
)
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
if report is not None:
if report == Check_Report_AWS(self.metadata()):
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
findings.append(report)
return findings
@@ -82,10 +82,10 @@ class Logs(AWSService):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.log_group_arn_template = f"arn:{self.audited_partition}:logs:{self.region}:{self.audited_account}:log-group"
self.metric_filters = []
self.log_groups = []
self.__threading_call__(self._describe_metric_filters)
self.__threading_call__(self._describe_log_groups)
self.metric_filters = []
self.__threading_call__(self._describe_metric_filters)
if self.log_groups:
if (
"cloudwatch_log_group_no_secrets_in_logs"
@@ -111,13 +111,20 @@ class Logs(AWSService):
):
if self.metric_filters is None:
self.metric_filters = []
log_group = None
for lg in self.log_groups:
if lg.name == filter["logGroupName"]:
log_group = lg
break
self.metric_filters.append(
MetricFilter(
arn=arn,
name=filter["filterName"],
metric=filter["metricTransformations"][0]["metricName"],
pattern=filter.get("filterPattern", ""),
log_group=filter["logGroupName"],
log_group=log_group,
region=regional_client.region,
)
)
@@ -127,7 +134,7 @@ class Logs(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
if not self.metric_filters:
self.metric_filters = []
self.metric_filters = None
else:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -242,15 +249,6 @@ class MetricAlarm(BaseModel):
tags: Optional[list] = []
class MetricFilter(BaseModel):
arn: str
name: str
metric: str
pattern: str
log_group: str
region: str
class LogGroup(BaseModel):
arn: str
name: str
@@ -264,6 +262,15 @@ class LogGroup(BaseModel):
tags: Optional[list] = []
class MetricFilter(BaseModel):
arn: str
name: str
metric: str
pattern: str
log_group: Optional[LogGroup]
region: str
def convert_to_cloudwatch_timestamp_format(epoch_time):
date_time = datetime.fromtimestamp(
epoch_time / 1000, datetime.now(timezone.utc).astimezone().tzinfo
@@ -8,28 +8,34 @@ def check_cloudwatch_log_metric_filter(
trails: list,
metric_filters: list,
metric_alarms: list,
report: Check_Report_AWS,
metadata: dict,
):
report = None
# 1. Iterate for CloudWatch Log Group in CloudTrail trails
log_groups = []
if trails is not None:
if trails is not None and metric_filters is not None and metric_alarms is not None:
report = Check_Report_AWS(metadata)
for trail in trails.values():
if trail.log_group_arn:
log_groups.append(trail.log_group_arn.split(":")[6])
# 2. Describe metric filters for previous log groups
for metric_filter in metric_filters:
if metric_filter.log_group in log_groups:
if re.search(metric_filter_pattern, metric_filter.pattern, flags=re.DOTALL):
report.resource_id = metric_filter.log_group
report.resource_arn = metric_filter.arn
report.region = metric_filter.region
report.status = "FAIL"
report.status_extended = f"CloudWatch log group {metric_filter.log_group} found with metric filter {metric_filter.name} but no alarms associated."
# 3. Check if there is an alarm for the metric
for alarm in metric_alarms:
if alarm.metric == metric_filter.metric:
report.status = "PASS"
report.status_extended = f"CloudWatch log group {metric_filter.log_group} found with metric filter {metric_filter.name} and alarms set."
break
if metric_filter.log_group.name in log_groups and re.search(
metric_filter_pattern, metric_filter.pattern, flags=re.DOTALL
):
report.resource_id = metric_filter.log_group.name
report.resource_arn = metric_filter.log_group.arn
report.region = metric_filter.log_group.region
report.resource_tags = getattr(metric_filter.log_group, "tags", [])
report.status = "FAIL"
report.status_extended = f"CloudWatch log group {metric_filter.log_group.name} found with metric filter {metric_filter.name} but no alarms associated."
# 3. Check if there is an alarm for the metric
for alarm in metric_alarms:
if alarm.metric == metric_filter.metric:
report.status = "PASS"
report.status_extended = f"CloudWatch log group {metric_filter.log_group.name} found with metric filter {metric_filter.name} and alarms set."
break
if report.status == "PASS":
break
return report
@@ -16,7 +16,7 @@ class cognito_user_pool_waf_acl_attached(Check):
report.status_extended = (
f"Cognito User Pool {pool.name} is not associated with a WAF Web ACL."
)
for acl in wafv2_client.web_acls:
for acl in wafv2_client.web_acls.values():
if pool.arn in acl.user_pools:
report.status = "PASS"
report.status_extended = f"Cognito User Pool {pool.name} is associated with the WAF Web ACL {acl.name}."
@@ -11,7 +11,6 @@ from prowler.providers.aws.lib.service.service import AWSService
class DynamoDB(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.tables = {}
self.__threading_call__(self._list_tables)
@@ -49,6 +48,9 @@ class DynamoDB(AWSService):
properties = regional_client.describe_table(TableName=table.name)[
"Table"
]
table.billing_mode = properties.get("BillingModeSummary", {}).get(
"BillingMode", "PROVISIONED"
)
if "SSEDescription" in properties:
if "SSEType" in properties["SSEDescription"]:
table.encryption_type = properties["SSEDescription"]["SSEType"]
@@ -152,7 +154,6 @@ class DynamoDB(AWSService):
class DAX(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.clusters = []
self.__threading_call__(self._describe_clusters)
@@ -217,6 +218,7 @@ class DAX(AWSService):
class Table(BaseModel):
name: str
billing_mode: str = "PROVISIONED"
encryption_type: Optional[str]
kms_arn: Optional[str]
pitr: bool = False
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "dynamodb_table_autoscaling_enabled",
"CheckTitle": "Check if DynamoDB tables automatically scale capacity with demand.",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "dynamodb",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:dynamodb:region:account-id:table/table-name",
"Severity": "medium",
"ResourceType": "AwsDynamoDbTable",
"Description": "This check ensures that DynamoDB tables can scale their read and write capacity as needed, either using on-demand capacity mode or provisioned mode with auto scaling configured.",
"Risk": "If DynamoDB tables do not automatically scale capacity with demand, they may experience throttling exceptions, leading to reduced availability and performance of applications.",
"RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.Console.html#AutoScaling.Console.ExistingTable",
"Remediation": {
"Code": {
"CLI": "aws dynamodb update-table --table-name <table-name> --billing-mode PAY_PER_REQUEST",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dynamodb-controls.html#dynamodb-1",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable DynamoDB automatic scaling on existing tables by configuring on-demand capacity mode or provisioned mode with auto scaling.",
"Url": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.Console.html#AutoScaling.Console.ExistingTable"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,67 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.autoscaling.applicationautoscaling_client import (
applicationautoscaling_client,
)
from prowler.providers.aws.services.dynamodb.dynamodb_client import dynamodb_client
class dynamodb_table_autoscaling_enabled(Check):
def execute(self):
findings = []
scalable_targets = applicationautoscaling_client.scalable_targets
dynamodb_scalable_targets = [
target
for target in scalable_targets
if target.service_namespace == "dynamodb"
and target.resource_id.startswith("table/")
]
autoscaling_mapping = {}
for target in dynamodb_scalable_targets:
table_name = target.resource_id.split("/")[1]
if table_name not in autoscaling_mapping:
autoscaling_mapping[table_name] = {}
autoscaling_mapping[table_name][target.scalable_dimension] = target
for table_arn, table in dynamodb_client.tables.items():
report = Check_Report_AWS(self.metadata())
report.region = table.region
report.resource_id = table.name
report.resource_arn = table_arn
report.resource_tags = table.tags
report.status = "PASS"
report.status_extended = (
f"DynamoDB table {table.name} automatically scales capacity on demand."
)
if table.billing_mode == "PROVISIONED":
read_autoscaling = False
write_autoscaling = False
if table.name in autoscaling_mapping:
if (
"dynamodb:table:ReadCapacityUnits"
in autoscaling_mapping[table.name]
):
read_autoscaling = True
if (
"dynamodb:table:WriteCapacityUnits"
in autoscaling_mapping[table.name]
):
write_autoscaling = True
if read_autoscaling and write_autoscaling:
report.status = "PASS"
report.status_extended = f"DynamoDB table {table.name} is in provisioned mode with auto scaling enabled for both read and write capacity units."
else:
missing_autoscaling = []
if not read_autoscaling:
missing_autoscaling.append("read")
if not write_autoscaling:
missing_autoscaling.append("write")
if missing_autoscaling:
report.status = "FAIL"
report.status_extended = f"DynamoDB table {table.name} is in provisioned mode without auto scaling enabled for {', '.join(missing_autoscaling)}."
findings.append(report)
return findings
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "ecs_cluster_container_insights_enabled",
"CheckTitle": "ECS clusters should use Container Insights",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "ecs",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:cluster/{cluster-name}",
"Severity": "medium",
"ResourceType": "AwsEcsCluster",
"Description": "This control checks if ECS clusters use Container Insights. This control fails if Container Insights are not set up for a cluster.",
"Risk": "Without Container Insights, important performance metrics and diagnostic information from containerized applications may not be captured, which can hinder monitoring and troubleshooting.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-container-insights-enabled.html",
"Remediation": {
"Code": {
"CLI": "aws ecs update-cluster-settings --cluster <cluster-name> --settings name=containerInsights,value=enabled",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-12",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Container Insights for your ECS clusters to collect and monitor key performance metrics and diagnostic data from your containers.",
"Url": "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/deploy-container-insights-ECS-cluster.html"
}
},
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,27 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.ecs.ecs_client import ecs_client
class ecs_cluster_container_insights_enabled(Check):
def execute(self):
findings = []
for cluster in ecs_client.clusters.values():
report = Check_Report_AWS(self.metadata())
report.region = cluster.region
report.resource_id = cluster.name
report.resource_arn = cluster.arn
report.resource_tags = cluster.tags
report.status = "FAIL"
report.status_extended = (
f"ECS cluster {cluster.name} does not have container insights enabled."
)
if cluster.settings:
for setting in cluster.settings:
if (
setting["name"] == "containerInsights"
and setting["value"] == "enabled"
):
report.status = "PASS"
report.status_extended = f"ECS cluster {cluster.name} has container insights enabled."
findings.append(report)
return findings
@@ -8,7 +8,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered
from prowler.providers.aws.lib.service.service import AWSService
################################ ECS
class ECS(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
@@ -35,7 +34,7 @@ class ECS(AWSService):
):
self.task_definitions[task_definition] = TaskDefinition(
# we want the family name without the revision
name=sub(":.*", "", task_definition.split("/")[1]),
name=sub(":.*", "", task_definition.split("/")[-1]),
arn=task_definition,
revision=task_definition.split(":")[-1],
region=regional_client.region,
@@ -111,7 +110,7 @@ class ECS(AWSService):
service_desc = describe_response["services"][0]
service_arn = service_desc["serviceArn"]
service_obj = Service(
name=sub(":.*", "", service_arn.split("/")[2]),
name=sub(":.*", "", service_arn.split("/")[-1]),
arn=service_arn,
region=cluster.region,
assign_public_ip=(
@@ -120,6 +119,9 @@ class ECS(AWSService):
.get("assignPublicIp", "DISABLED")
== "ENABLED"
),
launch_type=service_desc.get("launchType", ""),
platform_version=service_desc.get("platformVersion", ""),
platform_family=service_desc.get("platformFamily", ""),
tags=service_desc.get("tags", []),
)
cluster.services[service_arn] = service_obj
@@ -139,7 +141,7 @@ class ECS(AWSService):
is_resource_filtered(cluster, self.audit_resources)
):
self.clusters[cluster] = Cluster(
name=sub(":.*", "", cluster.split("/")[1]),
name=sub(":.*", "", cluster.split("/")[-1]),
arn=cluster,
region=regional_client.region,
)
@@ -158,6 +160,7 @@ class ECS(AWSService):
"TAGS",
],
)
cluster.settings = response["clusters"][0].get("settings", [])
cluster.tags = response["clusters"][0].get("tags", [])
except Exception as error:
logger.error(
@@ -194,6 +197,9 @@ class Service(BaseModel):
name: str
arn: str
region: str
launch_type: str = ""
platform_version: Optional[str]
platform_family: Optional[str]
assign_public_ip: Optional[bool]
tags: Optional[list] = []
@@ -203,4 +209,5 @@ class Cluster(BaseModel):
arn: str
region: str
services: dict = {}
settings: Optional[list] = []
tags: Optional[list] = []
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "ecs_service_fargate_latest_platform_version",
"CheckTitle": "ECS Fargate services should run on the latest Fargate platform version",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "ecs",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:service/{service-name}",
"Severity": "medium",
"ResourceType": "AwsEcsService",
"Description": "This control checks if Amazon ECS Fargate services are running the latest Fargate platform version. The control fails if the platform version is not the latest.",
"Risk": "Not running the latest Fargate platform version may expose your services to security vulnerabilities and bugs that are resolved in newer versions.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-fargate-latest-platform-version.html",
"Remediation": {
"Code": {
"CLI": "aws ecs update-service --cluster <cluster-name> --service <service-name> --platform-version LATEST",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-10",
"Terraform": ""
},
"Recommendation": {
"Text": "Update your ECS Fargate services to the latest platform version to ensure they are running in a secure and optimized environment.",
"Url": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html"
}
},
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,38 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.ecs.ecs_client import ecs_client
class ecs_service_fargate_latest_platform_version(Check):
def execute(self):
findings = []
for service in ecs_client.services.values():
if service.launch_type == "FARGATE":
report = Check_Report_AWS(self.metadata())
report.region = service.region
report.resource_id = service.name
report.resource_arn = service.arn
report.resource_tags = service.tags
fargate_latest_linux_version = ecs_client.audit_config.get(
"fargate_linux_latest_version", "1.4.0"
)
fargate_latest_windows_version = ecs_client.audit_config.get(
"fargate_windows_latest_version", "1.0.0"
)
report.status = "PASS"
report.status_extended = f"ECS Service {service.name} is using latest FARGATE {service.platform_family} version {fargate_latest_linux_version if service.platform_family == 'Linux' else fargate_latest_windows_version}."
if (
service.platform_version != "LATEST"
and (
service.platform_family == "Linux"
and service.platform_version != fargate_latest_linux_version
)
or (
service.platform_family == "Windows"
and service.platform_version != fargate_latest_windows_version
)
):
report.status = "FAIL"
report.status_extended = f"ECS Service {service.name} is not using latest FARGATE {service.platform_family} version {fargate_latest_linux_version if service.platform_family == 'Linux' else fargate_latest_windows_version}, currently using {service.platform_version}."
findings.append(report)
return findings
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "efs_access_point_enforce_root_directory",
"CheckTitle": "EFS access points should enforce a root directory",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "efs",
"SubServiceName": "access-point",
"ResourceIdTemplate": "arn:aws:elasticfilesystem:{region}:{account-id}:access-point/{access-point-id}",
"Severity": "medium",
"ResourceType": "AwsEfsAccessPoint",
"Description": "This control checks if Amazon EFS access points are configured to enforce a root directory. The control fails if the value of Path is set to / (the default root directory of the file system).",
"Risk": "Access points without enforced root directories can potentially expose the entire file system's root directory to clients, which may result in unauthorized access.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/efs-access-point-enforce-root-directory.html",
"Remediation": {
"Code": {
"CLI": "aws efs update-access-point --access-point-id <access-point-id> --root-directory Path=<non-root-directory-path>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/efs-controls.html#efs-3",
"Terraform": ""
},
"Recommendation": {
"Text": "Update the EFS access point to enforce a non-root directory. This ensures clients can only access a specified subdirectory.",
"Url": "https://docs.aws.amazon.com/efs/latest/ug/enforce-root-directory-access-point.html"
}
},
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,25 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.efs.efs_client import efs_client
class efs_access_point_enforce_root_directory(Check):
def execute(self):
findings = []
for fs in efs_client.filesystems.values():
if fs.access_points:
report = Check_Report_AWS(self.metadata())
report.region = fs.region
report.resource_id = fs.id
report.resource_arn = fs.arn
report.resource_tags = fs.tags
report.status = "PASS"
report.status_extended = f"EFS {fs.id} does not have any access point allowing access to the root directory."
access_points = []
for access_point in fs.access_points:
if access_point.root_directory_path == "/":
access_points.append(access_point)
if access_points:
report.status = "FAIL"
report.status_extended = f"EFS {fs.id} has access points which allow access to the root directory: {', '.join([ap.id for ap in access_points])}."
findings.append(report)
return findings
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "efs_access_point_enforce_user_identity",
"CheckTitle": "EFS access points should enforce a user identity",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "efs",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:elasticfilesystem:{region}:{account-id}:access-point/{access-point-id}",
"Severity": "medium",
"ResourceType": "AwsEfsAccessPoint",
"Description": "This control checks whether Amazon EFS access points are configured to enforce a user identity. This control fails if a POSIX user identity is not defined while creating the EFS access point.",
"Risk": "Without enforcing a user identity, access to the file system can become less controlled, leading to potential unauthorized access or modifications.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/efs-access-point-enforce-user-identity.html",
"Remediation": {
"Code": {
"CLI": "aws efs create-access-point --file-system-id <file-system-id> --posix-user Uid=<uid>,Gid=<gid>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/efs-controls.html#efs-4",
"Terraform": ""
},
"Recommendation": {
"Text": "Create or update the EFS access point to enforce a user identity using POSIX attributes for Uid and Gid.",
"Url": "https://docs.aws.amazon.com/efs/latest/ug/enforce-identity-access-points.html"
}
},
"Categories": [
"trustboundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,28 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.efs.efs_client import efs_client
class efs_access_point_enforce_user_identity(Check):
def execute(self):
findings = []
for fs in efs_client.filesystems.values():
if fs.access_points:
report = Check_Report_AWS(self.metadata())
report.region = fs.region
report.resource_id = fs.id
report.resource_arn = fs.arn
report.resource_tags = fs.tags
report.status = "PASS"
report.status_extended = (
f"EFS {fs.id} has all access points with defined POSIX user."
)
access_points = []
for access_point in fs.access_points:
if not access_point.posix_user:
access_points.append(access_point)
if access_points:
report.status = "FAIL"
report.status_extended = f"EFS {fs.id} has access points with no POSIX user: {', '.join([ap.id for ap in access_points])}."
findings.append(report)
return findings
@@ -5,7 +5,7 @@ from prowler.providers.aws.services.efs.efs_client import efs_client
class efs_encryption_at_rest_enabled(Check):
def execute(self):
findings = []
for fs in efs_client.filesystems:
for fs in efs_client.filesystems.values():
report = Check_Report_AWS(self.metadata())
report.region = fs.region
report.resource_id = fs.id
@@ -5,7 +5,7 @@ from prowler.providers.aws.services.efs.efs_client import efs_client
class efs_have_backup_enabled(Check):
def execute(self):
findings = []
for fs in efs_client.filesystems:
for fs in efs_client.filesystems.values():
report = Check_Report_AWS(self.metadata())
report.region = fs.region
report.resource_id = fs.id
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "efs_mount_target_not_publicly_accessible",
"CheckTitle": "EFS mount targets should not be publicly accessible",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "efs",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:elasticfilesystem:{region}:{account-id}:file-system/{filesystem-id}",
"Severity": "medium",
"ResourceType": "AwsEfsFileSystem",
"Description": "This control checks whether an Amazon EFS mount target is associated with a public subnet since it can be accessed from the internet.",
"Risk": "Mount targets in public subnets may expose your EFS to unauthorized access or attacks.",
"RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/efs-mount-target-public-accessible.html",
"Remediation": {
"Code": {
"CLI": "aws efs create-mount-target --file-system-id <filesystem-id> --subnet-id <private-subnet-id> --security-groups <sg-ids>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/efs-controls.html#efs-6",
"Terraform": ""
},
"Recommendation": {
"Text": "Recreate the EFS mount target in a private subnet to ensure it is not publicly accessible.",
"Url": "https://docs.aws.amazon.com/efs/latest/ug/accessing-fs.html"
}
},
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,28 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.efs.efs_client import efs_client
from prowler.providers.aws.services.vpc.vpc_client import vpc_client
class efs_mount_target_not_publicly_accessible(Check):
def execute(self):
findings = []
for fs in efs_client.filesystems.values():
report = Check_Report_AWS(self.metadata())
report.region = fs.region
report.resource_id = fs.id
report.resource_arn = fs.arn
report.resource_tags = fs.tags
report.status = "PASS"
report.status_extended = (
f"EFS {fs.id} does not have any public mount targets."
)
mount_targets = []
for mt in fs.mount_targets:
if vpc_client.vpc_subnets[mt.subnet_id].public:
mount_targets.append(mt)
if mount_targets:
report.status = "FAIL"
report.status_extended = f"EFS {fs.id} has public mount targets: {', '.join([mt.id for mt in mount_targets])}"
findings.append(report)
return findings
@@ -6,7 +6,7 @@ from prowler.providers.aws.services.iam.lib.policy import is_policy_public
class efs_not_publicly_accessible(Check):
def execute(self):
findings = []
for fs in efs_client.filesystems:
for fs in efs_client.filesystems.values():
report = Check_Report_AWS(self.metadata())
report.region = fs.region
report.resource_id = fs.id
+119 -33
View File
@@ -14,9 +14,13 @@ class EFS(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.filesystems = []
self.filesystems = {}
self.__threading_call__(self._describe_file_systems)
self._describe_file_system_policies()
self.__threading_call__(
self._describe_file_system_policies, self.filesystems.values()
)
self.__threading_call__(self._describe_mount_targets, self.filesystems.values())
self.__threading_call__(self._describe_access_points, self.filesystems.values())
def _describe_file_systems(self, regional_client):
logger.info("EFS - Describing file systems...")
@@ -31,55 +35,137 @@ class EFS(AWSService):
if not self.audit_resources or (
is_resource_filtered(efs_arn, self.audit_resources)
):
self.filesystems.append(
FileSystem(
id=efs_id,
arn=efs_arn,
region=regional_client.region,
policy=None,
backup_policy=None,
encrypted=efs["Encrypted"],
tags=efs.get("Tags"),
)
self.filesystems[efs_arn] = FileSystem(
id=efs_id,
arn=efs_arn,
region=regional_client.region,
encrypted=efs["Encrypted"],
tags=efs.get("Tags"),
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _describe_file_system_policies(self):
def _describe_file_system_policies(self, filesystem):
logger.info("EFS - Describing file system policies...")
try:
for filesystem in self.filesystems:
for region, client in self.regional_clients.items():
if filesystem.region == region:
try:
filesystem.backup_policy = client.describe_backup_policy(
FileSystemId=filesystem.id
)["BackupPolicy"]["Status"]
except ClientError as e:
if e.response["Error"]["Code"] == "PolicyNotFound":
filesystem.backup_policy = "DISABLED"
try:
fs_policy = client.describe_file_system_policy(
FileSystemId=filesystem.id
client = self.regional_clients[filesystem.region]
try:
filesystem.backup_policy = client.describe_backup_policy(
FileSystemId=filesystem.id
)["BackupPolicy"]["Status"]
except ClientError as error:
if error.response["Error"]["Code"] == "PolicyNotFound":
filesystem.backup_policy = "DISABLED"
logger.warning(
f"{client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
else:
logger.error(
f"{client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
try:
fs_policy = client.describe_file_system_policy(
FileSystemId=filesystem.id
)
if "Policy" in fs_policy:
filesystem.policy = json.loads(fs_policy["Policy"])
except ClientError as error:
if error.response["Error"]["Code"] == "PolicyNotFound":
filesystem.policy = {}
logger.warning(
f"{client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
else:
logger.error(
f"{client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _describe_mount_targets(self, filesystem):
logger.info("EFS - Describing mount targets...")
try:
client = self.regional_clients[filesystem.region]
describe_mount_target_paginator = client.get_paginator(
"describe_mount_targets"
)
for page in describe_mount_target_paginator.paginate(
FileSystemId=filesystem.id
):
for mount_target in page["MountTargets"]:
mount_target_id = mount_target["MountTargetId"]
mount_target_arn = f"arn:{self.audited_partition}:elasticfilesystem:{client.region}:{self.audited_account}:mount-target/{mount_target_id}"
if not self.audit_resources or (
is_resource_filtered(mount_target_arn, self.audit_resources)
):
self.filesystems[filesystem.arn].mount_targets.append(
MountTarget(
id=mount_target_id,
file_system_id=mount_target["FileSystemId"],
subnet_id=mount_target["SubnetId"],
)
if "Policy" in fs_policy:
filesystem.policy = json.loads(fs_policy["Policy"])
except ClientError as e:
if e.response["Error"]["Code"] == "PolicyNotFound":
filesystem.policy = {}
)
except Exception as error:
logger.error(
f"{client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _describe_access_points(self, filesystem):
logger.info("EFS - Describing access points...")
try:
client = self.regional_clients[filesystem.region]
describe_access_point_paginator = client.get_paginator(
"describe_access_points"
)
for page in describe_access_point_paginator.paginate(
FileSystemId=filesystem.id
):
for access_point in page["AccessPoints"]:
access_point_id = access_point["AccessPointId"]
access_point_arn = access_point["AccessPointArn"]
if not self.audit_resources or (
is_resource_filtered(access_point_arn, self.audit_resources)
):
self.filesystems[filesystem.arn].access_points.append(
AccessPoint(
id=access_point_id,
file_system_id=access_point["FileSystemId"],
root_directory_path=access_point["RootDirectory"][
"Path"
],
posix_user=access_point.get("PosixUser", {}),
)
)
except Exception as error:
logger.error(
f"{client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class MountTarget(BaseModel):
id: str
file_system_id: str
subnet_id: str
class AccessPoint(BaseModel):
id: str
file_system_id: str
root_directory_path: str
posix_user: dict = {}
class FileSystem(BaseModel):
id: str
arn: str
region: str
policy: Optional[dict]
backup_policy: Optional[str]
policy: Optional[dict] = {}
backup_policy: Optional[str] = "DISABLED"
encrypted: bool
mount_targets: list[MountTarget] = []
access_points: list[AccessPoint] = []
tags: Optional[list] = []
@@ -0,0 +1,6 @@
from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_service import (
ElasticBeanstalk,
)
from prowler.providers.common.provider import Provider
elasticbeanstalk_client = ElasticBeanstalk(Provider.get_global_provider())
@@ -0,0 +1,106 @@
from typing import Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
from prowler.providers.aws.lib.service.service import AWSService
class ElasticBeanstalk(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.environments = {}
self.__threading_call__(self._describe_environments)
self.__threading_call__(
self._describe_configuration_settings, self.environments.values()
)
self.__threading_call__(
self._list_tags_for_resource, self.environments.values()
)
def _describe_environments(self, regional_client):
logger.info("ElasticBeanstalk - Describing environments...")
try:
describe_environment_paginator = regional_client.get_paginator(
"describe_environments"
)
for page in describe_environment_paginator.paginate():
for environment in page["Environments"]:
environment_arn = environment["EnvironmentArn"]
if not self.audit_resources or (
is_resource_filtered(environment_arn, self.audit_resources)
):
self.environments[environment_arn] = Environment(
id=environment["EnvironmentId"],
arn=environment_arn,
application_name=environment["ApplicationName"],
name=environment["EnvironmentName"],
region=regional_client.region,
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _describe_configuration_settings(self, environment):
logger.info("ElasticBeanstalk - Describing configuration settings...")
try:
regional_client = self.regional_clients[environment.region]
configuration_settings = regional_client.describe_configuration_settings(
ApplicationName=environment.application_name,
EnvironmentName=environment.name,
)
option_settings = configuration_settings["ConfigurationSettings"][0].get(
"OptionSettings", {}
)
for option in option_settings:
if (
option["Namespace"] == "aws:elasticbeanstalk:healthreporting:system"
and option["OptionName"] == "SystemType"
):
environment.health_reporting = option.get("Value", "basic")
elif (
option["Namespace"] == "aws:elasticbeanstalk:managedactions"
and option["OptionName"] == "ManagedActionsEnabled"
):
environment.managed_platform_updates = option.get("Value", "false")
elif (
option["Namespace"] == "aws:elasticbeanstalk:cloudwatch:logs"
and option["OptionName"] == "StreamLogs"
):
environment.cloudwatch_stream_logs = option.get("Value", "false")
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_tags_for_resource(self, resource: any):
logger.info("ElasticBeanstalk - List Tags...")
try:
regional_client = self.regional_clients[resource.region]
response = regional_client.list_tags_for_resource(ResourceArn=resource.arn)[
"ResourceTags"
]
resource.tags = response
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class Environment(BaseModel):
id: str
name: str
arn: str
region: str
application_name: str
health_reporting: Optional[str]
managed_platform_updates: Optional[str]
cloudwatch_stream_logs: Optional[str]
tags: Optional[list] = []
@@ -18,7 +18,7 @@ class elbv2_waf_acl_attached(Check):
report.status_extended = (
f"ELBv2 ALB {lb.name} is not protected by WAF Web ACL."
)
for acl in wafv2_client.web_acls:
for acl in wafv2_client.web_acls.values():
if lb_arn in acl.albs:
report.status = "PASS"
report.status_extended = f"ELBv2 ALB {lb.name} is protected by WAFv2 Web ACL {acl.name}."
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "glue_ml_transform_encrypted_at_rest",
"CheckTitle": "Check if Glue ML Transform Encryption at Rest is Enabled",
"CheckType": [],
"ServiceName": "glue",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:glue:region:account-id:mlTransform/transform-id",
"Severity": "medium",
"ResourceType": "Other",
"Description": "This control checks whether an AWS Glue machine learning transform is encrypted at rest. The control fails if the machine learning transform isn't encrypted at rest.",
"Risk": "Data at rest refers to data that's stored in persistent, non-volatile storage for any duration. Encrypting data at rest helps you protect its confidentiality, which reduces the risk that an unauthorized user can access it.",
"RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/encryption-at-rest.html",
"Remediation": {
"Code": {
"CLI": "aws glue update-ml-transform --transform-id <transform-id> --encryption-at-rest {\"Enabled\":true,\"KmsKey\":\"<kms-key-arn>\"}",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/glue-controls.html#glue-3",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable encryption at rest for Glue ML Transforms using AWS KMS keys.",
"Url": "https://docs.aws.amazon.com/glue/latest/dg/encryption-at-rest.html"
}
},
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,28 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.glue.glue_client import glue_client
class glue_ml_transform_encrypted_at_rest(Check):
def execute(self):
findings = []
for ml_transform_arn, ml_transform in glue_client.ml_transforms.items():
report = Check_Report_AWS(self.metadata())
report.resource_id = ml_transform.id
report.resource_arn = ml_transform_arn
report.region = ml_transform.region
report.resource_tags = ml_transform.tags
report.status = "PASS"
report.status_extended = (
f"Glue ML Transform {ml_transform.name} is encrypted at rest."
)
if ml_transform.user_data_encryption == "DISABLED":
report.status = "FAIL"
report.status_extended = (
f"Glue ML Transform {ml_transform.name} is not encrypted at rest."
)
findings.append(report)
return findings
@@ -27,6 +27,9 @@ class Glue(AWSService):
self.jobs = []
self.__threading_call__(self._get_jobs)
self.__threading_call__(self._list_tags, self.jobs)
self.ml_transforms = {}
self.__threading_call__(self._get_ml_transforms)
self.__threading_call__(self._list_tags, self.ml_transforms.values())
def _get_data_catalog_arn_template(self, region):
return f"arn:{self.audited_partition}:glue:{region}:{self.audited_account}:data-catalog"
@@ -219,6 +222,30 @@ class Glue(AWSService):
f"{resource.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _get_ml_transforms(self, regional_client):
logger.info("Glue - Getting ML Transforms...")
try:
transforms = regional_client.get_ml_transforms()["Transforms"]
for transform in transforms:
ml_transform_arn = f"arn:{self.audited_partition}:glue:{regional_client.region}:{self.audited_account}:mlTransform/{transform['TransformId']}"
if not self.audit_resources or is_resource_filtered(
ml_transform_arn, self.audit_resources
):
self.ml_transforms[ml_transform_arn] = MLTransform(
arn=ml_transform_arn,
id=transform["TransformId"],
name=transform["Name"],
user_data_encryption=transform.get("TransformEncryption", {})
.get("MlUserDataEncryption", {})
.get("MlUserDataEncryptionMode", "DISABLED"),
region=regional_client.region,
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class Connection(BaseModel):
name: str
@@ -272,3 +299,12 @@ class SecurityConfig(BaseModel):
jb_encryption: str
jb_key_arn: Optional[str]
region: str
class MLTransform(BaseModel):
arn: str
id: str
name: str
user_data_encryption: str
region: str
tags: Optional[list]
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "guardduty_ec2_malware_protection_enabled",
"CheckTitle": "Ensure that GuardDuty Malware Protection for EC2 is enabled.",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "guardduty",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:guardduty:region:account-id/detector-id",
"Severity": "high",
"ResourceType": "AwsGuardDutyDetector",
"Description": "GuardDuty Malware Protection for EC2 helps you detect the potential presence of malware by scanning the Amazon Elastic Block Store (Amazon EBS) volumes that are attached to Amazon Elastic Compute Cloud (Amazon EC2) instances and container workloads.",
"Risk": "Malware can compromise your EC2 instances and container workloads, leading to data breaches, data exfiltration, and other security incidents.",
"RelatedUrl": "https://docs.aws.amazon.com/guardduty/latest/ug/malware-protection.html",
"Remediation": {
"Code": {
"CLI": "aws guardduty update-detector --detector-id <detector-id> --data-sources MalwareProtection={ScanEc2InstanceWithFindings={EbsVolumes=true}}",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-8",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Malware Protection for EC2 in GuardDuty.",
"Url": "https://docs.aws.amazon.com/guardduty/latest/ug/configure-malware-protection-single-account.html"
}
},
"Categories": [],
"Notes": "",
"DependsOn": [],
"RelatedTo": []
}
@@ -0,0 +1,21 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client
class guardduty_ec2_malware_protection_enabled(Check):
def execute(self):
findings = []
for detector in guardduty_client.detectors:
if detector.status:
report = Check_Report_AWS(self.metadata())
report.region = detector.region
report.resource_id = detector.id
report.resource_arn = detector.arn
report.resource_tags = detector.tags
report.status = "FAIL"
report.status_extended = f"GuardDuty detector {detector.id} does not have Malware Protection for EC2 enabled."
if detector.ec2_malware_protection:
report.status = "PASS"
report.status_extended = f"GuardDuty detector {detector.id} has Malware Protection for EC2 enabled."
findings.append(report)
return findings
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "guardduty_eks_audit_log_enabled",
"CheckTitle": "GuardDuty EKS Audit Log Monitoring Enabled",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices/Runtime Behavior Analysis"
],
"ServiceName": "guardduty",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:guardduty:region:account-id/detector-id",
"Severity": "high",
"ResourceType": "AwsGuardDutyDetector",
"Description": "Checks whether GuardDuty EKS Audit Log Monitoring is enabled as source in a detector.",
"Risk": "Without GuardDuty EKS Audit Log Monitoring enabled, you may not be able to detect potentially suspicious activities in your Amazon Elastic Kubernetes Service (Amazon EKS) clusters.",
"RelatedUrl": "https://docs.aws.amazon.com/guardduty/latest/ug/kubernetes-protection.html",
"Remediation": {
"Code": {
"CLI": "aws guardduty update-detector --detector-id <detector-id> --data-sources Kubernetes={AuditLogs={Enable=true}}",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-5",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable GuardDuty EKS Audit Log Monitoring to detect potentially suspicious activities in your Amazon Elastic Kubernetes Service (Amazon EKS) clusters.",
"Url": "https://docs.aws.amazon.com/guardduty/latest/ug/eks-protection-enable-standalone-account.html"
}
},
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,21 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client
class guardduty_eks_audit_log_enabled(Check):
def execute(self):
findings = []
for detector in guardduty_client.detectors:
if detector.status:
report = Check_Report_AWS(self.metadata())
report.region = detector.region
report.resource_id = detector.id
report.resource_arn = detector.arn
report.resource_tags = detector.tags
report.status = "FAIL"
report.status_extended = f"GuardDuty detector {detector.id} does not have EKS Audit Log Monitoring enabled."
if detector.eks_audit_log_protection:
report.status = "PASS"
report.status_extended = f"GuardDuty detector {detector.id} has EKS Audit Log Monitoring enabled."
findings.append(report)
return findings
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "guardduty_lambda_protection_enabled",
"CheckTitle": "Check if GuardDuty Lambda Protection is enabled.",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "guardduty",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:guardduty:region:account-id/detector-id",
"Severity": "high",
"ResourceType": "",
"Description": "GuardDuty Lambda Protection helps you identify potential security threats when an AWS Lambda function gets invoked. After you enable Lambda Protection, GuardDuty starts monitoring Lambda network activity logs associated with the Lambda functions in your AWS account.",
"Risk": "If Lambda Protection is not enabled, GuardDuty will not be able to monitor Lambda network activity logs and may miss potential security threats.",
"RelatedUrl": "https://docs.aws.amazon.com/guardduty/latest/ug/lambda-protection.html",
"Remediation": {
"Code": {
"CLI": "aws guardduty update-detector --detector-id <detector-id> --features Name=LAMBDA_NETWORK_LOGS,Status=ENABLED",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-6",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Lambda Protection in your GuardDuty detector to start monitoring Lambda Network Activity in your account.",
"Url": "https://docs.aws.amazon.com/guardduty/latest/ug/configure-lambda-protection-standalone-acc.html"
}
},
"Categories": [],
"Notes": "",
"DependsOn": [],
"RelatedTo": []
}
@@ -0,0 +1,21 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client
class guardduty_lambda_protection_enabled(Check):
def execute(self):
findings = []
for detector in guardduty_client.detectors:
if detector.status:
report = Check_Report_AWS(self.metadata())
report.region = detector.region
report.resource_id = detector.id
report.resource_arn = detector.arn
report.resource_tags = detector.tags
report.status = "FAIL"
report.status_extended = f"GuardDuty detector {detector.id} does not have Lambda Protection enabled."
if detector.lambda_protection:
report.status = "PASS"
report.status_extended = f"GuardDuty detector {detector.id} has Lambda Protection enabled."
findings.append(report)
return findings
@@ -15,7 +15,7 @@
"RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/guard-duty-rds-protection.html",
"Remediation": {
"Code": {
"CLI": "",
"CLI": "aws guardduty update-detector --detector-id <detector-id> --features Name=RDS_LOGIN_EVENTS,Status=ENABLED",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-9",
"Terraform": ""
@@ -15,7 +15,7 @@
"RelatedUrl": "https://docs.aws.amazon.com/guardduty/latest/ug/s3_detection.html",
"Remediation": {
"Code": {
"CLI": "aws guardduty update-detector --detector-id <detector-id> --data-sources '{\"S3Logs\": {\"Enable\": true}}'",
"CLI": "aws guardduty update-detector --detector-id <detector-id> --data-sources S3Logs={Enable=true}}'",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-10",
"Terraform": ""
@@ -13,7 +13,7 @@ class GuardDuty(AWSService):
super().__init__(__class__.__name__, provider)
self.detectors = []
self.__threading_call__(self._list_detectors)
self._get_detector()
self.__threading_call__(self._get_detector, self.detectors)
self._list_findings()
self._list_members()
self._get_administrator_account()
@@ -33,7 +33,10 @@ class GuardDuty(AWSService):
):
self.detectors.append(
Detector(
id=detector, arn=arn, region=regional_client.region
id=detector,
arn=arn,
region=regional_client.region,
enabled_in_account=True,
)
)
if not detectors:
@@ -50,38 +53,53 @@ class GuardDuty(AWSService):
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _get_detector(self):
def _get_detector(self, detector):
logger.info("GuardDuty - getting detector info...")
try:
for detector in self.detectors:
try:
if detector.id and detector.enabled_in_account:
regional_client = self.regional_clients[detector.region]
detector_info = regional_client.get_detector(
DetectorId=detector.id
)
if (
"Status" in detector_info
and detector_info["Status"] == "ENABLED"
):
detector.status = True
if detector.id and detector.enabled_in_account:
detector_info = self.regional_clients[detector.region].get_detector(
DetectorId=detector.id
)
if detector_info.get("Status", "DISABLED") == "ENABLED":
detector.status = True
data_sources = detector_info.get("DataSources", {})
s3_logs = data_sources.get("S3Logs", {})
if s3_logs.get("Status") == "ENABLED":
detector.s3_protection = True
data_sources = detector_info.get("DataSources", {})
for feat in detector_info.get("Features", []):
if (
feat.get("Name") == "RDS_LOGIN_EVENTS"
and feat.get("Status", "DISABLED") == "ENABLED"
):
detector.rds_protection = True
s3_logs = data_sources.get("S3Logs", {})
if s3_logs.get("Status", "DISABLED") == "ENABLED":
detector.s3_protection = True
detector.eks_audit_log_protection = (
True
if data_sources.get("Kubernetes", {})
.get("AuditLogs", {})
.get("Status", "DISABLED")
== "ENABLED"
else False
)
detector.ec2_malware_protection = (
True
if data_sources.get("MalwareProtection", {})
.get("ScanEc2InstanceWithFindings", {})
.get("EbsVolumes", {})
.get("Status", "DISABLED")
== "ENABLED"
else False
)
for feat in detector_info.get("Features", []):
if (
feat.get("Name", "") == "RDS_LOGIN_EVENTS"
and feat.get("Status", "DISABLED") == "ENABLED"
):
detector.rds_protection = True
elif (
feat.get("Name", "") == "LAMBDA_NETWORK_LOGS"
and feat.get("Status", "DISABLED") == "ENABLED"
):
detector.lambda_protection = True
except Exception as error:
logger.error(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
@@ -196,7 +214,7 @@ class Detector(BaseModel):
id: str
arn: str
region: str
enabled_in_account: bool = True
enabled_in_account: bool
status: bool = None
findings: list = []
member_accounts: list = []
@@ -204,3 +222,6 @@ class Detector(BaseModel):
tags: Optional[list] = []
s3_protection: bool = False
rds_protection: bool = False
eks_audit_log_protection: bool = False
lambda_protection: bool = False
ec2_malware_protection: bool = False
@@ -5,17 +5,23 @@ from prowler.providers.aws.services.iam.iam_client import iam_client
class iam_check_saml_providers_sts(Check):
def execute(self) -> Check_Report_AWS:
findings = []
if iam_client.saml_providers:
for provider in iam_client.saml_providers:
report = Check_Report_AWS(self.metadata())
provider_name = provider["Arn"].split("/")[1]
report.resource_id = provider_name
report.resource_arn = provider["Arn"]
report.region = iam_client.region
report.status = "PASS"
report.status_extended = (
f"SAML Provider {provider_name} has been found."
)
findings.append(report)
if not iam_client.saml_providers and iam_client.saml_providers is not None:
report = Check_Report_AWS(self.metadata())
report.resource_id = iam_client.audited_account
report.resource_arn = iam_client.audited_account_arn
report.region = iam_client.region
report.status = "FAIL"
report.status_extended = "No SAML Providers found."
findings.append(report)
for provider_arn, provider in iam_client.saml_providers.items():
report = Check_Report_AWS(self.metadata())
report.resource_id = provider.name
report.resource_arn = provider_arn
report.resource_tags = provider.tags
report.region = iam_client.region
report.status = "PASS"
report.status_extended = f"SAML Provider {provider.name} has been found."
findings.append(report)
return findings
@@ -13,6 +13,7 @@ class iam_no_expired_server_certificates_stored(Check):
report.region = iam_client.region
report.resource_id = certificate.id
report.resource_arn = certificate.arn
report.resource_tags = certificate.tags
expiration_days = (datetime.now(timezone.utc) - certificate.expiration).days
if expiration_days >= 0:
report.status = "FAIL"
@@ -16,6 +16,7 @@ class iam_policy_attached_only_to_group_or_roles(Check):
report.status_extended = f"User {user.name} has the policy {policy['PolicyName']} attached."
report.resource_id = f"{user.name}/{policy['PolicyName']}"
report.resource_arn = user.arn
report.resource_tags = user.tags
findings.append(report)
if user.inline_policies:
for policy in user.inline_policies:
@@ -25,6 +26,7 @@ class iam_policy_attached_only_to_group_or_roles(Check):
report.status_extended = f"User {user.name} has the inline policy {policy} attached."
report.resource_id = f"{user.name}/{policy}"
report.resource_arn = user.arn
report.resource_tags = user.tags
findings.append(report)
else:
@@ -32,6 +34,7 @@ class iam_policy_attached_only_to_group_or_roles(Check):
report.region = iam_client.region
report.resource_id = user.name
report.resource_arn = user.arn
report.resource_tags = user.tags
report.status = "PASS"
report.status_extended = (
f"User {user.name} has no inline or attached policies."
@@ -12,9 +12,15 @@ maximum_expiration_days = 90
class iam_rotate_access_key_90_days(Check):
def execute(self) -> Check_Report_AWS:
findings = []
response = iam_client.credential_report
for user in response:
for user in iam_client.credential_report:
# Search user in iam_client.users to get tags
user_tags = []
for iam_user in iam_client.users:
if iam_user.arn == user["arn"]:
user_tags = iam_user.tags
break
if (
user["access_key_1_last_rotated"] == "N/A"
and user["access_key_2_last_rotated"] == "N/A"
@@ -23,6 +29,7 @@ class iam_rotate_access_key_90_days(Check):
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "PASS"
report.status_extended = (
f"User {user['user']} does not have access keys."
@@ -44,6 +51,7 @@ class iam_rotate_access_key_90_days(Check):
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "FAIL"
report.status_extended = f"User {user['user']} has not rotated access key 1 in over 90 days ({access_key_1_last_rotated.days} days)."
findings.append(report)
@@ -60,6 +68,7 @@ class iam_rotate_access_key_90_days(Check):
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "FAIL"
report.status_extended = f"User {user['user']} has not rotated access key 2 in over 90 days ({access_key_2_last_rotated.days} days)."
findings.append(report)
@@ -69,6 +78,7 @@ class iam_rotate_access_key_90_days(Check):
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "PASS"
report.status_extended = f"User {user['user']} does not have access keys older than 90 days."
findings.append(report)
@@ -47,7 +47,6 @@ def is_service_role(role):
return False
################## IAM
class IAM(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
@@ -89,13 +88,21 @@ class IAM(AWSService):
self._list_inline_role_policies()
self.saml_providers = self._list_saml_providers()
self.server_certificates = self._list_server_certificates()
self._list_tags_for_resource()
self.access_keys_metadata = {}
self._get_access_keys_metadata()
self.last_accessed_services = {}
self._get_last_accessed_services()
self.user_temporary_credentials_usage = {}
self._get_user_temporary_credentials_usage()
# List missing tags
self.__threading_call__(self._list_tags, self.users)
self.__threading_call__(self._list_tags, self.roles)
self.__threading_call__(
self._list_tags,
[policy for policy in self.policies if policy.type == "Custom"],
)
self.__threading_call__(self._list_tags, self.server_certificates)
self.__threading_call__(self._list_tags, self.saml_providers.values())
def _get_client(self):
return self.client
@@ -733,17 +740,31 @@ class IAM(AWSService):
def _list_saml_providers(self):
logger.info("IAM - List SAML Providers...")
saml_providers = {}
try:
saml_providers = self.client.list_saml_providers()["SAMLProviderList"]
saml_providers_list = self.client.list_saml_providers()["SAMLProviderList"]
for provider in saml_providers_list:
if not self.audit_resources or (
is_resource_filtered(provider["Arn"], self.audit_resources)
):
saml_providers[provider["Arn"]] = SAMLProvider(
name=provider["Arn"].split("/")[-1], arn=provider["Arn"]
)
except ClientError as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
if error.response["Error"]["Code"] == "AccessDenied":
saml_providers = None
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
saml_providers = None
finally:
return saml_providers
def _list_server_certificates(self):
return saml_providers
def _list_server_certificates(self) -> list:
logger.info("IAM - List Server Certificates...")
try:
server_certificates = []
@@ -768,71 +789,30 @@ class IAM(AWSService):
finally:
return server_certificates
def _list_tags_for_resource(self):
def _list_tags(self, resource: any):
logger.info("IAM - List Tags...")
try:
if self.roles:
for role in self.roles:
try:
response = self.client.list_role_tags(RoleName=role.name)[
"Tags"
]
role.tags = response
except ClientError as error:
if error.response["Error"]["Code"] == "NoSuchEntity":
role.tags = []
else:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
try:
for user in self.users:
try:
response = self.client.list_user_tags(UserName=user.name)["Tags"]
user.tags = response
except ClientError as error:
if error.response["Error"]["Code"] == "NoSuchEntity":
user.tags = []
else:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
try:
for policy in self.policies:
try:
if policy.type != "Inline":
response = self.client.list_policy_tags(PolicyArn=policy.arn)[
"Tags"
]
policy.tags = response
except ClientError as error:
if error.response["Error"]["Code"] == "NoSuchEntity":
policy.tags = []
else:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
if isinstance(resource, Role):
resource.tags = self.client.list_role_tags(RoleName=resource.name).get(
"Tags", []
)
elif isinstance(resource, User):
resource.tags = self.client.list_user_tags(UserName=resource.name).get(
"Tags", []
)
elif isinstance(resource, Policy):
if resource.type == "Custom":
resource.tags = self.client.list_policy_tags(
PolicyArn=resource.arn
).get("Tags", [])
elif isinstance(resource, Certificate):
resource.tags = self.client.list_server_certificate_tags(
ServerCertificateName=resource.name
).get("Tags", [])
elif isinstance(resource, SAMLProvider):
resource.tags = self.client.list_saml_provider_tags(
SAMLProviderArn=resource.arn
).get("Tags", [])
except Exception as error:
logger.error(
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
@@ -952,7 +932,7 @@ class User(BaseModel):
console_access: Optional[bool]
attached_policies: list[dict] = []
inline_policies: list[str] = []
tags: Optional[list] = []
tags: Optional[list]
class Role(BaseModel):
@@ -962,7 +942,7 @@ class Role(BaseModel):
is_service_role: bool
attached_policies: list[dict] = []
inline_policies: list[str] = []
tags: Optional[list] = []
tags: Optional[list]
class Group(BaseModel):
@@ -991,6 +971,7 @@ class Certificate(BaseModel):
id: str
arn: str
expiration: datetime
tags: Optional[list]
class Policy(BaseModel):
@@ -1002,3 +983,9 @@ class Policy(BaseModel):
attached: bool
document: Optional[dict]
tags: Optional[list] = []
class SAMLProvider(BaseModel):
name: str
arn: str
tags: Optional[list]
@@ -14,6 +14,13 @@ class iam_user_accesskey_unused(Check):
)
findings = []
for user in iam_client.credential_report:
# Search user in iam_client.users to get tags
user_tags = []
for iam_user in iam_client.users:
if iam_user.arn == user["arn"]:
user_tags = iam_user.tags
break
if (
user["access_key_1_active"] != "true"
and user["access_key_2_active"] != "true"
@@ -22,6 +29,7 @@ class iam_user_accesskey_unused(Check):
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "PASS"
report.status_extended = (
f"User {user['user']} does not have access keys."
@@ -41,6 +49,7 @@ class iam_user_accesskey_unused(Check):
report.region = iam_client.region
report.resource_id = user["user"] + "/AccessKey1"
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "FAIL"
report.status_extended = f"User {user['user']} has not used access key 1 in the last {maximum_expiration_days} days ({access_key_1_last_used_date.days} days)."
findings.append(report)
@@ -56,6 +65,7 @@ class iam_user_accesskey_unused(Check):
report.region = iam_client.region
report.resource_id = user["user"] + "/AccessKey2"
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "FAIL"
report.status_extended = f"User {user['user']} has not used access key 2 in the last {maximum_expiration_days} days ({access_key_2_last_used_date.days} days)."
findings.append(report)
@@ -65,6 +75,7 @@ class iam_user_accesskey_unused(Check):
report.region = iam_client.region
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.resource_tags = user_tags
report.status = "PASS"
report.status_extended = f"User {user['user']} does not have unused access keys for {maximum_expiration_days} days."
findings.append(report)
@@ -13,6 +13,11 @@ class iam_user_mfa_enabled_console_access(Check):
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.region = iam_client.region
# Search user in iam_client.users to get tags
for iam_user in iam_client.users:
if iam_user.arn == user["arn"]:
report.resource_tags = iam_user.tags
break
# check if the user has password enabled
if user["password_enabled"] == "true":
if user["mfa_active"] == "false":
@@ -54,4 +54,9 @@ class iam_user_no_setup_initial_access_key(Check):
report.resource_arn = user["arn"]
report.status = status
report.status_extended = status_extended
# Search user in iam_client.users to get tags
for iam_user in iam_client.users:
if iam_user.arn == user["arn"]:
report.resource_tags = iam_user.tags
break
findings.append(report)
@@ -13,6 +13,11 @@ class iam_user_two_active_access_key(Check):
report.resource_id = user["user"]
report.resource_arn = user["arn"]
report.region = iam_client.region
# Search user in iam_client.users to get tags
for iam_user in iam_client.users:
if iam_user.arn == user["arn"]:
report.resource_tags = iam_user.tags
break
if (
user["access_key_1_active"] == "true"
and user["access_key_2_active"] == "true"
@@ -17,6 +17,11 @@ class iam_user_with_temporary_credentials(Check):
report.resource_id = user_name
report.resource_arn = user_arn
report.region = iam_client.region
# Search user in iam_client.users to get tags
for iam_user in iam_client.users:
if iam_user.arn == user_arn:
report.resource_tags = iam_user.tags
break
report.status = "PASS"
report.status_extended = f"User {user_name} doesn't have long lived credentials with access to other services than IAM or STS."
@@ -0,0 +1,4 @@
from prowler.providers.aws.services.kinesis.kinesis_service import Kinesis
from prowler.providers.common.provider import Provider
kinesis_client = Kinesis(Provider.get_global_provider())
@@ -0,0 +1,95 @@
from enum import Enum
from typing import Optional
from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
from prowler.providers.aws.lib.service.service import AWSService
class Kinesis(AWSService):
def __init__(self, provider):
# Call AWSService's __init__
super().__init__(__class__.__name__, provider)
self.streams = {}
self.__threading_call__(self._list_streams)
self.__threading_call__(self._describe_stream, self.streams.values())
self.__threading_call__(self._list_tags_for_stream, self.streams.values())
def _list_streams(self, regional_client):
logger.info("Kinesis - Listing Kinesis Streams...")
try:
list_streams_paginator = regional_client.get_paginator("list_streams")
for page in list_streams_paginator.paginate():
for stream in page["StreamSummaries"]:
arn = stream["StreamARN"]
if not self.audit_resources or (
is_resource_filtered(arn, self.audit_resources)
):
self.streams[arn] = Stream(
arn=arn,
name=stream["StreamName"],
region=regional_client.region,
status=StreamStatus(stream.get("StreamStatus", "ACTIVE")),
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _describe_stream(self, stream):
logger.info(f"Kinesis - Describing Stream {stream.name}...")
try:
stream_description = (
self.regional_clients[stream.region]
.describe_stream(StreamName=stream.name)
.get("StreamDescription", {})
)
stream.encrypted_at_rest = EncryptionType(
stream_description.get("EncryptionType", "NONE")
)
except Exception as error:
logger.error(
f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_tags_for_stream(self, stream):
logger.info(f"Kinesis - Listing tags for Stream {stream.name}...")
try:
stream.tags = (
self.regional_clients[stream.region]
.list_tags_for_stream(StreamName=stream.name)
.get("Tags", [])
)
except Exception as error:
logger.error(
f"{stream.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class EncryptionType(Enum):
"""Enum for Kinesis Stream Encryption Type"""
NONE = "NONE"
KMS = "KMS"
class StreamStatus(Enum):
"""Enum for Kinesis Stream Status"""
ACTIVE = "ACTIVE"
CREATING = "CREATING"
DELETING = "DELETING"
UPDATING = "UPDATING"
class Stream(BaseModel):
"""Model for Kinesis Stream"""
arn: str
region: str
name: str
status: StreamStatus
tags: Optional[list]
encrypted_at_rest: EncryptionType = EncryptionType.NONE
@@ -0,0 +1,32 @@
{
"Provider": "aws",
"CheckID": "kinesis_stream_encrypted_at_rest",
"CheckTitle": "Kinesis streams should be encrypted at rest.",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
],
"ServiceName": "kinesis",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:kinesis::account-id:stream/stream-name",
"Severity": "medium",
"ResourceType": "AwsKinesisStream",
"Description": "Ensure Kinesis streams use server-side encryption with AWS KMS keys for data protection.",
"Risk": "If Kinesis streams are not encrypted at rest, sensitive data stored in the stream could be exposed to unauthorized access or breaches. This could lead to potential data theft or misuse of unencrypted data.",
"RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html",
"Remediation": {
"Code": {
"CLI": "aws kinesis start-stream-encryption --stream-name <your-stream-name> --encryption-type KMS --key-id <your-kms-key-id>",
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_22/#cloudformation",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/kinesis-controls.html#kinesis-1",
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_22/#terraform"
},
"Recommendation": {
"Text": "Enable server-side encryption for Kinesis streams using AWS KMS keys to ensure that all data is encrypted before it is stored, protecting data at rest and reducing the risk of unauthorized access.",
"Url": "https://docs.aws.amazon.com/streams/latest/dev/getting-started-with-sse.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,28 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.kinesis.kinesis_client import kinesis_client
from prowler.providers.aws.services.kinesis.kinesis_service import EncryptionType
class kinesis_stream_encrypted_at_rest(Check):
def execute(self):
findings = []
for stream in kinesis_client.streams.values():
report = Check_Report_AWS(self.metadata())
report.region = stream.region
report.resource_id = stream.name
report.resource_arn = stream.arn
report.resource_tags = stream.tags
report.status = "FAIL"
report.status_extended = (
f"Kinesis Stream {stream.name} is not encrypted at rest."
)
if stream.encrypted_at_rest == EncryptionType.KMS:
report.status = "PASS"
report.status_extended = (
f"Kinesis Stream {stream.name} is encrypted at rest."
)
findings.append(report)
return findings
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "networkfirewall_logging_enabled",
"CheckTitle": "Ensure Network Firewall Logging is Enabled",
"CheckType": [
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53"
],
"ServiceName": "network-firewall",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name",
"Severity": "medium",
"ResourceType": "AwsNetworkFirewallFirewall",
"Description": "This control checks whether logging is enabled for an AWS Network Firewall firewall. The control fails if logging isn't enabled for at least one log type or if the logging destination doesn't exist.",
"Risk": "Failing to enable logging on an AWS Network Firewall can lead to a lack of visibility into network traffic, making it difficult to monitor and respond to security incidents effectively, which could jeopardize the security and integrity of your infrastructure.",
"RelatedUrl": "https://docs.aws.amazon.com/network-firewall/latest/developerguide/firewall-logging.html",
"Remediation": {
"Code": {
"CLI": "aws network-firewall update-logging-configuration --firewall-arn <firewall-arn> --logging-configuration <configuration>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/networkfirewall-controls.html#networkfirewall-2",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable logging for your AWS Network Firewall by updating its logging configuration to ensure comprehensive tracking of network traffic and facilitate better incident response and auditing capabilities.",
"Url": "https://docs.aws.amazon.com/network-firewall/latest/developerguide/firewall-update-logging-configuration.html"
}
},
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,31 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.networkfirewall.networkfirewall_client import (
networkfirewall_client,
)
class networkfirewall_logging_enabled(Check):
def execute(self):
findings = []
for arn, firewall in networkfirewall_client.network_firewalls.items():
report = Check_Report_AWS(self.metadata())
report.region = firewall.region
report.resource_id = firewall.name
report.resource_arn = arn
report.resource_tags = firewall.tags
report.status = "FAIL"
report.status_extended = (
f"Network Firewall {firewall.name} does not have logging enabled."
)
for configuration in firewall.logging_configuration:
if configuration.log_type or configuration.log_destination:
report.status = "PASS"
report.status_extended = (
f"Network Firewall {firewall.name} has logging enabled."
)
break
findings.append(report)
return findings

Some files were not shown because too many files have changed in this diff Show More