mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-23 06:12:00 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
000b5ec3d5 |
@@ -2,18 +2,11 @@
|
||||
|
||||
Please include relevant motivation and context for this PR.
|
||||
|
||||
If fixes an issue please add it with `Fix #XXXX`
|
||||
|
||||
### Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change.
|
||||
|
||||
### Checklist
|
||||
|
||||
- Are there new checks included in this PR? Yes / No
|
||||
- If so, do we need to update permissions for the provider? Please review this carefully.
|
||||
- [ ] Review if the code is being covered by tests.
|
||||
- [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
|
||||
|
||||
### License
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
prowler_version_major: ${{ steps.get-prowler-version.outputs.PROWLER_VERSION_MAJOR }}
|
||||
prowler_version: ${{ steps.get-prowler-version.outputs.PROWLER_VERSION }}
|
||||
prowler_version: ${{ steps.update-prowler-version.outputs.PROWLER_VERSION }}
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
|
||||
@@ -65,8 +65,6 @@ jobs:
|
||||
id: get-prowler-version
|
||||
run: |
|
||||
PROWLER_VERSION="$(poetry version -s 2>/dev/null)"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# Store prowler version major just for the release
|
||||
PROWLER_VERSION_MAJOR="${PROWLER_VERSION%%.*}"
|
||||
@@ -91,6 +89,15 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Update Prowler version (release)
|
||||
id: update-prowler-version
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
PROWLER_VERSION="${{ github.event.release.tag_name }}"
|
||||
poetry version "${PROWLER_VERSION}"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
|
||||
@@ -13,10 +13,10 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "v3", "v4.*" ]
|
||||
branches: [ "master", "v3" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master", "v3", "v4.*" ]
|
||||
branches: [ "master", "v3" ]
|
||||
schedule:
|
||||
- cron: '00 12 * * *'
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@3.80.4
|
||||
uses: trufflesecurity/trufflehog@v3.80.2
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -5,12 +5,10 @@ on:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -8,6 +8,8 @@ env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
PYTHON_VERSION: 3.11
|
||||
CACHE: "poetry"
|
||||
# TODO: create a bot user for this kind of tasks, like prowler-bot
|
||||
GIT_COMMITTER_EMAIL: "sergio@prowler.com"
|
||||
|
||||
jobs:
|
||||
release-prowler-job:
|
||||
@@ -38,6 +40,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pipx install poetry
|
||||
pipx inject poetry poetry-bumpversion
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
@@ -45,6 +48,34 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: ${{ env.CACHE }}
|
||||
|
||||
- name: Update Poetry and config version
|
||||
run: |
|
||||
poetry version ${{ env.RELEASE_TAG }}
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Push updated version to the release tag
|
||||
run: |
|
||||
# Configure Git
|
||||
git config user.name "github-actions"
|
||||
git config user.email "${{ env.GIT_COMMITTER_EMAIL }}"
|
||||
|
||||
# Add the files with the version changed
|
||||
git add prowler/config/config.py pyproject.toml
|
||||
git commit -m "chore(release): ${{ env.RELEASE_TAG }}" --no-verify -S
|
||||
|
||||
# Replace the tag with the version updated
|
||||
git tag -fa ${{ env.RELEASE_TAG }} -m "chore(release): ${{ env.RELEASE_TAG }}" --sign
|
||||
|
||||
# Push the tag
|
||||
git push -f origin ${{ env.RELEASE_TAG }}
|
||||
|
||||
- name: Build Prowler package
|
||||
run: |
|
||||
poetry build
|
||||
|
||||
@@ -127,7 +127,6 @@ aws:
|
||||
]
|
||||
|
||||
# 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.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -10,7 +10,7 @@ Execute Prowler in verbose mode (like in Version 2):
|
||||
prowler <provider> --verbose
|
||||
```
|
||||
## Filter findings by status
|
||||
Prowler can filter the findings by their status, so you can see only in the CLI and in the reports the findings with a specific status:
|
||||
Prowler can filter the findings by their status:
|
||||
```console
|
||||
prowler <provider> --status [PASS, FAIL, MANUAL]
|
||||
```
|
||||
|
||||
+84
-134
@@ -7,147 +7,97 @@ Mutelist option works along with other options and will modify the output in the
|
||||
- CSV: `muted` is `True`. The field `status` will keep the original status, `MANUAL`, `PASS` or `FAIL`, of the finding.
|
||||
|
||||
|
||||
## How the Mutelist Works
|
||||
|
||||
The **Mutelist** uses both "AND" and "OR" logic to determine which resources, checks, regions, and tags should be muted. For each check, the Mutelist evaluates whether the account, region, and resource match the specified criteria using "AND" logic. If tags are specified, the Mutelist can apply either "AND" or "OR" logic.
|
||||
|
||||
If any of the criteria do not match, the check is not muted.
|
||||
|
||||
???+ note
|
||||
Remember that mutelist can be used with regular expressions.
|
||||
|
||||
## Mutelist Specification
|
||||
|
||||
???+ note
|
||||
- For Azure provider, the Account ID is the Subscription Name and the Region is the Location.
|
||||
- For GCP provider, the Account ID is the Project ID and the Region is the Zone.
|
||||
- For Kubernetes provider, the Account ID is the Cluster Name and the Region is the Namespace.
|
||||
|
||||
The Mutelist file uses the [YAML](https://en.wikipedia.org/wiki/YAML) format with the following syntax:
|
||||
|
||||
```yaml
|
||||
### Account, Check and/or Region can be * to apply for all the cases.
|
||||
### Resources and tags are lists that can have either Regex or Keywords.
|
||||
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
|
||||
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
|
||||
### For each check you can except Accounts, Regions, Resources and/or Tags.
|
||||
########################### MUTELIST EXAMPLE ###########################
|
||||
Mutelist:
|
||||
Accounts:
|
||||
"123456789012":
|
||||
Checks:
|
||||
"iam_user_hardware_mfa_enabled":
|
||||
Regions:
|
||||
- "us-east-1"
|
||||
Resources:
|
||||
- "user-1" # Will ignore user-1 in check iam_user_hardware_mfa_enabled
|
||||
- "user-2" # Will ignore user-2 in check iam_user_hardware_mfa_enabled
|
||||
"ec2_*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*" # Will ignore every EC2 check in every account and region
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and
|
||||
- "project=test|project=stage" # either of ('project=test' OR project=stage) in account 123456789012 and every region
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "test=test"
|
||||
- "project=test" # This will mute every resource containing the string "test" and BOTH tags at the same time.
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "test"
|
||||
Tags: # This will mute every resource containing the string "test" and the ones that contain EITHER the `test=test` OR `project=test` OR `project=dev`
|
||||
- "test=test|project=(test|dev)"
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "test=test" # This will mute every resource containing the string "test" and the tags `test=test` and either `project=test` OR `project=stage` in every account and region.
|
||||
- "project=test|project=stage"
|
||||
|
||||
"*":
|
||||
Checks:
|
||||
"s3_bucket_object_versioning":
|
||||
Regions:
|
||||
- "eu-west-1"
|
||||
- "us-east-1"
|
||||
Resources:
|
||||
- "ci-logs" # Will ignore bucket "ci-logs" AND ALSO bucket "ci-logs-replica" in specified check and regions
|
||||
- "logs" # Will ignore EVERY BUCKET containing the string "logs" in specified check and regions
|
||||
- ".+-logs" # Will ignore all buckets containing the terms ci-logs, qa-logs, etc. in specified check and regions
|
||||
"ecs_task_definitions_no_environment_secrets":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Exceptions:
|
||||
Accounts:
|
||||
- "0123456789012"
|
||||
Regions:
|
||||
- "eu-west-1"
|
||||
- "eu-south-2" # Will ignore every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Tags:
|
||||
- "environment=dev" # Will ignore every resource containing the tag 'environment=dev' in every account and region
|
||||
|
||||
"123456789012":
|
||||
Checks:
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Exceptions:
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod
|
||||
```
|
||||
|
||||
### Account, Check, Region, Resource, and Tag
|
||||
|
||||
| Field | Description | Logic |
|
||||
|----------|----------|----------|
|
||||
| `<account_id>` | Use `*` to apply the mutelist to all accounts. | `ANDed` |
|
||||
| `<check_name>` | The name of the Prowler check. Use `*` to apply the mutelist to all checks. | `ANDed` |
|
||||
| `<region>` | The region identifier. Use `*` to apply the mutelist to all regions. | `ANDed` |
|
||||
| `<resource>` | The resource identifier. Use `*` to apply the mutelist to all resources. | `ANDed` |
|
||||
| `<tag>` | The tag value. | `ORed` |
|
||||
|
||||
|
||||
## How to Use the Mutelist
|
||||
|
||||
To use the Mutelist, you need to specify the path to the Mutelist YAML file using the `-w` or `--mutelist-file` option when running Prowler:
|
||||
|
||||
You can use `-w`/`--mutelist-file` with the path of your mutelist yaml file:
|
||||
```
|
||||
prowler <provider> -w mutelist.yaml
|
||||
```
|
||||
|
||||
Replace `<provider>` with the appropriate provider name.
|
||||
## Mutelist YAML File Syntax
|
||||
|
||||
## Considerations
|
||||
???+ note
|
||||
For Azure provider, the Account ID is the Subscription Name and the Region is the Location.
|
||||
|
||||
- The Mutelist can be used in combination with other Prowler options, such as the `--service` or `--checks` option, to further customize the scanning process.
|
||||
- Make sure to review and update the Mutelist regularly to ensure it reflects the desired exclusions and remains up to date with your infrastructure.
|
||||
???+ note
|
||||
For GCP provider, the Account ID is the Project ID and the Region is the Zone.
|
||||
|
||||
???+ note
|
||||
For Kubernetes provider, the Account ID is the Cluster Name and the Region is the Namespace.
|
||||
|
||||
The Mutelist file is a YAML file with the following syntax:
|
||||
|
||||
```yaml
|
||||
### Account, Check and/or Region can be * to apply for all the cases.
|
||||
### Resources and tags are lists that can have either Regex or Keywords.
|
||||
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
|
||||
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
|
||||
### For each check you can except Accounts, Regions, Resources and/or Tags.
|
||||
########################### MUTELIST EXAMPLE ###########################
|
||||
Mutelist:
|
||||
Accounts:
|
||||
"123456789012":
|
||||
Checks:
|
||||
"iam_user_hardware_mfa_enabled":
|
||||
Regions:
|
||||
- "us-east-1"
|
||||
Resources:
|
||||
- "user-1" # Will ignore user-1 in check iam_user_hardware_mfa_enabled
|
||||
- "user-2" # Will ignore user-2 in check iam_user_hardware_mfa_enabled
|
||||
"ec2_*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*" # Will ignore every EC2 check in every account and region
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and
|
||||
- "project=test|project=stage" # either of ('project=test' OR project=stage) in account 123456789012 and every region
|
||||
|
||||
"*":
|
||||
Checks:
|
||||
"s3_bucket_object_versioning":
|
||||
Regions:
|
||||
- "eu-west-1"
|
||||
- "us-east-1"
|
||||
Resources:
|
||||
- "ci-logs" # Will ignore bucket "ci-logs" AND ALSO bucket "ci-logs-replica" in specified check and regions
|
||||
- "logs" # Will ignore EVERY BUCKET containing the string "logs" in specified check and regions
|
||||
- ".+-logs" # Will ignore all buckets containing the terms ci-logs, qa-logs, etc. in specified check and regions
|
||||
"ecs_task_definitions_no_environment_secrets":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Exceptions:
|
||||
Accounts:
|
||||
- "0123456789012"
|
||||
Regions:
|
||||
- "eu-west-1"
|
||||
- "eu-south-2" # Will ignore every resource in check ecs_task_definitions_no_environment_secrets except the ones in account 0123456789012 located in eu-south-2 or eu-west-1
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Tags:
|
||||
- "environment=dev" # Will ignore every resource containing the tag 'environment=dev' in every account and region
|
||||
|
||||
"123456789012":
|
||||
Checks:
|
||||
"*":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Exceptions:
|
||||
Resources:
|
||||
- "test"
|
||||
Tags:
|
||||
- "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod
|
||||
```
|
||||
|
||||
## AWS Mutelist
|
||||
### Mute specific AWS regions
|
||||
|
||||
@@ -36,11 +36,10 @@ If EBS default encyption is not enabled, sensitive information at rest is not pr
|
||||
|
||||
- `ec2_ebs_default_encryption`
|
||||
|
||||
If your Security groups are not properly configured the attack surface is increased, nonetheless, Prowler will detect those security groups that are being used (they are attached) to only notify those that are being used. This logic applies to the 15 checks related to open ports in security groups, the check for the default security group and for the security groups that allow ingress and egress traffic.
|
||||
If your Security groups are not properly configured the attack surface is increased, nonetheless, Prowler will detect those security groups that are being used (they are attached) to only notify those that are being used. This logic applies to the 15 checks related to open ports in security groups and the check for the default security group.
|
||||
|
||||
- `ec2_securitygroup_allow_ingress_from_internet_to_port_X` (15 checks)
|
||||
- `ec2_securitygroup_default_restrict_traffic`
|
||||
- `ec2_securitygroup_allow_wide_open_public_ipv4`
|
||||
|
||||
Prowler will also check for used Network ACLs to only alerts those with open ports that are being used.
|
||||
|
||||
|
||||
@@ -58,28 +58,22 @@ Resources:
|
||||
- 'account:Get*'
|
||||
- 'appstream:Describe*'
|
||||
- 'appstream:List*'
|
||||
- 'backup:List*'
|
||||
- 'cloudtrail:GetInsightSelectors'
|
||||
- 'codeartifact:List*'
|
||||
- 'codebuild:BatchGet*'
|
||||
- 'cognito-idp:GetUserPoolMfaConfig'
|
||||
- 'dlm:Get*'
|
||||
- 'drs:Describe*'
|
||||
- 'ds:Get*'
|
||||
- 'ds:Describe*'
|
||||
- 'ds:List*'
|
||||
- 'dynamodb:GetResourcePolicy'
|
||||
- 'ec2:GetEbsEncryptionByDefault'
|
||||
- 'ec2:GetSnapshotBlockPublicAccessState'
|
||||
- 'ec2:GetInstanceMetadataDefaults'
|
||||
- 'ecr:Describe*'
|
||||
- 'ecr:GetRegistryScanningConfiguration'
|
||||
- 'elasticfilesystem:DescribeBackupPolicy'
|
||||
- 'glue:GetConnections'
|
||||
- 'glue:GetSecurityConfiguration*'
|
||||
- 'glue:SearchTables'
|
||||
- 'lambda:GetFunction*'
|
||||
- 'logs:FilterLogEvents'
|
||||
- 'lightsail:GetRelationalDatabases'
|
||||
- 'macie2:GetMacieSession'
|
||||
- 's3:GetAccountPublicAccessBlock'
|
||||
@@ -88,10 +82,8 @@ Resources:
|
||||
- 'securityhub:BatchImportFindings'
|
||||
- 'securityhub:GetFindings'
|
||||
- 'ssm:GetDocument'
|
||||
- 'ssm-incidents:List*'
|
||||
- 'support:Describe*'
|
||||
- 'tag:GetTagKeys'
|
||||
- 'wellarchitected:List*'
|
||||
Resource: '*'
|
||||
- PolicyName: ProwlerScanRoleAdditionalViewPrivilegesApiGateway
|
||||
PolicyDocument:
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"ds:List*",
|
||||
"dynamodb:GetResourcePolicy",
|
||||
"ec2:GetEbsEncryptionByDefault",
|
||||
"ec2:GetSnapshotBlockPublicAccessState",
|
||||
"ec2:GetInstanceMetadataDefaults",
|
||||
"ecr:Describe*",
|
||||
"ecr:GetRegistryScanningConfiguration",
|
||||
|
||||
Generated
+11
-11
@@ -712,17 +712,17 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.34.151"
|
||||
version = "1.34.149"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "boto3-1.34.151-py3-none-any.whl", hash = "sha256:35bc76faacf1667d3fbb66c1966acf2230ef26206557efc26d9d9d79337bef43"},
|
||||
{file = "boto3-1.34.151.tar.gz", hash = "sha256:30498a76b6f651ee2af7ae8edc1704379279ab8b91f1a8dd1f4ddf51259b0bc2"},
|
||||
{file = "boto3-1.34.149-py3-none-any.whl", hash = "sha256:11edeeacdd517bda3b7615b754d8440820cdc9ddd66794cc995a9693ddeaa3be"},
|
||||
{file = "boto3-1.34.149.tar.gz", hash = "sha256:f4e6489ba9dc7fb37d53e0e82dbc97f2cb0a4969ef3970e2c88b8f94023ae81a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.34.151,<1.35.0"
|
||||
botocore = ">=1.34.149,<1.35.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.10.0,<0.11.0"
|
||||
|
||||
@@ -731,13 +731,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.34.151"
|
||||
version = "1.34.150"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "botocore-1.34.151-py3-none-any.whl", hash = "sha256:9018680d7d4a8060c26d127ceec5ab5b270879f423ea39b863d8a46f3e34c404"},
|
||||
{file = "botocore-1.34.151.tar.gz", hash = "sha256:0d0968e427a94378f295b49d59170dad539938487ec948de3d030f06092ec6dc"},
|
||||
{file = "botocore-1.34.150-py3-none-any.whl", hash = "sha256:b988d47f4d502df85befce11a48002421e4e6ea4289997b5e0261bac5fa76ce6"},
|
||||
{file = "botocore-1.34.150.tar.gz", hash = "sha256:4d23387e0f076d87b637a2a35c0ff2b8daca16eace36b63ce27f65630c6b375a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1586,13 +1586,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-python-client"
|
||||
version = "2.139.0"
|
||||
version = "2.138.0"
|
||||
description = "Google API Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google_api_python_client-2.139.0-py2.py3-none-any.whl", hash = "sha256:1850a92505d91a82e2ca1635ab2b8dff179f4b67082c2651e1db332e8039840c"},
|
||||
{file = "google_api_python_client-2.139.0.tar.gz", hash = "sha256:ed4bc3abe2c060a87412465b4e8254620bbbc548eefc5388e2c5ff912d36a68b"},
|
||||
{file = "google_api_python_client-2.138.0-py2.py3-none-any.whl", hash = "sha256:1dd279124e4e77cbda4769ffb4abe7e7c32528ef1e18739320fef2a07b750764"},
|
||||
{file = "google_api_python_client-2.138.0.tar.gz", hash = "sha256:31080fbf0e64687876135cc23d1bec1ca3b80d7702177dd17b04131ea889eb70"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4898,4 +4898,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9,<3.13"
|
||||
content-hash = "324ea427d651cea1513f4a7be9b86f420eb75efcd4e54e7021835e517cd81525"
|
||||
content-hash = "97181474bd8e13193f35529d5a173633b0c14079676c10536839912a136a95e3"
|
||||
|
||||
@@ -3,5 +3,7 @@ import sys
|
||||
|
||||
from prowler.__main__ import prowler
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
sys.exit(prowler())
|
||||
|
||||
+56
-7
@@ -5,6 +5,15 @@ import sys
|
||||
from os import environ
|
||||
|
||||
from colorama import Fore, Style
|
||||
from prowler.providers.aws.services.ec2.ec2_service import PaginatedDict, PaginatedList
|
||||
import pdb
|
||||
import psutil
|
||||
import os
|
||||
|
||||
def check_memory_usage():
|
||||
process = psutil.Process(os.getpid())
|
||||
memory_info = process.memory_info()
|
||||
return memory_info.rss # Resident Set Size: memory in bytes
|
||||
|
||||
from prowler.config.config import (
|
||||
csv_file_suffix,
|
||||
@@ -70,9 +79,18 @@ from prowler.providers.aws.lib.s3.s3 import S3
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
from memory_profiler import profile
|
||||
|
||||
from pympler import asizeof
|
||||
from pympler import tracker
|
||||
from pympler import muppy
|
||||
from pympler import summary
|
||||
import objgraph
|
||||
|
||||
from memory_profiler import profile
|
||||
|
||||
def prowler():
|
||||
#tr = tracker.SummaryTracker()
|
||||
# Parse Arguments
|
||||
# Refactor(CLI)
|
||||
parser = ProwlerArgumentParser()
|
||||
@@ -178,7 +196,9 @@ def prowler():
|
||||
categories,
|
||||
provider,
|
||||
)
|
||||
|
||||
#pdb.set_trace() # Break
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at checks_to_execute: {memory_usage / (1024 * 1024)} MB")
|
||||
# if --list-checks-json, dump a json file and exit
|
||||
if args.list_checks_json:
|
||||
print(list_checks_json(provider, sorted(checks_to_execute)))
|
||||
@@ -193,6 +213,10 @@ def prowler():
|
||||
Provider.set_global_provider(args)
|
||||
global_provider = Provider.get_global_provider()
|
||||
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at global_provider = Provider. __main__.py:217 : {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
|
||||
# Print Provider Credentials
|
||||
if not args.only_logs:
|
||||
global_provider.print_credentials()
|
||||
@@ -224,8 +248,7 @@ def prowler():
|
||||
# Once the provider is set and we have the eventual checks based on the resource identifier,
|
||||
# it is time to check what Prowler's checks are going to be executed
|
||||
checks_from_resources = global_provider.get_checks_to_execute_by_audit_resources()
|
||||
# Intersect checks from resources with checks to execute so we only run the checks that apply to the resources with the specified ARNs or tags
|
||||
if getattr(args, "resource_arn", None) or getattr(args, "resource_tag", None):
|
||||
if checks_from_resources:
|
||||
checks_to_execute = checks_to_execute.intersection(checks_from_resources)
|
||||
|
||||
# Sort final check list
|
||||
@@ -243,7 +266,11 @@ def prowler():
|
||||
sys.exit()
|
||||
|
||||
# Execute checks
|
||||
findings = []
|
||||
paginated = 0
|
||||
if paginated:
|
||||
findings = PaginatedList()
|
||||
else:
|
||||
findings = []
|
||||
|
||||
if len(checks_to_execute):
|
||||
findings = execute_checks(
|
||||
@@ -256,7 +283,9 @@ def prowler():
|
||||
logger.error(
|
||||
"There are no checks to execute. Please, check your input arguments"
|
||||
)
|
||||
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at execute_checks __main__.py:284 {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
# Prowler Fixer
|
||||
if global_provider.output_options.fixer:
|
||||
print(f"{Style.BRIGHT}\nRunning Prowler Fixer, please wait...{Style.RESET_ALL}")
|
||||
@@ -308,6 +337,11 @@ def prowler():
|
||||
]
|
||||
|
||||
generated_outputs = {"regular": [], "compliance": []}
|
||||
logger.debug("Output generated")
|
||||
|
||||
#pdb.set_trace() # Break
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at findings_output: {memory_usage / (1024 * 1024)} MB")
|
||||
|
||||
if args.output_formats:
|
||||
for mode in args.output_formats:
|
||||
@@ -324,7 +358,9 @@ def prowler():
|
||||
generated_outputs["regular"].append(csv_output)
|
||||
# Write CSV Finding Object to file
|
||||
csv_output.batch_write_data_to_file()
|
||||
|
||||
#pdb.set_trace() # Break
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at csv_output_batch_write_data: {memory_usage / (1024 * 1024)} MB")
|
||||
if mode == "json-asff":
|
||||
asff_output = ASFF(
|
||||
findings=finding_outputs,
|
||||
@@ -353,7 +389,9 @@ def prowler():
|
||||
html_output.batch_write_data_to_file(
|
||||
provider=global_provider, stats=stats
|
||||
)
|
||||
|
||||
#pdb.set_trace() # Break
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at html_output_batch_write: {memory_usage / (1024 * 1024)} MB")
|
||||
# Compliance Frameworks
|
||||
input_compliance_frameworks = set(
|
||||
global_provider.output_options.output_modes
|
||||
@@ -594,6 +632,7 @@ def prowler():
|
||||
aws_partition=global_provider.identity.partition,
|
||||
aws_session=global_provider.session.current_session,
|
||||
findings=asff_output.data,
|
||||
status=global_provider.output_options.status,
|
||||
send_only_fails=global_provider.output_options.send_sh_only_fails,
|
||||
aws_security_hub_available_regions=security_hub_regions,
|
||||
)
|
||||
@@ -647,12 +686,22 @@ def prowler():
|
||||
print(
|
||||
f"\nDetailed compliance results are in {Fore.YELLOW}{global_provider.output_options.output_directory}/compliance/{Style.RESET_ALL}\n"
|
||||
)
|
||||
# Print the memory usage of the largest objects
|
||||
#all_objects = muppy.get_objects()
|
||||
#sum1 = summary.summarize(all_objects)
|
||||
#summary.print_(sum1)
|
||||
#objgraph.show_most_common_types(limit=20)
|
||||
#objgraph.show_growth()
|
||||
|
||||
|
||||
# If custom checks were passed, remove the modules
|
||||
if checks_folder:
|
||||
remove_custom_checks_module(checks_folder, provider)
|
||||
|
||||
# If there are failed findings exit code 3, except if -z is input
|
||||
#pdb.set_trace() # Break
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at ending: {memory_usage / (1024 * 1024)} MB")
|
||||
if (
|
||||
not args.ignore_exit_code_3
|
||||
and stats["total_fail"] > 0
|
||||
|
||||
@@ -3044,7 +3044,7 @@
|
||||
"Id": "9.4",
|
||||
"Description": "Ensure that Register with Entra ID is enabled on App Service",
|
||||
"Checks": [
|
||||
""
|
||||
"app_client_certificates_on"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -3066,7 +3066,7 @@
|
||||
"Id": "9.5",
|
||||
"Description": "Ensure That 'PHP version' is the Latest, If Used to Run the Web App",
|
||||
"Checks": [
|
||||
"app_ensure_php_version_is_latest"
|
||||
"app_register_with_identity"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -3088,7 +3088,7 @@
|
||||
"Id": "9.6",
|
||||
"Description": "Ensure that 'Python version' is the Latest Stable Version, if Used to Run the Web App",
|
||||
"Checks": [
|
||||
"app_ensure_python_version_is_latest"
|
||||
"app_ensure_php_version_is_latest"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -3110,7 +3110,7 @@
|
||||
"Id": "9.7",
|
||||
"Description": "Ensure that 'Java version' is the latest, if used to run the Web App",
|
||||
"Checks": [
|
||||
"app_ensure_java_version_is_latest"
|
||||
"app_ensure_python_version_is_latest"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -3132,7 +3132,7 @@
|
||||
"Id": "9.8",
|
||||
"Description": "Ensure that 'HTTP Version' is the Latest, if Used to Run the Web App",
|
||||
"Checks": [
|
||||
"app_ensure_using_http20"
|
||||
"app_ensure_java_version_is_latest"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -3154,7 +3154,7 @@
|
||||
"Id": "9.9",
|
||||
"Description": "Ensure FTP deployments are Disabled",
|
||||
"Checks": [
|
||||
"app_ftp_deployment_disabled"
|
||||
"app_ensure_using_http20"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -3176,7 +3176,7 @@
|
||||
"Id": "9.10",
|
||||
"Description": "Ensure Azure Key Vaults are Used to Store Secrets",
|
||||
"Checks": [
|
||||
""
|
||||
"app_ftp_deployment_disabled"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -3213,6 +3213,66 @@
|
||||
"References": "https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-lock-resources:https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-subscription-governance#azure-resource-locks:https://docs.microsoft.com/en-us/azure/governance/blueprints/concepts/resource-locking:https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-asset-management#am-4-limit-access-to-asset-management"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "9.10",
|
||||
"Description": "Ensure FTP deployments are Disabled",
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "9. AppService",
|
||||
"Profile": "Level 1",
|
||||
"AssessmentStatus": "Automated",
|
||||
"Description": "By default, Azure Functions, Web, and API Services can be deployed over FTP. If FTP is required for an essential deployment workflow, FTPS should be required for FTP login for all App Service Apps and Functions.",
|
||||
"RationaleStatement": "Azure FTP deployment endpoints are public. An attacker listening to traffic on a wifi network used by a remote employee or a corporate network could see login traffic in clear-text which would then grant them full control of the code base of the app or service. This finding is more severe if User Credentials for deployment are set at the subscription level rather than using the default Application Credentials which are unique per App.",
|
||||
"ImpactStatement": "Any deployment workflows that rely on FTP or FTPs rather than the WebDeploy or HTTPs endpoints may be affected.",
|
||||
"RemediationProcedure": "**From Azure Portal** 1. Go to the Azure Portal 2. Select `App Services` 3. Click on an app 4. Select `Settings` and then `Configuration` 5. Under `General Settings`, for the `Platform Settings`, the `FTP state` should be set to `Disabled` or `FTPS Only` **From Azure CLI** For each out of compliance application, run the following choosing either 'disabled' or 'FtpsOnly' as appropriate: ``` az webapp config set --resource-group <resource group name> --name <app name> --ftps-state [disabled|FtpsOnly] ``` **From PowerShell** For each out of compliance application, run the following: ``` Set-AzWebApp -ResourceGroupName <resource group name> -Name <app name> -FtpsState <Disabled or FtpsOnly> ```",
|
||||
"AuditProcedure": "**From Azure Portal** 1. Go to the Azure Portal 2. Select `App Services` 3. Click on an app 4. Select `Settings` and then `Configuration` 5. Under `General Settings`, for the `Platform Settings`, the `FTP state` should not be set to `All allowed` **From Azure CLI** List webapps to obtain the ids. ``` az webapp list ``` List the publish profiles to obtain the username, password and ftp server url. ``` az webapp deployment list-publishing-profiles --ids <ids> { publishUrl: <URL_FOR_WEB_APP>, userName: <USER_NAME>, userPWD: <USER_PASSWORD>, } ``` **From PowerShell** List all Web Apps: ``` Get-AzWebApp ``` For each app: ``` Get-AzWebApp -ResourceGroupName <resource group name> -Name <app name> | Select-Object -ExpandProperty SiteConfig ``` In the output, look for the value of **FtpsState**. If its value is **AllAllowed** the setting is out of compliance. Any other value is considered in compliance with this check.",
|
||||
"AdditionalInformation": "",
|
||||
"DefaultValue": "[Azure Web Service Deploy via FTP](https://docs.microsoft.com/en-us/azure/app-service/deploy-ftp):[Azure Web Service Deployment](https://docs.microsoft.com/en-us/azure/app-service/overview-security):https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-4-encrypt-sensitive-information-in-transit:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-posture-vulnerability-management#pv-7-rapidly-and-automatically-remediate-software-vulnerabilities",
|
||||
"References": "TA0008, T1570, M1031"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "9.11",
|
||||
"Description": "Ensure Azure Key Vaults are Used to Store Secrets",
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "9. AppService",
|
||||
"Profile": "Level 2",
|
||||
"AssessmentStatus": "Manual",
|
||||
"Description": "Azure Key Vault will store multiple types of sensitive information such as encryption keys, certificate thumbprints, and Managed Identity Credentials. Access to these 'Secrets' can be controlled through granular permissions.",
|
||||
"RationaleStatement": "The credentials given to an application have permissions to create, delete, or modify data stored within the systems they access. If these credentials are stored within the application itself, anyone with access to the application or a copy of the code has access to them. Storing within Azure Key Vault as secrets increases security by controlling access. This also allows for updates of the credentials without redeploying the entire application.",
|
||||
"ImpactStatement": "Integrating references to secrets within the key vault are required to be specifically integrated within the application code. This will require additional configuration to be made during the writing of an application, or refactoring of an already written one. There are also additional costs that are charged per 10000 requests to the Key Vault.",
|
||||
"RemediationProcedure": "Remediation has 2 steps 1. Setup the Key Vault 2. Setup the App Service to use the Key Vault **Step 1: Set up the Key Vault** **From Azure CLI** ``` az keyvault create --name <name> --resource-group <myResourceGroup> --location myLocation ``` **From Powershell** ``` New-AzKeyvault -name <name> -ResourceGroupName <myResourceGroup> -Location <myLocation> ``` **Step 2: Set up the App Service to use the Key Vault** Sample JSON Template for App Service Configuration: ``` { //... resources: [ { type: Microsoft.Storage/storageAccounts, name: [variables('storageAccountName')], //... }, { type: Microsoft.Insights/components, name: [variables('appInsightsName')], //... }, { type: Microsoft.Web/sites, name: [variables('functionAppName')], identity: { type: SystemAssigned }, //... resources: [ { type: config, name: appsettings, //... dependsOn: [ [resourceId('Microsoft.Web/sites', variables('functionAppName'))], [resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))], [resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('storageConnectionStringName'))], [resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('appInsightsKeyName'))] ], properties: { AzureWebJobsStorage: [concat('@Microsoft.KeyVault(SecretUri=', reference(variables('storageConnectionStringResourceId')).secretUriWithVersion, ')')], WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: [concat('@Microsoft.KeyVault(SecretUri=', reference(variables('storageConnectionStringResourceId')).secretUriWithVersion, ')')], APPINSIGHTS_INSTRUMENTATIONKEY: [concat('@Microsoft.KeyVault(SecretUri=', reference(variables('appInsightsKeyResourceId')).secretUriWithVersion, ')')], WEBSITE_ENABLE_SYNC_UPDATE_SITE: true //... } }, { type: sourcecontrols, name: web, //... dependsOn: [ [resourceId('Microsoft.Web/sites', variables('functionAppName'))], [resourceId('Microsoft.Web/sites/config', variables('functionAppName'), 'appsettings')] ], } ] }, { type: Microsoft.KeyVault/vaults, name: [variables('keyVaultName')], //... dependsOn: [ [resourceId('Microsoft.Web/sites', variables('functionAppName'))] ], properties: { //... accessPolicies: [ { tenantId: [reference(concat('Microsoft.Web/sites/', variables('functionAppName'), '/providers/Microsoft.ManagedIdentity/Identities/default'), '2015-08-31-PREVIEW').tenantId], objectId: [reference(concat('Microsoft.Web/sites/', variables('functionAppName'), '/providers/Microsoft.ManagedIdentity/Identities/default'), '2015-08-31-PREVIEW').principalId], permissions: { secrets: [ get ] } } ] }, resources: [ { type: secrets, name: [variables('storageConnectionStringName')], //... dependsOn: [ [resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))], [resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))] ], properties: { value: [concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountResourceId'),'2015-05-01-preview').key1)] } }, { type: secrets, name: [variables('appInsightsKeyName')], //... dependsOn: [ [resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))], [resourceId('Microsoft.Insights/components', variables('appInsightsName'))] ], properties: { value: [reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey] } } ] } ] } ```",
|
||||
"AuditProcedure": "**From Azure Portal** 1. Login to Azure Portal 2. In the expandable menu on the left go to `Key Vaults` 3. View the Key Vaults listed. **From Azure CLI** To list key vaults within a subscription run the following command: ``` Get-AzKeyVault ``` To list the secrets within these key vaults run the following command: ``` Get-AzKeyVaultSecret [-VaultName] <vault name> ``` **From Powershell** To list key vaults within a subscription run the following command: ``` Get-AzKeyVault ``` To list all secrets in a key vault run the following command: ``` Get-AzKeyVaultSecret -VaultName '<vaultName' ```",
|
||||
"AdditionalInformation": "",
|
||||
"DefaultValue": "https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-identity-management#im-2-manage-application-identities-securely-and-automatically:https://docs.microsoft.com/en-us/cli/azure/keyvault?view=azure-cli-latest:https://docs.microsoft.com/en-us/cli/azure/keyvault?view=azure-cli-latest",
|
||||
"References": "TA0006, T1552, M1041"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "10.1",
|
||||
"Description": "Ensure that Resource Locks are set for Mission-Critical Azure Resources",
|
||||
"Checks": [],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "10. Miscellaneous",
|
||||
"Profile": "Level 2",
|
||||
"AssessmentStatus": "Manual",
|
||||
"Description": "Resource Manager Locks provide a way for administrators to lock down Azure resources to prevent deletion of, or modifications to, a resource. These locks sit outside of the Role Based Access Controls (RBAC) hierarchy and, when applied, will place restrictions on the resource for all users. These locks are very useful when there is an important resource in a subscription that users should not be able to delete or change. Locks can help prevent accidental and malicious changes or deletion.",
|
||||
"RationaleStatement": "As an administrator, it may be necessary to lock a subscription, resource group, or resource to prevent other users in the organization from accidentally deleting or modifying critical resources. The lock level can be set to to `CanNotDelete` or `ReadOnly` to achieve this purpose. - `CanNotDelete` means authorized users can still read and modify a resource, but they cannot delete the resource. - `ReadOnly` means authorized users can read a resource, but they cannot delete or update the resource. Applying this lock is similar to restricting all authorized users to the permissions granted by the Reader role.",
|
||||
"ImpactStatement": "There can be unintended outcomes of locking a resource. Applying a lock to a parent service will cause it to be inherited by all resources within. Conversely, applying a lock to a resource may not apply to connected storage, leaving it unlocked. Please see the documentation for further information.",
|
||||
"RemediationProcedure": "**From Azure Portal** 1. Navigate to the specific Azure Resource or Resource Group 2. For each mission critical resource, click on `Locks` 3. Click `Add` 4. Give the lock a name and a description, then select the type, `Read-only` or `Delete` as appropriate 5. Click OK **From Azure CLI** To lock a resource, provide the name of the resource, its resource type, and its resource group name. ``` az lock create --name <LockName> --lock-type <CanNotDelete/Read-only> --resource-group <resourceGroupName> --resource-name <resourceName> --resource-type <resourceType> ``` **From Powershell** ``` Get-AzResourceLock -ResourceName <Resource Name> -ResourceType <Resource Type> -ResourceGroupName <Resource Group Name> -Locktype <CanNotDelete/Read-only> ```",
|
||||
"AuditProcedure": "**From Azure Portal** 1. Navigate to the specific Azure Resource or Resource Group 2. Click on `Locks` 3. Ensure the lock is defined with name and description, with type `Read-only` or `Delete` as appropriate. **From Azure CLI** Review the list of all locks set currently: ``` az lock list --resource-group <resourcegroupname> --resource-name <resourcename> --namespace <Namespace> --resource-type <type> --parent ``` **From Powershell** Run the following command to list all resources. ``` Get-AzResource ``` For each resource, run the following command to check for Resource Locks. ``` Get-AzResourceLock -ResourceName <Resource Name> -ResourceType <Resource Type> -ResourceGroupName <Resource Group Name> ``` Review the output of the `Properties` setting. Compliant settings will have the `CanNotDelete` or `ReadOnly` value.",
|
||||
"AdditionalInformation": "",
|
||||
"DefaultValue": "https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-lock-resources:https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-subscription-governance#azure-resource-locks:https://docs.microsoft.com/en-us/azure/governance/blueprints/concepts/resource-locking:https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-asset-management#am-4-limit-access-to-asset-management",
|
||||
"References": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,11 +19,8 @@ Mutelist:
|
||||
- "StackSet-AWSControlTowerSecurityResources-*"
|
||||
- "StackSet-AWSControlTowerLoggingResources-*"
|
||||
- "StackSet-AWSControlTowerExecutionRole-*"
|
||||
- "AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER*"
|
||||
- "AWSControlTowerBP-BASELINE-CONFIG-MASTER*"
|
||||
- "StackSet-AWSControlTower*"
|
||||
- "CLOUDTRAIL-ENABLED-ON-SHARED-ACCOUNTS-*"
|
||||
- "AFT-Backend*"
|
||||
- "AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER"
|
||||
- "AWSControlTowerBP-BASELINE-CONFIG-MASTER"
|
||||
"cloudtrail_*":
|
||||
Regions:
|
||||
- "*"
|
||||
|
||||
@@ -5,13 +5,12 @@ from os import getcwd
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
from packaging import version
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "4.3.7"
|
||||
prowler_version = "4.3.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"
|
||||
@@ -87,7 +86,7 @@ def check_current_version():
|
||||
"https://api.github.com/repos/prowler-cloud/prowler/tags", timeout=1
|
||||
)
|
||||
latest_version = release_response.json()[0]["name"]
|
||||
if version.parse(latest_version) > version.parse(prowler_version):
|
||||
if latest_version != prowler_version:
|
||||
return f"{prowler_version_string} (latest is {latest_version}, upgrade for the latest features)"
|
||||
else:
|
||||
return (
|
||||
|
||||
@@ -43,7 +43,6 @@ aws:
|
||||
]
|
||||
|
||||
# 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.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
+15
-11
@@ -9,6 +9,7 @@ import traceback
|
||||
from pkgutil import walk_packages
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from memory_profiler import profile
|
||||
|
||||
from alive_progress import alive_bar
|
||||
from colorama import Fore, Style
|
||||
@@ -23,6 +24,14 @@ from prowler.lib.outputs.outputs import report
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
import pdb
|
||||
import psutil
|
||||
import os
|
||||
|
||||
def check_memory_usage():
|
||||
process = psutil.Process(os.getpid())
|
||||
memory_info = process.memory_info()
|
||||
return memory_info.rss # Resident Set Size: memory in bytes
|
||||
|
||||
# Load all checks metadata
|
||||
def bulk_load_checks_metadata(provider: str) -> dict:
|
||||
@@ -433,10 +442,13 @@ def list_modules(provider: str, service: str):
|
||||
|
||||
|
||||
# Import an input check using its path
|
||||
def import_check(check_path: str) -> ModuleType:
|
||||
lib = importlib.import_module(f"{check_path}")
|
||||
return lib
|
||||
|
||||
def import_check(check_path: str) -> ModuleType:
|
||||
|
||||
print(f"{check_memory_usage() / (1024 * 1024)} MB : Memory usage before import {check_path}")
|
||||
lib = importlib.import_module(f"{check_path}")
|
||||
print(f"{check_memory_usage() / (1024 * 1024)} MB : Memory usage after import {check_path}")
|
||||
return lib
|
||||
|
||||
def run_check(check: Check, verbose: bool = False, only_logs: bool = False) -> list:
|
||||
"""
|
||||
@@ -706,14 +718,6 @@ def execute(
|
||||
check_class, verbose, global_provider.output_options.only_logs
|
||||
)
|
||||
|
||||
# Exclude findings per status
|
||||
if global_provider.output_options.status:
|
||||
check_findings = [
|
||||
finding
|
||||
for finding in check_findings
|
||||
if finding.status in global_provider.output_options.status
|
||||
]
|
||||
|
||||
# Update Audit Status
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
|
||||
@@ -94,6 +94,7 @@ class Check(ABC, Check_Metadata_Model):
|
||||
)
|
||||
# Store it to validate them with Pydantic
|
||||
data = Check_Metadata_Model.parse_file(metadata_file).dict()
|
||||
# data = {}
|
||||
# Calls parents init function
|
||||
super().__init__(**data)
|
||||
# TODO: verify that the CheckID is the same as the filename and classname
|
||||
|
||||
@@ -8,26 +8,6 @@ from prowler.lib.mutelist.models import mutelist_schema
|
||||
|
||||
|
||||
class Mutelist(ABC):
|
||||
"""
|
||||
Abstract base class for managing a mutelist.
|
||||
|
||||
Attributes:
|
||||
_mutelist (dict): Dictionary containing information about muted checks for different accounts.
|
||||
_mutelist_file_path (str): Path to the mutelist file.
|
||||
MUTELIST_KEY (str): Key used to access the mutelist in the mutelist file.
|
||||
|
||||
Methods:
|
||||
__init__: Initializes a Mutelist object.
|
||||
mutelist: Property that returns the mutelist dictionary.
|
||||
mutelist_file_path: Property that returns the mutelist file path.
|
||||
is_finding_muted: Abstract method to check if a finding is muted.
|
||||
get_mutelist_file_from_local_file: Retrieves the mutelist file from a local file.
|
||||
validate_mutelist: Validates the mutelist against a schema.
|
||||
is_muted: Checks if a finding is muted for the audited account, check, region, resource, and tags.
|
||||
is_muted_in_check: Checks if a check is muted.
|
||||
is_excepted: Checks if the account, region, resource, and tags are excepted based on the exceptions.
|
||||
"""
|
||||
|
||||
_mutelist: dict = {}
|
||||
_mutelist_file_path: str = None
|
||||
|
||||
@@ -88,25 +68,6 @@ class Mutelist(ABC):
|
||||
"""
|
||||
Check if the provided finding is muted for the audited account, check, region, resource and tags.
|
||||
|
||||
The Mutelist works in a way that each field is ANDed, so if a check is muted for an account, region, resource and tags, it will be muted.
|
||||
The exceptions are ORed, so if a check is excepted for an account, region, resource or tags, it will not be muted.
|
||||
The only particularity is the tags, which are ORed.
|
||||
|
||||
So, for the following Mutelist:
|
||||
```
|
||||
Mutelist:
|
||||
Accounts:
|
||||
'*':
|
||||
Checks:
|
||||
ec2_instance_detailed_monitoring_enabled:
|
||||
Regions: ['*']
|
||||
Resources:
|
||||
- 'i-123456789'
|
||||
Tags:
|
||||
- 'Name=AdminInstance | Environment=Prod'
|
||||
```
|
||||
The check `ec2_instance_detailed_monitoring_enabled` will be muted for all accounts and regions and for the resource_id 'i-123456789' with at least one of the tags 'Name=AdminInstance' or 'Environment=Prod'.
|
||||
|
||||
Args:
|
||||
mutelist (dict): Dictionary containing information about muted checks for different accounts.
|
||||
audited_account (str): The account being audited.
|
||||
@@ -211,9 +172,7 @@ class Mutelist(ABC):
|
||||
muted_in_resource = self.is_item_matched(
|
||||
muted_resources, finding_resource
|
||||
)
|
||||
muted_in_tags = self.is_item_matched(
|
||||
muted_tags, finding_tags, tag=True
|
||||
)
|
||||
muted_in_tags = self.is_item_matched(muted_tags, finding_tags)
|
||||
|
||||
# For a finding to be muted requires the following set to True:
|
||||
# - muted_in_check -> True
|
||||
@@ -281,9 +240,7 @@ class Mutelist(ABC):
|
||||
)
|
||||
|
||||
excepted_tags = exceptions.get("Tags", [])
|
||||
is_tag_excepted = self.is_item_matched(
|
||||
excepted_tags, finding_tags, tag=True
|
||||
)
|
||||
is_tag_excepted = self.is_item_matched(excepted_tags, finding_tags)
|
||||
|
||||
if (
|
||||
not is_account_excepted
|
||||
@@ -307,7 +264,7 @@ class Mutelist(ABC):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_item_matched(matched_items, finding_items, tag=False) -> bool:
|
||||
def is_item_matched(matched_items, finding_items):
|
||||
"""
|
||||
Check if any of the items in matched_items are present in finding_items.
|
||||
|
||||
@@ -321,19 +278,12 @@ class Mutelist(ABC):
|
||||
try:
|
||||
is_item_matched = False
|
||||
if matched_items and (finding_items or finding_items == ""):
|
||||
if tag:
|
||||
is_item_matched = True
|
||||
for item in matched_items:
|
||||
if item.startswith("*"):
|
||||
item = ".*" + item[1:]
|
||||
if tag:
|
||||
if not re.search(item, finding_items):
|
||||
is_item_matched = False
|
||||
break
|
||||
else:
|
||||
if re.search(item, finding_items):
|
||||
is_item_matched = True
|
||||
break
|
||||
if re.match(item, finding_items):
|
||||
is_item_matched = True
|
||||
break
|
||||
return is_item_matched
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
|
||||
@@ -25,6 +25,7 @@ class ASFF(Output):
|
||||
- transform(findings: list[Finding]) -> None: Transforms a list of findings into ASFF format.
|
||||
- batch_write_data_to_file() -> None: Writes the findings data to a file in JSON ASFF format.
|
||||
- generate_status(status: str, muted: bool = False) -> str: Generates the ASFF status based on the provided status and muted flag.
|
||||
- format_resource_tags(tags: str) -> dict: Transforms a string of tags into a dictionary format.
|
||||
|
||||
References:
|
||||
- AWS Security Hub API Reference: https://docs.aws.amazon.com/securityhub/1.0/APIReference/API_Compliance.html
|
||||
@@ -61,6 +62,7 @@ class ASFF(Output):
|
||||
if finding.status == "MANUAL":
|
||||
continue
|
||||
timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
resource_tags = ASFF.format_resource_tags(finding.resource_tags)
|
||||
|
||||
associated_standards, compliance_summary = ASFF.format_compliance(
|
||||
finding.compliance
|
||||
@@ -68,6 +70,7 @@ class ASFF(Output):
|
||||
|
||||
# Ensures finding_status matches allowed values in ASFF
|
||||
finding_status = ASFF.generate_status(finding.status, finding.muted)
|
||||
|
||||
self._data.append(
|
||||
AWSSecurityFindingFormat(
|
||||
# The following line cannot be changed because it is the format we use to generate unique findings for AWS Security Hub
|
||||
@@ -89,18 +92,14 @@ class ASFF(Output):
|
||||
CreatedAt=timestamp,
|
||||
Severity=Severity(Label=finding.severity.value),
|
||||
Title=finding.check_title,
|
||||
Description=(
|
||||
(finding.status_extended[:1000] + "...")
|
||||
if len(finding.status_extended) > 1000
|
||||
else finding.status_extended
|
||||
),
|
||||
Description=finding.description,
|
||||
Resources=[
|
||||
Resource(
|
||||
Id=finding.resource_uid,
|
||||
Type=finding.resource_type,
|
||||
Partition=finding.partition,
|
||||
Region=finding.region,
|
||||
Tags=finding.resource_tags,
|
||||
Tags=resource_tags,
|
||||
)
|
||||
],
|
||||
Compliance=Compliance(
|
||||
@@ -196,6 +195,42 @@ class ASFF(Output):
|
||||
|
||||
return json_asff_status
|
||||
|
||||
@staticmethod
|
||||
def format_resource_tags(tags: str) -> dict:
|
||||
"""
|
||||
Transforms a string of tags into a dictionary format.
|
||||
|
||||
Parameters:
|
||||
- tags (str): A string containing tags separated by ' | ' and key-value pairs separated by '='.
|
||||
|
||||
Returns:
|
||||
- dict: A dictionary where keys are tag names and values are tag values.
|
||||
|
||||
Notes:
|
||||
- If the input string is empty or None, it returns None.
|
||||
- Each tag in the input string should be in the format 'key=value'.
|
||||
- If the input string is not formatted correctly, it logs an error and returns None.
|
||||
"""
|
||||
try:
|
||||
tags_dict = None
|
||||
if tags:
|
||||
tags = tags.split(" | ")
|
||||
tags_dict = {}
|
||||
for tag in tags:
|
||||
value = tag.split("=")
|
||||
tags_dict[value[0]] = value[1]
|
||||
return tags_dict
|
||||
except IndexError as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
except AttributeError as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def format_compliance(compliance: dict) -> tuple[list[dict], list[str]]:
|
||||
"""
|
||||
@@ -281,12 +316,6 @@ class Resource(BaseModel):
|
||||
Region: str
|
||||
Tags: Optional[dict]
|
||||
|
||||
@validator("Tags", pre=True, always=True)
|
||||
def tags_cannot_be_empty_dict(tags):
|
||||
if not tags:
|
||||
return None
|
||||
return tags
|
||||
|
||||
|
||||
class Compliance(BaseModel):
|
||||
"""
|
||||
|
||||
@@ -3,7 +3,7 @@ from csv import DictWriter
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.lib.outputs.output import Output
|
||||
from prowler.lib.outputs.utils import unroll_dict
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_list
|
||||
|
||||
|
||||
class CSV(Output):
|
||||
@@ -17,13 +17,8 @@ class CSV(Output):
|
||||
try:
|
||||
for finding in findings:
|
||||
finding_dict = {k.upper(): v for k, v in finding.dict().items()}
|
||||
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
|
||||
finding_dict["COMPLIANCE"] = unroll_dict(
|
||||
finding.compliance, separator=": "
|
||||
)
|
||||
finding_dict["ACCOUNT_TAGS"] = unroll_dict(
|
||||
finding.account_tags, separator=":"
|
||||
)
|
||||
finding_dict["COMPLIANCE"] = unroll_dict(finding.compliance)
|
||||
finding_dict["ACCOUNT_TAGS"] = unroll_list(finding.account_tags)
|
||||
finding_dict["STATUS"] = finding.status.value
|
||||
finding_dict["SEVERITY"] = finding.severity.value
|
||||
self._data.append(finding_dict)
|
||||
|
||||
@@ -50,7 +50,7 @@ class Finding(BaseModel):
|
||||
# Optional since it depends on permissions
|
||||
account_organization_name: Optional[str]
|
||||
# Optional since it depends on permissions
|
||||
account_tags: dict = {}
|
||||
account_tags: Optional[list[str]]
|
||||
finding_uid: str
|
||||
provider: str
|
||||
check_id: str
|
||||
@@ -66,7 +66,7 @@ class Finding(BaseModel):
|
||||
resource_uid: str
|
||||
resource_name: str
|
||||
resource_details: str
|
||||
resource_tags: dict = {}
|
||||
resource_tags: str
|
||||
# Only present for AWS and Azure
|
||||
partition: Optional[str]
|
||||
region: str
|
||||
@@ -86,6 +86,7 @@ class Finding(BaseModel):
|
||||
notes: str
|
||||
prowler_version: str = prowler_version
|
||||
|
||||
|
||||
@classmethod
|
||||
def generate_output(
|
||||
cls, provider: Provider, check_output: Check_Report
|
||||
@@ -186,7 +187,7 @@ class Finding(BaseModel):
|
||||
f"prowler-{provider.type}-{check_output.check_metadata.CheckID}-{output_data['account_uid']}-"
|
||||
f"{output_data['region']}-{output_data['resource_name']}"
|
||||
)
|
||||
|
||||
logger.debug("Generating finding: = %s", output_data["finding_uid"])
|
||||
return cls(**output_data)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
|
||||
@@ -45,11 +45,11 @@ class HTML(Output):
|
||||
<td>{finding.check_id.replace("_", "<wbr />_")}</td>
|
||||
<td>{finding.check_title}</td>
|
||||
<td>{finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "<wbr />_")}</td>
|
||||
<td>{parse_html_string(unroll_dict(finding.resource_tags))}</td>
|
||||
<td>{parse_html_string(finding.resource_tags)}</td>
|
||||
<td>{finding.status_extended.replace("<", "<").replace(">", ">").replace("_", "<wbr />_")}</td>
|
||||
<td><p class="show-read-more">{html.escape(finding.risk)}</p></td>
|
||||
<td><p class="show-read-more">{html.escape(finding.remediation_recommendation_text)}</p> <a class="read-more" href="{finding.remediation_recommendation_url}"><i class="fas fa-external-link-alt"></i></a></td>
|
||||
<td><p class="show-read-more">{parse_html_string(unroll_dict(finding.compliance, separator=": "))}</p></td>
|
||||
<td><p class="show-read-more">{parse_html_string(unroll_dict(finding.compliance))}</p></td>
|
||||
</tr>
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ from py_ocsf_models.objects.resource_details import ResourceDetails
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
from prowler.lib.outputs.output import Output
|
||||
from prowler.lib.outputs.utils import unroll_dict_to_list
|
||||
|
||||
|
||||
class OCSF(Output):
|
||||
@@ -98,7 +97,12 @@ class OCSF(Output):
|
||||
risk_details=finding.risk,
|
||||
resources=[
|
||||
ResourceDetails(
|
||||
labels=unroll_dict_to_list(finding.resource_tags),
|
||||
# TODO: Check labels for other providers
|
||||
labels=(
|
||||
finding.resource_tags.split(",")
|
||||
if finding.resource_tags
|
||||
else []
|
||||
),
|
||||
name=finding.resource_name,
|
||||
uid=finding.resource_uid,
|
||||
group=Group(name=finding.service_name),
|
||||
@@ -144,7 +148,7 @@ class OCSF(Output):
|
||||
type_id=cloud_account_type.value,
|
||||
type=cloud_account_type.name,
|
||||
uid=finding.account_uid,
|
||||
labels=unroll_dict_to_list(finding.account_tags),
|
||||
labels=finding.account_tags,
|
||||
),
|
||||
org=Organization(
|
||||
uid=finding.account_organization_uid,
|
||||
|
||||
@@ -25,7 +25,6 @@ def stdout_report(finding, color, verbose, status, fix):
|
||||
)
|
||||
|
||||
|
||||
# TODO: Only pass check_findings, provider.output_options and provider.type
|
||||
def report(check_findings, provider):
|
||||
try:
|
||||
output_options = provider.output_options
|
||||
|
||||
+49
-153
@@ -1,24 +1,4 @@
|
||||
def unroll_list(listed_items: list, separator: str = "|") -> str:
|
||||
"""
|
||||
Unrolls a list of items into a single string, separated by a specified separator.
|
||||
|
||||
Args:
|
||||
listed_items (list): The list of items to be unrolled.
|
||||
separator (str, optional): The separator to be used between the items. Defaults to "|".
|
||||
|
||||
Returns:
|
||||
str: The unrolled string.
|
||||
|
||||
Examples:
|
||||
>>> unroll_list(['apple', 'banana', 'orange'])
|
||||
'apple | banana | orange'
|
||||
|
||||
>>> unroll_list(['apple', 'banana', 'orange'], separator=',')
|
||||
'apple, banana, orange'
|
||||
|
||||
>>> unroll_list([])
|
||||
''
|
||||
"""
|
||||
def unroll_list(listed_items: list, separator: str = "|"):
|
||||
unrolled_items = ""
|
||||
if listed_items:
|
||||
for item in listed_items:
|
||||
@@ -33,138 +13,70 @@ def unroll_list(listed_items: list, separator: str = "|") -> str:
|
||||
return unrolled_items
|
||||
|
||||
|
||||
def unroll_tags(tags: list) -> dict:
|
||||
"""
|
||||
Unrolls a list of tags into a dictionary.
|
||||
|
||||
Args:
|
||||
tags (list): A list of tags.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the unrolled tags.
|
||||
|
||||
Examples:
|
||||
>>> tags = [{"key": "name", "value": "John"}, {"key": "age", "value": "30"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
|
||||
>>> tags = [{"Key": "name", "Value": "John"}, {"Key": "age", "Value": "30"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
|
||||
>>> tags = [{"key": "name"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': ''}
|
||||
|
||||
>>> tags = [{"Key": "name"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': ''}
|
||||
|
||||
>>> tags = [{"name": "John", "age": "30"}]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
|
||||
>>> tags = []
|
||||
>>> unroll_tags(tags)
|
||||
{}
|
||||
|
||||
>>> tags = {"name": "John", "age": "30"}
|
||||
>>> unroll_tags(tags)
|
||||
{'name': 'John', 'age': '30'}
|
||||
|
||||
>>> tags = ["name", "age"]
|
||||
>>> unroll_tags(tags)
|
||||
{'name': '', 'age': ''}
|
||||
"""
|
||||
if tags and tags != [{}] and tags != [None] and tags != []:
|
||||
if isinstance(tags, dict):
|
||||
return tags
|
||||
if isinstance(tags[0], str) and len(tags) > 0:
|
||||
return {tag: "" for tag in tags}
|
||||
if "key" in tags[0]:
|
||||
return {item["key"]: item.get("value", "") for item in tags}
|
||||
elif "Key" in tags[0]:
|
||||
return {item["Key"]: item.get("Value", "") for item in tags}
|
||||
else:
|
||||
return {key: value for d in tags for key, value in d.items()}
|
||||
return {}
|
||||
|
||||
|
||||
def unroll_dict(dict: dict, separator: str = "=") -> str:
|
||||
"""
|
||||
Unrolls a dictionary into a string representation.
|
||||
|
||||
Args:
|
||||
dict (dict): The dictionary to be unrolled.
|
||||
|
||||
Returns:
|
||||
str: The unrolled string representation of the dictionary.
|
||||
|
||||
Examples:
|
||||
>>> my_dict = {'name': 'John', 'age': 30, 'hobbies': ['reading', 'coding']}
|
||||
>>> unroll_dict(my_dict)
|
||||
'name: John | age: 30 | hobbies: reading, coding'
|
||||
"""
|
||||
|
||||
def unroll_tags(tags: list):
|
||||
unrolled_items = ""
|
||||
for key, value in dict.items():
|
||||
if isinstance(value, list):
|
||||
value = ", ".join(value)
|
||||
if not unrolled_items:
|
||||
unrolled_items = f"{key}{separator}{value}"
|
||||
else:
|
||||
unrolled_items = f"{unrolled_items} | {key}{separator}{value}"
|
||||
separator = "|"
|
||||
if tags and tags != [{}] and tags != [None]:
|
||||
for item in tags:
|
||||
# Check if there are tags in list
|
||||
if isinstance(item, dict):
|
||||
for key, value in item.items():
|
||||
if not unrolled_items:
|
||||
# Check the pattern of tags (Key:Value or Key:key/Value:value)
|
||||
if "Key" != key and "Value" != key:
|
||||
unrolled_items = f"{key}={value}"
|
||||
else:
|
||||
if "Key" == key:
|
||||
unrolled_items = f"{value}="
|
||||
else:
|
||||
unrolled_items = f"{value}"
|
||||
else:
|
||||
if "Key" != key and "Value" != key:
|
||||
unrolled_items = (
|
||||
f"{unrolled_items} {separator} {key}={value}"
|
||||
)
|
||||
else:
|
||||
if "Key" == key:
|
||||
unrolled_items = (
|
||||
f"{unrolled_items} {separator} {value}="
|
||||
)
|
||||
else:
|
||||
unrolled_items = f"{unrolled_items}{value}"
|
||||
elif not unrolled_items:
|
||||
unrolled_items = f"{item}"
|
||||
else:
|
||||
unrolled_items = f"{unrolled_items} {separator} {item}"
|
||||
|
||||
return unrolled_items
|
||||
|
||||
|
||||
def unroll_dict_to_list(dict: dict) -> list:
|
||||
"""
|
||||
Unrolls a dictionary into a list of key-value pairs.
|
||||
def unroll_dict(dict: dict):
|
||||
unrolled_items = ""
|
||||
separator = "|"
|
||||
for key, value in dict.items():
|
||||
if isinstance(value, list):
|
||||
value = ", ".join(value)
|
||||
if not unrolled_items:
|
||||
unrolled_items = f"{key}: {value}"
|
||||
else:
|
||||
unrolled_items = f"{unrolled_items} {separator} {key}: {value}"
|
||||
|
||||
Args:
|
||||
dict (dict): The dictionary to be unrolled.
|
||||
return unrolled_items
|
||||
|
||||
Returns:
|
||||
list: A list of key-value pairs, where each pair is represented as a string.
|
||||
|
||||
Examples:
|
||||
>>> my_dict = {'name': 'John', 'age': 30, 'hobbies': ['reading', 'coding']}
|
||||
>>> unroll_dict_to_list(my_dict)
|
||||
['name: John', 'age: 30', 'hobbies: reading, coding']
|
||||
"""
|
||||
|
||||
def unroll_dict_to_list(dict: dict):
|
||||
dict_list = []
|
||||
for key, value in dict.items():
|
||||
if isinstance(value, list):
|
||||
value = ", ".join(value)
|
||||
dict_list.append(f"{key}:{value}")
|
||||
dict_list.append(f"{key}: {value}")
|
||||
else:
|
||||
dict_list.append(f"{key}:{value}")
|
||||
dict_list.append(f"{key}: {value}")
|
||||
|
||||
return dict_list
|
||||
|
||||
|
||||
def parse_json_tags(tags: list) -> dict[str, str]:
|
||||
"""
|
||||
Parses a list of JSON tags and returns a dictionary of key-value pairs.
|
||||
|
||||
Args:
|
||||
tags (list): A list of JSON tags.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the parsed key-value pairs from the tags.
|
||||
|
||||
Examples:
|
||||
>>> tags = [
|
||||
... {"Key": "Name", "Value": "John"},
|
||||
... {"Key": "Age", "Value": "30"},
|
||||
... {"Key": "City", "Value": "New York"}
|
||||
... ]
|
||||
>>> parse_json_tags(tags)
|
||||
{'Name': 'John', 'Age': '30', 'City': 'New York'}
|
||||
"""
|
||||
|
||||
def parse_json_tags(tags: list):
|
||||
dict_tags = {}
|
||||
if tags and tags != [{}] and tags != [None]:
|
||||
for tag in tags:
|
||||
@@ -176,23 +88,7 @@ def parse_json_tags(tags: list) -> dict[str, str]:
|
||||
return dict_tags
|
||||
|
||||
|
||||
def parse_html_string(str: str) -> str:
|
||||
"""
|
||||
Parses a string and returns a formatted HTML string.
|
||||
|
||||
This function takes an input string and splits it using the delimiter " | ".
|
||||
It then formats each element of the split string as a bullet point in HTML format.
|
||||
|
||||
Args:
|
||||
str (str): The input string to be parsed.
|
||||
|
||||
Returns:
|
||||
str: The formatted HTML string.
|
||||
|
||||
Example:
|
||||
>>> parse_html_string("item1 | item2 | item3")
|
||||
'\n•item1\n\n•item2\n\n•item3\n'
|
||||
"""
|
||||
def parse_html_string(str: str):
|
||||
string = ""
|
||||
for elem in str.split(" | "):
|
||||
if elem:
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
from argparse import Namespace
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
from boto3 import client, session
|
||||
from boto3.session import Session
|
||||
from botocore.config import Config
|
||||
@@ -49,7 +50,6 @@ from prowler.providers.aws.models import (
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
class AwsProvider(Provider):
|
||||
_type: str = "aws"
|
||||
_identity: AWSIdentityInfo
|
||||
@@ -62,7 +62,7 @@ class AwsProvider(Provider):
|
||||
_output_options: AWSOutputOptions
|
||||
# TODO: this is not optional, enforce for all providers
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
|
||||
def __init__(self, arguments: Namespace):
|
||||
logger.info("Initializing AWS provider ...")
|
||||
######## Parse Arguments
|
||||
@@ -78,7 +78,7 @@ class AwsProvider(Provider):
|
||||
# MFA Configuration (false by default)
|
||||
input_mfa = getattr(arguments, "mfa", None)
|
||||
input_profile = getattr(arguments, "profile", None)
|
||||
input_regions = set(getattr(arguments, "region", []) or [])
|
||||
input_regions = getattr(arguments, "region", set())
|
||||
organizations_role_arn = getattr(arguments, "organizations_role", None)
|
||||
|
||||
# Set if unused services must be scanned
|
||||
@@ -531,7 +531,7 @@ class AwsProvider(Provider):
|
||||
token=assume_role_response.aws_session_token,
|
||||
expiry_time=assume_role_response.expiration.isoformat(),
|
||||
)
|
||||
logger.info("Refreshed Credentials")
|
||||
logger.info(f"Refreshed Credentials: {refreshed_credentials}")
|
||||
|
||||
return refreshed_credentials
|
||||
|
||||
@@ -540,7 +540,6 @@ class AwsProvider(Provider):
|
||||
regions = (
|
||||
", ".join(self._identity.audited_regions)
|
||||
if self._identity.audited_regions is not None
|
||||
and self._identity.audited_regions != set()
|
||||
else "all"
|
||||
)
|
||||
# Beautify audited profile, set "default" if there is no profile set
|
||||
@@ -741,22 +740,16 @@ class AwsProvider(Provider):
|
||||
|
||||
def get_default_region(self, service: str) -> str:
|
||||
"""get_default_region returns the default region based on the profile and audited service regions"""
|
||||
try:
|
||||
service_regions = self.get_available_aws_service_regions(service)
|
||||
default_region = self.get_global_region()
|
||||
# global region of the partition when all regions are audited and there is no profile region
|
||||
if self._identity.profile_region in service_regions:
|
||||
# return profile region only if it is audited
|
||||
default_region = self._identity.profile_region
|
||||
# return first audited region if specific regions are audited
|
||||
elif self._identity.audited_regions:
|
||||
default_region = list(self._identity.audited_regions)[0]
|
||||
return default_region
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
raise error
|
||||
service_regions = self.get_available_aws_service_regions(service)
|
||||
default_region = self.get_global_region()
|
||||
# global region of the partition when all regions are audited and there is no profile region
|
||||
if self._identity.profile_region in service_regions:
|
||||
# return profile region only if it is audited
|
||||
default_region = self._identity.profile_region
|
||||
# return first audited region if specific regions are audited
|
||||
elif self._identity.audited_regions:
|
||||
default_region = self._identity.audited_regions[0]
|
||||
return default_region
|
||||
|
||||
def get_global_region(self) -> str:
|
||||
"""get_global_region returns the global region based on the audited partition"""
|
||||
@@ -966,7 +959,7 @@ def get_aws_region_for_sts(session_region: str, input_regions: set[str]) -> str:
|
||||
aws_region = AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
else:
|
||||
# Get the first region passed to the -f/--region
|
||||
aws_region = list(input_regions)[0]
|
||||
aws_region = input_regions[0]
|
||||
|
||||
return aws_region
|
||||
|
||||
|
||||
@@ -2877,8 +2877,6 @@
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -7660,10 +7658,6 @@
|
||||
"payment-cryptography": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
|
||||
@@ -46,8 +46,6 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
arn_parsed.resource_type != "role"
|
||||
and arn_parsed.resource_type != "user"
|
||||
and arn_parsed.resource_type != "assumed-role"
|
||||
and arn_parsed.resource_type != "root"
|
||||
and arn_parsed.resource_type != "federated-user"
|
||||
):
|
||||
raise RoleArnParsingInvalidResourceType
|
||||
elif arn_parsed.resource == "":
|
||||
@@ -58,5 +56,5 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
|
||||
def is_valid_arn(arn: str) -> bool:
|
||||
"""is_valid_arn returns True or False whether the given AWS ARN (Amazon Resource Name) is valid or not."""
|
||||
regex = r"^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$"
|
||||
regex = r"^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.]+(:\d+)?$"
|
||||
return re.match(regex, arn) is not None
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from boto3 import Session
|
||||
from boto3.dynamodb.conditions import Attr
|
||||
|
||||
from prowler.lib.check.models import Check_Report_AWS
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
from prowler.lib.outputs.utils import unroll_tags
|
||||
|
||||
|
||||
class AWSMutelist(Mutelist):
|
||||
@@ -45,7 +45,7 @@ class AWSMutelist(Mutelist):
|
||||
|
||||
def is_finding_muted(
|
||||
self,
|
||||
finding: Check_Report_AWS,
|
||||
finding: Any,
|
||||
aws_account_id: str,
|
||||
) -> bool:
|
||||
return self.is_muted(
|
||||
@@ -53,7 +53,7 @@ class AWSMutelist(Mutelist):
|
||||
finding.check_metadata.CheckID,
|
||||
finding.region,
|
||||
finding.resource_id,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
unroll_tags(finding.resource_tags),
|
||||
)
|
||||
|
||||
def get_mutelist_file_from_s3(self, aws_session: Session = None):
|
||||
|
||||
@@ -30,9 +30,9 @@ def get_organizations_metadata(
|
||||
def parse_organizations_metadata(metadata: dict, tags: dict) -> AWSOrganizationsInfo:
|
||||
try:
|
||||
# Convert Tags dictionary to String
|
||||
account_details_tags = {}
|
||||
account_details_tags = []
|
||||
for tag in tags.get("Tags", {}):
|
||||
account_details_tags[tag["Key"]] = tag["Value"]
|
||||
account_details_tags.append(f"{tag['Key']}:{tag['Value']}")
|
||||
|
||||
account_details = metadata.get("Account", {})
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
def is_condition_block_restrictive(
|
||||
condition_statement: dict,
|
||||
source_account: str,
|
||||
is_cross_account_allowed=False,
|
||||
condition_statement: dict, source_account: str, is_cross_account_allowed=False
|
||||
):
|
||||
"""
|
||||
is_condition_block_restrictive parses the IAM Condition policy block and, by default, returns True if the source_account passed as argument is within, False if not.
|
||||
@@ -17,9 +15,6 @@ def is_condition_block_restrictive(
|
||||
}
|
||||
|
||||
@param source_account: str with a 12-digit AWS Account number, e.g.: 111122223333
|
||||
|
||||
@param is_cross_account_allowed: bool to allow cross-account access, e.g.: True
|
||||
|
||||
"""
|
||||
is_condition_valid = False
|
||||
|
||||
@@ -95,63 +90,3 @@ def is_condition_block_restrictive(
|
||||
is_condition_valid = True
|
||||
|
||||
return is_condition_valid
|
||||
|
||||
|
||||
def is_condition_block_restrictive_organization(
|
||||
condition_statement: dict,
|
||||
):
|
||||
"""
|
||||
is_condition_block_restrictive_organization parses the IAM Condition policy block and returns True if the condition_statement is restrictive for the organization, False if not.
|
||||
|
||||
@param condition_statement: dict with an IAM Condition block, e.g.:
|
||||
{
|
||||
"StringLike": {
|
||||
"AWS:PrincipalOrgID": "o-111122223333"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
is_condition_valid = False
|
||||
|
||||
# The conditions must be defined in lowercase since the context key names are not case-sensitive.
|
||||
# For example, including the aws:PrincipalOrgID context key is equivalent to testing for AWS:PrincipalOrgID
|
||||
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
|
||||
valid_condition_options = {
|
||||
"StringEquals": [
|
||||
"aws:principalorgid",
|
||||
],
|
||||
"StringLike": [
|
||||
"aws:principalorgid",
|
||||
],
|
||||
}
|
||||
|
||||
for condition_operator, condition_operator_key in valid_condition_options.items():
|
||||
if condition_operator in condition_statement:
|
||||
for value in condition_operator_key:
|
||||
# We need to transform the condition_statement into lowercase
|
||||
condition_statement[condition_operator] = {
|
||||
k.lower(): v
|
||||
for k, v in condition_statement[condition_operator].items()
|
||||
}
|
||||
|
||||
if value in condition_statement[condition_operator]:
|
||||
# values are a list
|
||||
if isinstance(
|
||||
condition_statement[condition_operator][value],
|
||||
list,
|
||||
):
|
||||
is_condition_valid = True
|
||||
for item in condition_statement[condition_operator][value]:
|
||||
if item == "*":
|
||||
is_condition_valid = False
|
||||
break
|
||||
|
||||
# value is a string
|
||||
elif isinstance(
|
||||
condition_statement[condition_operator][value],
|
||||
str,
|
||||
):
|
||||
if "*" not in condition_statement[condition_operator][value]:
|
||||
is_condition_valid = True
|
||||
|
||||
return is_condition_valid
|
||||
|
||||
@@ -22,7 +22,7 @@ class SecurityHub:
|
||||
|
||||
Methods:
|
||||
__init__: Initializes the SecurityHub object with necessary attributes.
|
||||
filter: Filters findings based on region, returning a dictionary with findings per region.
|
||||
filter: Filters findings based on region and status, returning a dictionary with findings per region.
|
||||
verify_enabled_per_region: Verifies and stores enabled regions with SecurityHub clients.
|
||||
batch_send_to_security_hub: Sends findings to Security Hub and returns the count of successfully sent findings.
|
||||
archive_previous_findings: Archives findings that are not present in the current execution.
|
||||
@@ -41,6 +41,7 @@ class SecurityHub:
|
||||
aws_account_id: str,
|
||||
aws_partition: str,
|
||||
findings: list[AWSSecurityFindingFormat] = [],
|
||||
status: list[str] = [],
|
||||
aws_security_hub_available_regions: list[str] = [],
|
||||
send_only_fails: bool = False,
|
||||
) -> "SecurityHub":
|
||||
@@ -49,19 +50,20 @@ class SecurityHub:
|
||||
self._aws_partition = aws_partition
|
||||
|
||||
self._enabled_regions = None
|
||||
self._findings_per_region = {}
|
||||
self._findings_per_region = None
|
||||
|
||||
if aws_security_hub_available_regions:
|
||||
self._enabled_regions = self.verify_enabled_per_region(
|
||||
aws_security_hub_available_regions
|
||||
)
|
||||
if findings and self._enabled_regions:
|
||||
self._findings_per_region = self.filter(findings, send_only_fails)
|
||||
self._findings_per_region = self.filter(findings, send_only_fails, status)
|
||||
|
||||
def filter(
|
||||
self,
|
||||
findings: list[AWSSecurityFindingFormat],
|
||||
send_only_fails: bool,
|
||||
status: list[str],
|
||||
) -> dict:
|
||||
"""
|
||||
Filters the given list of findings based on the provided criteria and returns a dictionary containing findings per region.
|
||||
@@ -69,38 +71,46 @@ class SecurityHub:
|
||||
Args:
|
||||
findings (list[AWSSecurityFindingFormat]): List of findings to filter.
|
||||
send_only_fails (bool): Flag indicating whether to send only findings with status 'FAILED'.
|
||||
status (list[str]): List of valid statuses to filter the findings.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing findings per region after applying the filtering criteria.
|
||||
"""
|
||||
|
||||
findings_per_region = {}
|
||||
try:
|
||||
# Create a key per audited region
|
||||
for region in self._enabled_regions.keys():
|
||||
findings_per_region[region] = []
|
||||
|
||||
for finding in findings:
|
||||
# We don't send findings to not enabled regions
|
||||
if finding.Resources[0].Region not in findings_per_region:
|
||||
# Create a key per audited region
|
||||
for region in self._enabled_regions.keys():
|
||||
findings_per_region[region] = []
|
||||
|
||||
for finding in findings:
|
||||
# We don't send findings to not enabled regions
|
||||
if finding.Resources[0].Region not in findings_per_region:
|
||||
continue
|
||||
|
||||
if (
|
||||
finding.Compliance.Status != "FAILED"
|
||||
or finding.Compliance.Status == "WARNING"
|
||||
) and send_only_fails:
|
||||
continue
|
||||
|
||||
# SecurityHub valid statuses are: PASSED, FAILED, WARNING
|
||||
if status:
|
||||
if finding.Compliance.Status == "PASSED" and "PASS" not in status:
|
||||
continue
|
||||
if finding.Compliance.Status == "FAILED" and "FAIL" not in status:
|
||||
continue
|
||||
# Check muted finding
|
||||
if finding.Compliance.Status == "WARNING":
|
||||
continue
|
||||
|
||||
if (
|
||||
finding.Compliance.Status != "FAILED"
|
||||
or finding.Compliance.Status == "WARNING"
|
||||
) and send_only_fails:
|
||||
continue
|
||||
# Get the finding region
|
||||
# We can do that since the finding always stores just one finding
|
||||
region = finding.Resources[0].Region
|
||||
|
||||
# Get the finding region
|
||||
# We can do that since the finding always stores just one finding
|
||||
region = finding.Resources[0].Region
|
||||
# Include that finding within their region
|
||||
findings_per_region[region].append(finding)
|
||||
|
||||
# Include that finding within their region
|
||||
findings_per_region[region].append(finding)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return findings_per_region
|
||||
|
||||
def verify_enabled_per_region(
|
||||
|
||||
+1
-1
@@ -28,5 +28,5 @@
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "It gives a false positive if the function is exposed publicly by an other public resource like an ALB or API Gateway in an AWS Account when an AWS account ID is set as the principal of the policy."
|
||||
"Notes": ""
|
||||
}
|
||||
|
||||
+13
-23
@@ -19,30 +19,20 @@ class awslambda_function_not_publicly_accessible(Check):
|
||||
if function.policy:
|
||||
for statement in function.policy["Statement"]:
|
||||
# Only check allow statements
|
||||
if statement["Effect"] == "Allow" and (
|
||||
"*" in statement["Principal"]
|
||||
or (
|
||||
isinstance(statement["Principal"], dict)
|
||||
and (
|
||||
"*" in statement["Principal"].get("AWS", "")
|
||||
or "*"
|
||||
in statement["Principal"].get("CanonicalUser", "")
|
||||
or ( # Check if function can be invoked by other AWS services
|
||||
(
|
||||
".amazonaws.com"
|
||||
in statement["Principal"].get("Service", "")
|
||||
)
|
||||
and (
|
||||
"*" in statement.get("Action", "")
|
||||
or "InvokeFunction"
|
||||
in statement.get("Action", "")
|
||||
)
|
||||
)
|
||||
if statement["Effect"] == "Allow":
|
||||
if (
|
||||
"*" in statement["Principal"]
|
||||
or (
|
||||
"AWS" in statement["Principal"]
|
||||
and "*" in statement["Principal"]["AWS"]
|
||||
)
|
||||
)
|
||||
):
|
||||
public_access = True
|
||||
break
|
||||
or (
|
||||
"CanonicalUser" in statement["Principal"]
|
||||
and "*" in statement["Principal"]["CanonicalUser"]
|
||||
)
|
||||
):
|
||||
public_access = True
|
||||
break
|
||||
|
||||
if public_access:
|
||||
report.status = "FAIL"
|
||||
|
||||
@@ -14,6 +14,7 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered
|
||||
from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
|
||||
################## Lambda
|
||||
class Lambda(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
|
||||
+5
-16
@@ -29,12 +29,7 @@ class cloudformation_stack_outputs_find_secrets(Check):
|
||||
|
||||
# Store the CloudFormation Stack Outputs into a file
|
||||
for output in stack.outputs:
|
||||
temp_output_file.write(
|
||||
bytes(
|
||||
f"{output}\n",
|
||||
encoding="raw_unicode_escape",
|
||||
)
|
||||
)
|
||||
temp_output_file.write(f"{output}".encode())
|
||||
temp_output_file.close()
|
||||
|
||||
# Init detect_secrets
|
||||
@@ -43,17 +38,11 @@ class cloudformation_stack_outputs_find_secrets(Check):
|
||||
with default_settings():
|
||||
secrets.scan_file(temp_output_file.name)
|
||||
|
||||
detect_secrets_output = secrets.json()
|
||||
# If secrets are found, update the report status
|
||||
if detect_secrets_output:
|
||||
secrets_string = ", ".join(
|
||||
[
|
||||
f"{secret['type']} in Output {int(secret['line_number'])}"
|
||||
for secret in detect_secrets_output[temp_output_file.name]
|
||||
]
|
||||
)
|
||||
if secrets.json():
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Potential secret found in Stack {stack.name} Outputs -> {secrets_string}."
|
||||
report.status_extended = (
|
||||
f"Potential secret found in Stack {stack.name} Outputs."
|
||||
)
|
||||
|
||||
os.remove(temp_output_file.name)
|
||||
else:
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ class documentdb_cluster_backup_enabled(Check):
|
||||
report.status_extended = (
|
||||
f"DocumentDB Cluster {cluster.id} does not have backup enabled."
|
||||
)
|
||||
if cluster.backup_retention_period >= documentdb_client.audit_config.get(
|
||||
if cluster.backup_retention_period > documentdb_client.audit_config.get(
|
||||
"minimum_backup_retention_period", 7
|
||||
):
|
||||
report.status = "PASS"
|
||||
|
||||
+3
-3
@@ -7,9 +7,9 @@
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AWSDocumentDBClusterSnapshot",
|
||||
"Description": "Check if DocumentDB Clusters has deletion protection enabled.",
|
||||
"Risk": "Enabling cluster deletion protection offers an additional layer of protection against accidental database deletion or deletion by an unauthorized user. A DocumentDB cluster can't be deleted while deletion protection is enabled. You must first disable deletion protection before a delete request can succeed.",
|
||||
"ResourceType": "AwsRdsDbClusters",
|
||||
"Description": "Check if Neptune Clusters has deletion protection enabled.",
|
||||
"Risk": "Enabling cluster deletion protection offers an additional layer of protection against accidental database deletion or deletion by an unauthorized user. A Neptune DB cluster can't be deleted while deletion protection is enabled. You must first disable deletion protection before a delete request can succeed.",
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-5",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
from prowler.providers.aws.services.ec2.ec2_service import PaginatedDict, PaginatedList
|
||||
|
||||
class ec2_instance_internet_facing_with_instance_profile(Check):
|
||||
def execute(self):
|
||||
|
||||
+9
-1
@@ -1,8 +1,10 @@
|
||||
from datetime import datetime, timezone
|
||||
from pympler import asizeof
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
import sys
|
||||
import gc
|
||||
|
||||
class ec2_instance_older_than_specific_days(Check):
|
||||
def execute(self):
|
||||
@@ -12,6 +14,10 @@ class ec2_instance_older_than_specific_days(Check):
|
||||
max_ec2_instance_age_in_days = ec2_client.audit_config.get(
|
||||
"max_ec2_instance_age_in_days", 180
|
||||
)
|
||||
size_bytes = asizeof.asizeof(ec2_client.instances)
|
||||
size_mb = size_bytes / (1024 * 1024)
|
||||
print("Size of dictionary:", size_mb, "MB")
|
||||
|
||||
for instance in ec2_client.instances:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = instance.region
|
||||
@@ -31,4 +37,6 @@ class ec2_instance_older_than_specific_days(Check):
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
ec2_client.cleanup()
|
||||
return findings
|
||||
|
||||
+6
-18
@@ -8,7 +8,6 @@ from detect_secrets.settings import default_settings
|
||||
|
||||
from prowler.config.config import encoding_format_utf_8
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
|
||||
@@ -25,23 +24,12 @@ class ec2_instance_secrets_user_data(Check):
|
||||
if instance.user_data:
|
||||
temp_user_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
user_data = b64decode(instance.user_data)
|
||||
try:
|
||||
if user_data[0:2] == b"\x1f\x8b": # GZIP magic number
|
||||
user_data = zlib.decompress(
|
||||
user_data, zlib.MAX_WBITS | 32
|
||||
).decode(encoding_format_utf_8)
|
||||
else:
|
||||
user_data = user_data.decode(encoding_format_utf_8)
|
||||
except UnicodeDecodeError as error:
|
||||
logger.warning(
|
||||
f"{instance.region} -- Unable to decode user data in EC2 instance {instance.id}: {error}"
|
||||
)
|
||||
continue
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{instance.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
if user_data[0:2] == b"\x1f\x8b": # GZIP magic number
|
||||
user_data = zlib.decompress(
|
||||
user_data, zlib.MAX_WBITS | 32
|
||||
).decode(encoding_format_utf_8)
|
||||
else:
|
||||
user_data = user_data.decode(encoding_format_utf_8)
|
||||
|
||||
temp_user_data_file.write(
|
||||
bytes(user_data, encoding="raw_unicode_escape")
|
||||
|
||||
+5
-17
@@ -8,7 +8,6 @@ from detect_secrets.settings import default_settings
|
||||
|
||||
from prowler.config.config import encoding_format_utf_8
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
|
||||
@@ -30,23 +29,12 @@ class ec2_launch_template_no_secrets(Check):
|
||||
temp_user_data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
user_data = b64decode(version.template_data["UserData"])
|
||||
|
||||
try:
|
||||
if user_data[0:2] == b"\x1f\x8b": # GZIP magic number
|
||||
user_data = zlib.decompress(
|
||||
user_data, zlib.MAX_WBITS | 32
|
||||
).decode(encoding_format_utf_8)
|
||||
else:
|
||||
user_data = user_data.decode(encoding_format_utf_8)
|
||||
except UnicodeDecodeError as error:
|
||||
logger.warning(
|
||||
f"{template.region} -- Unable to decode User Data in EC2 Launch Template {template.name} version {version.version_number}: {error}"
|
||||
if user_data[0:2] == b"\x1f\x8b": # GZIP magic number
|
||||
user_data = zlib.decompress(user_data, zlib.MAX_WBITS | 32).decode(
|
||||
encoding_format_utf_8
|
||||
)
|
||||
continue
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{template.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
user_data = user_data.decode(encoding_format_utf_8)
|
||||
|
||||
temp_user_data_file.write(
|
||||
bytes(user_data, encoding="raw_unicode_escape")
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "ec2_securitygroup_allow_wide_open_public_ipv4",
|
||||
"CheckTitle": "Ensure no security groups allow ingress and egress from wide-open IP address with a mask between 0 and 24.",
|
||||
"CheckTitle": "Ensure no security groups allow ingress from wide-open non-RFC1918 address.",
|
||||
"CheckType": [
|
||||
"Infrastructure Security"
|
||||
],
|
||||
@@ -10,7 +10,7 @@
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsEc2SecurityGroup",
|
||||
"Description": "Ensure no security groups allow ingress and egress from ide-open IP address with a mask between 0 and 24.",
|
||||
"Description": "Ensure no security groups allow ingress from wide-open non-RFC1918 address.",
|
||||
"Risk": "If Security groups are not properly configured the attack surface is increased.",
|
||||
"RelatedUrl": "",
|
||||
"Remediation": {
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ class ec2_securitygroup_allow_wide_open_public_ipv4(Check):
|
||||
for ingress_rule in security_group.ingress_rules:
|
||||
for ipv4 in ingress_rule["IpRanges"]:
|
||||
ip = ipaddress.ip_network(ipv4["CidrIp"])
|
||||
# Check if IP is public if 0 < prefixlen < 24
|
||||
# Check if IP is public according to RFC1918 and if 0 < prefixlen < 24
|
||||
if (
|
||||
ip.is_global
|
||||
and ip.prefixlen < cidr_treshold
|
||||
@@ -42,7 +42,7 @@ class ec2_securitygroup_allow_wide_open_public_ipv4(Check):
|
||||
for egress_rule in security_group.egress_rules:
|
||||
for ipv4 in egress_rule["IpRanges"]:
|
||||
ip = ipaddress.ip_network(ipv4["CidrIp"])
|
||||
# Check if IP is public if 0 < prefixlen < 24
|
||||
# Check if IP is public according to RFC1918 and if 0 < prefixlen < 24
|
||||
if (
|
||||
ip.is_global
|
||||
and ip.prefixlen < cidr_treshold
|
||||
|
||||
@@ -3,67 +3,430 @@ from typing import Optional
|
||||
|
||||
from botocore.client import ClientError
|
||||
from pydantic import BaseModel
|
||||
from pympler import asizeof
|
||||
|
||||
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
|
||||
import sys
|
||||
import gc
|
||||
|
||||
import dill as pickle
|
||||
import os
|
||||
import atexit
|
||||
from collections import deque
|
||||
from sys import getsizeof
|
||||
import tempfile
|
||||
|
||||
import boto3
|
||||
from moto import mock_aws
|
||||
from memory_profiler import profile
|
||||
import pdb
|
||||
import psutil
|
||||
import os
|
||||
|
||||
def check_memory_usage():
|
||||
process = psutil.Process(os.getpid())
|
||||
memory_info = process.memory_info()
|
||||
return memory_info.rss # Resident Set Size: memory in bytes
|
||||
|
||||
|
||||
class PaginatedList:
|
||||
instance_counter = 0
|
||||
|
||||
def __init__(self, page_size=1):
|
||||
self.page_size = page_size
|
||||
self.file_paths = []
|
||||
self.cache = {}
|
||||
self.length = 0 # Track the length dynamically
|
||||
self.instance_id = PaginatedList.instance_counter
|
||||
PaginatedList.instance_counter += 1
|
||||
self.temp_dir = tempfile.mkdtemp(prefix=f'paginated_list_{self.instance_id}_', dir='/Users/snaow/repos/prowler')
|
||||
atexit.register(self.cleanup)
|
||||
|
||||
def _save_page(self, page_data, page_num):
|
||||
file_path = os.path.join(self.temp_dir, f'page_{page_num}.pkl')
|
||||
with open(file_path, 'wb') as f:
|
||||
pickle.dump(page_data, f)
|
||||
if page_num >= len(self.file_paths):
|
||||
self.file_paths.append(file_path)
|
||||
else:
|
||||
self.file_paths[page_num] = file_path
|
||||
|
||||
def _load_page(self, page_num):
|
||||
if page_num in self.cache:
|
||||
return self.cache[page_num]
|
||||
with open(self.file_paths[page_num], 'rb') as f:
|
||||
page_data = pickle.load(f)
|
||||
self.cache[page_num] = page_data
|
||||
return page_data
|
||||
|
||||
def __getitem__(self, index):
|
||||
if index < 0 or index >= self.length:
|
||||
raise IndexError('Index out of range')
|
||||
page_num = index // self.page_size
|
||||
page_index = index % self.page_size
|
||||
page_data = self._load_page(page_num)
|
||||
return page_data[page_index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if index < 0 or index >= self.length:
|
||||
raise IndexError('Index out of range')
|
||||
page_num = index // self.page_size
|
||||
page_index = index % self.page_size
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[page_index] = value
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
|
||||
def __delitem__(self, index):
|
||||
if index < 0 or index >= self.length:
|
||||
raise IndexError('Index out of range')
|
||||
page_num = index // self.page_size
|
||||
page_index = index % self.page_size
|
||||
page_data = self._load_page(page_num)
|
||||
del page_data[page_index]
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
self.length -= 1
|
||||
|
||||
# Shift subsequent elements
|
||||
for i in range(index, self.length):
|
||||
next_page_num = (i + 1) // self.page_size
|
||||
next_page_index = (i + 1) % self.page_size
|
||||
if next_page_index == 0:
|
||||
self._save_page(page_data, page_num)
|
||||
page_num = next_page_num
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[page_index] = page_data.pop(next_page_index)
|
||||
page_index = next_page_index
|
||||
|
||||
# Save the last page
|
||||
self._save_page(page_data, page_num)
|
||||
|
||||
# Remove the last page if it's empty
|
||||
if self.length % self.page_size == 0:
|
||||
os.remove(self.file_paths.pop())
|
||||
self.cache.pop(page_num, None)
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
def __iter__(self):
|
||||
for page_num in range(len(self.file_paths)):
|
||||
page_data = self._load_page(page_num)
|
||||
for item in page_data:
|
||||
yield item
|
||||
|
||||
def append(self, value):
|
||||
page_num = self.length // self.page_size
|
||||
page_index = self.length % self.page_size
|
||||
if page_num >= len(self.file_paths):
|
||||
self._save_page([], page_num)
|
||||
page_data = self._load_page(page_num)
|
||||
page_data.append(value)
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
self.length += 1
|
||||
|
||||
def extend(self, values):
|
||||
for value in values:
|
||||
self.append(value)
|
||||
|
||||
def remove(self, value):
|
||||
for index, item in enumerate(self):
|
||||
if item == value:
|
||||
del self[index]
|
||||
return
|
||||
raise ValueError(f"{value} not in list")
|
||||
|
||||
def pop(self, index=-1):
|
||||
if self.length == 0:
|
||||
raise IndexError("pop from empty list")
|
||||
if index < 0:
|
||||
index += self.length
|
||||
value = self[index]
|
||||
del self[index]
|
||||
return value
|
||||
|
||||
def clear(self):
|
||||
self.cache.clear()
|
||||
self.file_paths = []
|
||||
self.length = 0
|
||||
|
||||
def index(self, value, start=0, stop=None):
|
||||
if stop is None:
|
||||
stop = self.length
|
||||
for i in range(start, stop):
|
||||
if self[i] == value:
|
||||
return i
|
||||
raise ValueError(f"{value} is not in list")
|
||||
|
||||
def get(self, index, default=None):
|
||||
try:
|
||||
return self[index]
|
||||
except IndexError:
|
||||
return default
|
||||
|
||||
def cleanup(self):
|
||||
for file_path in self.file_paths:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class PaginatedDict:
|
||||
instance_counter = 0
|
||||
|
||||
def __init__(self, page_size=1):
|
||||
self.page_size = page_size
|
||||
self.file_paths = []
|
||||
self.cache = {}
|
||||
self.key_to_page = {}
|
||||
self.length = 0 # Track the number of items
|
||||
self.instance_id = PaginatedDict.instance_counter
|
||||
PaginatedDict.instance_counter += 1
|
||||
self.temp_dir = tempfile.mkdtemp(prefix=f'paginated_dict_{self.instance_id}_', dir='/Users/snaow/repos/prowler')
|
||||
print(f"Temporary directory for instance {self.instance_id}: {self.temp_dir}")
|
||||
atexit.register(self.cleanup)
|
||||
|
||||
def _save_page(self, page_data, page_num):
|
||||
file_path = os.path.join(self.temp_dir, f'page_{page_num}.pkl')
|
||||
with open(file_path, 'wb') as f:
|
||||
pickle.dump(page_data, f)
|
||||
if page_num >= len(self.file_paths):
|
||||
self.file_paths.append(file_path)
|
||||
else:
|
||||
self.file_paths[page_num] = file_path
|
||||
|
||||
def _load_page(self, page_num):
|
||||
if page_num in self.cache:
|
||||
return self.cache[page_num]
|
||||
with open(self.file_paths[page_num], 'rb') as f:
|
||||
page_data = pickle.load(f)
|
||||
self.cache[page_num] = page_data
|
||||
return page_data
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self.key_to_page:
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[key] = value
|
||||
else:
|
||||
page_num = self.length // self.page_size
|
||||
if page_num >= len(self.file_paths):
|
||||
self._save_page({}, page_num)
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[key] = value
|
||||
self.key_to_page[key] = page_num
|
||||
self.length += 1
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key not in self.key_to_page:
|
||||
raise KeyError(f"Key {key} not found")
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
return page_data[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key not in self.key_to_page:
|
||||
raise KeyError(f"Key {key} not found")
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
del page_data[key]
|
||||
del self.key_to_page[key]
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
self.length -= 1
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
def __iter__(self):
|
||||
for page_num in range(len(self.file_paths)):
|
||||
page_data = self._load_page(page_num)
|
||||
for key in page_data:
|
||||
yield key
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def keys(self):
|
||||
for key in self:
|
||||
yield key
|
||||
|
||||
def values(self):
|
||||
for key in self:
|
||||
yield self[key]
|
||||
|
||||
def items(self):
|
||||
for key in self:
|
||||
yield (key, self[key])
|
||||
|
||||
def clear(self):
|
||||
self.cache.clear()
|
||||
self.key_to_page.clear()
|
||||
self.file_paths = []
|
||||
self.length = 0
|
||||
|
||||
def cleanup(self):
|
||||
for file_path in self.file_paths:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
################## EC2
|
||||
|
||||
class EC2(AWSService):
|
||||
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at __init__ ec2_service.py : {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.account_arn_template = f"arn:{self.audited_partition}:ec2:{self.region}:{self.audited_account}:account"
|
||||
self.instances = []
|
||||
paginated = 1
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at super() ec2_service.py : {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
if paginated:
|
||||
self.instances = PaginatedList()
|
||||
self.security_groups = PaginatedList()
|
||||
self.regions_with_sgs = PaginatedList()
|
||||
self.volumes_with_snapshots = PaginatedDict()
|
||||
self.regions_with_snapshots = PaginatedDict()
|
||||
self.network_acls = PaginatedList()
|
||||
self.snapshots = PaginatedList()
|
||||
self.network_interfaces = PaginatedList()
|
||||
self.images = PaginatedList()
|
||||
self.volumes = PaginatedList()
|
||||
self.attributes_for_regions = PaginatedDict()
|
||||
self.ebs_encryption_by_default = PaginatedList()
|
||||
self.elastic_ips = PaginatedList()
|
||||
self.ebs_block_public_access_snapshots_states = PaginatedList()
|
||||
self.instance_metadata_defaults = PaginatedList()
|
||||
self.launch_templates = PaginatedList()
|
||||
else:
|
||||
self.instances = []
|
||||
self.security_groups = []
|
||||
self.regions_with_sgs = []
|
||||
self.volumes_with_snapshots = {}
|
||||
self.regions_with_snapshots = {}
|
||||
self.network_acls = []
|
||||
self.snapshots = []
|
||||
self.network_interfaces = []
|
||||
self.images = []
|
||||
self.volumes = []
|
||||
self.attributes_for_regions = {}
|
||||
self.ebs_encryption_by_default = []
|
||||
self.elastic_ips = []
|
||||
self.ebs_block_public_access_snapshots_states = []
|
||||
self.instance_metadata_defaults = []
|
||||
self.launch_templates = []
|
||||
|
||||
|
||||
self.__threading_call__(self.__describe_instances__)
|
||||
#self.__describe_instances__(next(iter(self.regional_clients.values())))
|
||||
self.__threading_call__(self.__get_instance_user_data__, self.instances)
|
||||
self.security_groups = []
|
||||
self.regions_with_sgs = []
|
||||
self.__threading_call__(self.__describe_security_groups__)
|
||||
self.network_acls = []
|
||||
self.__threading_call__(self.__describe_network_acls__)
|
||||
self.snapshots = []
|
||||
self.volumes_with_snapshots = {}
|
||||
self.regions_with_snapshots = {}
|
||||
self.__threading_call__(self.__describe_snapshots__)
|
||||
self.__threading_call__(self.__determine_public_snapshots__, self.snapshots)
|
||||
self.network_interfaces = []
|
||||
self.__threading_call__(self.__describe_network_interfaces__)
|
||||
self.images = []
|
||||
self.__threading_call__(self.__describe_images__)
|
||||
self.volumes = []
|
||||
self.__threading_call__(self.__describe_volumes__)
|
||||
self.attributes_for_regions = {}
|
||||
self.__threading_call__(self.__get_resources_for_regions__)
|
||||
self.ebs_encryption_by_default = []
|
||||
self.__threading_call__(self.__get_ebs_encryption_settings__)
|
||||
self.elastic_ips = []
|
||||
self.__threading_call__(self.__describe_ec2_addresses__)
|
||||
self.ebs_block_public_access_snapshots_states = []
|
||||
self.__threading_call__(self.__get_snapshot_block_public_access_state__)
|
||||
self.instance_metadata_defaults = []
|
||||
self.__threading_call__(self.__get_instance_metadata_defaults__)
|
||||
self.launch_templates = []
|
||||
self.__threading_call__(self.__describe_launch_templates)
|
||||
self.__threading_call__(
|
||||
self.__get_launch_template_versions__, self.launch_templates
|
||||
)
|
||||
|
||||
print("MY DICT---<>")
|
||||
print(list(self.instances))
|
||||
def cleanup(self):
|
||||
del self.instances
|
||||
del self.security_groups
|
||||
del self.regions_with_sgs
|
||||
del self.volumes_with_snapshots
|
||||
del self.regions_with_snapshots
|
||||
del self.network_acls
|
||||
del self.snapshots
|
||||
del self.network_interfaces
|
||||
del self.images
|
||||
del self.volumes
|
||||
del self.attributes_for_regions
|
||||
del self.ebs_encryption_by_default
|
||||
del self.elastic_ips
|
||||
del self.ebs_block_public_access_snapshots_states
|
||||
del self.instance_metadata_defaults
|
||||
del self.launch_templates
|
||||
gc.collect()
|
||||
|
||||
def __get_volume_arn_template__(self, region):
|
||||
return (
|
||||
f"arn:{self.audited_partition}:ec2:{region}:{self.audited_account}:volume"
|
||||
)
|
||||
|
||||
|
||||
#@mock_aws
|
||||
def __describe_instances__(self, regional_client):
|
||||
try:
|
||||
mock_enabled = 0
|
||||
if mock_enabled:
|
||||
ec2 = boto3.resource('ec2', region_name='eu-west-1')
|
||||
instances = []
|
||||
counter = 0
|
||||
|
||||
instance = ec2.create_instances(
|
||||
ImageId='ami-12345678', # Example AMI ID, replace with a valid one if testing with real AWS
|
||||
MinCount=3000,
|
||||
MaxCount=3000,
|
||||
InstanceType='t2.micro'
|
||||
)[0]
|
||||
instance.wait_until_running()
|
||||
instance.reload()
|
||||
|
||||
describe_instances_paginator = regional_client.get_paginator(
|
||||
"describe_instances"
|
||||
)
|
||||
for page in describe_instances_paginator.paginate():
|
||||
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at regional_client.get_paginator ({regional_client.region}) : {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
describe_instances_paginator_iterator = describe_instances_paginator.paginate(PaginationConfig={'MaxItems': 1})
|
||||
#describe_instances_paginator_iterator = describe_instances_paginator.paginate()
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at describe_instances_paginator.paginate() ({regional_client.region}) : {memory_usage / (1024 * 1024)} MB")
|
||||
|
||||
for page in describe_instances_paginator_iterator:
|
||||
size_bytes = asizeof.asizeof(page)
|
||||
size_mb = size_bytes / (1024 * 1024)
|
||||
print("\tMemory usage of page", size_mb, "MB")
|
||||
#for page in describe_instances_paginator.paginate():
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"\tMemory usage at describe_instances_paginator.paginate() start : {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
for reservation in page["Reservations"]:
|
||||
for instance in reservation["Instances"]:
|
||||
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:instance/{instance['InstanceId']}"
|
||||
#print(arn)
|
||||
if not self.audit_resources or (
|
||||
is_resource_filtered(arn, self.audit_resources)
|
||||
):
|
||||
@@ -98,6 +461,17 @@ class EC2(AWSService):
|
||||
tags=instance.get("Tags"),
|
||||
)
|
||||
)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"\t\tMemory usage at self.instances.append : {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"\tMemory usage at describe_instances_paginator.paginate() end : {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage at the end of describe_instances_paginator ({regional_client.region}): {memory_usage / (1024 * 1024)} MB")
|
||||
|
||||
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -108,7 +482,8 @@ class EC2(AWSService):
|
||||
describe_security_groups_paginator = regional_client.get_paginator(
|
||||
"describe_security_groups"
|
||||
)
|
||||
for page in describe_security_groups_paginator.paginate():
|
||||
describe_security_groups_iterator = describe_security_groups_paginator.paginate(PaginationConfig={'MaxItems': 1})
|
||||
for page in describe_security_groups_iterator:
|
||||
for sg in page["SecurityGroups"]:
|
||||
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:security-group/{sg['GroupId']}"
|
||||
if not self.audit_resources or (
|
||||
@@ -135,6 +510,9 @@ class EC2(AWSService):
|
||||
)
|
||||
if sg["GroupName"] != "default":
|
||||
self.regions_with_sgs.append(regional_client.region)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __describe_security_groups__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -145,7 +523,7 @@ class EC2(AWSService):
|
||||
describe_network_acls_paginator = regional_client.get_paginator(
|
||||
"describe_network_acls"
|
||||
)
|
||||
for page in describe_network_acls_paginator.paginate():
|
||||
for page in describe_network_acls_paginator.paginate(PaginationConfig={'MaxItems': 1}):
|
||||
for nacl in page["NetworkAcls"]:
|
||||
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:network-acl/{nacl['NetworkAclId']}"
|
||||
if not self.audit_resources or (
|
||||
@@ -165,6 +543,9 @@ class EC2(AWSService):
|
||||
tags=nacl.get("Tags"),
|
||||
)
|
||||
)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __describe_network_acls__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -198,6 +579,9 @@ class EC2(AWSService):
|
||||
self.volumes_with_snapshots[snapshot["VolumeId"]] = True
|
||||
# Store that the region has at least one snapshot
|
||||
self.regions_with_snapshots[regional_client.region] = snapshots_in_region
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after describe_snapshots: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -256,6 +640,9 @@ class EC2(AWSService):
|
||||
self.__add_network_interfaces_to_security_groups__(
|
||||
eni, interface.get("Groups", [])
|
||||
)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after network_interfaces: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -283,6 +670,9 @@ class EC2(AWSService):
|
||||
)["UserData"]
|
||||
if "Value" in user_data:
|
||||
instance.user_data = user_data["Value"]
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __get_instance_user_data__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except ClientError as error:
|
||||
if error.response["Error"]["Code"] == "InvalidInstanceID.NotFound":
|
||||
logger.warning(
|
||||
@@ -310,6 +700,9 @@ class EC2(AWSService):
|
||||
tags=image.get("Tags"),
|
||||
)
|
||||
)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __describe_images__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -335,6 +728,9 @@ class EC2(AWSService):
|
||||
tags=volume.get("Tags"),
|
||||
)
|
||||
)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __describe_volumes__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -366,6 +762,9 @@ class EC2(AWSService):
|
||||
tags=address.get("Tags"),
|
||||
)
|
||||
)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __describe_ec2_addresses__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -426,6 +825,9 @@ class EC2(AWSService):
|
||||
region=regional_client.region,
|
||||
)
|
||||
)
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __get_instance_metadata_defaults__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -453,6 +855,9 @@ class EC2(AWSService):
|
||||
"has_snapshots": has_snapshots,
|
||||
"has_volumes": has_volumes,
|
||||
}
|
||||
memory_usage = check_memory_usage()
|
||||
print(f"Memory usage after __get_resources_for_regions__: {memory_usage / (1024 * 1024)} MB")
|
||||
#pdb.set_trace() # Break
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
+8
-21
@@ -15,27 +15,23 @@ class ecr_repositories_scan_vulnerabilities_in_latest_image(Check):
|
||||
for repository in registry.repositories:
|
||||
# First check if the repository has images
|
||||
if len(repository.images_details) > 0:
|
||||
# We only want to check the latest image pushed that is scannable
|
||||
# We only want to check the latest image pushed
|
||||
image = repository.images_details[-1]
|
||||
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = repository.region
|
||||
report.resource_id = repository.name
|
||||
report.resource_arn = repository.arn
|
||||
report.resource_tags = repository.tags
|
||||
report.status = "PASS"
|
||||
status_extended_prefix = f"ECR repository '{repository.name}' has scanned the {image.type} container image with digest '{image.latest_digest}' and tag '{image.latest_tag}' "
|
||||
report.status_extended = (
|
||||
status_extended_prefix + "without findings."
|
||||
)
|
||||
report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} scanned without findings."
|
||||
if not image.scan_findings_status:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
status_extended_prefix + "without a scan."
|
||||
)
|
||||
report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} without a scan."
|
||||
elif image.scan_findings_status == "FAILED":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
status_extended_prefix + "with scan status FAILED."
|
||||
f"ECR repository {repository.name} with scan status FAILED."
|
||||
)
|
||||
elif (
|
||||
image.scan_findings_status != "FAILED"
|
||||
@@ -46,29 +42,20 @@ class ecr_repositories_scan_vulnerabilities_in_latest_image(Check):
|
||||
and image.scan_findings_severity_count.critical
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
status_extended_prefix
|
||||
+ f"with findings: CRITICAL->{image.scan_findings_severity_count.critical}."
|
||||
)
|
||||
report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} scanned with findings: CRITICAL->{image.scan_findings_severity_count.critical}."
|
||||
elif minimum_severity == "HIGH" and (
|
||||
image.scan_findings_severity_count.critical
|
||||
or image.scan_findings_severity_count.high
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
status_extended_prefix
|
||||
+ f"with findings: CRITICAL->{image.scan_findings_severity_count.critical}, HIGH->{image.scan_findings_severity_count.high}."
|
||||
)
|
||||
report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} scanned with findings: CRITICAL->{image.scan_findings_severity_count.critical}, HIGH->{image.scan_findings_severity_count.high}."
|
||||
elif minimum_severity == "MEDIUM" and (
|
||||
image.scan_findings_severity_count.critical
|
||||
or image.scan_findings_severity_count.high
|
||||
or image.scan_findings_severity_count.medium
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
status_extended_prefix
|
||||
+ f"with findings: CRITICAL->{image.scan_findings_severity_count.critical}, HIGH->{image.scan_findings_severity_count.high}, MEDIUM->{image.scan_findings_severity_count.medium}."
|
||||
)
|
||||
report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} scanned with findings: CRITICAL->{image.scan_findings_severity_count.critical}, HIGH->{image.scan_findings_severity_count.high}, MEDIUM->{image.scan_findings_severity_count.medium}."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -17,14 +17,14 @@ class ECR(AWSService):
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.registry_id = self.audited_account
|
||||
self.registries = {}
|
||||
self.__threading_call__(self._describe_registries_and_repositories)
|
||||
self.__threading_call__(self._describe_repository_policies)
|
||||
self.__threading_call__(self._get_image_details)
|
||||
self.__threading_call__(self._get_repository_lifecycle_policy)
|
||||
self.__threading_call__(self._get_registry_scanning_configuration)
|
||||
self.__threading_call__(self._list_tags_for_resource)
|
||||
self.__threading_call__(self.__describe_registries_and_repositories__)
|
||||
self.__threading_call__(self.__describe_repository_policies__)
|
||||
self.__threading_call__(self.__get_image_details__)
|
||||
self.__threading_call__(self.__get_repository_lifecycle_policy__)
|
||||
self.__threading_call__(self.__get_registry_scanning_configuration__)
|
||||
self.__threading_call__(self.__list_tags_for_resource__)
|
||||
|
||||
def _describe_registries_and_repositories(self, regional_client):
|
||||
def __describe_registries_and_repositories__(self, regional_client):
|
||||
logger.info("ECR - Describing registries and repositories...")
|
||||
regional_registry_repositories = []
|
||||
try:
|
||||
@@ -64,7 +64,7 @@ class ECR(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _describe_repository_policies(self, regional_client):
|
||||
def __describe_repository_policies__(self, regional_client):
|
||||
logger.info("ECR - Describing repository policies...")
|
||||
try:
|
||||
if regional_client.region in self.registries:
|
||||
@@ -91,7 +91,7 @@ class ECR(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_repository_lifecycle_policy(self, regional_client):
|
||||
def __get_repository_lifecycle_policy__(self, regional_client):
|
||||
logger.info("ECR - Getting repository lifecycle policy...")
|
||||
try:
|
||||
if regional_client.region in self.registries:
|
||||
@@ -119,7 +119,7 @@ class ECR(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_image_details(self, regional_client):
|
||||
def __get_image_details__(self, regional_client):
|
||||
logger.info("ECR - Getting images details...")
|
||||
try:
|
||||
if regional_client.region in self.registries:
|
||||
@@ -139,115 +139,55 @@ class ECR(AWSService):
|
||||
# The following condition is required since sometimes
|
||||
# the AWS ECR API returns None using the iterator
|
||||
if image is not None:
|
||||
artifact_media_type = image.get(
|
||||
"artifactMediaType", None
|
||||
)
|
||||
tags = image.get("imageTags", [])
|
||||
if ECR._is_artifact_scannable(
|
||||
artifact_media_type, tags
|
||||
):
|
||||
severity_counts = None
|
||||
last_scan_status = None
|
||||
image_digest = image.get("imageDigest")
|
||||
latest_tag = image.get("imageTags", ["None"])[0]
|
||||
image_pushed_at = image.get("imagePushedAt")
|
||||
image_scan_findings_field_name = (
|
||||
severity_counts = None
|
||||
last_scan_status = None
|
||||
if "imageScanStatus" in image:
|
||||
last_scan_status = image["imageScanStatus"][
|
||||
"status"
|
||||
]
|
||||
|
||||
if "imageScanFindingsSummary" in image:
|
||||
severity_counts = FindingSeverityCounts(
|
||||
critical=0, high=0, medium=0
|
||||
)
|
||||
finding_severity_counts = image[
|
||||
"imageScanFindingsSummary"
|
||||
)
|
||||
if "docker" in artifact_media_type:
|
||||
type = "Docker"
|
||||
elif "oci" in artifact_media_type:
|
||||
type = "OCI"
|
||||
else:
|
||||
type = ""
|
||||
|
||||
# If imageScanStatus is not present or imageScanFindingsSummary is missing,
|
||||
# we need to call DescribeImageScanFindings because AWS' new version of
|
||||
# basic scanning does not support imageScanFindingsSummary and imageScanStatus
|
||||
# in the DescribeImages API.
|
||||
if "imageScanStatus" not in image:
|
||||
try:
|
||||
# use "image" for scan findings to get data the same way as for an image
|
||||
image = (
|
||||
client.describe_image_scan_findings(
|
||||
registryId=self.registries[
|
||||
regional_client.region
|
||||
].id,
|
||||
repositoryName=repository.name,
|
||||
imageId={
|
||||
"imageDigest": image_digest
|
||||
},
|
||||
)
|
||||
)
|
||||
image_scan_findings_field_name = (
|
||||
"imageScanFindings"
|
||||
)
|
||||
except (
|
||||
client.exceptions.ImageNotFoundException
|
||||
) as error:
|
||||
logger.warning(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
except (
|
||||
client.exceptions.ScanNotFoundException
|
||||
) as error:
|
||||
logger.warning(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
|
||||
if "imageScanStatus" in image:
|
||||
last_scan_status = image["imageScanStatus"][
|
||||
"status"
|
||||
]
|
||||
|
||||
if image_scan_findings_field_name in image:
|
||||
severity_counts = FindingSeverityCounts(
|
||||
critical=0, high=0, medium=0
|
||||
)
|
||||
finding_severity_counts = image[
|
||||
image_scan_findings_field_name
|
||||
].get("findingSeverityCounts", {})
|
||||
]["findingSeverityCounts"]
|
||||
if "CRITICAL" in finding_severity_counts:
|
||||
severity_counts.critical = (
|
||||
finding_severity_counts.get(
|
||||
"CRITICAL", 0
|
||||
)
|
||||
finding_severity_counts["CRITICAL"]
|
||||
)
|
||||
if "HIGH" in finding_severity_counts:
|
||||
severity_counts.high = (
|
||||
finding_severity_counts.get("HIGH", 0)
|
||||
finding_severity_counts["HIGH"]
|
||||
)
|
||||
if "MEDIUM" in finding_severity_counts:
|
||||
severity_counts.medium = (
|
||||
finding_severity_counts.get("MEDIUM", 0)
|
||||
)
|
||||
|
||||
repository.images_details.append(
|
||||
ImageDetails(
|
||||
latest_tag=latest_tag,
|
||||
image_pushed_at=image_pushed_at,
|
||||
latest_digest=image_digest,
|
||||
scan_findings_status=last_scan_status,
|
||||
scan_findings_severity_count=severity_counts,
|
||||
artifact_media_type=artifact_media_type,
|
||||
type=type,
|
||||
finding_severity_counts["MEDIUM"]
|
||||
)
|
||||
latest_tag = "None"
|
||||
if image.get("imageTags"):
|
||||
latest_tag = image["imageTags"][0]
|
||||
repository.images_details.append(
|
||||
ImageDetails(
|
||||
latest_tag=latest_tag,
|
||||
image_pushed_at=image["imagePushedAt"],
|
||||
latest_digest=image["imageDigest"],
|
||||
scan_findings_status=last_scan_status,
|
||||
scan_findings_severity_count=severity_counts,
|
||||
)
|
||||
# Sort the repository images by date pushed
|
||||
repository.images_details.sort(
|
||||
key=lambda image: image.image_pushed_at
|
||||
)
|
||||
)
|
||||
# Sort the repository images by date pushed
|
||||
repository.images_details.sort(
|
||||
key=lambda image: image.image_pushed_at
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_tags_for_resource(self, regional_client):
|
||||
def __list_tags_for_resource__(self, regional_client):
|
||||
logger.info("ECR - List Tags...")
|
||||
try:
|
||||
if regional_client.region in self.registries:
|
||||
@@ -275,7 +215,7 @@ class ECR(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_registry_scanning_configuration(self, regional_client):
|
||||
def __get_registry_scanning_configuration__(self, regional_client):
|
||||
logger.info("ECR - Getting Registry Scanning Configuration...")
|
||||
try:
|
||||
if regional_client.region in self.registries:
|
||||
@@ -311,44 +251,6 @@ class ECR(AWSService):
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_artifact_scannable(artifact_media_type: str, tags: list[str] = []) -> bool:
|
||||
"""
|
||||
Check if an artifact is scannable based on its media type and tags.
|
||||
|
||||
Args:
|
||||
artifact_media_type (str): The media type of the artifact.
|
||||
tags (list): The list of tags associated with the artifact.
|
||||
|
||||
Returns:
|
||||
bool: True if the artifact is scannable, False otherwise.
|
||||
"""
|
||||
try:
|
||||
if artifact_media_type is None:
|
||||
return False
|
||||
|
||||
# Tools like GoogleContainerTools/jib uses `application/vnd.oci.image.config.v1+json`` also for signatures, which are not scannable.
|
||||
# Luckily, these are tagged with sha-<HASH-CODE>.sig, so that they can still be easily recognized.
|
||||
for tag in tags:
|
||||
if tag.startswith("sha256-") and tag.endswith(".sig"):
|
||||
return False
|
||||
|
||||
scannable_artifact_media_types = [
|
||||
"application/vnd.docker.container.image.v1+json", # Docker image configuration
|
||||
"application/vnd.docker.image.rootfs.diff.tar", # Docker image layer as a tar archive
|
||||
"application/vnd.docker.image.rootfs.diff.tar.gzip", # Docker image layer that is compressed using gzip
|
||||
"application/vnd.oci.image.config.v1+json", # OCI image configuration, but also used by GoogleContainerTools/jib for signatures
|
||||
"application/vnd.oci.image.layer.v1.tar", # Uncompressed OCI image layer
|
||||
"application/vnd.oci.image.layer.v1.tar+gzip", # Compressed OCI image layer
|
||||
]
|
||||
|
||||
return artifact_media_type in scannable_artifact_media_types
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class FindingSeverityCounts(BaseModel):
|
||||
critical: int
|
||||
@@ -362,8 +264,6 @@ class ImageDetails(BaseModel):
|
||||
image_pushed_at: datetime
|
||||
scan_findings_status: Optional[str]
|
||||
scan_findings_severity_count: Optional[FindingSeverityCounts]
|
||||
artifact_media_type: Optional[str]
|
||||
type: str
|
||||
|
||||
|
||||
class Repository(BaseModel):
|
||||
|
||||
+11
-5
@@ -12,16 +12,22 @@ class iam_inline_policy_allows_privilege_escalation(Check):
|
||||
for policy in iam_client.policies:
|
||||
if policy.type == "Inline":
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.resource_id = f"{policy.entity}/{policy.name}"
|
||||
report.resource_id = policy.name
|
||||
report.resource_arn = policy.arn
|
||||
report.region = iam_client.region
|
||||
report.resource_tags = policy.tags
|
||||
report.status = "PASS"
|
||||
|
||||
resource_type_str = report.resource_arn.split(":")[-1].split("/")[0]
|
||||
resource_attached = report.resource_arn.split("/")[-1]
|
||||
if "role" in report.resource_arn:
|
||||
resource_type_str = "role"
|
||||
elif "group" in report.resource_arn:
|
||||
resource_type_str = "group"
|
||||
elif "user" in report.resource_arn:
|
||||
resource_type_str = "user"
|
||||
else:
|
||||
resource_type_str = "resource"
|
||||
|
||||
report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow privilege escalation."
|
||||
report.status_extended = f"Inline Policy '{report.resource_id}'{' attached to ' + resource_type_str + ' ' + report.resource_arn if policy.attached else ''} does not allow privilege escalation."
|
||||
|
||||
policies_affected = check_privilege_escalation(
|
||||
getattr(policy, "document", {})
|
||||
@@ -31,7 +37,7 @@ class iam_inline_policy_allows_privilege_escalation(Check):
|
||||
report.status = "FAIL"
|
||||
|
||||
report.status_extended = (
|
||||
f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows privilege escalation using the following actions: {policies_affected}".rstrip()
|
||||
f"Inline Policy '{report.resource_id}'{' attached to ' + resource_type_str + ' ' + report.resource_arn if policy.attached else ''} allows privilege escalation using the following actions: {policies_affected}".rstrip()
|
||||
+ "."
|
||||
)
|
||||
|
||||
|
||||
+10
-4
@@ -14,10 +14,16 @@ class iam_inline_policy_no_administrative_privileges(Check):
|
||||
report.resource_tags = policy.tags
|
||||
report.status = "PASS"
|
||||
|
||||
resource_type_str = report.resource_arn.split(":")[-1].split("/")[0]
|
||||
resource_attached = report.resource_arn.split("/")[-1]
|
||||
if "role" in report.resource_arn:
|
||||
resource_type_str = "role"
|
||||
elif "group" in report.resource_arn:
|
||||
resource_type_str = "group"
|
||||
elif "user" in report.resource_arn:
|
||||
resource_type_str = "user"
|
||||
else:
|
||||
resource_type_str = "resource"
|
||||
|
||||
report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {resource_attached} does not allow '*:*' administrative privileges."
|
||||
report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {report.resource_arn} does not allow '*:*' administrative privileges."
|
||||
if policy.document:
|
||||
# Check the statements, if one includes *:* stop iterating over the rest
|
||||
if not isinstance(policy.document["Statement"], list):
|
||||
@@ -39,7 +45,7 @@ class iam_inline_policy_no_administrative_privileges(Check):
|
||||
)
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {resource_attached} allows '*:*' administrative privileges."
|
||||
report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {report.resource_arn} allows '*:*' administrative privileges."
|
||||
break
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
+3
-7
@@ -15,20 +15,16 @@ class iam_inline_policy_no_full_access_to_cloudtrail(Check):
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = iam_client.region
|
||||
report.resource_arn = policy.arn
|
||||
report.resource_id = f"{policy.entity}/{policy.name}"
|
||||
report.resource_id = policy.name
|
||||
report.resource_tags = policy.tags
|
||||
report.status = "PASS"
|
||||
|
||||
resource_type_str = report.resource_arn.split(":")[-1].split("/")[0]
|
||||
resource_attached = report.resource_arn.split("/")[-1]
|
||||
|
||||
report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow '{critical_service}:*' privileges."
|
||||
report.status_extended = f"Inline Policy {policy.name} does not allow '{critical_service}:*' privileges."
|
||||
|
||||
if policy.document and check_full_service_access(
|
||||
critical_service, policy.document
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows '{critical_service}:*' privileges to all resources."
|
||||
report.status_extended = f"Inline Policy {policy.name} allows '{critical_service}:*' privileges to all resources."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-7
@@ -14,20 +14,16 @@ class iam_inline_policy_no_full_access_to_kms(Check):
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = iam_client.region
|
||||
report.resource_arn = policy.arn
|
||||
report.resource_id = f"{policy.entity}/{policy.name}"
|
||||
report.resource_id = policy.name
|
||||
report.resource_tags = policy.tags
|
||||
report.status = "PASS"
|
||||
|
||||
resource_type_str = report.resource_arn.split(":")[-1].split("/")[0]
|
||||
resource_attached = report.resource_arn.split("/")[-1]
|
||||
|
||||
report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow '{critical_service}:*' privileges."
|
||||
report.status_extended = f"Inline Policy {policy.name} does not allow '{critical_service}:*' privileges."
|
||||
|
||||
if policy.document and check_full_service_access(
|
||||
critical_service, policy.document
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows '{critical_service}:*' privileges."
|
||||
report.status_extended = f"Inline Policy {policy.name} allows '{critical_service}:*' privileges."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+3
-3
@@ -15,9 +15,9 @@ class iam_root_hardware_mfa_enabled(Check):
|
||||
report.resource_arn = iam_client.mfa_arn_template
|
||||
|
||||
if iam_client.account_summary["SummaryMap"]["AccountMFAEnabled"] > 0:
|
||||
for mfa in iam_client.virtual_mfa_devices:
|
||||
# If the ARN of the associated IAM user of the Virtual MFA device is "arn:aws:iam::[aws-account-id]:root", your AWS root account is not using a hardware-based MFA device for MFA protection.
|
||||
if "root" in mfa.get("User", {}).get("Arn", ""):
|
||||
virtual_mfas = iam_client.virtual_mfa_devices
|
||||
for mfa in virtual_mfas:
|
||||
if "root" in mfa["SerialNumber"]:
|
||||
virtual_mfa = True
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "Root account has a virtual MFA instead of a hardware MFA device enabled."
|
||||
|
||||
@@ -384,10 +384,9 @@ class IAM(AWSService):
|
||||
for page in list_mfa_devices_paginator.paginate(UserName=user.name):
|
||||
for mfa_device in page["MFADevices"]:
|
||||
mfa_serial_number = mfa_device["SerialNumber"]
|
||||
try:
|
||||
mfa_type = mfa_serial_number.split(":")[5].split("/")[0]
|
||||
except IndexError:
|
||||
mfa_type = "hardware"
|
||||
mfa_type = (
|
||||
mfa_device["SerialNumber"].split(":")[5].split("/")[0]
|
||||
)
|
||||
mfa_devices.append(
|
||||
MFADevice(serial_number=mfa_serial_number, type=mfa_type)
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ class Lightsail(AWSService):
|
||||
f"arn:{self.audited_partition}:lightsail:{regional_client.region}:{self.audited_account}:Instance",
|
||||
)
|
||||
|
||||
if not self.audit_resources or (
|
||||
if not self.audit_resources or is_resource_filtered(
|
||||
is_resource_filtered(arn, self.audit_resources)
|
||||
):
|
||||
ports = []
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ class neptune_cluster_backup_enabled(Check):
|
||||
report.status_extended = (
|
||||
f"Neptune Cluster {cluster.name} does not have backup enabled."
|
||||
)
|
||||
if cluster.backup_retention_period >= neptune_client.audit_config.get(
|
||||
if cluster.backup_retention_period > neptune_client.audit_config.get(
|
||||
"minimum_backup_retention_period", 7
|
||||
):
|
||||
report.status = "PASS"
|
||||
|
||||
+1
-4
@@ -16,10 +16,7 @@ class rds_instance_event_subscription_security_groups(Check):
|
||||
)
|
||||
report.region = db_event.region
|
||||
if db_event.source_type == "db-security-group" and db_event.enabled:
|
||||
if db_event.event_list == [] or set(db_event.event_list) == {
|
||||
"failure",
|
||||
"configuration change",
|
||||
}:
|
||||
if db_event.event_list == []:
|
||||
report.resource_id = db_event.id
|
||||
report.resource_arn = db_event.arn
|
||||
report.status = "PASS"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
import gc
|
||||
|
||||
class s3_bucket_object_lock(Check):
|
||||
def execute(self):
|
||||
@@ -23,4 +25,7 @@ class s3_bucket_object_lock(Check):
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
|
||||
del sys.modules['prowler.providers.aws.services.s3.s3_client']
|
||||
gc.collect()
|
||||
return findings
|
||||
|
||||
@@ -7,8 +7,273 @@ 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
|
||||
import os
|
||||
import dill as pickle
|
||||
import os
|
||||
import atexit
|
||||
from collections import deque
|
||||
from sys import getsizeof
|
||||
import tempfile
|
||||
|
||||
|
||||
class PaginatedList:
|
||||
instance_counter = 0
|
||||
|
||||
def __init__(self, page_size=10):
|
||||
self.page_size = page_size
|
||||
self.file_paths = []
|
||||
self.cache = {}
|
||||
self.length = 0 # Track the length dynamically
|
||||
self.instance_id = PaginatedList.instance_counter
|
||||
PaginatedList.instance_counter += 1
|
||||
self.temp_dir = tempfile.mkdtemp(prefix=f'paginated_list_{self.instance_id}_', dir='/Users/snaow/repos/prowler')
|
||||
atexit.register(self.cleanup)
|
||||
|
||||
def _save_page(self, page_data, page_num):
|
||||
file_path = os.path.join(self.temp_dir, f'page_{page_num}.pkl')
|
||||
with open(file_path, 'wb') as f:
|
||||
pickle.dump(page_data, f)
|
||||
if page_num >= len(self.file_paths):
|
||||
self.file_paths.append(file_path)
|
||||
else:
|
||||
self.file_paths[page_num] = file_path
|
||||
|
||||
def _load_page(self, page_num):
|
||||
if page_num in self.cache:
|
||||
return self.cache[page_num]
|
||||
with open(self.file_paths[page_num], 'rb') as f:
|
||||
page_data = pickle.load(f)
|
||||
self.cache[page_num] = page_data
|
||||
return page_data
|
||||
|
||||
def __getitem__(self, index):
|
||||
if index < 0 or index >= self.length:
|
||||
raise IndexError('Index out of range')
|
||||
page_num = index // self.page_size
|
||||
page_index = index % self.page_size
|
||||
page_data = self._load_page(page_num)
|
||||
return page_data[page_index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if index < 0 or index >= self.length:
|
||||
raise IndexError('Index out of range')
|
||||
page_num = index // self.page_size
|
||||
page_index = index % self.page_size
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[page_index] = value
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
|
||||
def __delitem__(self, index):
|
||||
if index < 0 or index >= self.length:
|
||||
raise IndexError('Index out of range')
|
||||
page_num = index // self.page_size
|
||||
page_index = index % self.page_size
|
||||
page_data = self._load_page(page_num)
|
||||
del page_data[page_index]
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
self.length -= 1
|
||||
|
||||
# Shift subsequent elements
|
||||
for i in range(index, self.length):
|
||||
next_page_num = (i + 1) // self.page_size
|
||||
next_page_index = (i + 1) % self.page_size
|
||||
if next_page_index == 0:
|
||||
self._save_page(page_data, page_num)
|
||||
page_num = next_page_num
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[page_index] = page_data.pop(next_page_index)
|
||||
page_index = next_page_index
|
||||
|
||||
# Save the last page
|
||||
self._save_page(page_data, page_num)
|
||||
|
||||
# Remove the last page if it's empty
|
||||
if self.length % self.page_size == 0:
|
||||
os.remove(self.file_paths.pop())
|
||||
self.cache.pop(page_num, None)
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
def __iter__(self):
|
||||
for page_num in range(len(self.file_paths)):
|
||||
page_data = self._load_page(page_num)
|
||||
for item in page_data:
|
||||
yield item
|
||||
|
||||
def append(self, value):
|
||||
page_num = self.length // self.page_size
|
||||
page_index = self.length % self.page_size
|
||||
if page_num >= len(self.file_paths):
|
||||
self._save_page([], page_num)
|
||||
page_data = self._load_page(page_num)
|
||||
page_data.append(value)
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
self.length += 1
|
||||
|
||||
def extend(self, values):
|
||||
for value in values:
|
||||
self.append(value)
|
||||
|
||||
def remove(self, value):
|
||||
for index, item in enumerate(self):
|
||||
if item == value:
|
||||
del self[index]
|
||||
return
|
||||
raise ValueError(f"{value} not in list")
|
||||
|
||||
def pop(self, index=-1):
|
||||
if self.length == 0:
|
||||
raise IndexError("pop from empty list")
|
||||
if index < 0:
|
||||
index += self.length
|
||||
value = self[index]
|
||||
del self[index]
|
||||
return value
|
||||
|
||||
def clear(self):
|
||||
self.cache.clear()
|
||||
self.file_paths = []
|
||||
self.length = 0
|
||||
|
||||
def index(self, value, start=0, stop=None):
|
||||
if stop is None:
|
||||
stop = self.length
|
||||
for i in range(start, stop):
|
||||
if self[i] == value:
|
||||
return i
|
||||
raise ValueError(f"{value} is not in list")
|
||||
|
||||
def get(self, index, default=None):
|
||||
try:
|
||||
return self[index]
|
||||
except IndexError:
|
||||
return default
|
||||
|
||||
def cleanup(self):
|
||||
if hasattr(self, 'file_paths'):
|
||||
for file_path in self.file_paths:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class PaginatedDict:
|
||||
instance_counter = 0
|
||||
|
||||
def __init__(self, page_size=1):
|
||||
self.page_size = page_size
|
||||
self.file_paths = []
|
||||
self.cache = {}
|
||||
self.key_to_page = {}
|
||||
self.length = 0 # Track the number of items
|
||||
self.instance_id = PaginatedDict.instance_counter
|
||||
PaginatedDict.instance_counter += 1
|
||||
self.temp_dir = tempfile.mkdtemp(prefix=f'paginated_dict_{self.instance_id}_', dir='/Users/snaow/repos/prowler')
|
||||
print(f"Temporary directory for instance {self.instance_id}: {self.temp_dir}")
|
||||
atexit.register(self.cleanup)
|
||||
|
||||
def _save_page(self, page_data, page_num):
|
||||
file_path = os.path.join(self.temp_dir, f'page_{page_num}.pkl')
|
||||
with open(file_path, 'wb') as f:
|
||||
pickle.dump(page_data, f)
|
||||
if page_num >= len(self.file_paths):
|
||||
self.file_paths.append(file_path)
|
||||
else:
|
||||
self.file_paths[page_num] = file_path
|
||||
|
||||
def _load_page(self, page_num):
|
||||
if page_num in self.cache:
|
||||
return self.cache[page_num]
|
||||
with open(self.file_paths[page_num], 'rb') as f:
|
||||
page_data = pickle.load(f)
|
||||
self.cache[page_num] = page_data
|
||||
return page_data
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self.key_to_page:
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[key] = value
|
||||
else:
|
||||
page_num = self.length // self.page_size
|
||||
if page_num >= len(self.file_paths):
|
||||
self._save_page({}, page_num)
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[key] = value
|
||||
self.key_to_page[key] = page_num
|
||||
self.length += 1
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key not in self.key_to_page:
|
||||
raise KeyError(f"Key {key} not found")
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
return page_data[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key not in self.key_to_page:
|
||||
raise KeyError(f"Key {key} not found")
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
del page_data[key]
|
||||
del self.key_to_page[key]
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
self.length -= 1
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
def __iter__(self):
|
||||
for page_num in range(len(self.file_paths)):
|
||||
page_data = self._load_page(page_num)
|
||||
for key in page_data:
|
||||
yield key
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def keys(self):
|
||||
for key in self:
|
||||
yield key
|
||||
|
||||
def values(self):
|
||||
for key in self:
|
||||
yield self[key]
|
||||
|
||||
def items(self):
|
||||
for key in self:
|
||||
yield (key, self[key])
|
||||
|
||||
def clear(self):
|
||||
self.cache.clear()
|
||||
self.key_to_page.clear()
|
||||
self.file_paths = []
|
||||
self.length = 0
|
||||
|
||||
def cleanup(self):
|
||||
for file_path in self.file_paths:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
################## S3
|
||||
class S3(AWSService):
|
||||
def __init__(self, provider):
|
||||
@@ -27,9 +292,13 @@ class S3(AWSService):
|
||||
self.__threading_call__(self.__get_object_lock_configuration__, self.buckets)
|
||||
self.__threading_call__(self.__get_bucket_tagging__, self.buckets)
|
||||
|
||||
def cleanup(self):
|
||||
del self.regions_with_buckets
|
||||
del self.buckets
|
||||
|
||||
def __list_buckets__(self, provider):
|
||||
logger.info("S3 - Listing buckets...")
|
||||
buckets = []
|
||||
buckets = PaginatedList()
|
||||
try:
|
||||
list_buckets = self.client.list_buckets()
|
||||
for bucket in list_buckets["Buckets"]:
|
||||
|
||||
+2
-20
@@ -1,7 +1,6 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import (
|
||||
is_condition_block_restrictive,
|
||||
is_condition_block_restrictive_organization,
|
||||
)
|
||||
from prowler.providers.aws.services.sns.sns_client import sns_client
|
||||
|
||||
@@ -34,30 +33,13 @@ class sns_topics_not_publicly_accessible(Check):
|
||||
and "*" in statement["Principal"]["CanonicalUser"]
|
||||
)
|
||||
):
|
||||
condition_account = False
|
||||
condition_org = False
|
||||
if (
|
||||
"Condition" in statement
|
||||
and is_condition_block_restrictive(
|
||||
statement["Condition"],
|
||||
sns_client.audited_account,
|
||||
statement["Condition"], sns_client.audited_account
|
||||
)
|
||||
):
|
||||
condition_account = True
|
||||
if (
|
||||
"Condition" in statement
|
||||
and is_condition_block_restrictive_organization(
|
||||
statement["Condition"],
|
||||
)
|
||||
):
|
||||
condition_org = True
|
||||
|
||||
if condition_account and condition_org:
|
||||
report.status_extended = f"SNS topic {topic.name} is not public because its policy only allows access from the account {sns_client.audited_account} and an organization."
|
||||
elif condition_account:
|
||||
report.status_extended = f"SNS topic {topic.name} is not public because its policy only allows access from the account {sns_client.audited_account}."
|
||||
elif condition_org:
|
||||
report.status_extended = f"SNS topic {topic.name} is not public because its policy only allows access from an organization."
|
||||
report.status_extended = f"SNS topic {topic.name} is not public because its policy only allows access from the same account."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"SNS topic {topic.name} is public because its policy allows public access."
|
||||
|
||||
+3
-16
@@ -11,25 +11,12 @@ class ssm_documents_set_as_public(Check):
|
||||
report.resource_arn = document.arn
|
||||
report.resource_id = document.name
|
||||
report.resource_tags = document.tags
|
||||
trusted_account_ids = ssm_client.audit_config.get("trusted_account_ids", [])
|
||||
if ssm_client.audited_account not in trusted_account_ids:
|
||||
trusted_account_ids.append(ssm_client.audited_account)
|
||||
if not document.account_owners or document.account_owners == [
|
||||
ssm_client.audited_account
|
||||
]:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"SSM Document {document.name} is not public."
|
||||
elif document.account_owners == ["all"]:
|
||||
if document.account_owners:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"SSM Document {document.name} is public."
|
||||
elif all(owner in trusted_account_ids for owner in document.account_owners):
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"SSM Document {document.name} is shared to trusted AWS accounts: {', '.join(document.account_owners)}."
|
||||
elif not all(
|
||||
owner in trusted_account_ids for owner in document.account_owners
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"SSM Document {document.name} is shared to non-trusted AWS accounts: {', '.join(document.account_owners)}."
|
||||
report.status_extended = f"SSM Document {document.name} is not public."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -10,15 +10,114 @@ 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
|
||||
|
||||
import pickle
|
||||
import os
|
||||
import atexit
|
||||
from collections import deque
|
||||
from sys import getsizeof
|
||||
import tempfile
|
||||
from memory_profiler import profile
|
||||
|
||||
class PaginatedDict:
|
||||
instance_counter = 0
|
||||
|
||||
def __init__(self, page_size=100):
|
||||
self.page_size = page_size
|
||||
self.file_paths = []
|
||||
self.cache = {}
|
||||
self.key_to_page = {}
|
||||
self.length = 0 # Track the number of items
|
||||
self.instance_id = PaginatedDict.instance_counter
|
||||
PaginatedDict.instance_counter += 1
|
||||
self.temp_dir = tempfile.mkdtemp(prefix=f'paginated_dict_{self.instance_id}_', dir='/Users/snaow/repos/prowler')
|
||||
print(f"Temporary directory for instance {self.instance_id}: {self.temp_dir}")
|
||||
atexit.register(self.cleanup)
|
||||
|
||||
def _save_page(self, page_data, page_num):
|
||||
file_path = os.path.join(self.temp_dir, f'page_{page_num}.pkl')
|
||||
with open(file_path, 'wb') as f:
|
||||
pickle.dump(page_data, f)
|
||||
if page_num >= len(self.file_paths):
|
||||
self.file_paths.append(file_path)
|
||||
else:
|
||||
self.file_paths[page_num] = file_path
|
||||
|
||||
def _load_page(self, page_num):
|
||||
if page_num in self.cache:
|
||||
return self.cache[page_num]
|
||||
with open(self.file_paths[page_num], 'rb') as f:
|
||||
page_data = pickle.load(f)
|
||||
self.cache[page_num] = page_data
|
||||
return page_data
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self.key_to_page:
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[key] = value
|
||||
else:
|
||||
page_num = self.length // self.page_size
|
||||
if page_num >= len(self.file_paths):
|
||||
self._save_page({}, page_num)
|
||||
page_data = self._load_page(page_num)
|
||||
page_data[key] = value
|
||||
self.key_to_page[key] = page_num
|
||||
self.length += 1
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key not in self.key_to_page:
|
||||
raise KeyError(f"Key {key} not found")
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
return page_data[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key not in self.key_to_page:
|
||||
raise KeyError(f"Key {key} not found")
|
||||
page_num = self.key_to_page[key]
|
||||
page_data = self._load_page(page_num)
|
||||
del page_data[key]
|
||||
del self.key_to_page[key]
|
||||
self.cache[page_num] = page_data
|
||||
self._save_page(page_data, page_num)
|
||||
self.length -= 1
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
def __iter__(self):
|
||||
for page_num in range(len(self.file_paths)):
|
||||
page_data = self._load_page(page_num)
|
||||
for key in page_data:
|
||||
yield key
|
||||
|
||||
def cleanup(self):
|
||||
for file_path in self.file_paths:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
################## SSM
|
||||
class SSM(AWSService):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.documents = {}
|
||||
self.compliance_resources = {}
|
||||
self.managed_instances = {}
|
||||
paginated = 0
|
||||
if paginated == 1:
|
||||
self.documents = PaginatedDict()
|
||||
self.compliance_resources = PaginatedDict()
|
||||
self.managed_instances = PaginatedDict()
|
||||
else:
|
||||
self.documents = {}
|
||||
self.compliance_resources = {}
|
||||
self.managed_instances = {}
|
||||
|
||||
self.__threading_call__(self.__list_documents__)
|
||||
self.__threading_call__(self.__get_document__)
|
||||
self.__threading_call__(self.__describe_document_permission__)
|
||||
|
||||
@@ -328,8 +328,6 @@ class VPC(AWSService):
|
||||
regional_client_for_subnet = self.regional_clients[
|
||||
regional_client.region
|
||||
]
|
||||
public = False
|
||||
nat_gateway = False
|
||||
route_tables_for_subnet = (
|
||||
regional_client_for_subnet.describe_route_tables(
|
||||
Filters=[
|
||||
@@ -352,20 +350,21 @@ class VPC(AWSService):
|
||||
]
|
||||
)
|
||||
)
|
||||
for route_table in route_tables_for_subnet.get(
|
||||
"RouteTables"
|
||||
):
|
||||
for route in route_table.get("Routes"):
|
||||
if (
|
||||
"GatewayId" in route
|
||||
and "igw" in route["GatewayId"]
|
||||
and route.get("DestinationCidrBlock", "")
|
||||
== "0.0.0.0/0"
|
||||
):
|
||||
# If the route table has a default route to an internet gateway, the subnet is public
|
||||
public = True
|
||||
if "NatGatewayId" in route:
|
||||
nat_gateway = True
|
||||
public = False
|
||||
nat_gateway = False
|
||||
for route in route_tables_for_subnet.get("RouteTables")[
|
||||
0
|
||||
].get("Routes"):
|
||||
if (
|
||||
"GatewayId" in route
|
||||
and "igw" in route["GatewayId"]
|
||||
and route.get("DestinationCidrBlock", "")
|
||||
== "0.0.0.0/0"
|
||||
):
|
||||
# If the route table has a default route to an internet gateway, the subnet is public
|
||||
public = True
|
||||
if "NatGatewayId" in route:
|
||||
nat_gateway = True
|
||||
subnet_name = ""
|
||||
for tag in subnet.get("Tags", []):
|
||||
if tag["Key"] == "Name":
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from prowler.lib.check.models import Check_Report_Azure
|
||||
from typing import Any
|
||||
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
from prowler.lib.outputs.utils import unroll_tags
|
||||
|
||||
|
||||
class AzureMutelist(Mutelist):
|
||||
def is_finding_muted(
|
||||
self,
|
||||
finding: Check_Report_Azure,
|
||||
finding: Any,
|
||||
) -> bool:
|
||||
return self.is_muted(
|
||||
finding.subscription,
|
||||
finding.check_metadata.CheckID,
|
||||
finding.location,
|
||||
finding.resource_name,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
unroll_tags(finding.resource_tags),
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ class GcpProvider(Provider):
|
||||
self._impersonated_service_account = arguments.impersonate_service_account
|
||||
list_project_ids = arguments.list_project_id
|
||||
|
||||
self._session, self._default_project_id = self.setup_session(
|
||||
self._session = self.setup_session(
|
||||
credentials_file, self._impersonated_service_account
|
||||
)
|
||||
|
||||
@@ -128,10 +128,6 @@ class GcpProvider(Provider):
|
||||
def projects(self):
|
||||
return self._projects
|
||||
|
||||
@property
|
||||
def default_project_id(self):
|
||||
return self._default_project_id
|
||||
|
||||
@property
|
||||
def impersonated_service_account(self):
|
||||
return self._impersonated_service_account
|
||||
@@ -202,14 +198,14 @@ class GcpProvider(Provider):
|
||||
# "partition": "identity.partition",
|
||||
}
|
||||
|
||||
def setup_session(self, credentials_file: str, service_account: str) -> tuple:
|
||||
def setup_session(self, credentials_file: str, service_account: str) -> Credentials:
|
||||
"""
|
||||
Setup the GCP session with the provided credentials file or service account to impersonate
|
||||
Args:
|
||||
credentials_file: str
|
||||
service_account: str
|
||||
Returns:
|
||||
Credentials object and default project ID
|
||||
Credentials object
|
||||
"""
|
||||
try:
|
||||
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
|
||||
@@ -219,7 +215,7 @@ class GcpProvider(Provider):
|
||||
self.__set_gcp_creds_env_var__(credentials_file)
|
||||
|
||||
# Get default credentials
|
||||
credentials, default_project_id = default(scopes=scopes)
|
||||
credentials, _ = default(scopes=scopes)
|
||||
|
||||
# Refresh the credentials to ensure they are valid
|
||||
credentials.refresh(Request())
|
||||
@@ -235,7 +231,7 @@ class GcpProvider(Provider):
|
||||
)
|
||||
logger.info(f"Impersonated credentials: {credentials}")
|
||||
|
||||
return credentials, default_project_id
|
||||
return credentials
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -283,9 +279,9 @@ class GcpProvider(Provider):
|
||||
response = request.execute()
|
||||
|
||||
for project in response.get("projects", []):
|
||||
labels = {}
|
||||
labels = []
|
||||
for key, value in project.get("labels", {}).items():
|
||||
labels[key] = value
|
||||
labels.append(f"{key}:{value}")
|
||||
|
||||
project_id = project["projectId"]
|
||||
gcp_project = GCPProject(
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from prowler.lib.check.models import Check_Report_GCP
|
||||
from typing import Any
|
||||
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
from prowler.lib.outputs.utils import unroll_tags
|
||||
|
||||
|
||||
class GCPMutelist(Mutelist):
|
||||
def is_finding_muted(
|
||||
self,
|
||||
finding: Check_Report_GCP,
|
||||
finding: Any,
|
||||
) -> bool:
|
||||
return self.is_muted(
|
||||
finding.project_id,
|
||||
finding.check_metadata.CheckID,
|
||||
finding.location,
|
||||
finding.resource_name,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
unroll_tags(finding.resource_tags),
|
||||
)
|
||||
|
||||
@@ -30,7 +30,6 @@ class GCPService:
|
||||
)
|
||||
# Only project ids that have their API enabled will be scanned
|
||||
self.project_ids = self.__is_api_active__(provider.project_ids)
|
||||
self.default_project_id = provider.default_project_id
|
||||
self.audit_config = provider.audit_config
|
||||
self.fixer_config = provider.fixer_config
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class GCPProject(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
organization: Optional[GCPOrganization]
|
||||
labels: dict
|
||||
labels: list[str]
|
||||
lifecycle_state: str
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -16,12 +16,12 @@ class apikeys_api_restrictions_configured(Check):
|
||||
if key.restrictions == {} or any(
|
||||
[
|
||||
target.get("service") == "cloudapis.googleapis.com"
|
||||
for target in key.restrictions.get("apiTargets", [])
|
||||
for target in key.restrictions["apiTargets"]
|
||||
]
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"API key {key.name} does not have restrictions configured."
|
||||
f"API key {key.name} doens't have restrictions configured."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ class cloudsql_instance_ssl_connections(Check):
|
||||
report.status_extended = (
|
||||
f"Database Instance {instance.name} requires SSL connections."
|
||||
)
|
||||
if not instance.require_ssl or instance.ssl_mode != "ENCRYPTED_ONLY":
|
||||
if not instance.ssl:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Database Instance {instance.name} does not require SSL connections."
|
||||
findings.append(report)
|
||||
|
||||
@@ -31,12 +31,9 @@ class CloudSQL(GCPService):
|
||||
region=instance["region"],
|
||||
ip_addresses=instance.get("ipAddresses", []),
|
||||
public_ip=public_ip,
|
||||
require_ssl=instance["settings"]["ipConfiguration"].get(
|
||||
ssl=instance["settings"]["ipConfiguration"].get(
|
||||
"requireSsl", False
|
||||
),
|
||||
ssl_mode=instance["settings"]["ipConfiguration"].get(
|
||||
"sslMode", "ALLOW_UNENCRYPTED_AND_ENCRYPTED"
|
||||
),
|
||||
automated_backups=instance["settings"][
|
||||
"backupConfiguration"
|
||||
]["enabled"],
|
||||
@@ -64,8 +61,7 @@ class Instance(BaseModel):
|
||||
region: str
|
||||
public_ip: bool
|
||||
authorized_networks: list
|
||||
require_ssl: bool
|
||||
ssl_mode: str
|
||||
ssl: bool
|
||||
automated_backups: bool
|
||||
flags: list
|
||||
project_id: str
|
||||
|
||||
@@ -283,23 +283,20 @@ class Compute(GCPService):
|
||||
|
||||
def __describe_backend_service__(self):
|
||||
for balancer in self.load_balancers:
|
||||
if balancer.service:
|
||||
try:
|
||||
response = (
|
||||
self.client.backendServices()
|
||||
.get(
|
||||
project=balancer.project_id,
|
||||
backendService=balancer.service.split("/")[-1],
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
balancer.logging = response.get("logConfig", {}).get(
|
||||
"enable", False
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
try:
|
||||
response = (
|
||||
self.client.backendServices()
|
||||
.get(
|
||||
project=balancer.project_id,
|
||||
backendService=balancer.service.split("/")[-1],
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
balancer.logging = response.get("logConfig", {}).get("enable", False)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
class Instance(BaseModel):
|
||||
|
||||
@@ -25,9 +25,8 @@ class DNS(GCPService):
|
||||
ManagedZone(
|
||||
name=managed_zone["name"],
|
||||
id=managed_zone["id"],
|
||||
dnssec=managed_zone.get("dnssecConfig", {})["state"]
|
||||
== "on",
|
||||
key_specs=managed_zone.get("dnssecConfig", {})[
|
||||
dnssec=managed_zone["dnssecConfig"]["state"] == "on",
|
||||
key_specs=managed_zone["dnssecConfig"][
|
||||
"defaultKeySpecs"
|
||||
],
|
||||
project_id=project_id,
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ class iam_organization_essential_contacts_configured(Check):
|
||||
findings = []
|
||||
for org in essentialcontacts_client.organizations:
|
||||
report = Check_Report_GCP(self.metadata())
|
||||
report.project_id = essentialcontacts_client.default_project_id
|
||||
report.project_id = org.id
|
||||
report.resource_id = org.id
|
||||
report.resource_name = org.name
|
||||
report.location = essentialcontacts_client.region
|
||||
|
||||
@@ -29,12 +29,12 @@ class IAM(GCPService):
|
||||
while request is not None:
|
||||
response = request.execute()
|
||||
|
||||
for account in response.get("accounts", []):
|
||||
for account in response["accounts"]:
|
||||
self.service_accounts.append(
|
||||
ServiceAccount(
|
||||
name=account["name"],
|
||||
email=account["email"],
|
||||
display_name=account["displayName"],
|
||||
display_name=account.get("displayName", ""),
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
@@ -65,7 +65,7 @@ class IAM(GCPService):
|
||||
)
|
||||
response = request.execute()
|
||||
|
||||
for key in response.get("keys", []):
|
||||
for key in response["keys"]:
|
||||
sa.keys.append(
|
||||
Key(
|
||||
name=key["name"].split("/")[-1],
|
||||
@@ -149,7 +149,7 @@ class EssentialContacts(GCPService):
|
||||
.contacts()
|
||||
.list(parent="organizations/" + org.id)
|
||||
).execute()
|
||||
if len(response.get("contacts", [])) > 0:
|
||||
if len(response["contacts"]) > 0:
|
||||
contacts = True
|
||||
|
||||
self.organizations.append(
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ class kms_key_not_publicly_accessible(Check):
|
||||
for key in kms_client.crypto_keys:
|
||||
report = Check_Report_GCP(self.metadata())
|
||||
report.project_id = key.project_id
|
||||
report.resource_id = key.id
|
||||
report.resource_id = key.name
|
||||
report.resource_name = key.name
|
||||
report.location = key.location
|
||||
report.status = "PASS"
|
||||
|
||||
+11
-31
@@ -1,5 +1,3 @@
|
||||
import datetime
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||
from prowler.providers.gcp.services.kms.kms_client import kms_client
|
||||
|
||||
@@ -10,39 +8,21 @@ class kms_key_rotation_enabled(Check):
|
||||
for key in kms_client.crypto_keys:
|
||||
report = Check_Report_GCP(self.metadata())
|
||||
report.project_id = key.project_id
|
||||
report.resource_id = key.id
|
||||
report.resource_id = key.name
|
||||
report.resource_name = key.name
|
||||
report.location = key.location
|
||||
now = datetime.datetime.now()
|
||||
condition_next_rotation_time = False
|
||||
if key.next_rotation_time:
|
||||
try:
|
||||
next_rotation_time = datetime.datetime.strptime(
|
||||
key.next_rotation_time, "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
)
|
||||
except ValueError:
|
||||
next_rotation_time = datetime.datetime.strptime(
|
||||
key.next_rotation_time, "%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
condition_next_rotation_time = (
|
||||
abs((next_rotation_time - now).days) <= 90
|
||||
)
|
||||
condition_rotation_period = False
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Key {key.name} is not rotated every 90 days or less."
|
||||
)
|
||||
if key.rotation_period:
|
||||
condition_rotation_period = (
|
||||
if (
|
||||
int(key.rotation_period[:-1]) // (24 * 3600) <= 90
|
||||
)
|
||||
if condition_rotation_period and condition_next_rotation_time:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Key {key.name} is rotated every 90 days or less and the next rotation time is in less than 90 days."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
if condition_rotation_period:
|
||||
report.status_extended = f"Key {key.name} is rotated every 90 days or less but the next rotation time is in more than 90 days."
|
||||
elif condition_next_rotation_time:
|
||||
report.status_extended = f"Key {key.name} is not rotated every 90 days or less but the next rotation time is in less than 90 days."
|
||||
else:
|
||||
report.status_extended = f"Key {key.name} is not rotated every 90 days or less and the next rotation time is in more than 90 days."
|
||||
): # Convert seconds to days and check if less or equal than 90
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Key {key.name} is rotated every 90 days or less."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
@@ -88,11 +88,9 @@ class KMS(GCPService):
|
||||
for key in response.get("cryptoKeys", []):
|
||||
self.crypto_keys.append(
|
||||
CriptoKey(
|
||||
id=key["name"],
|
||||
name=key["name"].split("/")[-1],
|
||||
location=key["name"].split("/")[3],
|
||||
rotation_period=key.get("rotationPeriod"),
|
||||
next_rotation_time=key.get("nextRotationTime"),
|
||||
key_ring=ring.name,
|
||||
project_id=ring.project_id,
|
||||
)
|
||||
@@ -141,11 +139,9 @@ class KeyRing(BaseModel):
|
||||
|
||||
|
||||
class CriptoKey(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
location: str
|
||||
rotation_period: Optional[str]
|
||||
next_rotation_time: Optional[str]
|
||||
key_ring: str
|
||||
members: list = []
|
||||
project_id: str
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from prowler.lib.check.models import Check_Report_Kubernetes
|
||||
from typing import Any
|
||||
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
from prowler.lib.outputs.utils import unroll_tags
|
||||
|
||||
|
||||
class KubernetesMutelist(Mutelist):
|
||||
def is_finding_muted(
|
||||
self,
|
||||
finding: Check_Report_Kubernetes,
|
||||
finding: Any,
|
||||
cluster: str,
|
||||
) -> bool:
|
||||
return self.is_muted(
|
||||
@@ -14,5 +15,5 @@ class KubernetesMutelist(Mutelist):
|
||||
finding.check_metadata.CheckID,
|
||||
finding.namespace,
|
||||
finding.resource_name,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
unroll_tags(finding.resource_tags),
|
||||
)
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ packages = [
|
||||
{include = "dashboard"}
|
||||
]
|
||||
readme = "README.md"
|
||||
version = "4.3.7"
|
||||
version = "4.3.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
alive-progress = "3.1.5"
|
||||
@@ -46,14 +46,14 @@ azure-mgmt-storage = "21.2.1"
|
||||
azure-mgmt-subscription = "3.1.1"
|
||||
azure-mgmt-web = "7.3.0"
|
||||
azure-storage-blob = "12.21.0"
|
||||
boto3 = "1.34.151"
|
||||
botocore = "1.34.151"
|
||||
boto3 = "1.34.149"
|
||||
botocore = "1.34.150"
|
||||
colorama = "0.4.6"
|
||||
cryptography = "43.0.0"
|
||||
dash = "2.17.1"
|
||||
dash-bootstrap-components = "1.6.0"
|
||||
detect-secrets = "1.5.0"
|
||||
google-api-python-client = "2.139.0"
|
||||
google-api-python-client = "2.138.0"
|
||||
google-auth-httplib2 = ">=0.1,<0.3"
|
||||
jsonschema = "4.23.0"
|
||||
kubernetes = "30.1.0"
|
||||
|
||||
@@ -15,7 +15,6 @@ from prowler.providers.aws.aws_provider import get_aws_available_regions
|
||||
|
||||
MOCK_PROWLER_VERSION = "3.3.0"
|
||||
MOCK_OLD_PROWLER_VERSION = "0.0.0"
|
||||
MOCK_PROWLER_MASTER_VERSION = "3.4.0"
|
||||
|
||||
|
||||
def mock_prowler_get_latest_release(_, **kwargs):
|
||||
@@ -327,18 +326,6 @@ class Test_Config:
|
||||
== f"Prowler {MOCK_OLD_PROWLER_VERSION} (latest is {MOCK_PROWLER_VERSION}, upgrade for the latest features)"
|
||||
)
|
||||
|
||||
@mock.patch(
|
||||
"prowler.config.config.requests.get", new=mock_prowler_get_latest_release
|
||||
)
|
||||
@mock.patch(
|
||||
"prowler.config.config.prowler_version", new=MOCK_PROWLER_MASTER_VERSION
|
||||
)
|
||||
def test_check_current_version_with_master_version(self):
|
||||
assert (
|
||||
check_current_version()
|
||||
== f"Prowler {MOCK_PROWLER_MASTER_VERSION} (You are running the latest version, yay!)"
|
||||
)
|
||||
|
||||
def test_get_available_compliance_frameworks(self):
|
||||
compliance_frameworks = [
|
||||
"cisa_aws",
|
||||
|
||||
@@ -43,7 +43,6 @@ aws:
|
||||
]
|
||||
|
||||
# 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.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -19,7 +19,6 @@ ec2_allowed_instance_owners:
|
||||
]
|
||||
|
||||
# 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.
|
||||
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
|
||||
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
|
||||
|
||||
@@ -6,7 +6,6 @@ from argparse import Namespace
|
||||
from importlib.machinery import FileFinder
|
||||
from logging import DEBUG, ERROR
|
||||
from pkgutil import ModuleInfo
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from colorama import Fore, Style
|
||||
@@ -16,7 +15,6 @@ from moto import mock_aws
|
||||
from prowler.lib.check.check import (
|
||||
exclude_checks_to_run,
|
||||
exclude_services_to_run,
|
||||
execute,
|
||||
list_categories,
|
||||
list_checks_json,
|
||||
list_modules,
|
||||
@@ -31,16 +29,8 @@ from prowler.lib.check.check import (
|
||||
)
|
||||
from prowler.lib.check.models import load_check_metadata
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.services.accessanalyzer.accessanalyzer_service import (
|
||||
Analyzer,
|
||||
)
|
||||
from tests.lib.check.fixtures.bulk_checks_metadata import test_bulk_checks_metadata
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_ARN,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1
|
||||
|
||||
# AWS_ACCOUNT_NUMBER = "123456789012"
|
||||
# AWS_REGION = "us-east-1"
|
||||
@@ -802,65 +792,6 @@ class TestCheck:
|
||||
== '{\n "aws": [\n "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled",\n "awslambda_function_no_secrets_in_code",\n "awslambda_function_no_secrets_in_variables",\n "awslambda_function_not_publicly_accessible",\n "awslambda_function_url_cors_policy",\n "awslambda_function_url_public",\n "awslambda_function_using_supported_runtimes"\n ]\n}'
|
||||
)
|
||||
|
||||
def test_execute(self):
|
||||
accessanalyzer_client = mock.MagicMock
|
||||
accessanalyzer_client.region = AWS_REGION_US_EAST_1
|
||||
accessanalyzer_client.analyzers = [
|
||||
Analyzer(
|
||||
arn=AWS_ACCOUNT_ARN,
|
||||
name=AWS_ACCOUNT_NUMBER,
|
||||
status="NOT_AVAILABLE",
|
||||
tags=[],
|
||||
type="",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
)
|
||||
]
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.accessanalyzer.accessanalyzer_service.AccessAnalyzer",
|
||||
accessanalyzer_client,
|
||||
):
|
||||
findings = execute(
|
||||
service="accessanalyzer",
|
||||
check_name="accessanalyzer_enabled",
|
||||
global_provider=set_mocked_aws_provider(
|
||||
expected_checks=["accessanalyzer_enabled"]
|
||||
),
|
||||
services_executed={"accessanalyzer"},
|
||||
checks_executed={"accessanalyzer_enabled"},
|
||||
custom_checks_metadata=None,
|
||||
)
|
||||
assert len(findings) == 1
|
||||
|
||||
def test_execute_with_filtering_status(self):
|
||||
accessanalyzer_client = mock.MagicMock
|
||||
accessanalyzer_client.region = AWS_REGION_US_EAST_1
|
||||
accessanalyzer_client.analyzers = [
|
||||
Analyzer(
|
||||
arn=AWS_ACCOUNT_ARN,
|
||||
name=AWS_ACCOUNT_NUMBER,
|
||||
status="NOT_AVAILABLE",
|
||||
tags=[],
|
||||
type="",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
)
|
||||
]
|
||||
status = ["PASS"]
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.accessanalyzer.accessanalyzer_service.AccessAnalyzer",
|
||||
accessanalyzer_client,
|
||||
):
|
||||
findings = execute(
|
||||
service="accessanalyzer",
|
||||
check_name="accessanalyzer_enabled",
|
||||
global_provider=set_mocked_aws_provider(
|
||||
status=status, expected_checks=["accessanalyzer_enabled"]
|
||||
),
|
||||
services_executed={"accessanalyzer"},
|
||||
checks_executed={"accessanalyzer_enabled"},
|
||||
custom_checks_metadata=None,
|
||||
)
|
||||
assert len(findings) == 0
|
||||
|
||||
def test_run_check(self, caplog):
|
||||
caplog.set_level(DEBUG)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class TestASFF:
|
||||
resource_details="Test resource details",
|
||||
resource_name="test-resource",
|
||||
resource_uid="test-arn",
|
||||
resource_tags={"key1": "value1"},
|
||||
resource_tags="key1=value1",
|
||||
)
|
||||
|
||||
timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
@@ -70,7 +70,7 @@ class TestASFF:
|
||||
Type=finding.resource_type,
|
||||
Partition=AWS_COMMERCIAL_PARTITION,
|
||||
Region=AWS_REGION_EU_WEST_1,
|
||||
Tags={"key1": "value1"},
|
||||
Tags=ASFF.format_resource_tags(finding.resource_tags),
|
||||
)
|
||||
],
|
||||
Compliance=Compliance(
|
||||
@@ -84,7 +84,7 @@ class TestASFF:
|
||||
Url=finding.remediation_recommendation_url,
|
||||
)
|
||||
),
|
||||
Description=finding.status_extended,
|
||||
Description=finding.description,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -103,7 +103,7 @@ class TestASFF:
|
||||
resource_details="Test resource details",
|
||||
resource_name="test-resource",
|
||||
resource_uid="test-arn",
|
||||
resource_tags={"key1": "value1"},
|
||||
resource_tags="key1=value1",
|
||||
)
|
||||
finding.remediation_recommendation_url = ""
|
||||
|
||||
@@ -136,7 +136,7 @@ class TestASFF:
|
||||
Type=finding.resource_type,
|
||||
Partition=AWS_COMMERCIAL_PARTITION,
|
||||
Region=AWS_REGION_EU_WEST_1,
|
||||
Tags={"key1": "value1"},
|
||||
Tags=ASFF.format_resource_tags(finding.resource_tags),
|
||||
)
|
||||
],
|
||||
Compliance=Compliance(
|
||||
@@ -150,72 +150,7 @@ class TestASFF:
|
||||
Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html",
|
||||
)
|
||||
),
|
||||
Description=finding.status_extended,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
|
||||
assert len(asff.data) == 1
|
||||
asff_finding = asff.data[0]
|
||||
|
||||
assert asff_finding == expected
|
||||
|
||||
def test_asff_without_resource_tags(self):
|
||||
status = "PASS"
|
||||
finding = generate_finding_output(
|
||||
status=status,
|
||||
status_extended="This is a test",
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
resource_details="Test resource details",
|
||||
resource_name="test-resource",
|
||||
resource_uid="test-arn",
|
||||
)
|
||||
finding.remediation_recommendation_url = ""
|
||||
|
||||
timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
associated_standards, compliance_summary = ASFF.format_compliance(
|
||||
finding.compliance
|
||||
)
|
||||
|
||||
timestamp = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
expected = AWSSecurityFindingFormat(
|
||||
Id=f"prowler-{finding.check_id}-{AWS_ACCOUNT_NUMBER}-{AWS_REGION_EU_WEST_1}-{hash_sha512(finding.resource_uid)}",
|
||||
ProductArn=f"arn:{AWS_COMMERCIAL_PARTITION}:securityhub:{AWS_REGION_EU_WEST_1}::product/prowler/prowler",
|
||||
ProductFields=ProductFields(
|
||||
ProviderVersion=prowler_version,
|
||||
ProwlerResourceName=finding.resource_uid,
|
||||
),
|
||||
GeneratorId="prowler-" + finding.check_id,
|
||||
AwsAccountId=AWS_ACCOUNT_NUMBER,
|
||||
Types=finding.check_type.split(","),
|
||||
FirstObservedAt=timestamp,
|
||||
UpdatedAt=timestamp,
|
||||
CreatedAt=timestamp,
|
||||
Severity=Severity(Label=finding.severity),
|
||||
Title=finding.check_title,
|
||||
Resources=[
|
||||
Resource(
|
||||
Id=finding.resource_uid,
|
||||
Type=finding.resource_type,
|
||||
Partition=AWS_COMMERCIAL_PARTITION,
|
||||
Region=AWS_REGION_EU_WEST_1,
|
||||
Tags=None,
|
||||
)
|
||||
],
|
||||
Compliance=Compliance(
|
||||
Status=ASFF.generate_status(status),
|
||||
RelatedRequirements=compliance_summary,
|
||||
AssociatedStandards=associated_standards,
|
||||
),
|
||||
Remediation=Remediation(
|
||||
Recommendation=Recommendation(
|
||||
Text=finding.remediation_recommendation_text,
|
||||
Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html",
|
||||
)
|
||||
),
|
||||
Description=finding.status_extended,
|
||||
Description=finding.description,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -236,7 +171,7 @@ class TestASFF:
|
||||
resource_details="Test resource details",
|
||||
resource_name="test-resource",
|
||||
resource_uid="test-arn",
|
||||
resource_tags={"key1": "value1"},
|
||||
resource_tags="key1=value1",
|
||||
)
|
||||
finding.remediation_recommendation_url = ""
|
||||
finding.remediation_recommendation_text = "x" * 513
|
||||
@@ -270,7 +205,7 @@ class TestASFF:
|
||||
Type=finding.resource_type,
|
||||
Partition=AWS_COMMERCIAL_PARTITION,
|
||||
Region=AWS_REGION_EU_WEST_1,
|
||||
Tags={"key1": "value1"},
|
||||
Tags=ASFF.format_resource_tags(finding.resource_tags),
|
||||
)
|
||||
],
|
||||
Compliance=Compliance(
|
||||
@@ -284,7 +219,7 @@ class TestASFF:
|
||||
Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html",
|
||||
)
|
||||
),
|
||||
Description=finding.status_extended,
|
||||
Description=finding.description,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -304,7 +239,7 @@ class TestASFF:
|
||||
resource_details="Test resource details",
|
||||
resource_name="test-resource",
|
||||
resource_uid="test-arn",
|
||||
resource_tags={"key1": "value1"},
|
||||
resource_tags="key1=value1",
|
||||
compliance={
|
||||
"CISA": ["your-systems-3", "your-data-2"],
|
||||
"SOC2": ["cc_2_1", "cc_7_2", "cc_a_1_2"],
|
||||
@@ -477,7 +412,7 @@ class TestASFF:
|
||||
Type=finding.resource_type,
|
||||
Partition=AWS_COMMERCIAL_PARTITION,
|
||||
Region=AWS_REGION_EU_WEST_1,
|
||||
Tags={"key1": "value1"},
|
||||
Tags=ASFF.format_resource_tags(finding.resource_tags),
|
||||
)
|
||||
],
|
||||
Compliance=Compliance(
|
||||
@@ -491,7 +426,7 @@ class TestASFF:
|
||||
Url=finding.remediation_recommendation_url,
|
||||
)
|
||||
),
|
||||
Description=finding.status_extended,
|
||||
Description=finding.description,
|
||||
)
|
||||
|
||||
asff = ASFF(findings=[finding])
|
||||
@@ -513,7 +448,7 @@ class TestASFF:
|
||||
resource_details="Test resource details",
|
||||
resource_name="test-resource",
|
||||
resource_uid="test-arn",
|
||||
resource_tags={"key1": "value1"},
|
||||
resource_tags="key1=value1",
|
||||
)
|
||||
finding.remediation_recommendation_url = ""
|
||||
|
||||
@@ -538,7 +473,7 @@ class TestASFF:
|
||||
"CreatedAt": timestamp,
|
||||
"Severity": {"Label": "HIGH"},
|
||||
"Title": "test-check-id",
|
||||
"Description": "This is a test",
|
||||
"Description": "check description",
|
||||
"Resources": [
|
||||
{
|
||||
"Type": "test-resource",
|
||||
@@ -582,3 +517,14 @@ class TestASFF:
|
||||
assert ASFF.generate_status("FAIL") == "FAILED"
|
||||
assert ASFF.generate_status("FAIL", True) == "WARNING"
|
||||
assert ASFF.generate_status("SOMETHING ELSE") == "NOT_AVAILABLE"
|
||||
|
||||
def test_asff_format_resource_tags(self):
|
||||
assert ASFF.format_resource_tags(None) is None
|
||||
assert ASFF.format_resource_tags("") is None
|
||||
assert ASFF.format_resource_tags([]) is None
|
||||
assert ASFF.format_resource_tags([{}]) is None
|
||||
assert ASFF.format_resource_tags("key1=value1") == {"key1": "value1"}
|
||||
assert ASFF.format_resource_tags("key1=value1 | key2=value2") == {
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestCSV:
|
||||
resource_uid="resource-123",
|
||||
resource_name="Example Resource",
|
||||
resource_details="Detailed information about the resource",
|
||||
resource_tags={"tag1": "value1", "tag2": "value2"},
|
||||
resource_tags="tag1,tag2",
|
||||
partition="aws",
|
||||
description="Description of the finding",
|
||||
risk="High",
|
||||
@@ -78,7 +78,7 @@ class TestCSV:
|
||||
assert (
|
||||
output_data["RESOURCE_DETAILS"] == "Detailed information about the resource"
|
||||
)
|
||||
assert output_data["RESOURCE_TAGS"] == "tag1=value1 | tag2=value2"
|
||||
assert output_data["RESOURCE_TAGS"] == "tag1,tag2"
|
||||
assert output_data["PARTITION"] == "aws"
|
||||
assert output_data["REGION"] == AWS_REGION_EU_WEST_1
|
||||
assert output_data["DESCRIPTION"] == "Description of the finding"
|
||||
|
||||
@@ -12,7 +12,7 @@ def mock_get_provider_data_mapping_aws(_):
|
||||
"account_email": "mock_account_email",
|
||||
"account_organization_uid": "mock_account_org_uid",
|
||||
"account_organization_name": "mock_account_org_name",
|
||||
"account_tags": {"tag1": "value1"},
|
||||
"account_tags": ["tag1", "tag2"],
|
||||
"finding_uid": "mock_finding_uid",
|
||||
"provider": "aws",
|
||||
"check_id": "mock_check_id",
|
||||
@@ -28,7 +28,7 @@ def mock_get_provider_data_mapping_aws(_):
|
||||
"resource_uid": "mock_resource_uid",
|
||||
"resource_name": "mock_resource_name",
|
||||
"resource_details": "mock_resource_details",
|
||||
"resource_tags": {"tag1": "value1"},
|
||||
"resource_tags": "mock_resource_tags",
|
||||
"partition": None,
|
||||
"region": "mock_region",
|
||||
"description": "mock_description",
|
||||
@@ -58,7 +58,7 @@ def mock_get_provider_data_mapping_azure(_):
|
||||
"account_email": "mock_account_email",
|
||||
"account_organization_uid": "mock_account_org_uid",
|
||||
"account_organization_name": "mock_account_org_name",
|
||||
"account_tags": {"tag1": "value1"},
|
||||
"account_tags": ["tag1", "tag2"],
|
||||
"finding_uid": "mock_finding_uid",
|
||||
"provider": "azure",
|
||||
"check_id": "mock_check_id",
|
||||
@@ -74,7 +74,7 @@ def mock_get_provider_data_mapping_azure(_):
|
||||
"resource_uid": "mock_resource_uid",
|
||||
"resource_name": "mock_resource_name",
|
||||
"resource_details": "mock_resource_details",
|
||||
"resource_tags": {"tag1": "value1"},
|
||||
"resource_tags": "mock_resource_tags",
|
||||
"partition": None,
|
||||
"description": "mock_description",
|
||||
"risk": "mock_risk",
|
||||
@@ -103,7 +103,7 @@ def mock_get_provider_data_mapping_gcp(_):
|
||||
"account_email": "mock_account_email",
|
||||
"account_organization_uid": "mock_account_org_uid",
|
||||
"account_organization_name": "mock_account_org_name",
|
||||
"account_tags": {"tag1": "value1"},
|
||||
"account_tags": ["tag1", "tag2"],
|
||||
"finding_uid": "mock_finding_uid",
|
||||
"provider": "gcp",
|
||||
"check_id": "mock_check_id",
|
||||
@@ -119,7 +119,7 @@ def mock_get_provider_data_mapping_gcp(_):
|
||||
"resource_uid": "mock_resource_uid",
|
||||
"resource_name": "mock_resource_name",
|
||||
"resource_details": "mock_resource_details",
|
||||
"resource_tags": {"tag1": "value1"},
|
||||
"resource_tags": "mock_resource_tags",
|
||||
"partition": None,
|
||||
"description": "mock_description",
|
||||
"risk": "mock_risk",
|
||||
@@ -148,7 +148,7 @@ def mock_get_provider_data_mapping_kubernetes(_):
|
||||
"account_email": "mock_account_email",
|
||||
"account_organization_uid": "mock_account_org_uid",
|
||||
"account_organization_name": "mock_account_org_name",
|
||||
"account_tags": {"tag1": "value1"},
|
||||
"account_tags": ["tag1", "tag2"],
|
||||
"finding_uid": "mock_finding_uid",
|
||||
"provider": "kubernetes",
|
||||
"check_id": "mock_check_id",
|
||||
@@ -164,7 +164,7 @@ def mock_get_provider_data_mapping_kubernetes(_):
|
||||
"resource_uid": "mock_resource_uid",
|
||||
"resource_name": "mock_resource_name",
|
||||
"resource_details": "mock_resource_details",
|
||||
"resource_tags": {"tag1": "value1"},
|
||||
"resource_tags": "mock_resource_tags",
|
||||
"partition": None,
|
||||
"description": "mock_description",
|
||||
"risk": "mock_risk",
|
||||
@@ -240,7 +240,7 @@ class TestFinding:
|
||||
assert finding_output.subservice_name == "mock_subservice_name"
|
||||
assert finding_output.severity == Severity.high
|
||||
assert finding_output.resource_type == "mock_resource_type"
|
||||
assert finding_output.resource_tags == {"tag1": "value1"}
|
||||
assert finding_output.resource_tags == "mock_resource_tags"
|
||||
assert finding_output.partition is None
|
||||
assert finding_output.description == "mock_description"
|
||||
assert finding_output.risk == "mock_risk"
|
||||
@@ -260,7 +260,7 @@ class TestFinding:
|
||||
assert finding_output.account_email == "mock_account_email"
|
||||
assert finding_output.account_organization_uid == "mock_account_org_uid"
|
||||
assert finding_output.account_organization_name == "mock_account_org_name"
|
||||
assert finding_output.account_tags == {"tag1": "value1"}
|
||||
assert finding_output.account_tags == ["tag1", "tag2"]
|
||||
assert finding_output.prowler_version == "1.0.0"
|
||||
|
||||
@patch(
|
||||
@@ -318,7 +318,7 @@ class TestFinding:
|
||||
assert finding_output.subservice_name == "mock_subservice_name"
|
||||
assert finding_output.severity == Severity.high
|
||||
assert finding_output.resource_type == "mock_resource_type"
|
||||
assert finding_output.resource_tags == {"tag1": "value1"}
|
||||
assert finding_output.resource_tags == "mock_resource_tags"
|
||||
assert finding_output.partition is None
|
||||
assert finding_output.description == "mock_description"
|
||||
assert finding_output.risk == "mock_risk"
|
||||
@@ -353,7 +353,7 @@ class TestFinding:
|
||||
organization.display_name = "mock_organization_name"
|
||||
project.id = "mock_project_id"
|
||||
project.name = "mock_project_name"
|
||||
project.labels = {"tag1": "value1"}
|
||||
project.labels = ["label1", "label2"]
|
||||
project.organization = organization
|
||||
|
||||
provider.projects = {"mock_project_id": project}
|
||||
@@ -388,7 +388,7 @@ class TestFinding:
|
||||
assert finding_output.subservice_name == "mock_subservice_name"
|
||||
assert finding_output.severity == Severity.high
|
||||
assert finding_output.resource_type == "mock_resource_type"
|
||||
assert finding_output.resource_tags == {"tag1": "value1"}
|
||||
assert finding_output.resource_tags == "mock_resource_tags"
|
||||
assert finding_output.partition is None
|
||||
assert finding_output.description == "mock_description"
|
||||
assert finding_output.risk == "mock_risk"
|
||||
@@ -408,7 +408,7 @@ class TestFinding:
|
||||
assert finding_output.account_email == "mock_account_email"
|
||||
assert finding_output.account_organization_uid == "mock_organization_id"
|
||||
assert finding_output.account_organization_name == "mock_account_org_name"
|
||||
assert finding_output.account_tags == {"tag1": "value1"}
|
||||
assert finding_output.account_tags == ["label1", "label2"]
|
||||
assert finding_output.prowler_version == "1.0.0"
|
||||
assert finding_output.timestamp == 1622520000
|
||||
|
||||
@@ -459,7 +459,7 @@ class TestFinding:
|
||||
assert finding_output.subservice_name == "mock_subservice_name"
|
||||
assert finding_output.severity == Severity.high
|
||||
assert finding_output.resource_type == "mock_resource_type"
|
||||
assert finding_output.resource_tags == {"tag1": "value1"}
|
||||
assert finding_output.resource_tags == "mock_resource_tags"
|
||||
assert finding_output.partition is None
|
||||
assert finding_output.description == "mock_description"
|
||||
assert finding_output.risk == "mock_risk"
|
||||
@@ -479,6 +479,6 @@ class TestFinding:
|
||||
assert finding_output.account_email == "mock_account_email"
|
||||
assert finding_output.account_organization_uid == "mock_account_org_uid"
|
||||
assert finding_output.account_organization_name == "mock_account_org_name"
|
||||
assert finding_output.account_tags == {"tag1": "value1"}
|
||||
assert finding_output.account_tags == ["tag1", "tag2"]
|
||||
assert finding_output.prowler_version == "1.0.0"
|
||||
assert finding_output.timestamp == 1622520000
|
||||
|
||||
@@ -16,7 +16,7 @@ def generate_finding_output(
|
||||
resource_details: str = "",
|
||||
resource_uid: str = "",
|
||||
resource_name: str = "",
|
||||
resource_tags: dict = {},
|
||||
resource_tags: str = "",
|
||||
compliance: dict = {"test-compliance": "test-compliance"},
|
||||
timestamp: datetime = None,
|
||||
provider: str = "aws",
|
||||
@@ -34,10 +34,6 @@ def generate_finding_output(
|
||||
depends_on: str = "test-dependency",
|
||||
related_to: str = "test-related-to",
|
||||
notes: str = "test-notes",
|
||||
service_name: str = "test-service",
|
||||
check_id: str = "test-check-id",
|
||||
check_title: str = "test-check-id",
|
||||
check_type: str = "test-type",
|
||||
) -> Finding:
|
||||
return Finding(
|
||||
auth_method="profile: default",
|
||||
@@ -47,16 +43,16 @@ def generate_finding_output(
|
||||
account_email="",
|
||||
account_organization_uid="test-organization-id",
|
||||
account_organization_name="test-organization",
|
||||
account_tags={"test-tag": "test-value"},
|
||||
account_tags=["test-tag:test-value"],
|
||||
finding_uid="test-unique-finding",
|
||||
provider=provider,
|
||||
check_id=check_id,
|
||||
check_title=check_title,
|
||||
check_type=check_type,
|
||||
check_id="test-check-id",
|
||||
check_title="test-check-id",
|
||||
check_type="test-type",
|
||||
status=status,
|
||||
status_extended=status_extended,
|
||||
muted=muted,
|
||||
service_name=service_name,
|
||||
service_name="test-service",
|
||||
subservice_name="",
|
||||
severity=severity,
|
||||
resource_type="test-resource",
|
||||
|
||||
@@ -45,15 +45,11 @@ fail_html_finding = """
|
||||
<td>eu-west-1</td>
|
||||
<td>test-check-id</td>
|
||||
<td>test-check-id</td>
|
||||
<td>test-resource-uid</td>
|
||||
<td>
|
||||
•key1=value1
|
||||
|
||||
•key2=value2
|
||||
</td>
|
||||
<td>test-status-extended</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><p class="show-read-more">test-risk</p></td>
|
||||
<td><p class="show-read-more">test-remediation-recommendation-text</p> <a class="read-more" href=""><i class="fas fa-external-link-alt"></i></a></td>
|
||||
<td><p class="show-read-more"></p> <a class="read-more" href=""><i class="fas fa-external-link-alt"></i></a></td>
|
||||
<td><p class="show-read-more">
|
||||
•test-compliance: test-compliance
|
||||
</p></td>
|
||||
@@ -425,23 +421,7 @@ html_footer = """
|
||||
|
||||
class TestHTML:
|
||||
def test_transform_fail_finding(self):
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
status="FAIL",
|
||||
resource_tags={"key1": "value1", "key2": "value2"},
|
||||
severity="high",
|
||||
service_name="test-service",
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
check_id="test-check-id",
|
||||
check_title="test-check-id",
|
||||
resource_uid="test-resource-uid",
|
||||
status_extended="test-status-extended",
|
||||
risk="test-risk",
|
||||
remediation_recommendation_text="test-remediation-recommendation-text",
|
||||
compliance={"test-compliance": "test-compliance"},
|
||||
)
|
||||
]
|
||||
|
||||
findings = [generate_finding_output(status="FAIL")]
|
||||
html = HTML(findings)
|
||||
output_data = html.data[0]
|
||||
assert isinstance(output_data, str)
|
||||
|
||||
@@ -30,11 +30,7 @@ class TestOCSF:
|
||||
def test_transform(self):
|
||||
findings = [
|
||||
generate_finding_output(
|
||||
status="FAIL",
|
||||
severity="low",
|
||||
muted=False,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
resource_tags={"Name": "test", "Environment": "dev"},
|
||||
status="FAIL", severity="low", muted=False, region=AWS_REGION_EU_WEST_1
|
||||
)
|
||||
]
|
||||
|
||||
@@ -62,7 +58,7 @@ class TestOCSF:
|
||||
assert output_data.status_code == findings[0].status
|
||||
assert output_data.status_detail == findings[0].status_extended
|
||||
assert output_data.risk_details == findings[0].risk
|
||||
assert output_data.resources[0].labels == ["Name:test", "Environment:dev"]
|
||||
assert output_data.resources[0].labels == []
|
||||
assert output_data.resources[0].name == findings[0].resource_name
|
||||
assert output_data.resources[0].uid == findings[0].resource_uid
|
||||
assert output_data.resources[0].type == findings[0].resource_type
|
||||
@@ -194,11 +190,7 @@ class TestOCSF:
|
||||
|
||||
def test_finding_output_cloud_pass_low_muted(self):
|
||||
finding_output = generate_finding_output(
|
||||
status="PASS",
|
||||
severity="low",
|
||||
muted=True,
|
||||
region=AWS_REGION_EU_WEST_1,
|
||||
resource_tags={"Name": "test", "Environment": "dev"},
|
||||
status="PASS", severity="low", muted=True, region=AWS_REGION_EU_WEST_1
|
||||
)
|
||||
|
||||
finding_ocsf = OCSF([finding_output])
|
||||
@@ -256,7 +248,7 @@ class TestOCSF:
|
||||
assert len(resource_details) == 1
|
||||
assert isinstance(resource_details, list)
|
||||
assert isinstance(resource_details[0], ResourceDetails)
|
||||
assert resource_details[0].labels == ["Name:test", "Environment:dev"]
|
||||
assert resource_details[0].labels == []
|
||||
assert resource_details[0].name == finding_output.resource_name
|
||||
assert resource_details[0].uid == finding_output.resource_uid
|
||||
assert resource_details[0].type == finding_output.resource_type
|
||||
@@ -295,7 +287,7 @@ class TestOCSF:
|
||||
assert cloud_account.type_id == TypeID.AWS_Account
|
||||
assert cloud_account.type == TypeID.AWS_Account.name
|
||||
assert cloud_account.uid == finding_output.account_uid
|
||||
assert cloud_account.labels == ["test-tag:test-value"]
|
||||
assert cloud_account.labels == finding_output.account_tags
|
||||
|
||||
cloud_organization = cloud.org
|
||||
assert isinstance(cloud_organization, Organization)
|
||||
|
||||
@@ -98,30 +98,6 @@ class TestOutputs:
|
||||
{"Key": "environment", "Value": "dev"},
|
||||
{"Key": "terraform", "Value": "true"},
|
||||
]
|
||||
|
||||
assert unroll_tags(dict_list) == {
|
||||
"environment": "dev",
|
||||
"name": "test",
|
||||
"project": "prowler",
|
||||
"terraform": "true",
|
||||
}
|
||||
|
||||
def test_unroll_dict_tags(self):
|
||||
tags_dict = {
|
||||
"environment": "dev",
|
||||
"name": "test",
|
||||
"project": "prowler",
|
||||
"terraform": "true",
|
||||
}
|
||||
|
||||
assert unroll_tags(tags_dict) == {
|
||||
"environment": "dev",
|
||||
"name": "test",
|
||||
"project": "prowler",
|
||||
"terraform": "true",
|
||||
}
|
||||
|
||||
def test_unroll_tags_unique(self):
|
||||
unique_dict_list = [
|
||||
{
|
||||
"test1": "value1",
|
||||
@@ -129,40 +105,14 @@ class TestOutputs:
|
||||
"test3": "value3",
|
||||
}
|
||||
]
|
||||
assert unroll_tags(unique_dict_list) == {
|
||||
"test1": "value1",
|
||||
"test2": "value2",
|
||||
"test3": "value3",
|
||||
}
|
||||
|
||||
def test_unroll_tags_lowercase(self):
|
||||
dict_list = [
|
||||
{"key": "name", "value": "test"},
|
||||
{"key": "project", "value": "prowler"},
|
||||
{"key": "environment", "value": "dev"},
|
||||
{"key": "terraform", "value": "true"},
|
||||
]
|
||||
|
||||
assert unroll_tags(dict_list) == {
|
||||
"environment": "dev",
|
||||
"name": "test",
|
||||
"project": "prowler",
|
||||
"terraform": "true",
|
||||
}
|
||||
|
||||
def test_unroll_tags_only_list(self):
|
||||
tags_list = ["tag1", "tag2", "tag3"]
|
||||
|
||||
assert unroll_tags(tags_list) == {
|
||||
"tag1": "",
|
||||
"tag2": "",
|
||||
"tag3": "",
|
||||
}
|
||||
|
||||
def test_unroll_tags_with_key_only(self):
|
||||
tags = [{"key": "name"}]
|
||||
|
||||
assert unroll_tags(tags) == {"name": ""}
|
||||
assert (
|
||||
unroll_tags(dict_list)
|
||||
== "name=test | project=prowler | environment=dev | terraform=true"
|
||||
)
|
||||
assert (
|
||||
unroll_tags(unique_dict_list)
|
||||
== "test1=value1 | test2=value2 | test3=value3"
|
||||
)
|
||||
|
||||
def test_unroll_dict(self):
|
||||
test_compliance_dict = {
|
||||
@@ -206,18 +156,18 @@ class TestOutputs:
|
||||
"FedRAMP-Low-Revision-4": ["sc-13"],
|
||||
}
|
||||
assert (
|
||||
unroll_dict(test_compliance_dict, separator=": ")
|
||||
unroll_dict(test_compliance_dict)
|
||||
== "CISA: your-systems-3, your-data-1, your-data-2 | CIS-1.4: 2.1.1 | CIS-1.5: 2.1.1 | GDPR: article_32 | AWS-Foundational-Security-Best-Practices: s3 | HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii | GxP-21-CFR-Part-11: 11.10-c, 11.30 | GxP-EU-Annex-11: 7.1-data-storage-damage-protection | NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16 | NIST-800-53-Revision-4: sc_28 | NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4 | ENS-RD2022: mp.si.2.aws.s3.1 | NIST-CSF-1.1: ds_1 | RBI-Cyber-Security-Framework: annex_i_1_3 | FFIEC: d3-pc-am-b-12 | PCI-3.2.1: s3 | FedRamp-Moderate-Revision-4: sc-13, sc-28 | FedRAMP-Low-Revision-4: sc-13"
|
||||
)
|
||||
|
||||
def test_unroll_dict_to_list(self):
|
||||
dict_A = {"A": "B"}
|
||||
list_A = ["A:B"]
|
||||
list_A = ["A: B"]
|
||||
|
||||
assert unroll_dict_to_list(dict_A) == list_A
|
||||
|
||||
dict_B = {"A": ["B", "C"]}
|
||||
list_B = ["A:B, C"]
|
||||
list_B = ["A: B, C"]
|
||||
|
||||
assert unroll_dict_to_list(dict_B) == list_B
|
||||
|
||||
|
||||
@@ -7,11 +7,9 @@ from datetime import datetime, timedelta
|
||||
from json import dumps
|
||||
from os import rmdir
|
||||
from re import search
|
||||
from unittest import mock
|
||||
|
||||
import botocore
|
||||
from boto3 import client, resource, session
|
||||
from colorama import Fore, Style
|
||||
from freezegun import freeze_time
|
||||
from mock import patch
|
||||
from moto import mock_aws
|
||||
@@ -272,7 +270,7 @@ class TestAWSProvider:
|
||||
assert isinstance(aws_provider.organizations_metadata, AWSOrganizationsInfo)
|
||||
assert aws_provider.organizations_metadata.account_email == "master@example.com"
|
||||
assert aws_provider.organizations_metadata.account_name == "master"
|
||||
assert aws_provider.organizations_metadata.account_tags == {"tagged": "true"}
|
||||
assert aws_provider.organizations_metadata.account_tags == ["tagged:true"]
|
||||
assert (
|
||||
aws_provider.organizations_metadata.organization_account_arn
|
||||
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:account/{organization['Id']}/{AWS_ACCOUNT_NUMBER}"
|
||||
@@ -353,7 +351,7 @@ class TestAWSProvider:
|
||||
assert isinstance(aws_provider.organizations_metadata, AWSOrganizationsInfo)
|
||||
assert aws_provider.organizations_metadata.account_email == "master@example.com"
|
||||
assert aws_provider.organizations_metadata.account_name == "master"
|
||||
assert aws_provider.organizations_metadata.account_tags == {"tagged": "true"}
|
||||
assert aws_provider.organizations_metadata.account_tags == ["tagged:true"]
|
||||
assert (
|
||||
aws_provider.organizations_metadata.organization_account_arn
|
||||
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:account/{organization['Id']}/{AWS_ACCOUNT_NUMBER}"
|
||||
@@ -757,14 +755,6 @@ aws:
|
||||
assert aws_provider.mutelist.mutelist == mutelist["Mutelist"]
|
||||
assert aws_provider.mutelist.mutelist_file_path == dynamodb_mutelist_path
|
||||
|
||||
@mock_aws
|
||||
def test_empty_input_regions_in_arguments(self):
|
||||
arguments = Namespace()
|
||||
arguments.region = None
|
||||
aws_provider = AwsProvider(arguments)
|
||||
|
||||
assert isinstance(aws_provider, AwsProvider)
|
||||
|
||||
@mock_aws
|
||||
def test_generate_regional_clients_all_enabled_regions(self):
|
||||
arguments = Namespace()
|
||||
@@ -1687,114 +1677,3 @@ aws:
|
||||
|
||||
assert len(session_token) == 356
|
||||
assert search(r"^FQoGZXIvYXdzE.*$", session_token)
|
||||
|
||||
|
||||
def mock_print_boxes(report_lines, report_title):
|
||||
return report_lines, report_title
|
||||
|
||||
|
||||
class TestPrintCredentials:
|
||||
@mock.patch("prowler.providers.aws.aws_provider.print_boxes")
|
||||
def test_print_credentials(self, mock_print_boxes):
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
|
||||
mock_self = AwsProvider.__new__(AwsProvider)
|
||||
|
||||
mock_self._identity = mock.MagicMock()
|
||||
mock_self._identity.audited_regions = ["us-east-1", "us-west-2"]
|
||||
mock_self._identity.profile = "my-profile"
|
||||
mock_self._identity.account = "123456789012"
|
||||
mock_self._identity.user_id = "AID1234567890"
|
||||
mock_self._identity.identity_arn = "arn:aws:iam::123456789012:user/my-user"
|
||||
|
||||
mock_self._assumed_role = mock.MagicMock()
|
||||
mock_self._assumed_role.info.role_arn.arn = (
|
||||
"arn:aws:sts::123456789012:assumed-role/my-role"
|
||||
)
|
||||
|
||||
mock_self.print_credentials()
|
||||
|
||||
expected_lines = [
|
||||
f"AWS-CLI Profile: {Fore.YELLOW}my-profile{Style.RESET_ALL}",
|
||||
f"AWS Regions: {Fore.YELLOW}us-east-1, us-west-2{Style.RESET_ALL}",
|
||||
f"AWS Account: {Fore.YELLOW}123456789012{Style.RESET_ALL}",
|
||||
f"User Id: {Fore.YELLOW}AID1234567890{Style.RESET_ALL}",
|
||||
f"Caller Identity ARN: {Fore.YELLOW}arn:aws:iam::123456789012:user/my-user{Style.RESET_ALL}",
|
||||
f"Assumed Role ARN: {Fore.YELLOW}[arn:aws:sts::123456789012:assumed-role/my-role]{Style.RESET_ALL}",
|
||||
]
|
||||
|
||||
expected_title = (
|
||||
f"{Style.BRIGHT}Using the AWS credentials below:{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
mock_print_boxes.assert_called_once_with(expected_lines, expected_title)
|
||||
|
||||
@mock.patch("prowler.providers.aws.aws_provider.print_boxes")
|
||||
def test_print_credentials_no_regions_None(self, mock_print_boxes):
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
|
||||
mock_self = AwsProvider.__new__(AwsProvider)
|
||||
|
||||
mock_self._identity = mock.MagicMock()
|
||||
mock_self._identity.audited_regions = None
|
||||
mock_self._identity.profile = "my-profile"
|
||||
mock_self._identity.account = "123456789012"
|
||||
mock_self._identity.user_id = "AID1234567890"
|
||||
mock_self._identity.identity_arn = "arn:aws:iam::123456789012:user/my-user"
|
||||
|
||||
mock_self._assumed_role = mock.MagicMock()
|
||||
mock_self._assumed_role.info.role_arn.arn = (
|
||||
"arn:aws:sts::123456789012:assumed-role/my-role"
|
||||
)
|
||||
|
||||
mock_self.print_credentials()
|
||||
|
||||
expected_lines = [
|
||||
f"AWS-CLI Profile: {Fore.YELLOW}my-profile{Style.RESET_ALL}",
|
||||
f"AWS Regions: {Fore.YELLOW}all{Style.RESET_ALL}",
|
||||
f"AWS Account: {Fore.YELLOW}123456789012{Style.RESET_ALL}",
|
||||
f"User Id: {Fore.YELLOW}AID1234567890{Style.RESET_ALL}",
|
||||
f"Caller Identity ARN: {Fore.YELLOW}arn:aws:iam::123456789012:user/my-user{Style.RESET_ALL}",
|
||||
f"Assumed Role ARN: {Fore.YELLOW}[arn:aws:sts::123456789012:assumed-role/my-role]{Style.RESET_ALL}",
|
||||
]
|
||||
|
||||
expected_title = (
|
||||
f"{Style.BRIGHT}Using the AWS credentials below:{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
mock_print_boxes.assert_called_once_with(expected_lines, expected_title)
|
||||
|
||||
@mock.patch("prowler.providers.aws.aws_provider.print_boxes")
|
||||
def test_print_credentials_no_regions_empty_set(self, mock_print_boxes):
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
|
||||
mock_self = AwsProvider.__new__(AwsProvider)
|
||||
|
||||
mock_self._identity = mock.MagicMock()
|
||||
mock_self._identity.audited_regions = set()
|
||||
mock_self._identity.profile = "my-profile"
|
||||
mock_self._identity.account = "123456789012"
|
||||
mock_self._identity.user_id = "AID1234567890"
|
||||
mock_self._identity.identity_arn = "arn:aws:iam::123456789012:user/my-user"
|
||||
|
||||
mock_self._assumed_role = mock.MagicMock()
|
||||
mock_self._assumed_role.info.role_arn.arn = (
|
||||
"arn:aws:sts::123456789012:assumed-role/my-role"
|
||||
)
|
||||
|
||||
mock_self.print_credentials()
|
||||
|
||||
expected_lines = [
|
||||
f"AWS-CLI Profile: {Fore.YELLOW}my-profile{Style.RESET_ALL}",
|
||||
f"AWS Regions: {Fore.YELLOW}all{Style.RESET_ALL}",
|
||||
f"AWS Account: {Fore.YELLOW}123456789012{Style.RESET_ALL}",
|
||||
f"User Id: {Fore.YELLOW}AID1234567890{Style.RESET_ALL}",
|
||||
f"Caller Identity ARN: {Fore.YELLOW}arn:aws:iam::123456789012:user/my-user{Style.RESET_ALL}",
|
||||
f"Assumed Role ARN: {Fore.YELLOW}[arn:aws:sts::123456789012:assumed-role/my-role]{Style.RESET_ALL}",
|
||||
]
|
||||
|
||||
expected_title = (
|
||||
f"{Style.BRIGHT}Using the AWS credentials below:{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
mock_print_boxes.assert_called_once_with(expected_lines, expected_title)
|
||||
|
||||
@@ -245,73 +245,6 @@ class Test_ARN_Parsing:
|
||||
"resource": IAM_ROLE,
|
||||
},
|
||||
},
|
||||
# Root user
|
||||
{
|
||||
"input_arn": f"arn:aws:{IAM_SERVICE}::{ACCOUNT_ID}:root",
|
||||
"expected": {
|
||||
"partition": COMMERCIAL_PARTITION,
|
||||
"service": IAM_SERVICE,
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "root",
|
||||
"resource": "root",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{CHINA_PARTITION}:{IAM_SERVICE}::{ACCOUNT_ID}:root",
|
||||
"expected": {
|
||||
"partition": CHINA_PARTITION,
|
||||
"service": IAM_SERVICE,
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "root",
|
||||
"resource": "root",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{GOVCLOUD_PARTITION}:{IAM_SERVICE}::{ACCOUNT_ID}:root",
|
||||
"expected": {
|
||||
"partition": GOVCLOUD_PARTITION,
|
||||
"service": IAM_SERVICE,
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "root",
|
||||
"resource": "root",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:aws:sts::{ACCOUNT_ID}:federated-user/Bob",
|
||||
"expected": {
|
||||
"partition": COMMERCIAL_PARTITION,
|
||||
"service": "sts",
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "federated-user",
|
||||
"resource": "Bob",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{CHINA_PARTITION}:sts::{ACCOUNT_ID}:federated-user/Bob",
|
||||
"expected": {
|
||||
"partition": CHINA_PARTITION,
|
||||
"service": "sts",
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "federated-user",
|
||||
"resource": "Bob",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{GOVCLOUD_PARTITION}:sts::{ACCOUNT_ID}:federated-user/Bob",
|
||||
"expected": {
|
||||
"partition": GOVCLOUD_PARTITION,
|
||||
"service": "sts",
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "federated-user",
|
||||
"resource": "Bob",
|
||||
},
|
||||
},
|
||||
]
|
||||
for test in test_cases:
|
||||
input_arn = test["input_arn"]
|
||||
@@ -386,7 +319,6 @@ class Test_ARN_Parsing:
|
||||
"arn:aws:lambda:eu-west-1:123456789012:function:lambda-function"
|
||||
)
|
||||
assert is_valid_arn("arn:aws:sns:eu-west-1:123456789012:test.fifo")
|
||||
assert is_valid_arn("arn:aws:logs:eu-west-1:123456789012:log-group:/ecs/test:")
|
||||
assert not is_valid_arn("arn:azure:::012345678910:user/test")
|
||||
assert not is_valid_arn("arn:aws:iam::account:user/test")
|
||||
assert not is_valid_arn("arn:aws:::012345678910:resource")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import io
|
||||
from json import dumps
|
||||
from os import path
|
||||
|
||||
import botocore
|
||||
import yaml
|
||||
@@ -844,134 +843,6 @@ class TestAWSMutelist:
|
||||
"",
|
||||
)
|
||||
|
||||
def test_is_muted_aws_default_mutelist(
|
||||
self,
|
||||
):
|
||||
|
||||
mutelist = AWSMutelist(
|
||||
mutelist_path=f"{path.dirname(path.realpath(__file__))}/../../../../../prowler/config/aws_mutelist.yaml"
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-CONFIG-AAAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-CLOUDWATCH-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerGuardrailAWS-GR-AUDIT-BUCKET-PUBLIC-READ-PROHIBITED-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerGuardrailAWS-GR-DETECT",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"CLOUDTRAIL-ENABLED-ON-SHARED-ACCOUNTS-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-SERVICE-LINKED-ROLE-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-ROLES-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-SECURITY-TOPICS-AAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-BASELINE-SERVICE-ROLES-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerSecurityResources-AAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerGuardrailAWS-GR-AUDIT-BUCKET-PUBLIC-WRITE-PROHIBITED-AAAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"AFT-Backend/AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"AWSControlTowerBP-BASELINE-CONFIG-MASTER/AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER/AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"cloudformation_stacks_termination_protection_enabled",
|
||||
AWS_REGION_EU_WEST_1,
|
||||
"StackSet-AWSControlTowerBP-VPC-ACCOUNT-FACTORY-V1-AAA",
|
||||
"",
|
||||
)
|
||||
|
||||
def test_is_muted_single_account(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
@@ -1000,46 +871,6 @@ class TestAWSMutelist:
|
||||
mutelist.is_muted(AWS_ACCOUNT_NUMBER, "check_test", "us-east-2", "test", "")
|
||||
)
|
||||
|
||||
def test_is_muted_search(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
"Accounts": {
|
||||
AWS_ACCOUNT_NUMBER: {
|
||||
"Checks": {
|
||||
"check_test": {
|
||||
"Regions": ["*"],
|
||||
"Resources": ["prowler"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_content)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"resource-prowler",
|
||||
"",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-resource",
|
||||
"",
|
||||
)
|
||||
|
||||
def test_is_muted_in_region(self):
|
||||
muted_regions = [AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1]
|
||||
finding_region = AWS_REGION_US_EAST_1
|
||||
@@ -1244,7 +1075,7 @@ class TestAWSMutelist:
|
||||
"",
|
||||
)
|
||||
|
||||
def test_is_muted_tags_example1(self):
|
||||
def test_is_muted_tags(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
"Accounts": {
|
||||
@@ -1261,7 +1092,7 @@ class TestAWSMutelist:
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_content)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
@@ -1287,203 +1118,6 @@ class TestAWSMutelist:
|
||||
)
|
||||
)
|
||||
|
||||
def test_is_muted_tags_example2(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
"Accounts": {
|
||||
"*": {
|
||||
"Checks": {
|
||||
"check_test": {
|
||||
"Regions": [AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1],
|
||||
"Resources": ["*"],
|
||||
"Tags": ["environment=dev", "project=test(?!\.)"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_content)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler",
|
||||
"environment=dev | project=test",
|
||||
)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler",
|
||||
"environment=dev",
|
||||
)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"environment=dev | project=prowler",
|
||||
)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"environment=dev | project=test.",
|
||||
)
|
||||
|
||||
def test_is_muted_tags_and_logic(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
"Accounts": {
|
||||
"*": {
|
||||
"Checks": {
|
||||
"check_test": {
|
||||
"Regions": [AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1],
|
||||
"Resources": ["*"],
|
||||
"Tags": ["environment=dev", "project=prowler"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_content)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"environment=dev | project=prowler",
|
||||
)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"environment=dev | project=myproj",
|
||||
)
|
||||
|
||||
def test_is_muted_tags_or_logic_example1(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
"Accounts": {
|
||||
"*": {
|
||||
"Checks": {
|
||||
"check_test": {
|
||||
"Regions": [AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1],
|
||||
"Resources": ["*"],
|
||||
"Tags": ["environment=dev|project=.*"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_content)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"environment=dev",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"project=prowler",
|
||||
)
|
||||
|
||||
def test_is_muted_tags_or_logic_example2(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
"Accounts": {
|
||||
"*": {
|
||||
"Checks": {
|
||||
"check_test": {
|
||||
"Regions": [AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1],
|
||||
"Resources": ["*"],
|
||||
"Tags": ["project=(test|stage)"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_content)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"project=test",
|
||||
)
|
||||
|
||||
def test_is_muted_tags_and_or_logic(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
"Accounts": {
|
||||
"*": {
|
||||
"Checks": {
|
||||
"check_test": {
|
||||
"Regions": [AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1],
|
||||
"Resources": ["*"],
|
||||
"Tags": ["team=dev", "environment=dev|project=.*"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_content)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"team=dev | environment=dev",
|
||||
)
|
||||
|
||||
assert mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"team=dev | project=prowler",
|
||||
)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"team=ops",
|
||||
)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"environment=dev",
|
||||
)
|
||||
|
||||
assert not mutelist.is_muted(
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"check_test",
|
||||
AWS_REGION_US_EAST_1,
|
||||
"prowler-test",
|
||||
"project=myproj",
|
||||
)
|
||||
|
||||
def test_is_muted_specific_account_with_other_account_excepted(self):
|
||||
# Mutelist
|
||||
mutelist_content = {
|
||||
@@ -1592,40 +1226,32 @@ class TestAWSMutelist:
|
||||
assert AWSMutelist.is_item_matched(mutelist_tags, "environment=dev")
|
||||
|
||||
assert AWSMutelist.is_item_matched(
|
||||
mutelist_tags, "environment=dev | project=prowler"
|
||||
mutelist_tags,
|
||||
"environment=dev | project=prowler",
|
||||
)
|
||||
|
||||
assert AWSMutelist.is_item_matched(
|
||||
mutelist_tags, "environment=pro | project=prowler"
|
||||
assert not (
|
||||
AWSMutelist.is_item_matched(
|
||||
mutelist_tags,
|
||||
"environment=pro",
|
||||
)
|
||||
)
|
||||
|
||||
assert not (AWSMutelist.is_item_matched(mutelist_tags, "environment=pro"))
|
||||
|
||||
def test_is_muted_in_tags_with_piped_tags(self):
|
||||
mutelist_tags = ["environment=dev|project=prowler"]
|
||||
|
||||
assert AWSMutelist.is_item_matched(mutelist_tags, "environment=dev")
|
||||
|
||||
assert AWSMutelist.is_item_matched(
|
||||
mutelist_tags, "environment=dev | project=prowler"
|
||||
)
|
||||
|
||||
assert AWSMutelist.is_item_matched(
|
||||
mutelist_tags, "environment=pro | project=prowler"
|
||||
)
|
||||
|
||||
assert not (AWSMutelist.is_item_matched(mutelist_tags, "environment=pro"))
|
||||
|
||||
def test_is_muted_in_tags_regex(self):
|
||||
mutelist_tags = ["environment=(dev|test)", ".*=prowler"]
|
||||
assert AWSMutelist.is_item_matched(
|
||||
mutelist_tags, "environment=test | proj=prowler"
|
||||
mutelist_tags,
|
||||
"environment=test | proj=prowler",
|
||||
)
|
||||
|
||||
assert AWSMutelist.is_item_matched(mutelist_tags, "env=prod | project=prowler")
|
||||
assert AWSMutelist.is_item_matched(
|
||||
mutelist_tags,
|
||||
"env=prod | project=prowler",
|
||||
)
|
||||
|
||||
assert not AWSMutelist.is_item_matched(
|
||||
mutelist_tags, "environment=prod | project=myproj"
|
||||
mutelist_tags,
|
||||
"environment=prod | project=myproj",
|
||||
)
|
||||
|
||||
def test_is_muted_in_tags_with_no_tags_in_finding(self):
|
||||
@@ -1641,7 +1267,8 @@ class TestAWSMutelist:
|
||||
"Tags": ["environment=test", "project=.*"],
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content={})
|
||||
assert not mutelist.is_excepted(
|
||||
|
||||
assert mutelist.is_excepted(
|
||||
exceptions,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"eu-central-1",
|
||||
@@ -1649,7 +1276,7 @@ class TestAWSMutelist:
|
||||
"environment=test",
|
||||
)
|
||||
|
||||
assert not mutelist.is_excepted(
|
||||
assert mutelist.is_excepted(
|
||||
exceptions,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"eu-south-3",
|
||||
@@ -1657,7 +1284,7 @@ class TestAWSMutelist:
|
||||
"environment=test",
|
||||
)
|
||||
|
||||
assert not mutelist.is_excepted(
|
||||
assert mutelist.is_excepted(
|
||||
exceptions,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
"eu-south-3",
|
||||
@@ -1738,7 +1365,7 @@ class TestAWSMutelist:
|
||||
"Accounts": [AWS_ACCOUNT_NUMBER],
|
||||
"Regions": [],
|
||||
"Resources": [],
|
||||
"Tags": ["environment=test", "project=example"],
|
||||
"Tags": ["environment=test"],
|
||||
}
|
||||
mutelist = AWSMutelist(mutelist_content={})
|
||||
|
||||
@@ -1747,7 +1374,7 @@ class TestAWSMutelist:
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_EU_CENTRAL_1,
|
||||
"resource_1",
|
||||
"environment=test | project=example",
|
||||
"environment=test",
|
||||
)
|
||||
|
||||
assert not mutelist.is_excepted(
|
||||
|
||||
@@ -42,7 +42,7 @@ class Test_AWS_Organizations:
|
||||
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:organization/{org_id}"
|
||||
)
|
||||
assert org.organization_id == org_id
|
||||
assert org.account_tags == {"key": "value"}
|
||||
assert org.account_tags == ["key:value"]
|
||||
|
||||
def test_parse_organizations_metadata(self):
|
||||
tags = {"Tags": [{"Key": "test-key", "Value": "test-value"}]}
|
||||
@@ -70,4 +70,4 @@ class Test_AWS_Organizations:
|
||||
== f"arn:aws:organizations::{AWS_ACCOUNT_NUMBER}:account/{organization_name}/{AWS_ACCOUNT_NUMBER}"
|
||||
)
|
||||
assert org.organization_arn == arn
|
||||
assert org.account_tags == {"test-key": "test-value"}
|
||||
assert org.account_tags == ["test-key:test-value"]
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import (
|
||||
is_condition_block_restrictive,
|
||||
is_condition_block_restrictive_organization,
|
||||
)
|
||||
|
||||
TRUSTED_AWS_ACCOUNT_NUMBER = "123456789012"
|
||||
NON_TRUSTED_AWS_ACCOUNT_NUMBER = "111222333444"
|
||||
|
||||
TRUSTED_ORGANIZATION_ID = "o-123456789012"
|
||||
NON_TRUSTED_ORGANIZATION_ID = "o-111222333444"
|
||||
|
||||
ALL_ORGS = "*"
|
||||
|
||||
|
||||
class Test_policy_condition_parser:
|
||||
# Test lowercase context key name --> aws
|
||||
@@ -1395,45 +1389,3 @@ class Test_policy_condition_parser:
|
||||
assert is_condition_block_restrictive(
|
||||
condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True
|
||||
)
|
||||
|
||||
def test_condition_parser_string_equals_aws_PrincipalOrgID_list(self):
|
||||
condition_statement = {
|
||||
"StringEquals": {"aws:PrincipalOrgID": [TRUSTED_ORGANIZATION_ID]}
|
||||
}
|
||||
assert is_condition_block_restrictive_organization(condition_statement)
|
||||
|
||||
def test_condition_parser_string_equals_aws_PrincipalOrgID_list_multiple_items(
|
||||
self,
|
||||
):
|
||||
condition_statement = {
|
||||
"StringEquals": {
|
||||
"aws:PrincipalOrgID": [
|
||||
TRUSTED_ORGANIZATION_ID,
|
||||
NON_TRUSTED_ORGANIZATION_ID,
|
||||
]
|
||||
}
|
||||
}
|
||||
assert is_condition_block_restrictive_organization(condition_statement)
|
||||
|
||||
def test_condition_parser_string_equals_aws_PrincipalOrgID_str(self):
|
||||
condition_statement = {
|
||||
"StringEquals": {"aws:PrincipalOrgID": TRUSTED_ORGANIZATION_ID}
|
||||
}
|
||||
assert is_condition_block_restrictive_organization(condition_statement)
|
||||
|
||||
def test_condition_parser_string_equals_aws_All_Orgs_list_multiple_items(
|
||||
self,
|
||||
):
|
||||
condition_statement = {
|
||||
"StringEquals": {
|
||||
"aws:PrincipalOrgID": [
|
||||
TRUSTED_ORGANIZATION_ID,
|
||||
ALL_ORGS,
|
||||
]
|
||||
}
|
||||
}
|
||||
assert not is_condition_block_restrictive_organization(condition_statement)
|
||||
|
||||
def test_condition_parser_string_equals_aws_All_Orgs_str(self):
|
||||
condition_statement = {"StringEquals": {"aws:PrincipalOrgID": ALL_ORGS}}
|
||||
assert not is_condition_block_restrictive_organization(condition_statement)
|
||||
|
||||
@@ -26,7 +26,7 @@ FINDING = generate_finding_output(
|
||||
resource_uid="resource-123",
|
||||
resource_name="Example Resource",
|
||||
resource_details="Detailed information about the resource",
|
||||
resource_tags={"key1": "tag1", "key2": "tag2"},
|
||||
resource_tags="tag1,tag2",
|
||||
partition="aws",
|
||||
description="Description of the finding",
|
||||
risk="High",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user