mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-19 02:32:54 +00:00
Compare commits
211 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac9bc2c916 | |||
| 16191a7b15 | |||
| 0c149461b3 | |||
| 3ee39cff2a | |||
| 41ba118cc4 | |||
| e0587fe0cf | |||
| 50481665ce | |||
| a49c744e08 | |||
| aa32634105 | |||
| b27898de1d | |||
| b703357027 | |||
| 27cd9b22df | |||
| 5bf85366e0 | |||
| 30bc971f4b | |||
| 3950d7eba8 | |||
| 2f8a3d2ef8 | |||
| 3b64bbd3a8 | |||
| 09d099891a | |||
| a6b10a8611 | |||
| c239ede3f9 | |||
| 66f2754017 | |||
| 9138ecdce9 | |||
| 2b66368cf2 | |||
| aa3425a7de | |||
| a31b15c26c | |||
| f2301d5ed6 | |||
| df10253056 | |||
| d5acdc766a | |||
| e389e0136f | |||
| 8bb3bd0dcb | |||
| 4d4bf3fa11 | |||
| e99c58405c | |||
| d437d1c4b8 | |||
| 2cf1f22235 | |||
| 2177704b4b | |||
| 2ffe7f3ef7 | |||
| 158263a8bf | |||
| 469986dd28 | |||
| ff101087bf | |||
| b2151e2e9c | |||
| 2c4244b1fb | |||
| 260cdf575a | |||
| ab4190c215 | |||
| 3ef1d41630 | |||
| 7f97b0a57f | |||
| 2c2dd82d0c | |||
| 2511df1732 | |||
| f955dd76d9 | |||
| a08cc769c8 | |||
| e7d719e514 | |||
| 2b51cf8fca | |||
| c5929efc99 | |||
| 77ac5e3b91 | |||
| 2da8f2b1eb | |||
| 38e024216c | |||
| 8e4847ec89 | |||
| c6d34e8089 | |||
| 880523076d | |||
| 3d2f1a3aa7 | |||
| c9ff96144d | |||
| 234f8c2958 | |||
| da87c0d81e | |||
| 7732ec7d34 | |||
| a1b9b2171f | |||
| 30e3fd9e46 | |||
| 3db541a42a | |||
| d5abe16180 | |||
| 564b18c388 | |||
| 13e40eb03e | |||
| b402ced402 | |||
| 6bbb9d04a6 | |||
| 6616657c91 | |||
| 853b833cfb | |||
| c047b29140 | |||
| c4a39662ae | |||
| 66e804f212 | |||
| 9d4fa55c13 | |||
| ff05ce4da1 | |||
| 0474c7995c | |||
| 1a679f371f | |||
| 05f7170add | |||
| 19acb873af | |||
| 0b566f9666 | |||
| 67bf89537a | |||
| d0681a9e20 | |||
| 31bff99b3d | |||
| 48c7e65a39 | |||
| 1b407639f0 | |||
| 4d7d5718d5 | |||
| 7955048e79 | |||
| 8e0b715f12 | |||
| 1d81261d97 | |||
| 114a3088a4 | |||
| bc8f3eba4d | |||
| 8e087196c9 | |||
| 744e7ff5ac | |||
| 90b84b57d3 | |||
| 0a2b7cf152 | |||
| ebbccd04f1 | |||
| 2b431fc79f | |||
| fe7c3e7548 | |||
| 0e5f929044 | |||
| 47a6e28d71 | |||
| de5742433b | |||
| 3fcccd0bcd | |||
| 00938cadb1 | |||
| 9fb26643ba | |||
| e4890f9d9d | |||
| 980b9b4770 | |||
| 348cea67c0 | |||
| f4d89066d9 | |||
| b26dc899be | |||
| 25327d618d | |||
| 3951295c0c | |||
| ff9c3b52d6 | |||
| af8c18eb4e | |||
| 6fbfcc7f5f | |||
| 7c7132f9c4 | |||
| 62e30f929c | |||
| ddaafd5876 | |||
| 1f43e6eff9 | |||
| aa118c05c5 | |||
| cca17b9378 | |||
| 14ed19e3a8 | |||
| 8caf8f794c | |||
| cba9ad61e4 | |||
| e64a0eff0f | |||
| 23c65b8fde | |||
| a7c93f3237 | |||
| 7b9402f3d0 | |||
| 4badcca4f8 | |||
| c6daa60f26 | |||
| f9aa2bb8be | |||
| 66ac395705 | |||
| 16a251254e | |||
| 751958907c | |||
| 60012ab19d | |||
| 65d7ba020b | |||
| 9456c6198a | |||
| 45ce1a0650 | |||
| 4c5db5295c | |||
| a2ad0cdf30 | |||
| 0c70a64e84 | |||
| 73c96f8346 | |||
| 0974c5f333 | |||
| 7db0746416 | |||
| 8f0bf5e896 | |||
| 57abe1c839 | |||
| 43183962ad | |||
| 87948b458e | |||
| ab5c3eb4f8 | |||
| 320a2a2c77 | |||
| dbc8e140e3 | |||
| 21ac395d4c | |||
| 8a8c2b5097 | |||
| 3bea772c6b | |||
| 34679c98d6 | |||
| 2b41445d57 | |||
| 796c87bc93 | |||
| a83e08aa9e | |||
| ae794c7c32 | |||
| edc78bfd6b | |||
| 9263adeb78 | |||
| bfdc87723b | |||
| 8d23e81b1c | |||
| f0cd924016 | |||
| c425e8249b | |||
| 1ece8bbcd6 | |||
| 5fb2d7c3ce | |||
| 64aebe84fe | |||
| de831b0abe | |||
| 68af4f6c73 | |||
| 52981b54b9 | |||
| a366594714 | |||
| 1fb36f316b | |||
| 30ffa8f00b | |||
| 5855918ade | |||
| f9005c875f | |||
| 91bf99ca45 | |||
| 8176063fef | |||
| 3373822240 | |||
| 7e16702b2f | |||
| f54b64f1f8 | |||
| 2c337ab3f6 | |||
| 5279d937d7 | |||
| 48c31a1616 | |||
| 917a2ad0fe | |||
| 8cfc4c56cf | |||
| 99e9e42a17 | |||
| 13c95ba131 | |||
| 600a8c7804 | |||
| 64fb52fc5e | |||
| 92b6e7230d | |||
| cc8bc781c1 | |||
| edbe463d73 | |||
| 8ace8c01cf | |||
| 8f37252676 | |||
| c0c59968bf | |||
| 9f5a909be3 | |||
| 90975bdadc | |||
| 7d1fad9eb7 | |||
| 983c79ad3b | |||
| 96e73fcb63 | |||
| 70a3736073 | |||
| 1e8e8ba65c | |||
| 359a1f2c8e | |||
| 2e4f8cbfc7 | |||
| 482aee0d9d | |||
| 0ae3374e81 | |||
| ddc088859e | |||
| 5e3da2d687 |
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ At the time of writing this documentation the available Azure Clouds from differ
|
||||
- AzureCloud
|
||||
- AzureChinaCloud
|
||||
- AzureUSGovernment
|
||||
- AzureGermanCloud
|
||||
|
||||
If you want to change the default one you must include the flag `--azure-region`, i.e.:
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Scan Inventory
|
||||
|
||||
The scan-inventory feature is a tool that generates a JSON report within the `/output/inventory/<provider>` directory and the scanned service. This feature allows you to perform a inventory of the resources existing in your provider that are scanned by Prowler.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the scan-inventory feature, run Prowler with the `--scan-inventory` option. For example:
|
||||
|
||||
```
|
||||
prowler <provider> --scan-inventory
|
||||
```
|
||||
|
||||
This will generate a JSON report within the `/output/inventory/<provider>` directory and the scanned service.
|
||||
|
||||
## Output Directory Contents
|
||||
|
||||
The contents of the `/output/<provider>` directory and the scanned service depend on the Prowler execution. This directory contains all the information gathered during scanning, including a JSON report containing all the gathered information.
|
||||
|
||||
## Limitations
|
||||
|
||||
The scan-inventory feature has some limitations. For example:
|
||||
|
||||
* It is only available for the AWS provider.
|
||||
* It only contains the information retrieved by Prowler during the execution.
|
||||
|
||||
## Example
|
||||
|
||||
Here's an example of how to use the scan-inventory feature and the contents of the `/output/inventory/<provider>` directory and the scanned service:
|
||||
|
||||
`prowler aws -s ec2 --scan-inventory`
|
||||
|
||||
```
|
||||
/output/inventory/aws directory
|
||||
|
|
||||
|-- ec2
|
||||
| |
|
||||
| |-- ec2_output.json
|
||||
```
|
||||
In this example, Prowler is run with the `-s ec2` and `--scan-inventory` options for the AWS provider. The `/output/inventory/aws` directory contains a JSON report showing all the information gathered during scanning.
|
||||
@@ -55,6 +55,7 @@ nav:
|
||||
- Dashboard: tutorials/dashboard.md
|
||||
- Fixer (remediations): tutorials/fixer.md
|
||||
- Quick Inventory: tutorials/quick-inventory.md
|
||||
- Scan Inventory: tutorials/scan-inventory.md
|
||||
- Slack Integration: tutorials/integrations.md
|
||||
- Configuration File: tutorials/configuration_file.md
|
||||
- Logging: tutorials/logging.md
|
||||
|
||||
Generated
+65
-57
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "about-time"
|
||||
@@ -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"
|
||||
|
||||
@@ -1414,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]
|
||||
@@ -2542,13 +2542,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.38"
|
||||
version = "9.5.39"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.5.38-py3-none-any.whl", hash = "sha256:d4779051d52ba9f1e7e344b34de95449c7c366c212b388e4a2db9a3db043c228"},
|
||||
{file = "mkdocs_material-9.5.38.tar.gz", hash = "sha256:1843c5171ad6b489550aeaf7358e5b7128cc03ddcf0fb4d91d19aa1e691a63b8"},
|
||||
{file = "mkdocs_material-9.5.39-py3-none-any.whl", hash = "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b"},
|
||||
{file = "mkdocs_material-9.5.39.tar.gz", hash = "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2598,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]
|
||||
@@ -3467,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"
|
||||
@@ -3543,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]
|
||||
@@ -5064,4 +5072,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9,<3.13"
|
||||
content-hash = "edaed8972a6e8dc5f6a5b58e4dd92df8a9985a03f0a45f086e5ae953e830d180"
|
||||
content-hash = "0b367fa80501022efe43dc1beaa7f3da278fb64ffaddece72a0e88b09a0e53a2"
|
||||
|
||||
@@ -73,6 +73,7 @@ from prowler.providers.aws.models import AWSOutputOptions
|
||||
from prowler.providers.azure.models import AzureOutputOptions
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
from prowler.providers.common.scan_inventory import run_prowler_scan_inventory
|
||||
from prowler.providers.gcp.models import GCPOutputOptions
|
||||
from prowler.providers.kubernetes.models import KubernetesOutputOptions
|
||||
|
||||
@@ -688,6 +689,11 @@ def prowler():
|
||||
if checks_folder:
|
||||
remove_custom_checks_module(checks_folder, provider)
|
||||
|
||||
# Run the quick inventory for the provider if available
|
||||
if hasattr(args, "scan_inventory") and args.scan_inventory:
|
||||
run_prowler_scan_inventory(checks_to_execute, args.provider)
|
||||
sys.exit()
|
||||
|
||||
# If there are failed findings exit code 3, except if -z is input
|
||||
if (
|
||||
not args.ignore_exit_code_3
|
||||
|
||||
@@ -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.2"
|
||||
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.
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"sa-east-1",
|
||||
@@ -1262,6 +1263,7 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
@@ -1272,6 +1274,7 @@
|
||||
"eu-west-3",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
@@ -6925,6 +6928,7 @@
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -7512,11 +7516,13 @@
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -7527,6 +7533,7 @@
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -7688,6 +7695,7 @@
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -10972,6 +10980,7 @@
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
|
||||
@@ -100,6 +100,13 @@ def init_parser(self):
|
||||
action="store_true",
|
||||
help="Run Prowler Quick Inventory. The inventory will be stored in an output csv by default",
|
||||
)
|
||||
# AWS Scan Inventory
|
||||
aws_scan_inventory_subparser = aws_parser.add_argument_group("Scan Inventory")
|
||||
aws_scan_inventory_subparser.add_argument(
|
||||
"--scan-inventory",
|
||||
action="store_true",
|
||||
help="Run Prowler Scan Inventory. The inventory will be stored in an output json file.",
|
||||
)
|
||||
# AWS Outputs
|
||||
aws_outputs_subparser = aws_parser.add_argument_group("AWS Outputs to S3")
|
||||
aws_outputs_bucket_parser = aws_outputs_subparser.add_mutually_exclusive_group()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from collections import deque
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
@@ -101,3 +104,47 @@ class AWSService:
|
||||
except Exception:
|
||||
# Handle exceptions if necessary
|
||||
pass # Replace 'pass' with any additional exception handling logic. Currently handled within the called function
|
||||
|
||||
def __to_dict__(self, seen=None) -> Dict[str, Any]:
|
||||
if seen is None:
|
||||
seen = set()
|
||||
|
||||
def convert_value(value):
|
||||
if isinstance(value, (AwsProvider,)):
|
||||
return {}
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat() # Convert datetime to ISO 8601 string
|
||||
elif isinstance(value, deque):
|
||||
return [convert_value(item) for item in value]
|
||||
elif isinstance(value, list):
|
||||
return [convert_value(item) for item in value]
|
||||
elif isinstance(value, tuple):
|
||||
return tuple(convert_value(item) for item in value)
|
||||
elif isinstance(value, dict):
|
||||
# Ensure keys are strings and values are processed
|
||||
return {
|
||||
convert_value(str(k)): convert_value(v) for k, v in value.items()
|
||||
}
|
||||
elif hasattr(value, "__dict__"):
|
||||
obj_id = id(value)
|
||||
if obj_id in seen:
|
||||
return None # Avoid infinite recursion
|
||||
seen.add(obj_id)
|
||||
return {key: convert_value(val) for key, val in value.__dict__.items()}
|
||||
else:
|
||||
return value # Handle basic types and non-serializable objects
|
||||
|
||||
return {
|
||||
key: convert_value(value)
|
||||
for key, value in self.__dict__.items()
|
||||
if key
|
||||
not in [
|
||||
"audit_config",
|
||||
"provider",
|
||||
"session",
|
||||
"regional_clients",
|
||||
"client",
|
||||
"thread_pool",
|
||||
"fixer_config",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "cloudfront_distributions_s3_origin_non_existent_bucket",
|
||||
"CheckTitle": "CloudFront distributions should not point to non-existent S3 origins without static website hosting.",
|
||||
"CheckTitle": "CloudFront distributions should not point to non-existent S3 origins.",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls"
|
||||
],
|
||||
@@ -10,7 +10,7 @@
|
||||
"ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsCloudFrontDistribution",
|
||||
"Description": "This control checks whether Amazon CloudFront distributions are pointing to non-existent Amazon S3 origins without static website hosting. The control fails if the origin is configured to point to a non-existent bucket.",
|
||||
"Description": "This control checks whether Amazon CloudFront distributions are pointing to non-existent Amazon S3 origins. The control fails if the origin is configured to point to a non-existent bucket.",
|
||||
"Risk": "Pointing a CloudFront distribution to a non-existent S3 bucket can allow malicious actors to create the bucket and potentially serve unauthorized content through your distribution, leading to security and integrity issues.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/whitepapers/latest/secure-content-delivery-amazon-cloudfront/s3-origin-with-cloudfront.html",
|
||||
"Remediation": {
|
||||
|
||||
+3
-4
@@ -19,10 +19,9 @@ class cloudfront_distributions_s3_origin_non_existent_bucket(Check):
|
||||
non_existent_buckets = []
|
||||
|
||||
for origin in distribution.origins:
|
||||
if origin.s3_origin_config:
|
||||
bucket_name = origin.domain_name.split(".")[0]
|
||||
if not s3_client._head_bucket(bucket_name):
|
||||
non_existent_buckets.append(bucket_name)
|
||||
bucket_name = origin.domain_name.split(".")[0]
|
||||
if not s3_client._head_bucket(bucket_name):
|
||||
non_existent_buckets.append(bucket_name)
|
||||
|
||||
if non_existent_buckets:
|
||||
report.status = "FAIL"
|
||||
|
||||
+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
|
||||
@@ -119,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
|
||||
@@ -157,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(
|
||||
@@ -193,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] = []
|
||||
|
||||
@@ -202,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()
|
||||
@@ -53,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}"
|
||||
@@ -207,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__
|
||||
@@ -69,9 +68,7 @@ class IAM(AWSService):
|
||||
self._list_attached_role_policies()
|
||||
self._list_mfa_devices()
|
||||
self.password_policy = self._get_password_policy()
|
||||
support_policy_arn = (
|
||||
f"arn:{self.audited_partition}:iam::aws:policy/AWSSupportAccess"
|
||||
)
|
||||
support_policy_arn = f"arn:{self.audited_partition}:iam::aws:policy/aws-service-role/AWSSupportServiceRolePolicy"
|
||||
self.entities_role_attached_to_support_policy = (
|
||||
self._list_entities_role_for_policy(support_policy_arn)
|
||||
)
|
||||
@@ -91,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
|
||||
@@ -735,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 = []
|
||||
@@ -770,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}"
|
||||
@@ -954,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):
|
||||
@@ -964,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):
|
||||
@@ -993,6 +971,7 @@ class Certificate(BaseModel):
|
||||
id: str
|
||||
arn: str
|
||||
expiration: datetime
|
||||
tags: Optional[list]
|
||||
|
||||
|
||||
class Policy(BaseModel):
|
||||
@@ -1004,3 +983,9 @@ class Policy(BaseModel):
|
||||
attached: bool
|
||||
document: Optional[dict]
|
||||
tags: Optional[list] = []
|
||||
|
||||
|
||||
class SAMLProvider(BaseModel):
|
||||
name: str
|
||||
arn: str
|
||||
tags: Optional[list]
|
||||
|
||||
+4
-8
@@ -8,17 +8,13 @@ class iam_support_role_created(Check):
|
||||
if iam_client.entities_role_attached_to_support_policy is not None:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = iam_client.region
|
||||
report.resource_id = iam_client.audited_account
|
||||
report.resource_arn = (
|
||||
f"arn:{iam_client.audited_partition}:iam::aws:policy/AWSSupportAccess"
|
||||
)
|
||||
report.resource_id = "AWSSupportServiceRolePolicy"
|
||||
report.resource_arn = f"arn:{iam_client.audited_partition}:iam::aws:policy/aws-service-role/AWSSupportServiceRolePolicy"
|
||||
if iam_client.entities_role_attached_to_support_policy:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"AWS Support Access policy attached to role {iam_client.entities_role_attached_to_support_policy[0]['RoleName']}."
|
||||
report.status_extended = f"Support policy attached to role {iam_client.entities_role_attached_to_support_policy[0]['RoleName']}."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"AWS Support Access policy is not attached to any role."
|
||||
)
|
||||
report.status_extended = "Support policy is not attached to any role."
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
+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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user