mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 12:31:54 +00:00
Merge branch 'master' into prowler-inventory
This commit is contained in:
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
-10
@@ -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)
|
||||
|
||||
+13
-10
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+1
@@ -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."
|
||||
|
||||
+1
@@ -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:
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+17
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+18
-18
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+32
@@ -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": ""
|
||||
}
|
||||
+67
@@ -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
|
||||
+34
@@ -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": ""
|
||||
}
|
||||
+27
@@ -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] = []
|
||||
|
||||
+34
@@ -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": ""
|
||||
}
|
||||
+38
@@ -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
|
||||
+34
@@ -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": ""
|
||||
}
|
||||
+25
@@ -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
|
||||
+34
@@ -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": ""
|
||||
}
|
||||
+28
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+34
@@ -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": ""
|
||||
}
|
||||
+28
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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] = []
|
||||
+1
-1
@@ -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}."
|
||||
|
||||
+32
@@ -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": ""
|
||||
}
|
||||
+28
@@ -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]
|
||||
|
||||
+32
@@ -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": []
|
||||
}
|
||||
+21
@@ -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
|
||||
+34
@@ -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": ""
|
||||
}
|
||||
+21
@@ -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
|
||||
+32
@@ -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": []
|
||||
}
|
||||
+21
@@ -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
|
||||
+1
-1
@@ -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": ""
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+18
-12
@@ -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
|
||||
|
||||
+1
@@ -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"
|
||||
|
||||
+3
@@ -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
-2
@@ -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]
|
||||
|
||||
+11
@@ -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)
|
||||
|
||||
+5
@@ -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":
|
||||
|
||||
+5
@@ -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)
|
||||
|
||||
+5
@@ -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"
|
||||
|
||||
+5
@@ -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
|
||||
+32
@@ -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": ""
|
||||
}
|
||||
+28
@@ -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
|
||||
+34
@@ -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": ""
|
||||
}
|
||||
+31
@@ -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
Reference in New Issue
Block a user