Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcfdcbde30 | |||
| 2f50aaa9c1 | |||
| 537081a0f6 | |||
| 2eb774bbc9 | |||
| 5419117842 | |||
| e72831d428 | |||
| 217b8ad250 | |||
| 09b4548445 | |||
| 0d96583769 | |||
| 722fe0a1bc | |||
| 445821eceb | |||
| c3d129a4b2 | |||
| 36fc575e40 | |||
| 24efb34d91 | |||
| c08e244c95 | |||
| c2f8980f1f | |||
| 028d29b8ff | |||
| b976cab926 | |||
| 197a08ab94 | |||
| 0d97780ade | |||
| f2f922d7e8 | |||
| 606b4b5a66 | |||
| 132056f4c1 | |||
| 4845d6033b | |||
| 57550e6984 | |||
| 040b780af7 | |||
| abaa7855d7 | |||
| e9c6b35698 | |||
| c92740869f | |||
| 49003fae08 | |||
| 01f3c8656c | |||
| ba705406ff | |||
| d8101acc9c | |||
| 0ef85b3dee | |||
| 126acc046a | |||
| f324f27016 | |||
| 93a2431211 | |||
| 5b80082491 | |||
| 2ca4656ef9 | |||
| cb4de850e9 | |||
| 92e0d74055 | |||
| 578b21f424 | |||
| 85c44f01c5 | |||
| fb5d6cfd7e | |||
| 1b3f830623 | |||
| 1fe74937c1 | |||
| 6ee016e577 | |||
| f7248dfb1c | |||
| 0481435846 | |||
| 5554e2be1b | |||
| e97e2e84fc | |||
| 19f38dbb63 | |||
| 06d9eccebd | |||
| 5dfd8460be | |||
| f71052bcfe | |||
| 7bfdb8c1f3 | |||
| dedb03cc6e | |||
| 856afb3966 |
@@ -13,8 +13,3 @@ updates:
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: master
|
||||
|
||||
@@ -32,11 +32,11 @@ jobs:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup python (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
poetry version ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Public ECR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
|
||||
@@ -67,11 +67,11 @@ jobs:
|
||||
AWS_REGION: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push container image (latest)
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Build and push container image (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
# Use local context to get changes
|
||||
# https://github.com/docker/build-push-action#path-context
|
||||
|
||||
@@ -37,11 +37,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -52,6 +52,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -7,11 +7,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.67.2
|
||||
uses: trufflesecurity/trufflehog@v3.4.4
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -14,13 +14,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@v42
|
||||
uses: tj-actions/changed-files@v39
|
||||
with:
|
||||
files: ./**
|
||||
files_ignore: |
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
pipx install poetry
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
@@ -88,6 +88,6 @@ jobs:
|
||||
poetry run pytest -n auto --cov=./prowler --cov-report=xml tests
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
name: Release Prowler to PyPI
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ env.GITHUB_BRANCH }}
|
||||
- name: Install dependencies
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
pipx install poetry
|
||||
pipx inject poetry poetry-bumpversion
|
||||
- name: setup python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
cache: 'poetry'
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
poetry publish
|
||||
# Create pull request with new version
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_ACCESS_TOKEN }}
|
||||
commit-message: "chore(release): update Prowler Version to ${{ env.RELEASE_TAG }}."
|
||||
|
||||
@@ -23,12 +23,12 @@ jobs:
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ env.GITHUB_BRANCH }}
|
||||
|
||||
- name: setup python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9 #install the python needed
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
pip install boto3
|
||||
|
||||
- name: Configure AWS Credentials -- DEV
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-region: ${{ env.AWS_REGION_DEV }}
|
||||
role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }}
|
||||
@@ -50,12 +50,12 @@ jobs:
|
||||
|
||||
# Create pull request
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_ACCESS_TOKEN }}
|
||||
commit-message: "feat(regions_update): Update regions for AWS services."
|
||||
branch: "aws-services-regions-updated-${{ github.sha }}"
|
||||
labels: "status/waiting-for-revision, severity/low, provider/aws"
|
||||
labels: "status/waiting-for-revision, severity/low"
|
||||
title: "chore(regions_update): Changes in regions for AWS services."
|
||||
body: |
|
||||
### Description
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
repos:
|
||||
## GENERAL
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
@@ -15,7 +15,7 @@ repos:
|
||||
|
||||
## TOML
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.12.0
|
||||
rev: v2.10.0
|
||||
hooks:
|
||||
- id: pretty-format-toml
|
||||
args: [--autofix]
|
||||
@@ -28,7 +28,7 @@ repos:
|
||||
- id: shellcheck
|
||||
## PYTHON
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v2.2.1
|
||||
rev: v2.2.0
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args:
|
||||
@@ -39,25 +39,25 @@ repos:
|
||||
]
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.13.2
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.1.1
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: contrib
|
||||
args: ["--ignore=E266,W503,E203,E501,W605"]
|
||||
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: 1.7.0
|
||||
rev: 1.6.0 # add version here
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
- id: poetry-lock
|
||||
@@ -80,12 +80,18 @@ repos:
|
||||
- id: trufflehog
|
||||
name: TruffleHog
|
||||
description: Detect secrets in your data.
|
||||
entry: bash -c 'trufflehog --no-update git file://. --only-verified --fail'
|
||||
# entry: bash -c 'trufflehog git file://. --only-verified --fail'
|
||||
# For running trufflehog in docker, use the following entry instead:
|
||||
# entry: bash -c 'docker run -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --only-verified --fail'
|
||||
entry: bash -c 'docker run -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --only-verified --fail'
|
||||
language: system
|
||||
stages: ["commit", "push"]
|
||||
|
||||
- id: pytest-check
|
||||
name: pytest-check
|
||||
entry: bash -c 'pytest tests -n auto'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
- id: bandit
|
||||
name: bandit
|
||||
description: "Bandit is a tool for finding common security issues in Python code"
|
||||
|
||||
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at [support.prowler.com](https://customer.support.prowler.com/servicedesk/customer/portals). All
|
||||
reported by contacting the project team at community@prowler.cloud. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
<p align="center">
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-black.png?raw=True#gh-light-mode-only" width="350" height="115">
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-white.png?raw=True#gh-dark-mode-only" width="350" height="115">
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/62c1ce73bbcdd6b9e5ba03dfcae26dfd165defd9/docs/img/prowler-pro-dark.png?raw=True#gh-dark-mode-only" width="150" height="36">
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/62c1ce73bbcdd6b9e5ba03dfcae26dfd165defd9/docs/img/prowler-pro-light.png?raw=True#gh-light-mode-only" width="15%" height="15%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<b><i>Prowler SaaS </b> and <b>Prowler Open Source</b> are as dynamic and adaptable as the environment they’re meant to protect. Trusted by the leaders in security.
|
||||
<b><i>See all the things you and your team can do with ProwlerPro at <a href="https://prowler.pro">prowler.pro</a></i></b>
|
||||
</p>
|
||||
<p align="center">
|
||||
<b>Learn more at <a href="https://prowler.com">prowler.com</i></b>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/3985464/113734260-7ba06900-96fb-11eb-82bc-d4f68a1e2710.png" />
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog"><img alt="Slack Shield" src="https://img.shields.io/badge/slack-prowler-brightgreen.svg?logo=slack"></a>
|
||||
<a href="https://pypi.org/project/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/v/prowler.svg"></a>
|
||||
<a href="https://pypi.python.org/pypi/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/pyversions/prowler.svg"></a>
|
||||
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Prowler Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=prowler%20downloads"></a>
|
||||
<a href="https://pypistats.org/packages/prowler-cloud"><img alt="PyPI Prowler-Cloud Downloads" src="https://img.shields.io/pypi/dw/prowler-cloud.svg?label=prowler-cloud%20downloads"></a>
|
||||
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/toniblyx/prowler"></a>
|
||||
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker" src="https://img.shields.io/docker/cloud/build/toniblyx/prowler"></a>
|
||||
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker" src="https://img.shields.io/docker/image-size/toniblyx/prowler"></a>
|
||||
<a href="https://gallery.ecr.aws/prowler-cloud/prowler"><img width="120" height=19" alt="AWS ECR Gallery" src="https://user-images.githubusercontent.com/3985464/151531396-b6535a68-c907-44eb-95a1-a09508178616.png"></a>
|
||||
<a href="https://codecov.io/gh/prowler-cloud/prowler"><img src="https://codecov.io/gh/prowler-cloud/prowler/graph/badge.svg?token=OflBGsdpDl"/></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/prowler-cloud/prowler"><img alt="Repo size" src="https://img.shields.io/github/repo-size/prowler-cloud/prowler"></a>
|
||||
@@ -31,7 +30,6 @@
|
||||
<a href="https://twitter.com/ToniBlyx"><img alt="Twitter" src="https://img.shields.io/twitter/follow/toniblyx?style=social"></a>
|
||||
<a href="https://twitter.com/prowlercloud"><img alt="Twitter" src="https://img.shields.io/twitter/follow/prowlercloud?style=social"></a>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
# Description
|
||||
|
||||
@@ -41,10 +39,10 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.cloud/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.cloud/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 302 | 61 -> `prowler aws --list-services` | 27 -> `prowler aws --list-compliance` | 6 -> `prowler aws --list-categories` |
|
||||
| AWS | 301 | 61 -> `prowler aws --list-services` | 25 -> `prowler aws --list-compliance` | 5 -> `prowler aws --list-categories` |
|
||||
| GCP | 73 | 11 -> `prowler gcp --list-services` | 1 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
|
||||
| Azure | 37 | 4 -> `prowler azure --list-services` | CIS soon | 1 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | Work In Progress | - | CIS soon | - |
|
||||
| Azure | 23 | 4 -> `prowler azure --list-services` | CIS soon | 1 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | Planned | - | - | - |
|
||||
|
||||
# 📖 Documentation
|
||||
|
||||
@@ -56,7 +54,7 @@ For Prowler v2 Documentation, please go to https://github.com/prowler-cloud/prow
|
||||
# ⚙️ Install
|
||||
|
||||
## Pip package
|
||||
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python >= 3.9, < 3.13:
|
||||
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python >= 3.9:
|
||||
|
||||
```console
|
||||
pip install prowler
|
||||
@@ -79,7 +77,7 @@ The container images are available here:
|
||||
|
||||
## From Github
|
||||
|
||||
Python >= 3.9, < 3.13 is required with pip and poetry:
|
||||
Python >= 3.9 is required with pip and poetry:
|
||||
|
||||
```
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
|
||||
@@ -14,7 +14,7 @@ As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (F
|
||||
|
||||
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or ProwlerPro service, please submit the information by contacting to help@prowler.pro.
|
||||
|
||||
The information you share with ProwlerPro as part of this process is kept confidential within ProwlerPro. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
The information you share with Verica as part of this process is kept confidential within Verica and the Prowler team. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
|
||||
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ All the checks MUST fill the `report.status` and `report.status_extended` with t
|
||||
- Status -- `report.status`
|
||||
- `PASS` --> If the check is passing against the configured value.
|
||||
- `FAIL` --> If the check is passing against the configured value.
|
||||
- `INFO` --> This value cannot be used unless a manual operation is required in order to determine if the `report.status` is whether `PASS` or `FAIL`.
|
||||
- `MANUAL` --> This value cannot be used unless a manual operation is required in order to determine if the `report.status` is whether `PASS` or `FAIL`.
|
||||
- Status Extended -- `report.status_extended`
|
||||
- MUST end in a dot `.`
|
||||
- MUST include the service audited with the resource and a brief explanation of the result generated, e.g.: `EC2 AMI ami-0123456789 is not public.`
|
||||
|
||||
@@ -523,7 +523,7 @@ from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
# Azure Constants
|
||||
AZURE_SUBSCRIPTION = str(uuid4())
|
||||
AZURE_SUSCRIPTION = str(uuid4())
|
||||
|
||||
|
||||
|
||||
@@ -542,7 +542,7 @@ class Test_defender_ensure_defender_for_arm_is_on:
|
||||
|
||||
# Create the custom Defender object to be tested
|
||||
defender_client.pricings = {
|
||||
AZURE_SUBSCRIPTION: {
|
||||
AZURE_SUSCRIPTION: {
|
||||
"Arm": Defender_Pricing(
|
||||
resource_id=resource_id,
|
||||
pricing_tier="Not Standard",
|
||||
@@ -580,9 +580,9 @@ class Test_defender_ensure_defender_for_arm_is_on:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Defender plan Defender for ARM from subscription {AZURE_SUBSCRIPTION} is set to OFF (pricing tier not standard)"
|
||||
== f"Defender plan Defender for ARM from subscription {AZURE_SUSCRIPTION} is set to OFF (pricing tier not standard)"
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION
|
||||
assert result[0].subscription == AZURE_SUSCRIPTION
|
||||
assert result[0].resource_name == "Defender plan ARM"
|
||||
assert result[0].resource_id == resource_id
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -15,7 +15,7 @@ As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (F
|
||||
|
||||
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or ProwlerPro service, please submit the information by contacting to help@prowler.pro.
|
||||
|
||||
The information you share with ProwlerPro as part of this process is kept confidential within ProwlerPro. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
The information you share with Verica as part of this process is kept confidential within Verica and the Prowler team. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
|
||||
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
|
||||
|
||||
|
||||
@@ -37,7 +37,3 @@ If your IAM entity enforces MFA you can use `--mfa` and Prowler will ask you to
|
||||
|
||||
- ARN of your MFA device
|
||||
- TOTP (Time-Based One-Time Password)
|
||||
|
||||
## STS Endpoint Region
|
||||
|
||||
If you are using Prowler in AWS regions that are not enabled by default you need to use the argument `--sts-endpoint-region` to point the AWS STS API calls `assume-role` and `get-caller-identity` to the non-default region, e.g.: `prowler aws --sts-endpoint-region eu-south-2`.
|
||||
|
||||
|
Before Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 273 KiB |
@@ -23,23 +23,6 @@ prowler aws -R arn:aws:iam::<account_id>:role/<role_name>
|
||||
prowler aws -T/--session-duration <seconds> -I/--external-id <external_id> -R arn:aws:iam::<account_id>:role/<role_name>
|
||||
```
|
||||
|
||||
## Custom Role Session Name
|
||||
|
||||
Prowler can use your custom Role Session name with:
|
||||
```console
|
||||
prowler aws --role-session-name <role_session_name>
|
||||
```
|
||||
|
||||
> It defaults to `ProwlerAssessmentSession`
|
||||
|
||||
## STS Endpoint Region
|
||||
|
||||
If you are using Prowler in AWS regions that are not enabled by default you need to use the argument `--sts-endpoint-region` to point the AWS STS API calls `assume-role` and `get-caller-identity` to the non-default region, e.g.: `prowler aws --sts-endpoint-region eu-south-2`.
|
||||
|
||||
> Since v3.11.0, Prowler uses a regional token in STS sessions so it can scan all AWS regions without needing the `--sts-endpoint-region` argument.
|
||||
|
||||
> Make sure that you have enabled the AWS Region you want to scan in BOTH AWS Accounts (assumed role account and account from which you assume the role).
|
||||
|
||||
## Role MFA
|
||||
|
||||
If your IAM Role has MFA configured you can use `--mfa` along with `-R`/`--role <role_arn>` and Prowler will ask you to input the following values to get a new temporary session for the IAM Role provided:
|
||||
|
||||
@@ -1,116 +1,48 @@
|
||||
# AWS Security Hub Integration
|
||||
|
||||
Prowler supports natively and as **official integration** sending findings to [AWS Security Hub](https://aws.amazon.com/security-hub). This integration allows **Prowler** to import its findings to AWS Security Hub.
|
||||
Prowler supports natively and as **official integration** sending findings to [AWS Security Hub](https://aws.amazon.com/security-hub). This integration allows Prowler to import its findings to AWS Security Hub.
|
||||
|
||||
With Security Hub, you now have a single place that aggregates, organizes, and prioritizes your security alerts, or findings, from multiple AWS services, such as Amazon GuardDuty, Amazon Inspector, Amazon Macie, AWS Identity and Access Management (IAM) Access Analyzer, and AWS Firewall Manager, as well as from AWS Partner solutions and from Prowler for free.
|
||||
|
||||
Before sending findings, you will need to enable AWS Security Hub and the **Prowler** integration.
|
||||
Before sending findings to Prowler, you will need to perform next steps:
|
||||
|
||||
## Enable AWS Security Hub
|
||||
1. Since Security Hub is a region based service, enable it in the region or regions you require. Use the AWS Management Console or using the AWS CLI with this command if you have enough permissions:
|
||||
- `aws securityhub enable-security-hub --region <region>`.
|
||||
2. Enable Prowler as partner integration integration. Use the AWS Management Console or using the AWS CLI with this command if you have enough permissions:
|
||||
- `aws securityhub enable-import-findings-for-product --region <region> --product-arn arn:aws:securityhub:<region>::product/prowler/prowler` (change region also inside the ARN).
|
||||
- Using the AWS Management Console:
|
||||

|
||||
3. Allow Prowler to import its findings to AWS Security Hub by adding the policy below to the role or user running Prowler:
|
||||
- [prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json)
|
||||
|
||||
To enable the integration you have to perform the following steps, in _at least_ one AWS region of a given AWS account, to enable **AWS Security Hub** and **Prowler** as a partner integration.
|
||||
|
||||
Since **AWS Security Hub** is a region based service, you will need to enable it in the region or regions you require. You can configure it using the AWS Management Console or the AWS CLI.
|
||||
|
||||
> Take into account that enabling this integration will incur in costs in AWS Security Hub, please refer to its pricing [here](https://aws.amazon.com/security-hub/pricing/) for more information.
|
||||
|
||||
### Using the AWS Management Console
|
||||
|
||||
#### Enable AWS Security Hub
|
||||
|
||||
If you have currently AWS Security Hub enabled you can skip to the [next section](#enable-prowler-integration).
|
||||
|
||||
1. Open the **AWS Security Hub** console at https://console.aws.amazon.com/securityhub/.
|
||||
|
||||
2. When you open the Security Hub console for the first time make sure that you are in the region you want to enable, then choose **Go to Security Hub**.
|
||||

|
||||
|
||||
3. On the next page, the Security standards section lists the security standards that Security Hub supports. Select the check box for a standard to enable it, and clear the check box to disable it.
|
||||
|
||||
4. Choose **Enable Security Hub**.
|
||||

|
||||
|
||||
#### Enable Prowler Integration
|
||||
|
||||
If you have currently the Prowler integration enabled in AWS Security Hub you can skip to the [next section](#send-findings) and start sending findings.
|
||||
|
||||
Once **AWS Security Hub** is enabled you will need to enable **Prowler** as partner integration to allow **Prowler** to send findings to your **AWS Security Hub**.
|
||||
|
||||
1. Open the **AWS Security Hub** console at https://console.aws.amazon.com/securityhub/.
|
||||
|
||||
2. Select the **Integrations** tab in the right-side menu bar.
|
||||

|
||||
|
||||
3. Search for _Prowler_ in the text search box and the **Prowler** integration will appear.
|
||||
|
||||
4. Once there, click on **Accept Findings** to allow **AWS Security Hub** to receive findings from **Prowler**.
|
||||

|
||||
|
||||
5. A new modal will appear to confirm that you are enabling the **Prowler** integration.
|
||||

|
||||
|
||||
6. Right after click on **Accept Findings**, you will see that the integration is enabled in **AWS Security Hub**.
|
||||

|
||||
|
||||
### Using the AWS CLI
|
||||
|
||||
To enable **AWS Security Hub** and the **Prowler** integration you have to run the following commands using the AWS CLI:
|
||||
|
||||
```shell
|
||||
aws securityhub enable-security-hub --region <region>
|
||||
```
|
||||
> For this command to work you will need the `securityhub:EnableSecurityHub` permission.
|
||||
> You will need to set the AWS region where you want to enable AWS Security Hub.
|
||||
|
||||
Once **AWS Security Hub** is enabled you will need to enable **Prowler** as partner integration to allow **Prowler** to send findings to your AWS Security Hub. You have to run the following commands using the AWS CLI:
|
||||
|
||||
```shell
|
||||
aws securityhub enable-import-findings-for-product --region eu-west-1 --product-arn arn:aws:securityhub:<region>::product/prowler/prowler
|
||||
```
|
||||
> You will need to set the AWS region where you want to enable the integration and also the AWS region also within the ARN.
|
||||
> For this command to work you will need the `securityhub:securityhub:EnableImportFindingsForProduct` permission.
|
||||
|
||||
|
||||
## Send Findings
|
||||
Once it is enabled, it is as simple as running the command below (for all regions):
|
||||
|
||||
```sh
|
||||
prowler aws --security-hub
|
||||
prowler aws -S
|
||||
```
|
||||
|
||||
or for only one filtered region like eu-west-1:
|
||||
|
||||
```sh
|
||||
prowler --security-hub --region eu-west-1
|
||||
prowler -S -f eu-west-1
|
||||
```
|
||||
|
||||
> **Note 1**: It is recommended to send only fails to Security Hub and that is possible adding `-q/--quiet` to the command. You can use, instead of the `-q/--quiet` argument, the `--send-sh-only-fails` argument to save all the findings in the Prowler outputs but just to send FAIL findings to AWS Security Hub.
|
||||
> **Note 1**: It is recommended to send only fails to Security Hub and that is possible adding `-q` to the command.
|
||||
|
||||
> **Note 2**: Since Prowler perform checks to all regions by default you may need to filter by region when running Security Hub integration, as shown in the example above. Remember to enable Security Hub in the region or regions you need by calling `aws securityhub enable-security-hub --region <region>` and run Prowler with the option `-f/--region <region>` (if no region is used it will try to push findings in all regions hubs). Prowler will send findings to the Security Hub on the region where the scanned resource is located.
|
||||
> **Note 2**: Since Prowler perform checks to all regions by default you may need to filter by region when running Security Hub integration, as shown in the example above. Remember to enable Security Hub in the region or regions you need by calling `aws securityhub enable-security-hub --region <region>` and run Prowler with the option `-f <region>` (if no region is used it will try to push findings in all regions hubs). Prowler will send findings to the Security Hub on the region where the scanned resource is located.
|
||||
|
||||
> **Note 3**: To have updated findings in Security Hub you have to run Prowler periodically. Once a day or every certain amount of hours.
|
||||
|
||||
### See you Prowler findings in AWS Security Hub
|
||||
Once you run findings for first time you will be able to see Prowler findings in Findings section:
|
||||
|
||||
Once configured the **AWS Security Hub** in your next scan you will receive the **Prowler** findings in the AWS regions configured. To review those findings in **AWS Security Hub**:
|
||||
|
||||
1. Open the **AWS Security Hub** console at https://console.aws.amazon.com/securityhub/.
|
||||
|
||||
2. Select the **Findings** tab in the right-side menu bar.
|
||||

|
||||
|
||||
3. Use the search box filters and use the **Product Name** filter with the value _Prowler_ to see the findings sent from **Prowler**.
|
||||
|
||||
4. Then, you can click on the check **Title** to see the details and the history of a finding.
|
||||

|
||||
|
||||
As you can see in the related requirements section, in the detailed view of the findings, **Prowler** also sends compliance information related to every finding.
|
||||

|
||||
|
||||
## Send findings to Security Hub assuming an IAM Role
|
||||
|
||||
When you are auditing a multi-account AWS environment, you can send findings to a Security Hub of another account by assuming an IAM role from that account using the `-R` flag in the Prowler command:
|
||||
|
||||
```sh
|
||||
prowler --security-hub --role arn:aws:iam::123456789012:role/ProwlerExecutionRole
|
||||
prowler -S -R arn:aws:iam::123456789012:role/ProwlerExecRole
|
||||
```
|
||||
|
||||
> Remember that the used role needs to have permissions to send findings to Security Hub. To get more information about the permissions required, please refer to the following IAM policy [prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json)
|
||||
@@ -118,17 +50,12 @@ prowler --security-hub --role arn:aws:iam::123456789012:role/ProwlerExecutionRol
|
||||
|
||||
## Send only failed findings to Security Hub
|
||||
|
||||
When using the **AWS Security Hub** integration you can send only the `FAIL` findings generated by **Prowler**. Therefore, the **AWS Security Hub** usage costs eventually would be lower. To follow that recommendation you could add the `-q/--quiet` flag to the Prowler command:
|
||||
When using Security Hub it is recommended to send only the failed findings generated. To follow that recommendation you could add the `-q` flag to the Prowler command:
|
||||
|
||||
```sh
|
||||
prowler --security-hub --quiet
|
||||
prowler -S -q
|
||||
```
|
||||
|
||||
You can use, instead of the `-q/--quiet` argument, the `--send-sh-only-fails` argument to save all the findings in the Prowler outputs but just to send FAIL findings to AWS Security Hub:
|
||||
|
||||
```sh
|
||||
prowler --security-hub --send-sh-only-fails
|
||||
```
|
||||
|
||||
## Skip sending updates of findings to Security Hub
|
||||
|
||||
@@ -136,5 +63,5 @@ By default, Prowler archives all its findings in Security Hub that have not appe
|
||||
You can skip this logic by using the option `--skip-sh-update` so Prowler will not archive older findings:
|
||||
|
||||
```sh
|
||||
prowler --security-hub --skip-sh-update
|
||||
prowler -S --skip-sh-update
|
||||
```
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# Compliance
|
||||
Prowler allows you to execute checks based on requirements defined in compliance frameworks.
|
||||
Prowler allows you to execute checks based on requirements defined in compliance frameworks. By default, it will execute and give you an overview of the status of each compliance framework:
|
||||
|
||||
<img src="../img/compliance.png"/>
|
||||
|
||||
> You can find CSVs containing detailed compliance results inside the compliance folder within Prowler's output folder.
|
||||
|
||||
## Execute Prowler based on Compliance Frameworks
|
||||
Prowler can analyze your environment based on a specific compliance framework and get more details, to do it, you can use option `--compliance`:
|
||||
```sh
|
||||
prowler <provider> --compliance <compliance_framework>
|
||||
```
|
||||
Standard results will be shown and additionally the framework information as the sample below for CIS AWS 1.5. For details a CSV file has been generated as well.
|
||||
|
||||
<img src="../img/compliance-cis-sample1.png"/>
|
||||
|
||||
## List Available Compliance Frameworks
|
||||
In order to see which compliance frameworks are cover by Prowler, you can use option `--list-compliance`:
|
||||
@@ -10,9 +23,12 @@ Currently, the available frameworks are:
|
||||
|
||||
- `cis_1.4_aws`
|
||||
- `cis_1.5_aws`
|
||||
- `cis_2.0_aws`
|
||||
- `cisa_aws`
|
||||
- `ens_rd2022_aws`
|
||||
- `aws_audit_manager_control_tower_guardrails_aws`
|
||||
- `aws_foundational_security_best_practices_aws`
|
||||
- `aws_well_architected_framework_reliability_pillar_aws`
|
||||
- `aws_well_architected_framework_security_pillar_aws`
|
||||
- `cisa_aws`
|
||||
- `fedramp_low_revision_4_aws`
|
||||
@@ -22,6 +38,9 @@ Currently, the available frameworks are:
|
||||
- `gxp_eu_annex_11_aws`
|
||||
- `gxp_21_cfr_part_11_aws`
|
||||
- `hipaa_aws`
|
||||
- `iso27001_2013_aws`
|
||||
- `iso27001_2013_aws`
|
||||
- `mitre_attack_aws`
|
||||
- `nist_800_53_revision_4_aws`
|
||||
- `nist_800_53_revision_5_aws`
|
||||
- `nist_800_171_revision_2_aws`
|
||||
@@ -38,7 +57,6 @@ prowler <provider> --list-compliance-requirements <compliance_framework(s)>
|
||||
```
|
||||
|
||||
Example for the first requirements of CIS 1.5 for AWS:
|
||||
|
||||
```
|
||||
Listing CIS 1.5 AWS Compliance Requirements:
|
||||
|
||||
@@ -71,15 +89,6 @@ Requirement Id: 1.5
|
||||
|
||||
```
|
||||
|
||||
## Execute Prowler based on Compliance Frameworks
|
||||
As we mentioned, Prowler can be execute to analyse you environment based on a specific compliance framework, to do it, you can use option `--compliance`:
|
||||
```sh
|
||||
prowler <provider> --compliance <compliance_framework>
|
||||
```
|
||||
Standard results will be shown and additionally the framework information as the sample below for CIS AWS 1.5. For details a CSV file has been generated as well.
|
||||
|
||||
<img src="../img/compliance-cis-sample1.png"/>
|
||||
|
||||
## Create and contribute adding other Security Frameworks
|
||||
|
||||
This information is part of the Developer Guide and can be found here: https://docs.prowler.cloud/en/latest/tutorials/developer-guide/.
|
||||
|
||||
@@ -29,10 +29,10 @@ The following list includes all the AWS checks with configurable variables that
|
||||
| `organizations_delegated_administrators` | `organizations_trusted_delegated_administrators` | List of Strings |
|
||||
| `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String |
|
||||
| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean |
|
||||
| `config_recorder_all_regions_enabled` | `allowlist_non_default_regions` | Boolean |
|
||||
| `drs_job_exist` | `allowlist_non_default_regions` | Boolean |
|
||||
| `guardduty_is_enabled` | `allowlist_non_default_regions` | Boolean |
|
||||
| `securityhub_enabled` | `allowlist_non_default_regions` | Boolean |
|
||||
| `config_recorder_all_regions_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `drs_job_exist` | `mute_non_default_regions` | Boolean |
|
||||
| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean |
|
||||
| `securityhub_enabled` | `mute_non_default_regions` | Boolean |
|
||||
|
||||
## Azure
|
||||
|
||||
@@ -50,8 +50,8 @@ The following list includes all the AWS checks with configurable variables that
|
||||
aws:
|
||||
|
||||
# AWS Global Configuration
|
||||
# aws.allowlist_non_default_regions --> Allowlist Failed Findings in non-default regions for GuardDuty, SecurityHub, DRS and Config
|
||||
allowlist_non_default_regions: False
|
||||
# aws.mute_non_default_regions --> Mute Failed Findings in non-default regions for GuardDuty, SecurityHub, DRS and Config
|
||||
mute_non_default_regions: False
|
||||
|
||||
# AWS IAM Configuration
|
||||
# aws.iam_user_accesskey_unused --> CIS recommends 45 days
|
||||
|
||||
@@ -47,7 +47,7 @@ It is a best practice to encrypt both metadata and connection passwords in AWS G
|
||||
#### Inspector
|
||||
Amazon Inspector is a vulnerability discovery service that automates continuous scanning for security vulnerabilities within your Amazon EC2, Amazon ECR, and AWS Lambda environments. Prowler recommends to enable it and resolve all the Inspector's findings. Ignoring the unused services, Prowler will only notify you if there are any Lambda functions, EC2 instances or ECR repositories in the region where Amazon inspector should be enabled.
|
||||
|
||||
- `inspector2_is_enabled`
|
||||
- `inspector2_findings_exist`
|
||||
|
||||
#### Macie
|
||||
Amazon Macie is a security service that uses machine learning to automatically discover, classify and protect sensitive data in S3 buckets. Prowler will only create a finding when Macie is not enabled if there are S3 buckets in your account.
|
||||
|
||||
|
After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
@@ -8,7 +8,7 @@ There are different log levels depending on the logging information that is desi
|
||||
|
||||
- **DEBUG**: It will show low-level logs from Python.
|
||||
- **INFO**: It will show all the API calls that are being invoked by the provider.
|
||||
- **WARNING**: It will show all resources that are being **allowlisted**.
|
||||
- **WARNING**: It will show all resources that are being **muted**.
|
||||
- **ERROR**: It will show any errors, e.g., not authorized actions.
|
||||
- **CRITICAL**: The default log level. If a critical log appears, it will **exit** Prowler’s execution.
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ Execute Prowler in verbose mode (like in Version 2):
|
||||
```console
|
||||
prowler <provider> --verbose
|
||||
```
|
||||
## Show only Fails
|
||||
Prowler can only display the failed findings:
|
||||
## Filter findings by status
|
||||
Prowler can filter the findings by their status:
|
||||
```console
|
||||
prowler <provider> -q/--quiet
|
||||
prowler <provider> --status [PASS, FAIL, MANUAL]
|
||||
```
|
||||
## Disable Exit Code 3
|
||||
Prowler does not trigger exit code 3 with failed checks:
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# Allowlisting
|
||||
# Mute Listing
|
||||
Sometimes you may find resources that are intentionally configured in a certain way that may be a bad practice but it is all right with it, for example an AWS S3 Bucket open to the internet hosting a web site, or an AWS Security Group with an open port needed in your use case.
|
||||
|
||||
Allowlist option works along with other options and adds a `WARNING` instead of `INFO`, `PASS` or `FAIL` to any output format.
|
||||
Mute List option works along with other options and adds a `MUTED` instead of `MANUAL`, `PASS` or `FAIL` to any output format.
|
||||
|
||||
You can use `-w`/`--allowlist-file` with the path of your allowlist yaml file, but first, let's review the syntax.
|
||||
You can use `-w`/`--mutelist-file` with the path of your mutelist yaml file, but first, let's review the syntax.
|
||||
|
||||
## Allowlist Yaml File Syntax
|
||||
## Mute List Yaml File Syntax
|
||||
|
||||
### 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.
|
||||
########################### ALLOWLIST EXAMPLE ###########################
|
||||
Allowlist:
|
||||
########################### MUTE LIST EXAMPLE ###########################
|
||||
Mute List:
|
||||
Accounts:
|
||||
"123456789012":
|
||||
Checks:
|
||||
@@ -79,10 +79,10 @@ You can use `-w`/`--allowlist-file` with the path of your allowlist yaml file, b
|
||||
Tags:
|
||||
- "environment=prod" # Will ignore every resource except in account 123456789012 except the ones containing the string "test" and tag environment=prod
|
||||
|
||||
## Allowlist specific regions
|
||||
If you want to allowlist/mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w allowlist.yaml`:
|
||||
## Mute specific regions
|
||||
If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`:
|
||||
|
||||
Allowlist:
|
||||
Mute List:
|
||||
Accounts:
|
||||
"*":
|
||||
Checks:
|
||||
@@ -93,50 +93,50 @@ If you want to allowlist/mute failed findings only in specific regions, create a
|
||||
Resources:
|
||||
- "*"
|
||||
|
||||
## Default AWS Allowlist
|
||||
Prowler provides you a Default AWS Allowlist with the AWS Resources that should be allowlisted such as all resources created by AWS Control Tower when setting up a landing zone.
|
||||
You can execute Prowler with this allowlist using the following command:
|
||||
## Default AWS Mute List
|
||||
Prowler provides you a Default AWS Mute List with the AWS Resources that should be muted such as all resources created by AWS Control Tower when setting up a landing zone.
|
||||
You can execute Prowler with this mutelist using the following command:
|
||||
```sh
|
||||
prowler aws --allowlist prowler/config/aws_allowlist.yaml
|
||||
prowler aws --mutelist prowler/config/aws_mutelist.yaml
|
||||
```
|
||||
## Supported Allowlist Locations
|
||||
## Supported Mute List Locations
|
||||
|
||||
The allowlisting flag supports the following locations:
|
||||
The mutelisting flag supports the following locations:
|
||||
|
||||
### Local file
|
||||
You will need to pass the local path where your Allowlist YAML file is located:
|
||||
You will need to pass the local path where your Mute List YAML file is located:
|
||||
```
|
||||
prowler <provider> -w allowlist.yaml
|
||||
prowler <provider> -w mutelist.yaml
|
||||
```
|
||||
### AWS S3 URI
|
||||
You will need to pass the S3 URI where your Allowlist YAML file was uploaded to your bucket:
|
||||
You will need to pass the S3 URI where your Mute List YAML file was uploaded to your bucket:
|
||||
```
|
||||
prowler aws -w s3://<bucket>/<prefix>/allowlist.yaml
|
||||
prowler aws -w s3://<bucket>/<prefix>/mutelist.yaml
|
||||
```
|
||||
> Make sure that the used AWS credentials have s3:GetObject permissions in the S3 path where the allowlist file is located.
|
||||
> Make sure that the used AWS credentials have s3:GetObject permissions in the S3 path where the mutelist file is located.
|
||||
|
||||
### AWS DynamoDB Table ARN
|
||||
|
||||
You will need to pass the DynamoDB Allowlist Table ARN:
|
||||
You will need to pass the DynamoDB Mute List Table ARN:
|
||||
|
||||
```
|
||||
prowler aws -w arn:aws:dynamodb:<region_name>:<account_id>:table/<table_name>
|
||||
```
|
||||
|
||||
1. The DynamoDB Table must have the following String keys:
|
||||
<img src="../img/allowlist-keys.png"/>
|
||||
<img src="../img/mutelist-keys.png"/>
|
||||
|
||||
- The Allowlist Table must have the following columns:
|
||||
- Accounts (String): This field can contain either an Account ID or an `*` (which applies to all the accounts that use this table as an allowlist).
|
||||
- The Mute List Table must have the following columns:
|
||||
- Accounts (String): This field can contain either an Account ID or an `*` (which applies to all the accounts that use this table as an mutelist).
|
||||
- Checks (String): This field can contain either a Prowler Check Name or an `*` (which applies to all the scanned checks).
|
||||
- Regions (List): This field contains a list of regions where this allowlist rule is applied (it can also contains an `*` to apply all scanned regions).
|
||||
- Resources (List): This field contains a list of regex expressions that applies to the resources that are wanted to be allowlisted.
|
||||
- Tags (List): -Optional- This field contains a list of tuples in the form of 'key=value' that applies to the resources tags that are wanted to be allowlisted.
|
||||
- Exceptions (Map): -Optional- This field contains a map of lists of accounts/regions/resources/tags that are wanted to be excepted in the allowlist.
|
||||
- Regions (List): This field contains a list of regions where this mutelist rule is applied (it can also contains an `*` to apply all scanned regions).
|
||||
- Resources (List): This field contains a list of regex expressions that applies to the resources that are wanted to be muted.
|
||||
- Tags (List): -Optional- This field contains a list of tuples in the form of 'key=value' that applies to the resources tags that are wanted to be muted.
|
||||
- Exceptions (Map): -Optional- This field contains a map of lists of accounts/regions/resources/tags that are wanted to be excepted in the mutelist.
|
||||
|
||||
The following example will allowlist all resources in all accounts for the EC2 checks in the regions `eu-west-1` and `us-east-1` with the tags `environment=dev` and `environment=prod`, except the resources containing the string `test` in the account `012345678912` and region `eu-west-1` with the tag `environment=prod`:
|
||||
The following example will mute all resources in all accounts for the EC2 checks in the regions `eu-west-1` and `us-east-1` with the tags `environment=dev` and `environment=prod`, except the resources containing the string `test` in the account `012345678912` and region `eu-west-1` with the tag `environment=prod`:
|
||||
|
||||
<img src="../img/allowlist-row.png"/>
|
||||
<img src="../img/mutelist-row.png"/>
|
||||
|
||||
> Make sure that the used AWS credentials have `dynamodb:PartiQLSelect` permissions in the table.
|
||||
|
||||
@@ -151,7 +151,7 @@ prowler aws -w arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME
|
||||
Make sure that the credentials that Prowler uses can invoke the Lambda Function:
|
||||
|
||||
```
|
||||
- PolicyName: GetAllowList
|
||||
- PolicyName: GetMuteList
|
||||
PolicyDocument:
|
||||
Version: '2012-10-17'
|
||||
Statement:
|
||||
@@ -160,14 +160,14 @@ Make sure that the credentials that Prowler uses can invoke the Lambda Function:
|
||||
Resource: arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME
|
||||
```
|
||||
|
||||
The Lambda Function can then generate an Allowlist dynamically. Here is the code an example Python Lambda Function that
|
||||
generates an Allowlist:
|
||||
The Lambda Function can then generate an Mute List dynamically. Here is the code an example Python Lambda Function that
|
||||
generates an Mute List:
|
||||
|
||||
```
|
||||
def handler(event, context):
|
||||
checks = {}
|
||||
checks["vpc_flow_logs_enabled"] = { "Regions": [ "*" ], "Resources": [ "" ], Optional("Tags"): [ "key:value" ] }
|
||||
|
||||
al = { "Allowlist": { "Accounts": { "*": { "Checks": checks } } } }
|
||||
al = { "Mute List": { "Accounts": { "*": { "Checks": checks } } } }
|
||||
return al
|
||||
```
|
||||
@@ -36,7 +36,7 @@ nav:
|
||||
- Slack Integration: tutorials/integrations.md
|
||||
- Configuration File: tutorials/configuration_file.md
|
||||
- Logging: tutorials/logging.md
|
||||
- Allowlist: tutorials/allowlist.md
|
||||
- Mute List: tutorials/mutelist.md
|
||||
- Check Aliases: tutorials/check-aliases.md
|
||||
- Custom Metadata: tutorials/custom-checks-metadata.md
|
||||
- Ignore Unused Services: tutorials/ignore-unused-services.md
|
||||
@@ -102,7 +102,7 @@ extra:
|
||||
link: https://twitter.com/prowlercloud
|
||||
|
||||
# Copyright
|
||||
copyright: Copyright © 2024 Toni de la Fuente, Maintained by the Prowler Team at ProwlerPro, Inc.</a>
|
||||
copyright: Copyright © 2022 Toni de la Fuente, Maintained by the Prowler Team at Verica, Inc.</a>
|
||||
|
||||
markdown_extensions:
|
||||
- abbr
|
||||
|
||||
@@ -6,13 +6,12 @@ import sys
|
||||
|
||||
from colorama import Fore, Style
|
||||
|
||||
from prowler.lib.banner import print_banner
|
||||
from prowler.config.config import get_available_compliance_frameworks
|
||||
from prowler.lib.check.check import (
|
||||
bulk_load_checks_metadata,
|
||||
bulk_load_compliance_frameworks,
|
||||
exclude_checks_to_run,
|
||||
exclude_services_to_run,
|
||||
execute_checks,
|
||||
list_categories,
|
||||
list_checks_json,
|
||||
list_services,
|
||||
@@ -30,15 +29,16 @@ from prowler.lib.check.custom_checks_metadata import (
|
||||
parse_custom_checks_metadata_file,
|
||||
update_checks_metadata,
|
||||
)
|
||||
from prowler.lib.check.managers import ExecutionManager
|
||||
from prowler.lib.cli.parser import ProwlerArgumentParser
|
||||
from prowler.lib.logger import logger, set_logging_config
|
||||
from prowler.lib.outputs.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.compliance.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.html import add_html_footer, fill_html_overview_statistics
|
||||
from prowler.lib.outputs.json import close_json
|
||||
from prowler.lib.outputs.outputs import extract_findings_statistics
|
||||
from prowler.lib.outputs.slack import send_slack_message
|
||||
from prowler.lib.outputs.summary_table import display_summary_table
|
||||
from prowler.providers.aws.aws_provider import get_available_aws_service_regions
|
||||
from prowler.lib.ui.live_display import live_display
|
||||
from prowler.providers.aws.lib.s3.s3 import send_to_s3_bucket
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import (
|
||||
batch_send_to_security_hub,
|
||||
@@ -46,11 +46,16 @@ from prowler.providers.aws.lib.security_hub.security_hub import (
|
||||
resolve_security_hub_previous_findings,
|
||||
verify_security_hub_integration_enabled_per_region,
|
||||
)
|
||||
from prowler.providers.common.allowlist import set_provider_allowlist
|
||||
from prowler.providers.common.audit_info import (
|
||||
set_provider_audit_info,
|
||||
set_provider_execution_parameters,
|
||||
)
|
||||
from prowler.providers.common.clean import clean_provider_local_output_directories
|
||||
from prowler.providers.common.common import (
|
||||
get_global_provider,
|
||||
set_global_provider_object,
|
||||
)
|
||||
from prowler.providers.common.mutelist import set_provider_mutelist
|
||||
from prowler.providers.common.outputs import set_provider_output_options
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
|
||||
@@ -73,12 +78,17 @@ def prowler():
|
||||
compliance_framework = args.compliance
|
||||
custom_checks_metadata_file = args.custom_checks_metadata_file
|
||||
|
||||
if not args.no_banner:
|
||||
print_banner(args)
|
||||
live_display.initialize(args)
|
||||
|
||||
# if not args.no_banner:
|
||||
# print_banner(args)
|
||||
|
||||
# We treat the compliance framework as another output format
|
||||
if compliance_framework:
|
||||
args.output_modes.extend(compliance_framework)
|
||||
# If no input compliance framework, set all
|
||||
else:
|
||||
args.output_modes.extend(get_available_compliance_frameworks(provider))
|
||||
|
||||
# Set Logger configuration
|
||||
set_logging_config(args.log_level, args.log_file, args.only_logs)
|
||||
@@ -148,6 +158,7 @@ def prowler():
|
||||
|
||||
# Set the audit info based on the selected provider
|
||||
audit_info = set_provider_audit_info(provider, args.__dict__)
|
||||
set_global_provider_object(args)
|
||||
|
||||
# Import custom checks from folder
|
||||
if checks_folder:
|
||||
@@ -172,12 +183,12 @@ def prowler():
|
||||
# Sort final check list
|
||||
checks_to_execute = sorted(checks_to_execute)
|
||||
|
||||
# Parse Allowlist
|
||||
allowlist_file = set_provider_allowlist(provider, audit_info, args)
|
||||
# Parse Mute List
|
||||
mutelist_file = set_provider_mutelist(provider, audit_info, args)
|
||||
|
||||
# Set output options based on the selected provider
|
||||
audit_output_options = set_provider_output_options(
|
||||
provider, args, audit_info, allowlist_file, bulk_checks_metadata
|
||||
provider, args, audit_info, mutelist_file, bulk_checks_metadata
|
||||
)
|
||||
|
||||
# Run the quick inventory for the provider if available
|
||||
@@ -187,14 +198,16 @@ def prowler():
|
||||
|
||||
# Execute checks
|
||||
findings = []
|
||||
|
||||
if len(checks_to_execute):
|
||||
findings = execute_checks(
|
||||
execution_manager = ExecutionManager(
|
||||
checks_to_execute,
|
||||
provider,
|
||||
audit_info,
|
||||
audit_output_options,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
findings = execution_manager.execute_checks()
|
||||
else:
|
||||
logger.error(
|
||||
"There are no checks to execute. Please, check your input arguments"
|
||||
@@ -256,9 +269,10 @@ def prowler():
|
||||
f"{Style.BRIGHT}\nSending findings to AWS Security Hub, please wait...{Style.RESET_ALL}"
|
||||
)
|
||||
# Verify where AWS Security Hub is enabled
|
||||
global_provider = get_global_provider()
|
||||
aws_security_enabled_regions = []
|
||||
security_hub_regions = (
|
||||
get_available_aws_service_regions("securityhub", audit_info)
|
||||
global_provider.get_available_aws_service_regions("securityhub")
|
||||
if not audit_info.audited_regions
|
||||
else audit_info.audited_regions
|
||||
)
|
||||
@@ -308,8 +322,12 @@ def prowler():
|
||||
provider,
|
||||
)
|
||||
|
||||
if compliance_framework and findings:
|
||||
for compliance in compliance_framework:
|
||||
if findings:
|
||||
compliance_overview = False
|
||||
if not compliance_framework:
|
||||
compliance_overview = True
|
||||
compliance_framework = get_available_compliance_frameworks(provider)
|
||||
for compliance in sorted(compliance_framework):
|
||||
# Display compliance table
|
||||
display_compliance_table(
|
||||
findings,
|
||||
@@ -317,12 +335,20 @@ def prowler():
|
||||
compliance,
|
||||
audit_output_options.output_filename,
|
||||
audit_output_options.output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
if compliance_overview:
|
||||
print(
|
||||
f"\nDetailed compliance results are in {Fore.YELLOW}{audit_output_options.output_directory}/compliance/{Style.RESET_ALL}\n"
|
||||
)
|
||||
|
||||
# If custom checks were passed, remove the modules
|
||||
if checks_folder:
|
||||
remove_custom_checks_module(checks_folder, provider)
|
||||
|
||||
# clean local directories
|
||||
clean_provider_local_output_directories(args)
|
||||
|
||||
# If there are failed findings exit code 3, except if -z is input
|
||||
if not args.ignore_exit_code_3 and stats["total_fail"] > 0:
|
||||
sys.exit(3)
|
||||
|
||||
@@ -468,6 +468,27 @@
|
||||
},
|
||||
{
|
||||
"Id": "2.1.1",
|
||||
"Description": "Ensure all S3 buckets employ encryption-at-rest",
|
||||
"Checks": [
|
||||
"s3_bucket_default_encryption"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
"Section": "2.1. Simple Storage Service (S3)",
|
||||
"Profile": "Level 2",
|
||||
"AssessmentStatus": "Automated",
|
||||
"Description": "Amazon S3 provides a variety of no, or low, cost encryption options to protect data at rest.",
|
||||
"RationaleStatement": "Encrypting data at rest reduces the likelihood that it is unintentionally exposed and can nullify the impact of disclosure if the encryption remains unbroken.",
|
||||
"ImpactStatement": "Amazon S3 buckets with default bucket encryption using SSE-KMS cannot be used as destination buckets for Amazon S3 server access logging. Only SSE-S3 default encryption is supported for server access log destination buckets.",
|
||||
"RemediationProcedure": "**From Console:** 1. Login to AWS Management Console and open the Amazon S3 console using https://console.aws.amazon.com/s3/ 2. Select a Bucket. 3. Click on 'Properties'. 4. Click edit on `Default Encryption`. 5. Select either `AES-256`, `AWS-KMS`, `SSE-KMS` or `SSE-S3`. 6. Click `Save` 7. Repeat for all the buckets in your AWS account lacking encryption. **From Command Line:** Run either ``` aws s3api put-bucket-encryption --bucket <bucket name> --server-side-encryption-configuration '{\"Rules\": [{\"ApplyServerSideEncryptionByDefault\": {\"SSEAlgorithm\": \"AES256\"}}]}' ``` or ``` aws s3api put-bucket-encryption --bucket <bucket name> --server-side-encryption-configuration '{\"Rules\": [{\"ApplyServerSideEncryptionByDefault\": {\"SSEAlgorithm\": \"aws:kms\",\"KMSMasterKeyID\": \"aws/s3\"}}]}' ``` **Note:** the KMSMasterKeyID can be set to the master key of your choosing; aws/s3 is an AWS preconfigured default.",
|
||||
"AuditProcedure": "**From Console:** 1. Login to AWS Management Console and open the Amazon S3 console using https://console.aws.amazon.com/s3/ 2. Select a Bucket. 3. Click on 'Properties'. 4. Verify that `Default Encryption` is enabled, and displays either `AES-256`, `AWS-KMS`, `SSE-KMS` or `SSE-S3`. 5. Repeat for all the buckets in your AWS account. **From Command Line:** 1. Run command to list buckets ``` aws s3 ls ``` 2. For each bucket, run ``` aws s3api get-bucket-encryption --bucket <bucket name> ``` 3. Verify that either ``` \"SSEAlgorithm\": \"AES256\" ``` or ``` \"SSEAlgorithm\": \"aws:kms\"``` is displayed.",
|
||||
"AdditionalInformation": "S3 bucket encryption only applies to objects as they are placed in the bucket. Enabling S3 bucket encryption does **not** encrypt objects previously stored within the bucket.",
|
||||
"References": "https://docs.aws.amazon.com/AmazonS3/latest/user-guide/default-bucket-encryption.html:https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html#bucket-encryption-related-resources"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "2.1.2",
|
||||
"Description": "Ensure S3 Bucket Policy is set to deny HTTP requests",
|
||||
"Checks": [
|
||||
"s3_bucket_secure_transport_policy"
|
||||
@@ -488,7 +509,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "2.1.2",
|
||||
"Id": "2.1.3",
|
||||
"Description": "Ensure MFA Delete is enabled on S3 buckets",
|
||||
"Checks": [
|
||||
"s3_bucket_no_mfa_delete"
|
||||
@@ -509,7 +530,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "2.1.3",
|
||||
"Id": "2.1.4",
|
||||
"Description": "Ensure all data in Amazon S3 has been discovered, classified and secured when required.",
|
||||
"Checks": [
|
||||
"macie_is_enabled"
|
||||
@@ -530,7 +551,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "2.1.4",
|
||||
"Id": "2.1.5",
|
||||
"Description": "Ensure that S3 Buckets are configured with 'Block public access (bucket settings)'",
|
||||
"Checks": [
|
||||
"s3_bucket_level_public_access_block",
|
||||
|
||||
@@ -211,31 +211,6 @@
|
||||
"iam_avoid_root_usage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "op.acc.4.aws.iam.8",
|
||||
"Description": "Proceso de gestión de derechos de acceso",
|
||||
"Attributes": [
|
||||
{
|
||||
"IdGrupoControl": "op.acc.4",
|
||||
"Marco": "operacional",
|
||||
"Categoria": "control de acceso",
|
||||
"DescripcionControl": "Se restringirá todo acceso a las acciones especificadas para el usuario root de una cuenta.",
|
||||
"Nivel": "alto",
|
||||
"Tipo": "requisito",
|
||||
"Dimensiones": [
|
||||
"confidencialidad",
|
||||
"integridad",
|
||||
"trazabilidad",
|
||||
"autenticidad"
|
||||
],
|
||||
"ModoEjecucion": "automático"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"organizations_account_part_of_organizations",
|
||||
"organizations_scp_check_deny_regions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "op.acc.4.aws.iam.9",
|
||||
"Description": "Proceso de gestión de derechos de acceso",
|
||||
@@ -814,8 +789,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist"
|
||||
"inspector2_findings_exist"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1147,30 +1121,6 @@
|
||||
"cloudtrail_insights_exist"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "op.exp.8.r1.aws.ct.3",
|
||||
"Description": "Revisión de los registros",
|
||||
"Attributes": [
|
||||
{
|
||||
"IdGrupoControl": "op.exp.8.r1",
|
||||
"Marco": "operacional",
|
||||
"Categoria": "explotación",
|
||||
"DescripcionControl": "Registrar los eventos de lectura y escritura de datos.",
|
||||
"Nivel": "alto",
|
||||
"Tipo": "refuerzo",
|
||||
"Dimensiones": [
|
||||
"trazabilidad"
|
||||
],
|
||||
"ModoEjecucion": "automático"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled",
|
||||
"cloudtrail_s3_dataevents_write_enabled",
|
||||
"cloudtrail_s3_dataevents_read_enabled",
|
||||
"cloudtrail_insights_exist"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "op.exp.8.r1.aws.ct.4",
|
||||
"Description": "Revisión de los registros",
|
||||
@@ -1283,33 +1233,6 @@
|
||||
"iam_role_cross_service_confused_deputy_prevention"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "op.exp.8.r4.aws.ct.1",
|
||||
"Description": "Control de acceso",
|
||||
"Attributes": [
|
||||
{
|
||||
"IdGrupoControl": "op.exp.8.r4",
|
||||
"Marco": "operacional",
|
||||
"Categoria": "explotación",
|
||||
"DescripcionControl": "Asignar correctamente las políticas AWS IAM para el acceso y borrado de los registros y sus copias de seguridad haciendo uso del principio de mínimo privilegio.",
|
||||
"Nivel": "alto",
|
||||
"Tipo": "refuerzo",
|
||||
"Dimensiones": [
|
||||
"trazabilidad"
|
||||
],
|
||||
"ModoEjecucion": "automático"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"iam_policy_allows_privilege_escalation",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_unattached_policy_no_administrative_privilege",
|
||||
"iam_no_custom_policy_permissive_role_assumption",
|
||||
"iam_policy_attached_only_to_group_or_roles",
|
||||
"iam_role_cross_service_confused_deputy_prevention",
|
||||
"iam_policy_no_full_access_to_cloudtrail"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "op.exp.8.r4.aws.ct.2",
|
||||
"Description": "Control de acceso",
|
||||
@@ -1936,8 +1859,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist"
|
||||
"inspector2_findings_exist"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2012,8 +1934,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist"
|
||||
"inspector2_findings_exist"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2189,7 +2110,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"fms_policy_compliant"
|
||||
"networkfirewall_in_all_vpc"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2330,31 +2251,6 @@
|
||||
"cloudfront_distributions_https_enabled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "mp.com.4.aws.ws.1",
|
||||
"Description": "Separación de flujos de información en la red",
|
||||
"Attributes": [
|
||||
{
|
||||
"IdGrupoControl": "mp.com.4",
|
||||
"Marco": "medidas de protección",
|
||||
"Categoria": "segregación de redes",
|
||||
"DescripcionControl": "Se deberán abrir solo los puertos necesarios para el uso del servicio AWS WorkSpaces.",
|
||||
"Nivel": "alto",
|
||||
"Tipo": "requisito",
|
||||
"Dimensiones": [
|
||||
"confidencialidad",
|
||||
"integridad",
|
||||
"trazabilidad",
|
||||
"autenticidad",
|
||||
"disponibilidad"
|
||||
],
|
||||
"ModoEjecucion": "automático"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"workspaces_vpc_2private_1public_subnets_nat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "mp.com.4.aws.vpc.1",
|
||||
"Description": "Separación de flujos de información en la red",
|
||||
@@ -2427,8 +2323,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"vpc_subnet_separate_private_public",
|
||||
"vpc_different_regions"
|
||||
"vpc_subnet_separate_private_public"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2475,8 +2370,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"vpc_subnet_different_az",
|
||||
"vpc_different_regions"
|
||||
"vpc_subnet_different_az"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
"securityhub_enabled",
|
||||
"elbv2_waf_acl_attached",
|
||||
"guardduty_is_enabled",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"inspector2_findings_exist",
|
||||
"awslambda_function_not_publicly_accessible",
|
||||
"ec2_instance_public_ip"
|
||||
],
|
||||
@@ -577,8 +576,7 @@
|
||||
"config_recorder_all_regions_enabled",
|
||||
"securityhub_enabled",
|
||||
"guardduty_is_enabled",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist"
|
||||
"inspector2_findings_exist"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -739,8 +737,7 @@
|
||||
"iam_user_hardware_mfa_enabled",
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"securityhub_enabled",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist"
|
||||
"inspector2_findings_exist"
|
||||
],
|
||||
"Attributes": [
|
||||
{
|
||||
@@ -1895,8 +1892,7 @@
|
||||
"networkfirewall_in_all_vpc",
|
||||
"elbv2_waf_acl_attached",
|
||||
"guardduty_is_enabled",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"inspector2_findings_exist",
|
||||
"ec2_networkacl_allow_ingress_any_port",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_22",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_3389",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"ItemId": "cc_1_1",
|
||||
"Section": "CC1.0 - Common Criteria Related to Control Environment",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -27,7 +27,7 @@
|
||||
"ItemId": "cc_1_2",
|
||||
"Section": "CC1.0 - Common Criteria Related to Control Environment",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -41,7 +41,7 @@
|
||||
"ItemId": "cc_1_3",
|
||||
"Section": "CC1.0 - Common Criteria Related to Control Environment",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -62,7 +62,7 @@
|
||||
"ItemId": "cc_1_4",
|
||||
"Section": "CC1.0 - Common Criteria Related to Control Environment",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -76,7 +76,7 @@
|
||||
"ItemId": "cc_1_5",
|
||||
"Section": "CC1.0 - Common Criteria Related to Control Environment",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -90,7 +90,7 @@
|
||||
"ItemId": "cc_2_1",
|
||||
"Section": "CC2.0 - Common Criteria Related to Communication and Information",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -109,7 +109,7 @@
|
||||
"ItemId": "cc_2_2",
|
||||
"Section": "CC2.0 - Common Criteria Related to Communication and Information",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -123,7 +123,7 @@
|
||||
"ItemId": "cc_2_3",
|
||||
"Section": "CC2.0 - Common Criteria Related to Communication and Information",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -137,7 +137,7 @@
|
||||
"ItemId": "cc_3_1",
|
||||
"Section": "CC3.0 - Common Criteria Related to Risk Assessment",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -155,7 +155,7 @@
|
||||
"ItemId": "cc_3_2",
|
||||
"Section": "CC3.0 - Common Criteria Related to Risk Assessment",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -175,7 +175,7 @@
|
||||
"ItemId": "cc_3_3",
|
||||
"Section": "CC3.0 - Common Criteria Related to Risk Assessment",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -189,7 +189,7 @@
|
||||
"ItemId": "cc_3_4",
|
||||
"Section": "CC3.0 - Common Criteria Related to Risk Assessment",
|
||||
"Service": "config",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -205,7 +205,7 @@
|
||||
"ItemId": "cc_4_1",
|
||||
"Section": "CC4.0 - Monitoring Activities",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -219,7 +219,7 @@
|
||||
"ItemId": "cc_4_2",
|
||||
"Section": "CC4.0 - Monitoring Activities",
|
||||
"Service": "guardduty",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -236,7 +236,7 @@
|
||||
"ItemId": "cc_5_1",
|
||||
"Section": "CC5.0 - Control Activities",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -250,7 +250,7 @@
|
||||
"ItemId": "cc_5_2",
|
||||
"Section": "CC5.0 - Control Activities",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -264,7 +264,7 @@
|
||||
"ItemId": "cc_5_3",
|
||||
"Section": "CC5.0 - Control Activities",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -278,7 +278,7 @@
|
||||
"ItemId": "cc_6_1",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "s3",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -294,7 +294,7 @@
|
||||
"ItemId": "cc_6_2",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "rds",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -310,7 +310,7 @@
|
||||
"ItemId": "cc_6_3",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "iam",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -328,7 +328,7 @@
|
||||
"ItemId": "cc_6_4",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -342,7 +342,7 @@
|
||||
"ItemId": "cc_6_5",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -356,7 +356,7 @@
|
||||
"ItemId": "cc_6_6",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "ec2",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -372,7 +372,7 @@
|
||||
"ItemId": "cc_6_7",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "acm",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -388,7 +388,7 @@
|
||||
"ItemId": "cc_6_8",
|
||||
"Section": "CC6.0 - Logical and Physical Access",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -405,7 +405,7 @@
|
||||
"ItemId": "cc_7_1",
|
||||
"Section": "CC7.0 - System Operations",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -424,7 +424,7 @@
|
||||
"ItemId": "cc_7_2",
|
||||
"Section": "CC7.0 - System Operations",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -460,7 +460,7 @@
|
||||
"ItemId": "cc_7_3",
|
||||
"Section": "CC7.0 - System Operations",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -492,7 +492,7 @@
|
||||
"ItemId": "cc_7_4",
|
||||
"Section": "CC7.0 - System Operations",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -523,7 +523,7 @@
|
||||
"ItemId": "cc_7_5",
|
||||
"Section": "CC7.0 - System Operations",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -537,7 +537,7 @@
|
||||
"ItemId": "cc_8_1",
|
||||
"Section": "CC8.0 - Change Management",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -553,7 +553,7 @@
|
||||
"ItemId": "cc_9_1",
|
||||
"Section": "CC9.0 - Risk Mitigation",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -567,7 +567,7 @@
|
||||
"ItemId": "cc_9_2",
|
||||
"Section": "CC9.0 - Risk Mitigation",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -581,7 +581,7 @@
|
||||
"ItemId": "cc_a_1_1",
|
||||
"Section": "CCA1.0 - Additional Criterial for Availability",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -595,7 +595,7 @@
|
||||
"ItemId": "cc_a_1_2",
|
||||
"Section": "CCA1.0 - Additional Criterial for Availability",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -626,7 +626,7 @@
|
||||
"ItemId": "cc_a_1_3",
|
||||
"Section": "CCA1.0 - Additional Criterial for Availability",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -640,7 +640,7 @@
|
||||
"ItemId": "cc_c_1_1",
|
||||
"Section": "CCC1.0 - Additional Criterial for Confidentiality",
|
||||
"Service": "aws",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -656,7 +656,7 @@
|
||||
"ItemId": "cc_c_1_2",
|
||||
"Section": "CCC1.0 - Additional Criterial for Confidentiality",
|
||||
"Service": "s3",
|
||||
"Type": "automated"
|
||||
"Soc_Type": "automated"
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
@@ -672,7 +672,7 @@
|
||||
"ItemId": "p_1_1",
|
||||
"Section": "P1.0 - Privacy Criteria Related to Notice and Communication of Objectives Related to Privacy",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -686,7 +686,7 @@
|
||||
"ItemId": "p_2_1",
|
||||
"Section": "P2.0 - Privacy Criteria Related to Choice and Consent",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -700,7 +700,7 @@
|
||||
"ItemId": "p_3_1",
|
||||
"Section": "P3.0 - Privacy Criteria Related to Collection",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -714,7 +714,7 @@
|
||||
"ItemId": "p_3_2",
|
||||
"Section": "P3.0 - Privacy Criteria Related to Collection",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -728,7 +728,7 @@
|
||||
"ItemId": "p_4_1",
|
||||
"Section": "P4.0 - Privacy Criteria Related to Use, Retention, and Disposal",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -742,7 +742,7 @@
|
||||
"ItemId": "p_4_2",
|
||||
"Section": "P4.0 - Privacy Criteria Related to Use, Retention, and Disposal",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -756,7 +756,7 @@
|
||||
"ItemId": "p_4_3",
|
||||
"Section": "P4.0 - Privacy Criteria Related to Use, Retention, and Disposal",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -770,7 +770,7 @@
|
||||
"ItemId": "p_5_1",
|
||||
"Section": "P5.0 - Privacy Criteria Related to Access",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -784,7 +784,7 @@
|
||||
"ItemId": "p_5_2",
|
||||
"Section": "P5.0 - Privacy Criteria Related to Access",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -798,7 +798,7 @@
|
||||
"ItemId": "p_6_1",
|
||||
"Section": "P6.0 - Privacy Criteria Related to Disclosure and Notification",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -812,7 +812,7 @@
|
||||
"ItemId": "p_6_2",
|
||||
"Section": "P6.0 - Privacy Criteria Related to Disclosure and Notification",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -826,7 +826,7 @@
|
||||
"ItemId": "p_6_3",
|
||||
"Section": "P6.0 - Privacy Criteria Related to Disclosure and Notification",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -840,7 +840,7 @@
|
||||
"ItemId": "p_6_4",
|
||||
"Section": "P6.0 - Privacy Criteria Related to Disclosure and Notification",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -854,7 +854,7 @@
|
||||
"ItemId": "p_6_5",
|
||||
"Section": "P6.0 - Privacy Criteria Related to Disclosure and Notification",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -868,7 +868,7 @@
|
||||
"ItemId": "p_6_6",
|
||||
"Section": "P6.0 - Privacy Criteria Related to Disclosure and Notification",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -882,7 +882,7 @@
|
||||
"ItemId": "p_6_7",
|
||||
"Section": "P6.0 - Privacy Criteria Related to Disclosure and Notification",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -896,7 +896,7 @@
|
||||
"ItemId": "p_7_1",
|
||||
"Section": "P7.0 - Privacy Criteria Related to Quality",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
@@ -910,7 +910,7 @@
|
||||
"ItemId": "p_8_1",
|
||||
"Section": "P8.0 - Privacy Criteria Related to Monitoring and Enforcement",
|
||||
"Service": "aws",
|
||||
"Type": "manual"
|
||||
"Soc_Type": "manual"
|
||||
}
|
||||
],
|
||||
"Checks": []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Allowlist:
|
||||
Mute List:
|
||||
Accounts:
|
||||
"*":
|
||||
########################### AWS CONTROL TOWER ###########################
|
||||
@@ -3,8 +3,8 @@
|
||||
### 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.
|
||||
########################### ALLOWLIST EXAMPLE ###########################
|
||||
Allowlist:
|
||||
########################### MUTE LIST EXAMPLE ###########################
|
||||
Mute List:
|
||||
Accounts:
|
||||
"123456789012":
|
||||
Checks:
|
||||
@@ -11,7 +11,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "3.13.0"
|
||||
prowler_version = "3.11.3"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
html_logo_img = "https://user-images.githubusercontent.com/3985464/113734260-7ba06900-96fb-11eb-82bc-d4f68a1e2710.png"
|
||||
square_logo_img = "https://user-images.githubusercontent.com/38561120/235905862-9ece5bd7-9aa3-4e48-807a-3a9035eb8bfb.png"
|
||||
@@ -25,13 +25,19 @@ banner_color = "\033[1;92m"
|
||||
# Severities
|
||||
valid_severities = ["critical", "high", "medium", "low", "informational"]
|
||||
|
||||
# Statuses
|
||||
finding_statuses = ["PASS", "FAIL", "MANUAL"]
|
||||
|
||||
# Compliance
|
||||
actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
|
||||
def get_available_compliance_frameworks():
|
||||
def get_available_compliance_frameworks(provider=None):
|
||||
available_compliance_frameworks = []
|
||||
for provider in ["aws", "gcp", "azure"]:
|
||||
providers = ["aws", "gcp", "azure"]
|
||||
if provider:
|
||||
providers = [provider]
|
||||
for provider in providers:
|
||||
with os.scandir(f"{actual_directory}/../compliance/{provider}") as files:
|
||||
for file in files:
|
||||
if file.is_file() and file.name.endswith(".json"):
|
||||
@@ -50,7 +56,6 @@ aws_services_json_file = "aws_regions_by_service.json"
|
||||
# gcp_zones_json_file = "gcp_zones.json"
|
||||
|
||||
default_output_directory = getcwd() + "/output"
|
||||
|
||||
output_file_timestamp = timestamp.strftime("%Y%m%d%H%M%S")
|
||||
timestamp_iso = timestamp.isoformat(sep=" ", timespec="seconds")
|
||||
csv_file_suffix = ".csv"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
aws:
|
||||
|
||||
# AWS Global Configuration
|
||||
# aws.allowlist_non_default_regions --> Set to True to allowlist failed findings in non-default regions for AccessAnalyzer, GuardDuty, SecurityHub, DRS and Config
|
||||
allowlist_non_default_regions: False
|
||||
# If you want to allowlist/mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w allowlist.yaml`:
|
||||
# Allowlist:
|
||||
# aws.mute_non_default_regions --> Set to True to mute failed findings in non-default regions for GuardDuty, SecurityHub, DRS and Config
|
||||
mute_non_default_regions: False
|
||||
# If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`:
|
||||
# Mute List:
|
||||
# Accounts:
|
||||
# "*":
|
||||
# Checks:
|
||||
@@ -69,8 +69,8 @@ aws:
|
||||
# AWS Organizations
|
||||
# organizations_scp_check_deny_regions
|
||||
# organizations_enabled_regions: [
|
||||
# "eu-central-1",
|
||||
# "eu-west-1",
|
||||
# 'eu-central-1',
|
||||
# 'eu-west-1',
|
||||
# "us-east-1"
|
||||
# ]
|
||||
organizations_enabled_regions: []
|
||||
@@ -92,3 +92,6 @@ azure:
|
||||
|
||||
# GCP Configuration
|
||||
gcp:
|
||||
|
||||
# Kubernetes Configuration
|
||||
kubernetes:
|
||||
|
||||
@@ -13,3 +13,7 @@ CustomChecksMetadata:
|
||||
Checks:
|
||||
compute_instance_public_ip:
|
||||
Severity: critical
|
||||
kubernetes:
|
||||
Checks:
|
||||
apiserver_anonymous_requests:
|
||||
Severity: low
|
||||
|
||||
@@ -4,7 +4,7 @@ from prowler.config.config import banner_color, orange_color, prowler_version, t
|
||||
|
||||
|
||||
def print_banner(args):
|
||||
banner = rf"""{banner_color} _
|
||||
banner = f"""{banner_color} _
|
||||
_ __ _ __ _____ _| | ___ _ __
|
||||
| '_ \| '__/ _ \ \ /\ / / |/ _ \ '__|
|
||||
| |_) | | | (_) \ V V /| | __/ |
|
||||
@@ -15,13 +15,13 @@ def print_banner(args):
|
||||
"""
|
||||
print(banner)
|
||||
|
||||
if args.verbose or args.quiet:
|
||||
if args.verbose:
|
||||
print(
|
||||
f"""
|
||||
Color code for results:
|
||||
- {Fore.YELLOW}INFO (Information){Style.RESET_ALL}
|
||||
- {Fore.YELLOW}MANUAL (Manual check){Style.RESET_ALL}
|
||||
- {Fore.GREEN}PASS (Recommended value){Style.RESET_ALL}
|
||||
- {orange_color}WARNING (Ignored by allowlist){Style.RESET_ALL}
|
||||
- {orange_color}MUTED (Muted by muted list){Style.RESET_ALL}
|
||||
- {Fore.RED}FAIL (Fix required){Style.RESET_ALL}
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -10,18 +10,19 @@ from pkgutil import walk_packages
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from alive_progress import alive_bar
|
||||
from colorama import Fore, Style
|
||||
|
||||
import prowler
|
||||
from prowler.config.config import orange_color
|
||||
from prowler.lib.check.compliance_models import load_compliance_framework
|
||||
from prowler.lib.check.custom_checks_metadata import update_check_metadata
|
||||
from prowler.lib.check.managers import ExecutionManager
|
||||
from prowler.lib.check.models import Check, load_check_metadata
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.outputs import report
|
||||
from prowler.lib.ui.live_display import live_display
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file
|
||||
from prowler.providers.aws.lib.allowlist.allowlist import allowlist_findings
|
||||
from prowler.providers.aws.lib.mutelist.mutelist import mutelist_findings
|
||||
from prowler.providers.common.common import get_global_provider
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.outputs import Provider_Output_Options
|
||||
|
||||
@@ -67,9 +68,9 @@ def bulk_load_compliance_frameworks(provider: str) -> dict:
|
||||
# cis_v1.4_aws.json --> cis_v1.4_aws
|
||||
compliance_framework_name = filename.split(".json")[0]
|
||||
# Store the compliance info
|
||||
bulk_compliance_frameworks[compliance_framework_name] = (
|
||||
load_compliance_framework(file_path)
|
||||
)
|
||||
bulk_compliance_frameworks[
|
||||
compliance_framework_name
|
||||
] = load_compliance_framework(file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}] -- {e}")
|
||||
|
||||
@@ -431,8 +432,10 @@ def execute_checks(
|
||||
services_executed = set()
|
||||
checks_executed = set()
|
||||
|
||||
global_provider = get_global_provider()
|
||||
|
||||
# Initialize the Audit Metadata
|
||||
audit_info.audit_metadata = Audit_Metadata(
|
||||
global_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=checks_to_execute,
|
||||
completed_checks=0,
|
||||
@@ -492,47 +495,57 @@ def execute_checks(
|
||||
print(
|
||||
f"{Style.BRIGHT}Executing {checks_num} {check_noun}, please wait...{Style.RESET_ALL}\n"
|
||||
)
|
||||
with alive_bar(
|
||||
total=len(checks_to_execute),
|
||||
ctrl_c=False,
|
||||
bar="blocks",
|
||||
spinner="classic",
|
||||
stats=False,
|
||||
enrich_print=False,
|
||||
) as bar:
|
||||
for check_name in checks_to_execute:
|
||||
# Recover service from check name
|
||||
service = check_name.split("_")[0]
|
||||
bar.title = (
|
||||
f"-> Scanning {orange_color}{service}{Style.RESET_ALL} service"
|
||||
execution_manager = ExecutionManager(provider, checks_to_execute)
|
||||
total_checks = execution_manager.total_checks_per_service()
|
||||
completed_checks = {service: 0 for service in total_checks}
|
||||
service_findings = []
|
||||
for service, check_name in execution_manager.execute_checks():
|
||||
try:
|
||||
check_findings = execute(
|
||||
service,
|
||||
check_name,
|
||||
provider,
|
||||
audit_output_options,
|
||||
audit_info,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
try:
|
||||
check_findings = execute(
|
||||
service,
|
||||
check_name,
|
||||
provider,
|
||||
audit_output_options,
|
||||
audit_info,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
all_findings.extend(check_findings)
|
||||
all_findings.extend(check_findings)
|
||||
service_findings.extend(check_findings)
|
||||
# Update the completed checks count
|
||||
completed_checks[service] += 1
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {provider.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
bar()
|
||||
bar.title = f"-> {Fore.GREEN}Scan completed!{Style.RESET_ALL}"
|
||||
# Check if all checks for the service are completed
|
||||
if completed_checks[service] == total_checks[service]:
|
||||
# All checks for the service are completed
|
||||
# Add a summary table or perform other actions
|
||||
live_display.add_results_for_service(service, service_findings)
|
||||
# Clear service_findings
|
||||
service_findings = []
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {provider.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return all_findings
|
||||
|
||||
|
||||
def create_check_service_dict(checks_to_execute):
|
||||
output = {}
|
||||
for check_name in checks_to_execute:
|
||||
service = check_name.split("_")[0]
|
||||
if service not in output.keys():
|
||||
output[service] = []
|
||||
output[service].append(check_name)
|
||||
return output
|
||||
|
||||
|
||||
def execute(
|
||||
service: str,
|
||||
check_name: str,
|
||||
@@ -543,6 +556,7 @@ def execute(
|
||||
checks_executed: set,
|
||||
custom_checks_metadata: Any,
|
||||
):
|
||||
global_provider = get_global_provider()
|
||||
# Import check module
|
||||
check_module_path = (
|
||||
f"prowler.providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
@@ -562,15 +576,15 @@ def execute(
|
||||
# Update Audit Status
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
audit_info.audit_metadata = update_audit_metadata(
|
||||
audit_info.audit_metadata, services_executed, checks_executed
|
||||
global_provider.audit_metadata = update_audit_metadata(
|
||||
global_provider.audit_metadata, services_executed, checks_executed
|
||||
)
|
||||
|
||||
# Allowlist findings
|
||||
if audit_output_options.allowlist_file:
|
||||
check_findings = allowlist_findings(
|
||||
audit_output_options.allowlist_file,
|
||||
audit_info.audited_account,
|
||||
# Mute List findings
|
||||
if audit_output_options.mutelist_file:
|
||||
check_findings = mutelist_findings(
|
||||
audit_output_options.mutelist_file,
|
||||
global_provider.audited_account,
|
||||
check_findings,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import ast
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
class ImportFinder(ast.NodeVisitor):
|
||||
def __init__(self, provider):
|
||||
self.imports = set()
|
||||
self.provider = provider
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
if node.module and f"prowler.providers.{self.provider}.services" in node.module:
|
||||
for name in node.names:
|
||||
if "_client" in name.name:
|
||||
self.imports.add(name.name)
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
def analyze_check_file(file_path, provider):
|
||||
# Prase the check file
|
||||
with open(file_path, "r") as file:
|
||||
node = ast.parse(file.read(), filename=file_path)
|
||||
|
||||
finder = ImportFinder(provider)
|
||||
finder.visit(node)
|
||||
return list(finder.imports)
|
||||
|
||||
|
||||
def get_dependencies_for_checks(provider, checks_dict):
|
||||
|
||||
current_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
prowler_dir = current_directory.parent.parent
|
||||
check_dependencies = {}
|
||||
for service_name, checks in checks_dict.items():
|
||||
check_dependencies[service_name] = {}
|
||||
for check_name in checks:
|
||||
relative_path = f"providers/{provider}/services/{service_name}/{check_name}/{check_name}.py"
|
||||
check_file_path = prowler_dir / relative_path
|
||||
if not check_file_path.exists():
|
||||
logger.error(
|
||||
f"{check_name} does not exist at {relative_path}! Cannot determine service dependencies"
|
||||
)
|
||||
continue
|
||||
clients = analyze_check_file(str(check_file_path), provider)
|
||||
check_dependencies[service_name][check_name] = clients
|
||||
return check_dependencies
|
||||
@@ -34,9 +34,7 @@ def load_checks_to_execute(
|
||||
for check, metadata in bulk_checks_metadata.items():
|
||||
# Aliases
|
||||
for alias in metadata.CheckAliases:
|
||||
if alias not in check_aliases:
|
||||
check_aliases[alias] = []
|
||||
check_aliases[alias].append(check)
|
||||
check_aliases[alias] = check
|
||||
|
||||
# Severities
|
||||
if metadata.Severity:
|
||||
@@ -112,20 +110,15 @@ def update_checks_to_execute_with_aliases(
|
||||
) -> set:
|
||||
"""update_checks_to_execute_with_aliases returns the checks_to_execute updated using the check aliases."""
|
||||
# Verify if any input check is an alias of another check
|
||||
try:
|
||||
new_checks_to_execute = checks_to_execute.copy()
|
||||
for input_check in checks_to_execute:
|
||||
if input_check in check_aliases:
|
||||
# Remove input check name and add the real one
|
||||
new_checks_to_execute.remove(input_check)
|
||||
for alias in check_aliases[input_check]:
|
||||
if alias not in new_checks_to_execute:
|
||||
new_checks_to_execute.add(alias)
|
||||
print(
|
||||
f"\nUsing alias {Fore.YELLOW}{input_check}{Style.RESET_ALL} for check {Fore.YELLOW}{alias}{Style.RESET_ALL}..."
|
||||
)
|
||||
return new_checks_to_execute
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
for input_check in checks_to_execute:
|
||||
if (
|
||||
input_check in check_aliases
|
||||
and check_aliases[input_check] not in checks_to_execute
|
||||
):
|
||||
# Remove input check name and add the real one
|
||||
checks_to_execute.remove(input_check)
|
||||
checks_to_execute.add(check_aliases[input_check])
|
||||
print(
|
||||
f"\nUsing alias {Fore.YELLOW}{input_check}{Style.RESET_ALL} for check {Fore.YELLOW}{check_aliases[input_check]}{Style.RESET_ALL}...\n"
|
||||
)
|
||||
return checks_to_execute
|
||||
|
||||
@@ -52,12 +52,12 @@ class ENS_Requirement_Attribute(BaseModel):
|
||||
class Generic_Compliance_Requirement_Attribute(BaseModel):
|
||||
"""Generic Compliance Requirement Attribute"""
|
||||
|
||||
ItemId: Optional[str]
|
||||
ItemId: str
|
||||
Section: Optional[str]
|
||||
SubSection: Optional[str]
|
||||
SubGroup: Optional[str]
|
||||
Service: Optional[str]
|
||||
Type: Optional[str]
|
||||
Service: str
|
||||
Soc_Type: Optional[str]
|
||||
|
||||
|
||||
class CIS_Requirement_Attribute_Profile(str):
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from types import ModuleType
|
||||
from typing import Any, Set
|
||||
|
||||
from colorama import Fore, Style
|
||||
|
||||
from prowler.lib.check.check_to_client_mapper import get_dependencies_for_checks
|
||||
from prowler.lib.check.custom_checks_metadata import update_check_metadata
|
||||
from prowler.lib.check.models import Check
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.outputs import report
|
||||
from prowler.lib.ui.live_display import live_display
|
||||
from prowler.providers.aws.lib.mutelist.mutelist import mutelist_findings
|
||||
from prowler.providers.common.common import get_global_provider
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.outputs import Provider_Output_Options
|
||||
|
||||
|
||||
class ExecutionManager:
|
||||
def __init__(
|
||||
self,
|
||||
checks_to_execute: list,
|
||||
provider: str,
|
||||
audit_info: Any,
|
||||
audit_output_options: Provider_Output_Options,
|
||||
custom_checks_metadata: Any,
|
||||
):
|
||||
self.checks_to_execute = checks_to_execute
|
||||
self.provider = provider
|
||||
self.audit_info = audit_info
|
||||
self.audit_output_options = audit_output_options
|
||||
self.custom_checks_metadata = custom_checks_metadata
|
||||
|
||||
self.live_display = live_display
|
||||
self.live_display.start()
|
||||
self.loaded_clients = {} # defaultdict(lambda: False)
|
||||
self.check_dict = self.create_check_service_dict(checks_to_execute)
|
||||
self.check_dependencies = get_dependencies_for_checks(provider, self.check_dict)
|
||||
self.remaining_checks = self.initialize_remaining_checks(
|
||||
self.check_dependencies
|
||||
)
|
||||
self.services_queue = self.initialize_services_queue(self.check_dependencies)
|
||||
|
||||
# For tracking the executed services and checks
|
||||
self.services_executed: Set[str] = set()
|
||||
self.checks_executed: Set[str] = set()
|
||||
|
||||
# Initialize the Audit Metadata
|
||||
self.audit_info.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=self.checks_to_execute,
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
def update_tracking(self, service: str, check: str):
|
||||
self.services_executed.add(service)
|
||||
self.checks_executed.add(check)
|
||||
|
||||
@staticmethod
|
||||
def initialize_remaining_checks(check_dependencies):
|
||||
remaining_checks = {}
|
||||
for service, checks in check_dependencies.items():
|
||||
for check_name, clients in checks.items():
|
||||
remaining_checks[(service, check_name)] = clients
|
||||
return remaining_checks
|
||||
|
||||
@staticmethod
|
||||
def initialize_services_queue(check_dependencies):
|
||||
return list(check_dependencies.keys())
|
||||
|
||||
@staticmethod
|
||||
def create_check_service_dict(checks_to_execute):
|
||||
output = {}
|
||||
for check_name in checks_to_execute:
|
||||
service = check_name.split("_")[0]
|
||||
if service not in output.keys():
|
||||
output[service] = []
|
||||
output[service].append(check_name)
|
||||
return output
|
||||
|
||||
def total_checks_per_service(self):
|
||||
"""Returns a dictionary with the total number of checks for each service."""
|
||||
total_checks = {}
|
||||
for service, checks in self.check_dict.items():
|
||||
total_checks[service] = len(checks)
|
||||
return total_checks
|
||||
|
||||
def find_next_service(self):
|
||||
# Prioritize services that use already loaded clients
|
||||
for service in self.services_queue:
|
||||
checks = self.check_dependencies[service]
|
||||
if any(
|
||||
client in self.loaded_clients
|
||||
for check in checks.values()
|
||||
for client in check
|
||||
):
|
||||
return service
|
||||
return None if not self.services_queue else self.services_queue[0]
|
||||
|
||||
@staticmethod
|
||||
def import_check(check_path: str) -> ModuleType:
|
||||
"""
|
||||
Imports an input check using its path
|
||||
|
||||
When importing a module using importlib.import_module, it's loaded and added to the sys.modules cache.
|
||||
This means that the module remains in memory and is not garbage collected immediately after use, as it's still referenced in sys.modules.
|
||||
This behavior is intentional, as importing modules can be a costly operation, and keeping them in memory allows for faster re-use.
|
||||
release_check deletes this reference if it is no longer required by any of the remaining checks
|
||||
"""
|
||||
lib = importlib.import_module(f"{check_path}")
|
||||
return lib
|
||||
|
||||
# Imports service clients, and tracks if it needs to be imported
|
||||
def import_client(self, client_name):
|
||||
if not self.loaded_clients.get(client_name):
|
||||
# Dynamically import the client
|
||||
module_name, _ = client_name.rsplit("_", 1)
|
||||
client_module = importlib.import_module(
|
||||
f"prowler.providers.{self.provider}.services.{module_name}.{client_name}"
|
||||
)
|
||||
self.loaded_clients[client_name] = client_module
|
||||
|
||||
def release_clients(self, completed_check_clients):
|
||||
for client_name in completed_check_clients:
|
||||
# Determine if any of the remaining checks still require the client
|
||||
if not any(
|
||||
client == client_name
|
||||
for check in self.remaining_checks
|
||||
for client in self.remaining_checks[check]
|
||||
):
|
||||
# Delete the reference to the client for this object
|
||||
del self.loaded_clients[client_name]
|
||||
module_name, _ = client_name.rsplit("_", 1)
|
||||
# Delete the reference to the client in sys.modules
|
||||
del sys.modules[
|
||||
f"prowler.providers.aws.services.{module_name}.{client_name}"
|
||||
]
|
||||
|
||||
def generate_checks(self):
|
||||
"""
|
||||
This is a generator function, which will:
|
||||
* Determine the next service whose checks will be executed
|
||||
* Load all the clients which are required by the checks into memory (init them)
|
||||
* Yield the service and check name, 1-by-1, to be used within execute_checks
|
||||
* Pass the completed checks to release_clients to determine if the clients that were required by the check are no longer needed, and can be garabage collected
|
||||
It will complete the checks for a service, before moving onto the next one
|
||||
It uses find_next_service to prioritize the next service based on if any of that service's checks require a client that has already been loaded
|
||||
"""
|
||||
while self.remaining_checks:
|
||||
current_service = self.find_next_service()
|
||||
if not current_service:
|
||||
# Execution has completed, return
|
||||
break
|
||||
# Remove the service from the services_queue
|
||||
self.services_queue.remove(current_service)
|
||||
|
||||
checks = self.check_dependencies[current_service]
|
||||
clients_for_service = list(
|
||||
set(client for client_list in checks.values() for client in client_list)
|
||||
)
|
||||
|
||||
for client in clients_for_service:
|
||||
self.live_display.add_client_init_section(client)
|
||||
self.import_client(client)
|
||||
|
||||
# Add the display component
|
||||
total_checks = len(self.check_dict[current_service])
|
||||
self.live_display.add_service_section(current_service, total_checks)
|
||||
|
||||
for check_name, clients_for_check in checks.items():
|
||||
|
||||
yield current_service, check_name
|
||||
|
||||
self.live_display.increment_check_progress()
|
||||
self.live_display.increment_overall_check_progress()
|
||||
|
||||
del self.remaining_checks[(current_service, check_name)]
|
||||
self.release_clients(clients_for_check)
|
||||
|
||||
self.live_display.increment_overall_service_progress()
|
||||
|
||||
def execute_checks(self) -> list:
|
||||
# List to store all the check's findings
|
||||
all_findings = []
|
||||
# Services and checks executed for the Audit Status
|
||||
|
||||
global_provider = get_global_provider()
|
||||
|
||||
# Initialize the Audit Metadata
|
||||
global_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=self.checks_to_execute,
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
if os.name != "nt":
|
||||
try:
|
||||
from resource import RLIMIT_NOFILE, getrlimit
|
||||
|
||||
# Check ulimit for the maximum system open files
|
||||
soft, _ = getrlimit(RLIMIT_NOFILE)
|
||||
if soft < 4096:
|
||||
logger.warning(
|
||||
f"Your session file descriptors limit ({soft} open files) is below 4096. We recommend to increase it to avoid errors. Solve it running this command `ulimit -n 4096`. For more info visit https://docs.prowler.cloud/en/latest/troubleshooting/"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error("Unable to retrieve ulimit default settings")
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
# Execution with the --only-logs flag
|
||||
if self.audit_output_options.only_logs:
|
||||
for service, check_name in self.generate_checks():
|
||||
try:
|
||||
check_findings = self.execute(service, check_name)
|
||||
all_findings.extend(check_findings)
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {self.provider.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
else:
|
||||
# Default execution
|
||||
total_checks = self.total_checks_per_service()
|
||||
self.live_display.add_overall_progress_section(
|
||||
total_checks_dict=total_checks
|
||||
)
|
||||
# For tracking when a service is completed
|
||||
completed_checks = {service: 0 for service in total_checks}
|
||||
service_findings = []
|
||||
for service, check_name in self.generate_checks():
|
||||
try:
|
||||
check_findings = self.execute(
|
||||
service,
|
||||
check_name,
|
||||
)
|
||||
all_findings.extend(check_findings)
|
||||
service_findings.extend(check_findings)
|
||||
# Update the completed checks count
|
||||
completed_checks[service] += 1
|
||||
|
||||
# Check if all checks for the service are completed
|
||||
if completed_checks[service] == total_checks[service]:
|
||||
# All checks for the service are completed
|
||||
# Add a summary table or perform other actions
|
||||
live_display.add_results_for_service(service, service_findings)
|
||||
# Clear service_findings
|
||||
service_findings = []
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {self.provider.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
self.live_display.hide_service_section()
|
||||
return all_findings
|
||||
|
||||
def execute(
|
||||
self,
|
||||
service: str,
|
||||
check_name: str,
|
||||
):
|
||||
try:
|
||||
# Import check module
|
||||
check_module_path = f"prowler.providers.{self.provider}.services.{service}.{check_name}.{check_name}"
|
||||
lib = self.import_check(check_module_path)
|
||||
# Recover functions from check
|
||||
check_to_execute = getattr(lib, check_name)
|
||||
c = check_to_execute()
|
||||
|
||||
# Update check metadata to reflect that in the outputs
|
||||
if self.custom_checks_metadata and self.custom_checks_metadata[
|
||||
"Checks"
|
||||
].get(c.CheckID):
|
||||
c = update_check_metadata(
|
||||
c, self.custom_checks_metadata["Checks"][c.CheckID]
|
||||
)
|
||||
|
||||
# Run check
|
||||
check_findings = self.run_check(c, self.audit_output_options)
|
||||
|
||||
# Update Audit Status
|
||||
self.update_tracking(service, check_name)
|
||||
self.update_audit_metadata()
|
||||
|
||||
# Mutelist findings
|
||||
if self.audit_output_options.mutelist_file:
|
||||
check_findings = mutelist_findings(
|
||||
self.audit_output_options.mutelist_file,
|
||||
self.audit_info.audited_account,
|
||||
check_findings,
|
||||
)
|
||||
|
||||
# Report the check's findings
|
||||
report(check_findings, self.audit_output_options, self.audit_info)
|
||||
|
||||
if os.environ.get("PROWLER_REPORT_LIB_PATH"):
|
||||
try:
|
||||
logger.info("Using custom report interface ...")
|
||||
lib = os.environ["PROWLER_REPORT_LIB_PATH"]
|
||||
outputs_module = importlib.import_module(lib)
|
||||
custom_report_interface = getattr(outputs_module, "report")
|
||||
|
||||
custom_report_interface(
|
||||
check_findings, self.audit_output_options, self.audit_info
|
||||
)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
return check_findings
|
||||
|
||||
@staticmethod
|
||||
def run_check(check: Check, output_options: Provider_Output_Options) -> list:
|
||||
findings = []
|
||||
if output_options.verbose:
|
||||
print(
|
||||
f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}"
|
||||
)
|
||||
logger.debug(f"Executing check: {check.CheckID}")
|
||||
try:
|
||||
findings = check.execute()
|
||||
except Exception as error:
|
||||
if not output_options.only_logs:
|
||||
print(
|
||||
f"Something went wrong in {check.CheckID}, please use --log-level ERROR"
|
||||
)
|
||||
logger.error(
|
||||
f"{check.CheckID} -- {error.__class__.__name__}[{traceback.extract_tb(error.__traceback__)[-1].lineno}]: {error}"
|
||||
)
|
||||
finally:
|
||||
return findings
|
||||
|
||||
def update_audit_metadata(self):
|
||||
"""update_audit_metadata returns the audit_metadata updated with the new status
|
||||
|
||||
Updates the given audit_metadata using the length of the services_executed and checks_executed
|
||||
"""
|
||||
try:
|
||||
self.audit_info.audit_metadata.services_scanned = len(
|
||||
self.services_executed
|
||||
)
|
||||
self.audit_info.audit_metadata.completed_checks = len(self.checks_executed)
|
||||
self.audit_info.audit_metadata.audit_progress = (
|
||||
100
|
||||
* len(self.checks_executed)
|
||||
/ len(self.audit_info.audit_metadata.expected_checks)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
@@ -2,10 +2,13 @@ import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic.main import ModelMetaclass
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.ui.live_display import live_display
|
||||
|
||||
|
||||
class Code(BaseModel):
|
||||
@@ -57,9 +60,29 @@ class Check_Metadata_Model(BaseModel):
|
||||
Compliance: list = None
|
||||
|
||||
|
||||
class Check(ABC, Check_Metadata_Model):
|
||||
class CheckMeta(ModelMetaclass):
|
||||
"""
|
||||
Dynamically decorates the execute function of all subclasses of the Check class
|
||||
|
||||
By making CheckMeta inherit from ModelMetaclass, it ensures that all features provided by Pydantic's BaseModel (such as data validation, serialization, and so forth) are preserved. CheckMeta just adds additional behavior (decorator application) on top of the existing features.
|
||||
This also works because ModelMetaclass inherits from ABCMeta, as does the ABC class (its got to do with how metaclasses work when applying it to a class that inherits from other classes that have a metaclass).
|
||||
The primary role of CheckMeta is to automatically apply a decorator to the execute method of subclasses. This behavior does not conflict with the typical responsibilities of ModelMetaclass
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, dct):
|
||||
if "execute" in dct and not getattr(
|
||||
dct["execute"], "__isabstractmethod__", False
|
||||
):
|
||||
dct["execute"] = Check.update_title_with_findings_decorator(dct["execute"])
|
||||
return super(CheckMeta, cls).__new__(cls, name, bases, dct)
|
||||
|
||||
|
||||
class Check(ABC, Check_Metadata_Model, metaclass=CheckMeta):
|
||||
"""Prowler Check"""
|
||||
|
||||
title_bar_task: int = None
|
||||
progress_task: int = None
|
||||
|
||||
def __init__(self, **data):
|
||||
"""Check's init function. Calls the CheckMetadataModel init."""
|
||||
# Parse the Check's metadata file
|
||||
@@ -72,6 +95,43 @@ class Check(ABC, Check_Metadata_Model):
|
||||
# Calls parents init function
|
||||
super().__init__(**data)
|
||||
|
||||
self.live_display_enabled = False
|
||||
service_section = live_display.get_service_section()
|
||||
if service_section:
|
||||
self.live_display_enabled = True
|
||||
|
||||
self.title_bar_task = service_section.title_bar.add_task(
|
||||
f"{self.CheckTitle}...", start=False
|
||||
)
|
||||
|
||||
def increment_task_progress(self):
|
||||
if self.live_display_enabled:
|
||||
current_section = live_display.get_service_section()
|
||||
current_section.task_progress.update(self.progress_task, advance=1)
|
||||
|
||||
def start_task(self, message, count):
|
||||
if self.live_display_enabled:
|
||||
current_section = live_display.get_service_section()
|
||||
self.progress_task = current_section.task_progress.add_task(
|
||||
description=message, total=count, visible=True
|
||||
)
|
||||
|
||||
def update_title_with_findings(self, findings):
|
||||
if self.live_display_enabled:
|
||||
current_section = live_display.get_service_section()
|
||||
# current_section.task_progress.remove_task(self.progress_task)
|
||||
total_failed = len(
|
||||
[report for report in findings if report.status == "FAIL"]
|
||||
)
|
||||
total_checked = len(findings)
|
||||
if total_failed == 0:
|
||||
message = f"{self.CheckTitle} [pass]All resources passed ({total_checked})[/pass]"
|
||||
else:
|
||||
message = f"{self.CheckTitle} [fail]{total_failed}/{total_checked} failed![/fail]"
|
||||
current_section.title_bar.update(
|
||||
task_id=self.title_bar_task, description=message
|
||||
)
|
||||
|
||||
def metadata(self) -> dict:
|
||||
"""Return the JSON representation of the check's metadata"""
|
||||
return self.json()
|
||||
@@ -80,6 +140,24 @@ class Check(ABC, Check_Metadata_Model):
|
||||
def execute(self):
|
||||
"""Execute the check's logic"""
|
||||
|
||||
@staticmethod
|
||||
def update_title_with_findings_decorator(func):
|
||||
"""
|
||||
Decorator to update the title bar in the live_display with findings after executing a check.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(check_instance, *args, **kwargs):
|
||||
# Execute the original check's logic
|
||||
findings = func(check_instance, *args, **kwargs)
|
||||
|
||||
# Update the title bar with the findings
|
||||
check_instance.update_title_with_findings(findings)
|
||||
|
||||
return findings
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check_Report:
|
||||
@@ -146,6 +224,22 @@ class Check_Report_GCP(Check_Report):
|
||||
self.location = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check_Report_Kubernetes(Check_Report):
|
||||
# TODO change class name to CheckReportKubernetes
|
||||
"""Contains the Kubernetes Check's finding information."""
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
namespace: str
|
||||
|
||||
def __init__(self, metadata):
|
||||
super().__init__(metadata)
|
||||
self.resource_name = ""
|
||||
self.resource_id = ""
|
||||
self.namespace = ""
|
||||
|
||||
|
||||
# Testing Pending
|
||||
def load_check_metadata(metadata_file: str) -> Check_Metadata_Model:
|
||||
"""load_check_metadata loads and parse a Check's metadata file"""
|
||||
|
||||
@@ -8,6 +8,7 @@ from prowler.config.config import (
|
||||
default_config_file_path,
|
||||
default_output_directory,
|
||||
valid_severities,
|
||||
finding_statuses,
|
||||
)
|
||||
from prowler.providers.common.arguments import (
|
||||
init_providers_parser,
|
||||
@@ -116,10 +117,10 @@ Detailed documentation at https://docs.prowler.cloud
|
||||
"Outputs"
|
||||
)
|
||||
common_outputs_parser.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Store or send only Prowler failed findings",
|
||||
"--status",
|
||||
nargs="+",
|
||||
help=f"Filter by the status of the findings {finding_statuses}",
|
||||
choices=finding_statuses,
|
||||
)
|
||||
common_outputs_parser.add_argument(
|
||||
"-M",
|
||||
|
||||
@@ -330,7 +330,7 @@ def fill_compliance(output_options, finding, audit_info, file_descriptors):
|
||||
Requirements_Attributes_SubSection=attribute.SubSection,
|
||||
Requirements_Attributes_SubGroup=attribute.SubGroup,
|
||||
Requirements_Attributes_Service=attribute.Service,
|
||||
Requirements_Attributes_Type=attribute.Type,
|
||||
Requirements_Attributes_Soc_Type=attribute.Soc_Type,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
@@ -444,8 +444,8 @@ def display_compliance_table(
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / (fail_count + pass_count) * 100, 2)}% ({fail_count}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / (fail_count + pass_count) * 100, 2)}% ({pass_count}) CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(fail_count/(fail_count+pass_count)*100, 2)}% ({fail_count}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count/(fail_count+pass_count)*100, 2)}% ({pass_count}) CUMPLE{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
@@ -539,8 +539,8 @@ def display_compliance_table(
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / (fail_count + pass_count) * 100, 2)}% ({fail_count}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / (fail_count + pass_count) * 100, 2)}% ({pass_count}) PASS{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(fail_count/(fail_count+pass_count)*100, 2)}% ({fail_count}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count/(fail_count+pass_count)*100, 2)}% ({pass_count}) PASS{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
@@ -610,8 +610,8 @@ def display_compliance_table(
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / (fail_count + pass_count) * 100, 2)}% ({fail_count}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / (fail_count + pass_count) * 100, 2)}% ({pass_count}) PASS{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(fail_count/(fail_count+pass_count)*100, 2)}% ({fail_count}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count/(fail_count+pass_count)*100, 2)}% ({pass_count}) PASS{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.models import (
|
||||
Check_Output_CSV_AWS_Well_Architected,
|
||||
generate_csv_fields,
|
||||
)
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def write_compliance_row_aws_well_architected_framework(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
):
|
||||
compliance_output = compliance.Framework
|
||||
if compliance.Version != "":
|
||||
compliance_output += "_" + compliance.Version
|
||||
if compliance.Provider != "":
|
||||
compliance_output += "_" + compliance.Provider
|
||||
compliance_output = compliance_output.lower().replace("-", "_")
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_AWS_Well_Architected)
|
||||
csv_writer = DictWriter(
|
||||
file_descriptors[compliance_output],
|
||||
fieldnames=csv_header,
|
||||
delimiter=";",
|
||||
)
|
||||
for requirement in compliance.Requirements:
|
||||
requirement_description = requirement.Description
|
||||
requirement_id = requirement.Id
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = Check_Output_CSV_AWS_Well_Architected(
|
||||
Provider=finding.check_metadata.Provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=audit_info.audited_account,
|
||||
Region=finding.region,
|
||||
AssessmentDate=outputs_unix_timestamp(
|
||||
output_options.unix_timestamp, timestamp
|
||||
),
|
||||
Requirements_Id=requirement_id,
|
||||
Requirements_Description=requirement_description,
|
||||
Requirements_Attributes_Name=attribute.Name,
|
||||
Requirements_Attributes_WellArchitectedQuestionId=attribute.WellArchitectedQuestionId,
|
||||
Requirements_Attributes_WellArchitectedPracticeId=attribute.WellArchitectedPracticeId,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_SubSection=attribute.SubSection,
|
||||
Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk,
|
||||
Requirements_Attributes_AssessmentMethod=attribute.AssessmentMethod,
|
||||
Requirements_Attributes_Description=attribute.Description,
|
||||
Requirements_Attributes_ImplementationGuidanceUrl=attribute.ImplementationGuidanceUrl,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
@@ -0,0 +1,36 @@
|
||||
from prowler.lib.outputs.compliance.cis_aws import generate_compliance_row_cis_aws
|
||||
from prowler.lib.outputs.compliance.cis_gcp import generate_compliance_row_cis_gcp
|
||||
from prowler.lib.outputs.csv import write_csv
|
||||
|
||||
|
||||
def write_compliance_row_cis(
|
||||
file_descriptors,
|
||||
finding,
|
||||
compliance,
|
||||
output_options,
|
||||
audit_info,
|
||||
input_compliance_frameworks,
|
||||
):
|
||||
compliance_output = "cis_" + compliance.Version + "_" + compliance.Provider.lower()
|
||||
|
||||
# Only with the version of CIS that was selected
|
||||
if compliance_output in str(input_compliance_frameworks):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
if compliance.Provider == "AWS":
|
||||
(compliance_row, csv_header) = generate_compliance_row_cis_aws(
|
||||
finding,
|
||||
compliance,
|
||||
requirement,
|
||||
attribute,
|
||||
output_options,
|
||||
audit_info,
|
||||
)
|
||||
elif compliance.Provider == "GCP":
|
||||
(compliance_row, csv_header) = generate_compliance_row_cis_gcp(
|
||||
finding, compliance, requirement, attribute, output_options
|
||||
)
|
||||
|
||||
write_csv(
|
||||
file_descriptors[compliance_output], csv_header, compliance_row
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.models import Check_Output_CSV_AWS_CIS, generate_csv_fields
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def generate_compliance_row_cis_aws(
|
||||
finding, compliance, requirement, attribute, output_options, audit_info
|
||||
):
|
||||
compliance_row = Check_Output_CSV_AWS_CIS(
|
||||
Provider=finding.check_metadata.Provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=audit_info.audited_account,
|
||||
Region=finding.region,
|
||||
AssessmentDate=outputs_unix_timestamp(output_options.unix_timestamp, timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_Profile=attribute.Profile,
|
||||
Requirements_Attributes_AssessmentStatus=attribute.AssessmentStatus,
|
||||
Requirements_Attributes_Description=attribute.Description,
|
||||
Requirements_Attributes_RationaleStatement=attribute.RationaleStatement,
|
||||
Requirements_Attributes_ImpactStatement=attribute.ImpactStatement,
|
||||
Requirements_Attributes_RemediationProcedure=attribute.RemediationProcedure,
|
||||
Requirements_Attributes_AuditProcedure=attribute.AuditProcedure,
|
||||
Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation,
|
||||
Requirements_Attributes_References=attribute.References,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
)
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_AWS_CIS)
|
||||
|
||||
return compliance_row, csv_header
|
||||
@@ -0,0 +1,35 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.models import Check_Output_CSV_GCP_CIS, generate_csv_fields
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def generate_compliance_row_cis_gcp(
|
||||
finding, compliance, requirement, attribute, output_options
|
||||
):
|
||||
compliance_row = Check_Output_CSV_GCP_CIS(
|
||||
Provider=finding.check_metadata.Provider,
|
||||
Description=compliance.Description,
|
||||
ProjectId=finding.project_id,
|
||||
Location=finding.location.lower(),
|
||||
AssessmentDate=outputs_unix_timestamp(output_options.unix_timestamp, timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_Profile=attribute.Profile,
|
||||
Requirements_Attributes_AssessmentStatus=attribute.AssessmentStatus,
|
||||
Requirements_Attributes_Description=attribute.Description,
|
||||
Requirements_Attributes_RationaleStatement=attribute.RationaleStatement,
|
||||
Requirements_Attributes_ImpactStatement=attribute.ImpactStatement,
|
||||
Requirements_Attributes_RemediationProcedure=attribute.RemediationProcedure,
|
||||
Requirements_Attributes_AuditProcedure=attribute.AuditProcedure,
|
||||
Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation,
|
||||
Requirements_Attributes_References=attribute.References,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
)
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_GCP_CIS)
|
||||
|
||||
return compliance_row, csv_header
|
||||
@@ -0,0 +1,472 @@
|
||||
import sys
|
||||
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color
|
||||
from prowler.lib.check.models import Check_Report
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.compliance.aws_well_architected_framework import (
|
||||
write_compliance_row_aws_well_architected_framework,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.cis import write_compliance_row_cis
|
||||
from prowler.lib.outputs.compliance.ens_rd2022_aws import (
|
||||
write_compliance_row_ens_rd2022_aws,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.generic import write_compliance_row_generic
|
||||
from prowler.lib.outputs.compliance.iso27001_2013_aws import (
|
||||
write_compliance_row_iso27001_2013_aws,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.mitre_attack_aws import (
|
||||
write_compliance_row_mitre_attack_aws,
|
||||
)
|
||||
|
||||
|
||||
def add_manual_controls(
|
||||
output_options, audit_info, file_descriptors, input_compliance_frameworks
|
||||
):
|
||||
try:
|
||||
# Check if MANUAL control was already added to output
|
||||
if "manual_check" in output_options.bulk_checks_metadata:
|
||||
manual_finding = Check_Report(
|
||||
output_options.bulk_checks_metadata["manual_check"].json()
|
||||
)
|
||||
manual_finding.status = "MANUAL"
|
||||
manual_finding.status_extended = "Manual check"
|
||||
manual_finding.resource_id = "manual_check"
|
||||
manual_finding.resource_name = "Manual check"
|
||||
manual_finding.region = ""
|
||||
manual_finding.location = ""
|
||||
manual_finding.project_id = ""
|
||||
fill_compliance(
|
||||
output_options,
|
||||
manual_finding,
|
||||
audit_info,
|
||||
file_descriptors,
|
||||
input_compliance_frameworks,
|
||||
)
|
||||
del output_options.bulk_checks_metadata["manual_check"]
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
def get_check_compliance_frameworks_in_input(
|
||||
check_id, bulk_checks_metadata, input_compliance_frameworks
|
||||
):
|
||||
"""get_check_compliance_frameworks_in_input returns a list of Compliance for the given check if the compliance framework is present in the input compliance to execute"""
|
||||
check_compliances = []
|
||||
if bulk_checks_metadata and bulk_checks_metadata[check_id]:
|
||||
for compliance in bulk_checks_metadata[check_id].Compliance:
|
||||
compliance_name = ""
|
||||
if compliance.Version:
|
||||
compliance_name = (
|
||||
compliance.Framework.lower()
|
||||
+ "_"
|
||||
+ compliance.Version.lower()
|
||||
+ "_"
|
||||
+ compliance.Provider.lower()
|
||||
)
|
||||
else:
|
||||
compliance_name = (
|
||||
compliance.Framework.lower() + "_" + compliance.Provider.lower()
|
||||
)
|
||||
if compliance_name.replace("-", "_") in input_compliance_frameworks:
|
||||
check_compliances.append(compliance)
|
||||
|
||||
return check_compliances
|
||||
|
||||
|
||||
def fill_compliance(
|
||||
output_options, finding, audit_info, file_descriptors, input_compliance_frameworks
|
||||
):
|
||||
try:
|
||||
# We have to retrieve all the check's compliance requirements and get the ones matching with the input ones
|
||||
check_compliances = get_check_compliance_frameworks_in_input(
|
||||
finding.check_metadata.CheckID,
|
||||
output_options.bulk_checks_metadata,
|
||||
input_compliance_frameworks,
|
||||
)
|
||||
|
||||
for compliance in check_compliances:
|
||||
if compliance.Framework == "ENS" and compliance.Version == "RD2022":
|
||||
write_compliance_row_ens_rd2022_aws(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
)
|
||||
|
||||
elif compliance.Framework == "CIS":
|
||||
write_compliance_row_cis(
|
||||
file_descriptors,
|
||||
finding,
|
||||
compliance,
|
||||
output_options,
|
||||
audit_info,
|
||||
input_compliance_frameworks,
|
||||
)
|
||||
|
||||
elif (
|
||||
"AWS-Well-Architected-Framework" in compliance.Framework
|
||||
and compliance.Provider == "AWS"
|
||||
):
|
||||
write_compliance_row_aws_well_architected_framework(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
)
|
||||
|
||||
elif (
|
||||
compliance.Framework == "ISO27001"
|
||||
and compliance.Version == "2013"
|
||||
and compliance.Provider == "AWS"
|
||||
):
|
||||
write_compliance_row_iso27001_2013_aws(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
)
|
||||
|
||||
elif (
|
||||
compliance.Framework == "MITRE-ATTACK"
|
||||
and compliance.Version == ""
|
||||
and compliance.Provider == "AWS"
|
||||
):
|
||||
write_compliance_row_mitre_attack_aws(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
)
|
||||
|
||||
else:
|
||||
write_compliance_row_generic(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
def display_compliance_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
try:
|
||||
if "ens_rd2022_aws" == compliance_framework:
|
||||
marcos = {}
|
||||
ens_compliance_table = {
|
||||
"Proveedor": [],
|
||||
"Marco/Categoria": [],
|
||||
"Estado": [],
|
||||
"Alto": [],
|
||||
"Medio": [],
|
||||
"Bajo": [],
|
||||
"Opcional": [],
|
||||
}
|
||||
pass_count = fail_count = 0
|
||||
for finding in findings:
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if (
|
||||
compliance.Framework == "ENS"
|
||||
and compliance.Provider == "AWS"
|
||||
and compliance.Version == "RD2022"
|
||||
):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
marco_categoria = (
|
||||
f"{attribute.Marco}/{attribute.Categoria}"
|
||||
)
|
||||
# Check if Marco/Categoria exists
|
||||
if marco_categoria not in marcos:
|
||||
marcos[marco_categoria] = {
|
||||
"Estado": f"{Fore.GREEN}CUMPLE{Style.RESET_ALL}",
|
||||
"Opcional": 0,
|
||||
"Alto": 0,
|
||||
"Medio": 0,
|
||||
"Bajo": 0,
|
||||
}
|
||||
if finding.status == "FAIL":
|
||||
fail_count += 1
|
||||
marcos[marco_categoria][
|
||||
"Estado"
|
||||
] = f"{Fore.RED}NO CUMPLE{Style.RESET_ALL}"
|
||||
elif finding.status == "PASS":
|
||||
pass_count += 1
|
||||
if attribute.Nivel == "opcional":
|
||||
marcos[marco_categoria]["Opcional"] += 1
|
||||
elif attribute.Nivel == "alto":
|
||||
marcos[marco_categoria]["Alto"] += 1
|
||||
elif attribute.Nivel == "medio":
|
||||
marcos[marco_categoria]["Medio"] += 1
|
||||
elif attribute.Nivel == "bajo":
|
||||
marcos[marco_categoria]["Bajo"] += 1
|
||||
|
||||
# Add results to table
|
||||
for marco in sorted(marcos):
|
||||
ens_compliance_table["Proveedor"].append(compliance.Provider)
|
||||
ens_compliance_table["Marco/Categoria"].append(marco)
|
||||
ens_compliance_table["Estado"].append(marcos[marco]["Estado"])
|
||||
ens_compliance_table["Opcional"].append(
|
||||
f"{Fore.BLUE}{marcos[marco]['Opcional']}{Style.RESET_ALL}"
|
||||
)
|
||||
ens_compliance_table["Alto"].append(
|
||||
f"{Fore.LIGHTRED_EX}{marcos[marco]['Alto']}{Style.RESET_ALL}"
|
||||
)
|
||||
ens_compliance_table["Medio"].append(
|
||||
f"{orange_color}{marcos[marco]['Medio']}{Style.RESET_ALL}"
|
||||
)
|
||||
ens_compliance_table["Bajo"].append(
|
||||
f"{Fore.YELLOW}{marcos[marco]['Bajo']}{Style.RESET_ALL}"
|
||||
)
|
||||
if fail_count + pass_count < 1:
|
||||
print(
|
||||
f"\nThere are no resources for {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}.\n"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"\nEstado de Cumplimiento de {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / (fail_count + pass_count) * 100, 2)}% ({fail_count}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / (fail_count + pass_count) * 100, 2)}% ({pass_count}) CUMPLE{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
print(
|
||||
f"\nResultados de {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}:"
|
||||
)
|
||||
print(
|
||||
tabulate(
|
||||
ens_compliance_table,
|
||||
headers="keys",
|
||||
tablefmt="rounded_grid",
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"{Style.BRIGHT}* Solo aparece el Marco/Categoria que contiene resultados.{Style.RESET_ALL}"
|
||||
)
|
||||
print(
|
||||
f"\nResultados detallados de {compliance_framework.upper()} en:"
|
||||
)
|
||||
print(
|
||||
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
|
||||
)
|
||||
elif "cis_" in compliance_framework:
|
||||
sections = {}
|
||||
cis_compliance_table = {
|
||||
"Provider": [],
|
||||
"Section": [],
|
||||
"Level 1": [],
|
||||
"Level 2": [],
|
||||
}
|
||||
pass_count = fail_count = 0
|
||||
for finding in findings:
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if (
|
||||
compliance.Framework == "CIS"
|
||||
and compliance.Version in compliance_framework
|
||||
):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
# Check if Section exists
|
||||
if section not in sections:
|
||||
sections[section] = {
|
||||
"Status": f"{Fore.GREEN}PASS{Style.RESET_ALL}",
|
||||
"Level 1": {"FAIL": 0, "PASS": 0},
|
||||
"Level 2": {"FAIL": 0, "PASS": 0},
|
||||
}
|
||||
if finding.status == "FAIL":
|
||||
fail_count += 1
|
||||
elif finding.status == "PASS":
|
||||
pass_count += 1
|
||||
if attribute.Profile == "Level 1":
|
||||
if finding.status == "FAIL":
|
||||
sections[section]["Level 1"]["FAIL"] += 1
|
||||
else:
|
||||
sections[section]["Level 1"]["PASS"] += 1
|
||||
elif attribute.Profile == "Level 2":
|
||||
if finding.status == "FAIL":
|
||||
sections[section]["Level 2"]["FAIL"] += 1
|
||||
else:
|
||||
sections[section]["Level 2"]["PASS"] += 1
|
||||
|
||||
# Add results to table
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
cis_compliance_table["Provider"].append(compliance.Provider)
|
||||
cis_compliance_table["Section"].append(section)
|
||||
if sections[section]["Level 1"]["FAIL"] > 0:
|
||||
cis_compliance_table["Level 1"].append(
|
||||
f"{Fore.RED}FAIL({sections[section]['Level 1']['FAIL']}){Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
cis_compliance_table["Level 1"].append(
|
||||
f"{Fore.GREEN}PASS({sections[section]['Level 1']['PASS']}){Style.RESET_ALL}"
|
||||
)
|
||||
if sections[section]["Level 2"]["FAIL"] > 0:
|
||||
cis_compliance_table["Level 2"].append(
|
||||
f"{Fore.RED}FAIL({sections[section]['Level 2']['FAIL']}){Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
cis_compliance_table["Level 2"].append(
|
||||
f"{Fore.GREEN}PASS({sections[section]['Level 2']['PASS']}){Style.RESET_ALL}"
|
||||
)
|
||||
if fail_count + pass_count < 1:
|
||||
print(
|
||||
f"\nThere are no resources for {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}.\n"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / (fail_count + pass_count) * 100, 2)}% ({fail_count}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / (fail_count + pass_count) * 100, 2)}% ({pass_count}) PASS{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
print(
|
||||
f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
|
||||
)
|
||||
print(
|
||||
tabulate(
|
||||
cis_compliance_table,
|
||||
headers="keys",
|
||||
tablefmt="rounded_grid",
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"{Style.BRIGHT}* Only sections containing results appear.{Style.RESET_ALL}"
|
||||
)
|
||||
print(
|
||||
f"\nDetailed results of {compliance_framework.upper()} are in:"
|
||||
)
|
||||
print(
|
||||
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
|
||||
)
|
||||
elif "mitre_attack" in compliance_framework:
|
||||
tactics = {}
|
||||
mitre_compliance_table = {
|
||||
"Provider": [],
|
||||
"Tactic": [],
|
||||
"Status": [],
|
||||
}
|
||||
pass_count = fail_count = 0
|
||||
for finding in findings:
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if (
|
||||
"MITRE-ATTACK" in compliance.Framework
|
||||
and compliance.Version in compliance_framework
|
||||
):
|
||||
for requirement in compliance.Requirements:
|
||||
for tactic in requirement.Tactics:
|
||||
if tactic not in tactics:
|
||||
tactics[tactic] = {"FAIL": 0, "PASS": 0}
|
||||
if finding.status == "FAIL":
|
||||
fail_count += 1
|
||||
tactics[tactic]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
pass_count += 1
|
||||
tactics[tactic]["PASS"] += 1
|
||||
|
||||
# Add results to table
|
||||
tactics = dict(sorted(tactics.items()))
|
||||
for tactic in tactics:
|
||||
mitre_compliance_table["Provider"].append(compliance.Provider)
|
||||
mitre_compliance_table["Tactic"].append(tactic)
|
||||
if tactics[tactic]["FAIL"] > 0:
|
||||
mitre_compliance_table["Status"].append(
|
||||
f"{Fore.RED}FAIL({tactics[tactic]['FAIL']}){Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
mitre_compliance_table["Status"].append(
|
||||
f"{Fore.GREEN}PASS({tactics[tactic]['PASS']}){Style.RESET_ALL}"
|
||||
)
|
||||
if fail_count + pass_count < 1:
|
||||
print(
|
||||
f"\nThere are no resources for {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}.\n"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / (fail_count + pass_count) * 100, 2)}% ({fail_count}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / (fail_count + pass_count) * 100, 2)}% ({pass_count}) PASS{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
print(
|
||||
f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
|
||||
)
|
||||
print(
|
||||
tabulate(
|
||||
mitre_compliance_table,
|
||||
headers="keys",
|
||||
tablefmt="rounded_grid",
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"{Style.BRIGHT}* Only sections containing results appear.{Style.RESET_ALL}"
|
||||
)
|
||||
print(
|
||||
f"\nDetailed results of {compliance_framework.upper()} are in:"
|
||||
)
|
||||
print(
|
||||
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
|
||||
)
|
||||
else:
|
||||
pass_count = fail_count = 0
|
||||
for finding in findings:
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if (
|
||||
compliance.Framework.upper()
|
||||
in compliance_framework.upper().replace("_", "-")
|
||||
and compliance.Version in compliance_framework.upper()
|
||||
and compliance.Provider in compliance_framework.upper()
|
||||
):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
if finding.status == "FAIL":
|
||||
fail_count += 1
|
||||
elif finding.status == "PASS":
|
||||
pass_count += 1
|
||||
if fail_count + pass_count < 1:
|
||||
print(
|
||||
f"\nThere are no resources for {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}.\n"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / (fail_count + pass_count) * 100, 2)}% ({fail_count}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / (fail_count + pass_count) * 100, 2)}% ({pass_count}) PASS{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
print(f"\nDetailed results of {compliance_framework.upper()} are in:")
|
||||
print(
|
||||
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,45 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.models import Check_Output_CSV_ENS_RD2022, generate_csv_fields
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def write_compliance_row_ens_rd2022_aws(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
):
|
||||
compliance_output = "ens_rd2022_aws"
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_ENS_RD2022)
|
||||
csv_writer = DictWriter(
|
||||
file_descriptors[compliance_output],
|
||||
fieldnames=csv_header,
|
||||
delimiter=";",
|
||||
)
|
||||
for requirement in compliance.Requirements:
|
||||
requirement_description = requirement.Description
|
||||
requirement_id = requirement.Id
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = Check_Output_CSV_ENS_RD2022(
|
||||
Provider=finding.check_metadata.Provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=audit_info.audited_account,
|
||||
Region=finding.region,
|
||||
AssessmentDate=outputs_unix_timestamp(
|
||||
output_options.unix_timestamp, timestamp
|
||||
),
|
||||
Requirements_Id=requirement_id,
|
||||
Requirements_Description=requirement_description,
|
||||
Requirements_Attributes_IdGrupoControl=attribute.IdGrupoControl,
|
||||
Requirements_Attributes_Marco=attribute.Marco,
|
||||
Requirements_Attributes_Categoria=attribute.Categoria,
|
||||
Requirements_Attributes_DescripcionControl=attribute.DescripcionControl,
|
||||
Requirements_Attributes_Nivel=attribute.Nivel,
|
||||
Requirements_Attributes_Tipo=attribute.Tipo,
|
||||
Requirements_Attributes_Dimensiones=",".join(attribute.Dimensiones),
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
@@ -0,0 +1,51 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.models import (
|
||||
Check_Output_CSV_Generic_Compliance,
|
||||
generate_csv_fields,
|
||||
)
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def write_compliance_row_generic(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
):
|
||||
compliance_output = compliance.Framework
|
||||
if compliance.Version != "":
|
||||
compliance_output += "_" + compliance.Version
|
||||
if compliance.Provider != "":
|
||||
compliance_output += "_" + compliance.Provider
|
||||
|
||||
compliance_output = compliance_output.lower().replace("-", "_")
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_Generic_Compliance)
|
||||
csv_writer = DictWriter(
|
||||
file_descriptors[compliance_output],
|
||||
fieldnames=csv_header,
|
||||
delimiter=";",
|
||||
)
|
||||
for requirement in compliance.Requirements:
|
||||
requirement_description = requirement.Description
|
||||
requirement_id = requirement.Id
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = Check_Output_CSV_Generic_Compliance(
|
||||
Provider=finding.check_metadata.Provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=audit_info.audited_account,
|
||||
Region=finding.region,
|
||||
AssessmentDate=outputs_unix_timestamp(
|
||||
output_options.unix_timestamp, timestamp
|
||||
),
|
||||
Requirements_Id=requirement_id,
|
||||
Requirements_Description=requirement_description,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_SubSection=attribute.SubSection,
|
||||
Requirements_Attributes_SubGroup=attribute.SubGroup,
|
||||
Requirements_Attributes_Service=attribute.Service,
|
||||
Requirements_Attributes_Soc_Type=attribute.Soc_Type,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
)
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
@@ -0,0 +1,53 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.models import (
|
||||
Check_Output_CSV_AWS_ISO27001_2013,
|
||||
generate_csv_fields,
|
||||
)
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def write_compliance_row_iso27001_2013_aws(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
):
|
||||
compliance_output = compliance.Framework
|
||||
if compliance.Version != "":
|
||||
compliance_output += "_" + compliance.Version
|
||||
if compliance.Provider != "":
|
||||
compliance_output += "_" + compliance.Provider
|
||||
|
||||
compliance_output = compliance_output.lower().replace("-", "_")
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_AWS_ISO27001_2013)
|
||||
csv_writer = DictWriter(
|
||||
file_descriptors[compliance_output],
|
||||
fieldnames=csv_header,
|
||||
delimiter=";",
|
||||
)
|
||||
for requirement in compliance.Requirements:
|
||||
requirement_description = requirement.Description
|
||||
requirement_id = requirement.Id
|
||||
requirement_name = requirement.Name
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = Check_Output_CSV_AWS_ISO27001_2013(
|
||||
Provider=finding.check_metadata.Provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=audit_info.audited_account,
|
||||
Region=finding.region,
|
||||
AssessmentDate=outputs_unix_timestamp(
|
||||
output_options.unix_timestamp, timestamp
|
||||
),
|
||||
Requirements_Id=requirement_id,
|
||||
Requirements_Name=requirement_name,
|
||||
Requirements_Description=requirement_description,
|
||||
Requirements_Attributes_Category=attribute.Category,
|
||||
Requirements_Attributes_Objetive_ID=attribute.Objetive_ID,
|
||||
Requirements_Attributes_Objetive_Name=attribute.Objetive_Name,
|
||||
Requirements_Attributes_Check_Summary=attribute.Check_Summary,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
@@ -0,0 +1,66 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.outputs.models import (
|
||||
Check_Output_MITRE_ATTACK,
|
||||
generate_csv_fields,
|
||||
unroll_list,
|
||||
)
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
|
||||
|
||||
def write_compliance_row_mitre_attack_aws(
|
||||
file_descriptors, finding, compliance, output_options, audit_info
|
||||
):
|
||||
compliance_output = compliance.Framework
|
||||
if compliance.Version != "":
|
||||
compliance_output += "_" + compliance.Version
|
||||
if compliance.Provider != "":
|
||||
compliance_output += "_" + compliance.Provider
|
||||
|
||||
compliance_output = compliance_output.lower().replace("-", "_")
|
||||
csv_header = generate_csv_fields(Check_Output_MITRE_ATTACK)
|
||||
csv_writer = DictWriter(
|
||||
file_descriptors[compliance_output],
|
||||
fieldnames=csv_header,
|
||||
delimiter=";",
|
||||
)
|
||||
for requirement in compliance.Requirements:
|
||||
requirement_description = requirement.Description
|
||||
requirement_id = requirement.Id
|
||||
requirement_name = requirement.Name
|
||||
attributes_aws_services = ""
|
||||
attributes_categories = ""
|
||||
attributes_values = ""
|
||||
attributes_comments = ""
|
||||
for attribute in requirement.Attributes:
|
||||
attributes_aws_services += attribute.AWSService + "\n"
|
||||
attributes_categories += attribute.Category + "\n"
|
||||
attributes_values += attribute.Value + "\n"
|
||||
attributes_comments += attribute.Comment + "\n"
|
||||
compliance_row = Check_Output_MITRE_ATTACK(
|
||||
Provider=finding.check_metadata.Provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=audit_info.audited_account,
|
||||
Region=finding.region,
|
||||
AssessmentDate=outputs_unix_timestamp(
|
||||
output_options.unix_timestamp, timestamp
|
||||
),
|
||||
Requirements_Id=requirement_id,
|
||||
Requirements_Description=requirement_description,
|
||||
Requirements_Name=requirement_name,
|
||||
Requirements_Tactics=unroll_list(requirement.Tactics),
|
||||
Requirements_SubTechniques=unroll_list(requirement.SubTechniques),
|
||||
Requirements_Platforms=unroll_list(requirement.Platforms),
|
||||
Requirements_TechniqueURL=requirement.TechniqueURL,
|
||||
Requirements_Attributes_AWSServices=attributes_aws_services,
|
||||
Requirements_Attributes_Categories=attributes_categories,
|
||||
Requirements_Attributes_Values=attributes_values,
|
||||
Requirements_Attributes_Comments=attributes_comments,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
@@ -0,0 +1,10 @@
|
||||
from csv import DictWriter
|
||||
|
||||
|
||||
def write_csv(file_descriptor, headers, row):
|
||||
csv_writer = DictWriter(
|
||||
file_descriptor,
|
||||
fieldnames=headers,
|
||||
delimiter=";",
|
||||
)
|
||||
csv_writer.writerow(row.__dict__)
|
||||
@@ -23,6 +23,7 @@ from prowler.lib.outputs.models import (
|
||||
)
|
||||
from prowler.lib.utils.utils import file_exists, open_file
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info
|
||||
from prowler.providers.azure.lib.audit_info.models import Azure_Audit_Info
|
||||
from prowler.providers.common.outputs import get_provider_output_model
|
||||
from prowler.providers.gcp.lib.audit_info.models import GCP_Audit_Info
|
||||
|
||||
@@ -108,7 +109,7 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
|
||||
elif isinstance(audit_info, GCP_Audit_Info):
|
||||
if output_mode == "cis_2.0_gcp":
|
||||
filename = f"{output_directory}/{output_filename}_cis_2.0_gcp{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_cis_2.0_gcp{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename, output_mode, audit_info, Check_Output_CSV_GCP_CIS
|
||||
)
|
||||
@@ -123,7 +124,7 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
file_descriptors.update({output_mode: file_descriptor})
|
||||
|
||||
elif output_mode == "ens_rd2022_aws":
|
||||
filename = f"{output_directory}/{output_filename}_ens_rd2022_aws{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_ens_rd2022_aws{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename,
|
||||
output_mode,
|
||||
@@ -133,14 +134,14 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
file_descriptors.update({output_mode: file_descriptor})
|
||||
|
||||
elif output_mode == "cis_1.5_aws":
|
||||
filename = f"{output_directory}/{output_filename}_cis_1.5_aws{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_cis_1.5_aws{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename, output_mode, audit_info, Check_Output_CSV_AWS_CIS
|
||||
)
|
||||
file_descriptors.update({output_mode: file_descriptor})
|
||||
|
||||
elif output_mode == "cis_1.4_aws":
|
||||
filename = f"{output_directory}/{output_filename}_cis_1.4_aws{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_cis_1.4_aws{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename, output_mode, audit_info, Check_Output_CSV_AWS_CIS
|
||||
)
|
||||
@@ -150,7 +151,7 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
output_mode
|
||||
== "aws_well_architected_framework_security_pillar_aws"
|
||||
):
|
||||
filename = f"{output_directory}/{output_filename}_aws_well_architected_framework_security_pillar_aws{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_aws_well_architected_framework_security_pillar_aws{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename,
|
||||
output_mode,
|
||||
@@ -163,7 +164,7 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
output_mode
|
||||
== "aws_well_architected_framework_reliability_pillar_aws"
|
||||
):
|
||||
filename = f"{output_directory}/{output_filename}_aws_well_architected_framework_reliability_pillar_aws{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_aws_well_architected_framework_reliability_pillar_aws{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename,
|
||||
output_mode,
|
||||
@@ -173,7 +174,7 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
file_descriptors.update({output_mode: file_descriptor})
|
||||
|
||||
elif output_mode == "iso27001_2013_aws":
|
||||
filename = f"{output_directory}/{output_filename}_iso27001_2013_aws{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_iso27001_2013_aws{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename,
|
||||
output_mode,
|
||||
@@ -183,7 +184,7 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
file_descriptors.update({output_mode: file_descriptor})
|
||||
|
||||
elif output_mode == "mitre_attack_aws":
|
||||
filename = f"{output_directory}/{output_filename}_mitre_attack_aws{csv_file_suffix}"
|
||||
filename = f"{output_directory}/compliance/{output_filename}_mitre_attack_aws{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename,
|
||||
output_mode,
|
||||
@@ -194,14 +195,26 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit
|
||||
|
||||
else:
|
||||
# Generic Compliance framework
|
||||
filename = f"{output_directory}/{output_filename}_{output_mode}{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename,
|
||||
output_mode,
|
||||
audit_info,
|
||||
Check_Output_CSV_Generic_Compliance,
|
||||
)
|
||||
file_descriptors.update({output_mode: file_descriptor})
|
||||
if (
|
||||
isinstance(audit_info, AWS_Audit_Info)
|
||||
and "aws" in output_mode
|
||||
or (
|
||||
isinstance(audit_info, Azure_Audit_Info)
|
||||
and "azure" in output_mode
|
||||
)
|
||||
or (
|
||||
isinstance(audit_info, GCP_Audit_Info)
|
||||
and "gcp" in output_mode
|
||||
)
|
||||
):
|
||||
filename = f"{output_directory}/compliance/{output_filename}_{output_mode}{csv_file_suffix}"
|
||||
file_descriptor = initialize_file_descriptor(
|
||||
filename,
|
||||
output_mode,
|
||||
audit_info,
|
||||
Check_Output_CSV_Generic_Compliance,
|
||||
)
|
||||
file_descriptors.update({output_mode: file_descriptor})
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
|
||||
@@ -21,6 +21,7 @@ from prowler.lib.utils.utils import open_file
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info
|
||||
from prowler.providers.azure.lib.audit_info.models import Azure_Audit_Info
|
||||
from prowler.providers.gcp.lib.audit_info.models import GCP_Audit_Info
|
||||
from prowler.providers.kubernetes.lib.audit_info.models import Kubernetes_Audit_Info
|
||||
|
||||
|
||||
def add_html_header(file_descriptor, audit_info):
|
||||
@@ -169,11 +170,11 @@ def add_html_header(file_descriptor, audit_info):
|
||||
def fill_html(file_descriptor, finding, output_options):
|
||||
try:
|
||||
row_class = "p-3 mb-2 bg-success-custom"
|
||||
if finding.status == "INFO":
|
||||
if finding.status == "MANUAL":
|
||||
row_class = "table-info"
|
||||
elif finding.status == "FAIL":
|
||||
row_class = "table-danger"
|
||||
elif finding.status == "WARNING":
|
||||
elif finding.status == "MUTED":
|
||||
row_class = "table-warning"
|
||||
file_descriptor.write(
|
||||
f"""
|
||||
@@ -407,7 +408,7 @@ def get_azure_html_assessment_summary(audit_info):
|
||||
if isinstance(audit_info, Azure_Audit_Info):
|
||||
printed_subscriptions = []
|
||||
for key, value in audit_info.identity.subscriptions.items():
|
||||
intermediate = f"{key} : {value}"
|
||||
intermediate = key + " : " + value
|
||||
printed_subscriptions.append(intermediate)
|
||||
|
||||
# check if identity is str(coming from SP) or dict(coming from browser or)
|
||||
@@ -522,6 +523,53 @@ def get_gcp_html_assessment_summary(audit_info):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_kubernetes_html_assessment_summary(audit_info):
|
||||
try:
|
||||
if isinstance(audit_info, Kubernetes_Audit_Info):
|
||||
return (
|
||||
"""
|
||||
<div class="col-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Kubernetes Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>Kubernetes Context:</b> """
|
||||
+ audit_info.context["name"]
|
||||
+ """
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Kubernetes Credentials
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>Kubernetes Cluster:</b> """
|
||||
+ audit_info.context["context"]["cluster"]
|
||||
+ """
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<b>Kubernetes User:</b> """
|
||||
+ audit_info.context["context"]["user"]
|
||||
+ """
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_assessment_summary(audit_info):
|
||||
"""
|
||||
get_assessment_summary gets the HTML assessment summary for the provider
|
||||
@@ -532,6 +580,7 @@ def get_assessment_summary(audit_info):
|
||||
# AWS_Audit_Info --> aws
|
||||
# GCP_Audit_Info --> gcp
|
||||
# Azure_Audit_Info --> azure
|
||||
# Kubernetes_Audit_Info --> kubernetes
|
||||
provider = audit_info.__class__.__name__.split("_")[0].lower()
|
||||
|
||||
# Dynamically get the Provider quick inventory handler
|
||||
|
||||
@@ -51,9 +51,9 @@ def fill_json_asff(finding_output, audit_info, finding, output_options):
|
||||
finding_output.GeneratorId = "prowler-" + finding.check_metadata.CheckID
|
||||
finding_output.AwsAccountId = audit_info.audited_account
|
||||
finding_output.Types = finding.check_metadata.CheckType
|
||||
finding_output.FirstObservedAt = finding_output.UpdatedAt = (
|
||||
finding_output.CreatedAt
|
||||
) = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
finding_output.FirstObservedAt = (
|
||||
finding_output.UpdatedAt
|
||||
) = finding_output.CreatedAt = timestamp_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
finding_output.Severity = Severity(
|
||||
Label=finding.check_metadata.Severity.upper()
|
||||
)
|
||||
@@ -116,8 +116,8 @@ def generate_json_asff_status(status: str) -> str:
|
||||
json_asff_status = "PASSED"
|
||||
elif status == "FAIL":
|
||||
json_asff_status = "FAILED"
|
||||
elif status == "WARNING":
|
||||
json_asff_status = "WARNING"
|
||||
elif status == "MUTED":
|
||||
json_asff_status = "MUTED"
|
||||
else:
|
||||
json_asff_status = "NOT_AVAILABLE"
|
||||
|
||||
@@ -293,7 +293,7 @@ def generate_json_ocsf_status(status: str):
|
||||
json_ocsf_status = "Success"
|
||||
elif status == "FAIL":
|
||||
json_ocsf_status = "Failure"
|
||||
elif status == "WARNING":
|
||||
elif status == "MUTED":
|
||||
json_ocsf_status = "Other"
|
||||
else:
|
||||
json_ocsf_status = "Unknown"
|
||||
@@ -307,7 +307,7 @@ def generate_json_ocsf_status_id(status: str):
|
||||
json_ocsf_status_id = 1
|
||||
elif status == "FAIL":
|
||||
json_ocsf_status_id = 2
|
||||
elif status == "WARNING":
|
||||
elif status == "MUTED":
|
||||
json_ocsf_status_id = 99
|
||||
else:
|
||||
json_ocsf_status_id = 0
|
||||
|
||||
@@ -10,10 +10,19 @@ from prowler.config.config import prowler_version, timestamp
|
||||
from prowler.lib.check.models import Remediation
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Organizations_Info
|
||||
from prowler.providers.aws.lib.audit_info.models import AWSOrganizationsInfo
|
||||
|
||||
|
||||
def get_check_compliance(finding, provider, output_options):
|
||||
def get_check_compliance(finding, provider, output_options) -> dict:
|
||||
"""get_check_compliance returns a map with the compliance framework as key and the requirements where the finding's check is present.
|
||||
|
||||
Example:
|
||||
|
||||
{
|
||||
"CIS-1.4": ["2.1.3"],
|
||||
"CIS-1.5": ["2.1.3"],
|
||||
}
|
||||
"""
|
||||
try:
|
||||
check_compliance = {}
|
||||
# We have to retrieve all the check's compliance requirements
|
||||
@@ -55,9 +64,9 @@ def generate_provider_output_csv(
|
||||
data["resource_name"] = finding.resource_name
|
||||
data["subscription"] = finding.subscription
|
||||
data["tenant_domain"] = audit_info.identity.domain
|
||||
data["finding_unique_id"] = (
|
||||
f"prowler-{provider}-{finding.check_metadata.CheckID}-{finding.subscription}-{finding.resource_id}"
|
||||
)
|
||||
data[
|
||||
"finding_unique_id"
|
||||
] = f"prowler-{provider}-{finding.check_metadata.CheckID}-{finding.subscription}-{finding.resource_id}"
|
||||
data["compliance"] = unroll_dict(
|
||||
get_check_compliance(finding, provider, output_options)
|
||||
)
|
||||
@@ -68,9 +77,21 @@ def generate_provider_output_csv(
|
||||
data["resource_name"] = finding.resource_name
|
||||
data["project_id"] = finding.project_id
|
||||
data["location"] = finding.location.lower()
|
||||
data["finding_unique_id"] = (
|
||||
f"prowler-{provider}-{finding.check_metadata.CheckID}-{finding.project_id}-{finding.resource_id}"
|
||||
data[
|
||||
"finding_unique_id"
|
||||
] = f"prowler-{provider}-{finding.check_metadata.CheckID}-{finding.project_id}-{finding.resource_id}"
|
||||
data["compliance"] = unroll_dict(
|
||||
get_check_compliance(finding, provider, output_options)
|
||||
)
|
||||
finding_output = output_model(**data)
|
||||
|
||||
if provider == "kubernetes":
|
||||
data["resource_id"] = finding.resource_id
|
||||
data["resource_name"] = finding.resource_name
|
||||
data["namespace"] = finding.namespace
|
||||
data[
|
||||
"finding_unique_id"
|
||||
] = f"prowler-{provider}-{finding.check_metadata.CheckID}-{finding.namespace}-{finding.resource_id}"
|
||||
data["compliance"] = unroll_dict(
|
||||
get_check_compliance(finding, provider, output_options)
|
||||
)
|
||||
@@ -82,9 +103,9 @@ def generate_provider_output_csv(
|
||||
data["region"] = finding.region
|
||||
data["resource_id"] = finding.resource_id
|
||||
data["resource_arn"] = finding.resource_arn
|
||||
data["finding_unique_id"] = (
|
||||
f"prowler-{provider}-{finding.check_metadata.CheckID}-{audit_info.audited_account}-{finding.region}-{finding.resource_id}"
|
||||
)
|
||||
data[
|
||||
"finding_unique_id"
|
||||
] = f"prowler-{provider}-{finding.check_metadata.CheckID}-{audit_info.audited_account}-{finding.region}-{finding.resource_id}"
|
||||
data["compliance"] = unroll_dict(
|
||||
get_check_compliance(finding, provider, output_options)
|
||||
)
|
||||
@@ -348,6 +369,16 @@ class Gcp_Check_Output_CSV(Check_Output_CSV):
|
||||
resource_name: str = ""
|
||||
|
||||
|
||||
class Kubernetes_Check_Output_CSV(Check_Output_CSV):
|
||||
"""
|
||||
Kubernetes_Check_Output_CSV generates a finding's output in CSV format for the Kubernetes provider.
|
||||
"""
|
||||
|
||||
namespace: str = ""
|
||||
resource_id: str = ""
|
||||
resource_name: str = ""
|
||||
|
||||
|
||||
def generate_provider_output_json(
|
||||
provider: str, finding, audit_info, mode: str, output_options
|
||||
):
|
||||
@@ -452,7 +483,7 @@ class Aws_Check_Output_JSON(Check_Output_JSON):
|
||||
|
||||
Profile: str = ""
|
||||
AccountId: str = ""
|
||||
OrganizationsInfo: Optional[AWS_Organizations_Info]
|
||||
OrganizationsInfo: Optional[AWSOrganizationsInfo]
|
||||
Region: str = ""
|
||||
ResourceId: str = ""
|
||||
ResourceArn: str = ""
|
||||
@@ -478,7 +509,7 @@ class Azure_Check_Output_JSON(Check_Output_JSON):
|
||||
|
||||
class Gcp_Check_Output_JSON(Check_Output_JSON):
|
||||
"""
|
||||
Gcp_Check_Output_JSON generates a finding's output in JSON format for the AWS provider.
|
||||
Gcp_Check_Output_JSON generates a finding's output in JSON format for the GCP provider.
|
||||
"""
|
||||
|
||||
ProjectId: str = ""
|
||||
@@ -490,6 +521,19 @@ class Gcp_Check_Output_JSON(Check_Output_JSON):
|
||||
super().__init__(**metadata)
|
||||
|
||||
|
||||
class Kubernetes_Check_Output_JSON(Check_Output_JSON):
|
||||
"""
|
||||
Kubernetes_Check_Output_JSON generates a finding's output in JSON format for the Kubernetes provider.
|
||||
"""
|
||||
|
||||
ResourceId: str = ""
|
||||
ResourceName: str = ""
|
||||
Namespace: str = ""
|
||||
|
||||
def __init__(self, **metadata):
|
||||
super().__init__(**metadata)
|
||||
|
||||
|
||||
class Check_Output_MITRE_ATTACK(BaseModel):
|
||||
"""
|
||||
Check_Output_MITRE_ATTACK generates a finding's output in CSV MITRE ATTACK format.
|
||||
@@ -614,8 +658,8 @@ class Check_Output_CSV_Generic_Compliance(BaseModel):
|
||||
Requirements_Attributes_Section: Optional[str]
|
||||
Requirements_Attributes_SubSection: Optional[str]
|
||||
Requirements_Attributes_SubGroup: Optional[str]
|
||||
Requirements_Attributes_Service: Optional[str]
|
||||
Requirements_Attributes_Type: Optional[str]
|
||||
Requirements_Attributes_Service: str
|
||||
Requirements_Attributes_Soc_Type: Optional[str]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
|
||||
@@ -4,7 +4,10 @@ from colorama import Fore, Style
|
||||
|
||||
from prowler.config.config import available_compliance_frameworks, orange_color
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.compliance import add_manual_controls, fill_compliance
|
||||
from prowler.lib.outputs.compliance.compliance import (
|
||||
add_manual_controls,
|
||||
fill_compliance,
|
||||
)
|
||||
from prowler.lib.outputs.file_descriptors import fill_file_descriptors
|
||||
from prowler.lib.outputs.html import fill_html
|
||||
from prowler.lib.outputs.json import fill_json_asff, fill_json_ocsf
|
||||
@@ -17,15 +20,17 @@ from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info
|
||||
from prowler.providers.azure.lib.audit_info.models import Azure_Audit_Info
|
||||
|
||||
|
||||
def stdout_report(finding, color, verbose, is_quiet):
|
||||
def stdout_report(finding, color, verbose, status):
|
||||
if finding.check_metadata.Provider == "aws":
|
||||
details = finding.region
|
||||
if finding.check_metadata.Provider == "azure":
|
||||
details = finding.check_metadata.ServiceName
|
||||
if finding.check_metadata.Provider == "gcp":
|
||||
details = finding.location.lower()
|
||||
if finding.check_metadata.Provider == "kubernetes":
|
||||
details = finding.namespace.lower()
|
||||
|
||||
if verbose and not (is_quiet and finding.status != "FAIL"):
|
||||
if verbose and (not status or finding.status in status):
|
||||
print(
|
||||
f"\t{color}{finding.status}{Style.RESET_ALL} {details}: {finding.status_extended}"
|
||||
)
|
||||
@@ -57,28 +62,35 @@ def report(check_findings, output_options, audit_info):
|
||||
# Print findings by stdout
|
||||
color = set_report_color(finding.status)
|
||||
stdout_report(
|
||||
finding, color, output_options.verbose, output_options.is_quiet
|
||||
finding, color, output_options.verbose, output_options.status
|
||||
)
|
||||
|
||||
if file_descriptors:
|
||||
# Check if --quiet to only add fails to outputs
|
||||
if not (finding.status != "FAIL" and output_options.is_quiet):
|
||||
if any(
|
||||
compliance in output_options.output_modes
|
||||
for compliance in available_compliance_frameworks
|
||||
):
|
||||
fill_compliance(
|
||||
output_options,
|
||||
finding,
|
||||
audit_info,
|
||||
file_descriptors,
|
||||
# Check if --status is enabled and if the filter applies
|
||||
if (
|
||||
not output_options.status
|
||||
or finding.status in output_options.status
|
||||
):
|
||||
input_compliance_frameworks = list(
|
||||
set(output_options.output_modes).intersection(
|
||||
available_compliance_frameworks
|
||||
)
|
||||
)
|
||||
|
||||
add_manual_controls(
|
||||
output_options,
|
||||
audit_info,
|
||||
file_descriptors,
|
||||
)
|
||||
fill_compliance(
|
||||
output_options,
|
||||
finding,
|
||||
audit_info,
|
||||
file_descriptors,
|
||||
input_compliance_frameworks,
|
||||
)
|
||||
|
||||
add_manual_controls(
|
||||
output_options,
|
||||
audit_info,
|
||||
file_descriptors,
|
||||
input_compliance_frameworks,
|
||||
)
|
||||
|
||||
# AWS specific outputs
|
||||
if finding.check_metadata.Provider == "aws":
|
||||
@@ -140,7 +152,7 @@ def report(check_findings, output_options, audit_info):
|
||||
file_descriptors["json-ocsf"].write(",")
|
||||
|
||||
else: # No service resources in the whole account
|
||||
color = set_report_color("INFO")
|
||||
color = set_report_color("MANUAL")
|
||||
if output_options.verbose:
|
||||
print(f"\t{color}INFO{Style.RESET_ALL} There are no resources")
|
||||
# Separator between findings and bar
|
||||
@@ -165,12 +177,12 @@ def set_report_color(status: str) -> str:
|
||||
color = Fore.RED
|
||||
elif status == "ERROR":
|
||||
color = Fore.BLACK
|
||||
elif status == "WARNING":
|
||||
elif status == "MUTED":
|
||||
color = orange_color
|
||||
elif status == "INFO":
|
||||
elif status == "MANUAL":
|
||||
color = Fore.YELLOW
|
||||
else:
|
||||
raise Exception("Invalid Report Status. Must be PASS, FAIL, ERROR or WARNING")
|
||||
raise Exception("Invalid Report Status. Must be PASS, FAIL, ERROR or MUTED")
|
||||
return color
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ def send_slack_message(token, channel, stats, provider, audit_info):
|
||||
response = client.chat_postMessage(
|
||||
username="Prowler",
|
||||
icon_url=square_logo_img,
|
||||
channel=f"#{channel}",
|
||||
channel="#" + channel,
|
||||
blocks=create_message_blocks(identity, logo, stats),
|
||||
)
|
||||
return response
|
||||
@@ -35,7 +35,7 @@ def create_message_identity(provider, audit_info):
|
||||
elif provider == "azure":
|
||||
printed_subscriptions = []
|
||||
for key, value in audit_info.identity.subscriptions.items():
|
||||
intermediate = f"- *{key}: {value}*\n"
|
||||
intermediate = "- *" + key + ": " + value + "*\n"
|
||||
printed_subscriptions.append(intermediate)
|
||||
identity = f"Azure Subscriptions:\n{''.join(printed_subscriptions)}"
|
||||
logo = azure_logo
|
||||
@@ -66,14 +66,14 @@ def create_message_blocks(identity, logo, stats):
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n",
|
||||
"text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass']/stats['findings_count']*100,2)}%)\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ",
|
||||
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail']/stats['findings_count']*100,2)}%)\n ",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -39,6 +39,9 @@ def display_summary_table(
|
||||
elif provider == "gcp":
|
||||
entity_type = "Project ID/s"
|
||||
audited_entities = ", ".join(audit_info.project_ids)
|
||||
elif provider == "kubernetes":
|
||||
entity_type = "Context"
|
||||
audited_entities = audit_info.context["name"]
|
||||
|
||||
if findings:
|
||||
current = {
|
||||
@@ -96,8 +99,8 @@ def display_summary_table(
|
||||
print("\nOverview Results:")
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(fail_count / len(findings) * 100, 2)}% ({fail_count}) Failed{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count / len(findings) * 100, 2)}% ({pass_count}) Passed{Style.RESET_ALL}",
|
||||
f"{Fore.RED}{round(fail_count/len(findings)*100, 2)}% ({fail_count}) Failed{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(pass_count/len(findings)*100, 2)}% ({pass_count}) Passed{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
import os
|
||||
import pathlib
|
||||
from datetime import timedelta
|
||||
from time import time
|
||||
|
||||
from rich.align import Align
|
||||
from rich.console import Console, Group
|
||||
from rich.layout import Layout
|
||||
from rich.live import Live
|
||||
from rich.padding import Padding
|
||||
from rich.panel import Panel
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
MofNCompleteColumn,
|
||||
Progress,
|
||||
TextColumn,
|
||||
TimeElapsedColumn,
|
||||
TimeRemainingColumn,
|
||||
)
|
||||
from rich.rule import Rule
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
from rich.theme import Theme
|
||||
|
||||
from prowler.config.config import prowler_version, timestamp
|
||||
from prowler.providers.aws.models import AWSIdentityInfo, AWSAssumeRole
|
||||
|
||||
# Defines a subclass of Live for creating and managing the live display in the CLI
|
||||
class LiveDisplay(Live):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Load a theme for the console display from a file
|
||||
theme = self.load_theme_from_file()
|
||||
super().__init__(renderable=None, console=Console(theme=theme), *args, **kwargs)
|
||||
self.sections = {} # Stores different sections of the layout
|
||||
self.enabled = False # Flag to enable or disable the live display
|
||||
|
||||
# Sets up the layout of the live display
|
||||
def make_layout(self):
|
||||
"""
|
||||
Defines the layout.
|
||||
Making sections invisible so it doesnt show the default Layout metadata before content is added
|
||||
Text(" ") is to stop the layout metadata from rendering before the layout is updated with real content
|
||||
client_and_service handles client init (when importing clients) and service check execution
|
||||
"""
|
||||
self.layout = Layout(name="root")
|
||||
# Split layout into intro, overall progress, and main sections
|
||||
self.layout.split(
|
||||
Layout(name="intro", ratio=3, minimum_size=9),
|
||||
Layout(Text(" "), name="overall_progress", minimum_size=5),
|
||||
Layout(name="main", ratio=10),
|
||||
)
|
||||
# Further split intro layout into body and creds sections
|
||||
self.layout["intro"].split_row(
|
||||
Layout(name="body", ratio=3),
|
||||
Layout(name="creds", ratio=2, visible=False),
|
||||
)
|
||||
# Split main layout into client_and_service and results sections
|
||||
self.layout["main"].split_row(
|
||||
Layout(
|
||||
Text(" "), name="client_and_service", ratio=3
|
||||
), # For client_init and service
|
||||
Layout(name="results", ratio=2, visible=False),
|
||||
)
|
||||
|
||||
# Loads a theme from a YAML file located in the same directory as this file
|
||||
def load_theme_from_file(self):
|
||||
# Loads theme.yaml from the same folder as this file
|
||||
actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
with open(f"{actual_directory}/theme.yaml") as f:
|
||||
theme = Theme.from_file(f)
|
||||
return theme
|
||||
|
||||
# Initializes the layout and sections based on CLI arguments
|
||||
def initialize(self, args):
|
||||
# A way to get around parsing args to LiveDisplay when it is intialized
|
||||
# This is so that the live_display object can be intialized in this file, and imported to other parts of prowler
|
||||
self.cli_args = args
|
||||
|
||||
self.enabled = not args.only_logs
|
||||
|
||||
if self.enabled:
|
||||
# Initialize layout
|
||||
self.make_layout()
|
||||
# Apply layout
|
||||
self.update(self.layout)
|
||||
# Add Intro section
|
||||
intro_layout = self.layout["intro"]
|
||||
intro_section = IntroSection(args, intro_layout)
|
||||
self.sections["intro"] = intro_section
|
||||
# Start live display
|
||||
self.start()
|
||||
|
||||
# Adds AWS credentials to the display
|
||||
def print_aws_credentials(self, aws_identity_info: AWSIdentityInfo, assumed_role_info: AWSAssumeRole):
|
||||
# Adds the AWS credentials to the display - will need to extend to gcp and azure
|
||||
# Create a new function for gcp and azure in this class, that will call a function in the intro_section class
|
||||
intro_section = self.sections["intro"]
|
||||
intro_section.add_aws_credentials(aws_identity_info, assumed_role_info)
|
||||
|
||||
# Adds and manages the overall progress section
|
||||
def add_overall_progress_section(self, total_checks_dict):
|
||||
overall_progress_section = OverallProgressSection(total_checks_dict)
|
||||
overall_progress_layout = self.layout["overall_progress"]
|
||||
overall_progress_layout.update(overall_progress_section)
|
||||
overall_progress_layout.visible = True
|
||||
self.sections["overall_progress"] = overall_progress_section
|
||||
|
||||
# Add results section
|
||||
self.add_results_section()
|
||||
|
||||
# Wrapper function to increment the overall progress
|
||||
def increment_overall_check_progress(self):
|
||||
# Called by ExecutionManager
|
||||
if self.enabled:
|
||||
section = self.sections["overall_progress"]
|
||||
section.increment_check_progress()
|
||||
|
||||
# Wrapper function to increment the progress for the current service
|
||||
def increment_overall_service_progress(self):
|
||||
# Called by ExecutionManager
|
||||
if self.enabled:
|
||||
section = self.sections["overall_progress"]
|
||||
section.increment_service_progress()
|
||||
|
||||
# Adds and manages the results section
|
||||
def add_results_section(self):
|
||||
# Intializes the results section
|
||||
results_layout = self.layout["results"]
|
||||
results_section = ResultsSection()
|
||||
results_layout.update(results_section)
|
||||
results_layout.visible = True
|
||||
self.sections["results"] = results_section
|
||||
|
||||
def add_results_for_service(self, service_name, service_findings):
|
||||
# Adds rows to the Service Check Results table
|
||||
if self.enabled:
|
||||
results_section = self.sections["results"]
|
||||
results_section.add_results_for_service(service_name, service_findings)
|
||||
|
||||
# Client Init Section
|
||||
def add_client_init_section(self, service_name):
|
||||
# Used to track progress of client init process
|
||||
if self.enabled:
|
||||
client_init_section = ClientInitSection(service_name)
|
||||
self.sections["client_and_service"] = client_init_section
|
||||
self.layout["client_and_service"].update(client_init_section)
|
||||
self.layout["client_and_service"].visible = True
|
||||
|
||||
# Service Section
|
||||
def add_service_section(self, service_name, total_checks):
|
||||
# Used to create the ServiceSection when checks start to execute (after clients have been imported)
|
||||
if self.enabled:
|
||||
service_section = ServiceSection(service_name, total_checks)
|
||||
self.sections["client_and_service"] = service_section
|
||||
self.layout["client_and_service"].update(service_section)
|
||||
|
||||
def increment_check_progress(self):
|
||||
if self.enabled:
|
||||
service_section = self.sections["client_and_service"]
|
||||
service_section.increment_check_progress()
|
||||
|
||||
# Misc
|
||||
def get_service_section(self):
|
||||
# Used by Check
|
||||
if self.enabled:
|
||||
return self.sections["client_and_service"]
|
||||
|
||||
def get_client_init_section(self):
|
||||
# Used by AWSService
|
||||
if self.enabled:
|
||||
return self.sections["client_and_service"]
|
||||
|
||||
def hide_service_section(self):
|
||||
# To hide the last service after execution has completed
|
||||
self.layout["client_and_service"].visible = False
|
||||
|
||||
def print_message(self, message):
|
||||
# No use yet
|
||||
self.console.print(message)
|
||||
|
||||
# The following classes (ServiceSection, ClientInitSection, IntroSection, OverallProgressSection, ResultsSection)
|
||||
# are used to define different sections of the live display, each with its own layout, progress bars,
|
||||
|
||||
class ServiceSection:
|
||||
def __init__(self, service_name, total_checks) -> None:
|
||||
self.service_name = service_name
|
||||
self.total_checks = total_checks
|
||||
self.renderables = self.create_service_section()
|
||||
self.start_check_progress()
|
||||
|
||||
def __rich__(self):
|
||||
return Padding(self.renderables, (2, 2))
|
||||
|
||||
def create_service_section(self):
|
||||
# Create the progress components
|
||||
self.check_progress = Progress(
|
||||
TextColumn("[bold]{task.description}"),
|
||||
BarColumn(bar_width=None),
|
||||
MofNCompleteColumn(),
|
||||
transient=False, # Optional: set True if you want the progress bar to disappear after completion
|
||||
)
|
||||
|
||||
# Used to add titles that dont need progress bars
|
||||
self.title_bar = Progress(
|
||||
TextColumn("[progress.description]{task.description}"), transient=True
|
||||
)
|
||||
# Progress Bar for Service Init and Checks
|
||||
self.task_progress = Progress(
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(bar_width=None),
|
||||
MofNCompleteColumn(),
|
||||
TimeElapsedColumn(),
|
||||
TimeRemainingColumn(),
|
||||
transient=True,
|
||||
)
|
||||
|
||||
return Group(
|
||||
Panel(
|
||||
Group(
|
||||
self.check_progress,
|
||||
Rule(style="bold blue"),
|
||||
self.title_bar,
|
||||
Rule(style="bold blue"),
|
||||
self.task_progress,
|
||||
),
|
||||
title=f"Service: {self.service_name}",
|
||||
),
|
||||
)
|
||||
|
||||
def start_check_progress(self):
|
||||
self.check_progress_task_id = self.check_progress.add_task(
|
||||
"Checks executed", total=self.total_checks
|
||||
)
|
||||
|
||||
def increment_check_progress(self):
|
||||
self.check_progress.update(self.check_progress_task_id, advance=1)
|
||||
|
||||
|
||||
class ClientInitSection:
|
||||
def __init__(self, client_name) -> None:
|
||||
self.client_name = client_name
|
||||
self.renderables = self.create_client_init_section()
|
||||
|
||||
def __rich__(self):
|
||||
return Padding(self.renderables, (2, 2))
|
||||
|
||||
def create_client_init_section(self):
|
||||
# Progress Bar for Checks
|
||||
self.task_progress_bar = Progress(
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(bar_width=None),
|
||||
MofNCompleteColumn(),
|
||||
TimeElapsedColumn(),
|
||||
TimeRemainingColumn(),
|
||||
transient=True,
|
||||
)
|
||||
|
||||
return Group(
|
||||
Panel(
|
||||
Group(
|
||||
self.task_progress_bar,
|
||||
),
|
||||
title=f"Intializing {self.client_name.replace('_', ' ')}",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class IntroSection:
|
||||
def __init__(self, args, layout: Layout) -> None:
|
||||
self.body_layout = layout["body"]
|
||||
self.creds_layout = layout["creds"]
|
||||
self.renderables = []
|
||||
self.title = f"Prowler v{prowler_version}"
|
||||
if not args.no_banner:
|
||||
self.create_banner(args)
|
||||
|
||||
def __rich__(self):
|
||||
return Group(*self.renderables)
|
||||
|
||||
def create_banner(self, args):
|
||||
banner_text = f"""[banner_color] _
|
||||
_ __ _ __ _____ _| | ___ _ __
|
||||
| '_ \| '__/ _ \ \ /\ / / |/ _ \ '__|
|
||||
| |_) | | | (_) \ V V /| | __/ |
|
||||
| .__/|_| \___/ \_/\_/ |_|\___|_|v{prowler_version}
|
||||
|_|[/banner_color][banner_blue]the handy cloud security tool[/banner_blue]
|
||||
|
||||
[info]Date: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}[/info]
|
||||
"""
|
||||
|
||||
if args.verbose:
|
||||
banner_text += """
|
||||
Color code for results:
|
||||
- [info]INFO (Information)[/info]
|
||||
- [pass]PASS (Recommended value)[/pass]
|
||||
- [orange_color]WARNING (Ignored by mutelist)[/orange_color]
|
||||
- [fail]FAIL (Fix required)[/fail]
|
||||
"""
|
||||
self.renderables.append(banner_text)
|
||||
self.body_layout.update(Group(*self.renderables))
|
||||
self.body_layout.visible = True
|
||||
|
||||
def add_aws_credentials(self, aws_identity_info: AWSIdentityInfo, assumed_role_info: AWSAssumeRole):
|
||||
# Beautify audited regions, and set to "all" if there is no filter region
|
||||
regions = (
|
||||
", ".join(aws_identity_info.audited_regions)
|
||||
if aws_identity_info.audited_regions is not None
|
||||
else "all"
|
||||
)
|
||||
# Beautify audited profile, set and to "default" if there is no profile set
|
||||
profile = aws_identity_info.profile if aws_identity_info.profile is not None else "default"
|
||||
|
||||
content = Text()
|
||||
content.append(
|
||||
"This report is being generated using credentials below:\n\n", style="bold"
|
||||
)
|
||||
|
||||
content.append("AWS-CLI Profile: ", style="bold")
|
||||
content.append(f"[{profile}]\n", style="info")
|
||||
|
||||
content.append("AWS Filter Region: ", style="bold")
|
||||
content.append(f"[{regions}]\n", style="info")
|
||||
|
||||
content.append("AWS Account: ", style="bold")
|
||||
content.append(f"[{aws_identity_info.account}]\n", style="info")
|
||||
|
||||
content.append("UserId: ", style="bold")
|
||||
content.append(f"[{aws_identity_info.user_id}]\n", style="info")
|
||||
|
||||
content.append("Caller Identity ARN: ", style="bold")
|
||||
content.append(f"[{aws_identity_info.identity_arn}]\n", style="info")
|
||||
# If a role has been assumed, print the Assumed Role ARN
|
||||
if assumed_role_info.role_arn is not None:
|
||||
content.append("Assumed Role ARN: ", style="bold")
|
||||
content.append(f"[{assumed_role_info.role_arn}]\n", style="info")
|
||||
|
||||
self.creds_layout.update(content)
|
||||
self.creds_layout.visible = True
|
||||
|
||||
|
||||
class OverallProgressSection:
|
||||
def __init__(self, total_checks_dict: dict) -> None:
|
||||
self.start_time = time() # Start the timer
|
||||
self.renderables = self.create_renderable(total_checks_dict)
|
||||
|
||||
def __rich__(self):
|
||||
elapsed_time = self.total_time_taken()
|
||||
return Group(*self.renderables, f"Total time taken: {elapsed_time}")
|
||||
|
||||
def total_time_taken(self):
|
||||
elapsed_seconds = int(time() - self.start_time)
|
||||
elapsed_time = timedelta(seconds=elapsed_seconds)
|
||||
return elapsed_time
|
||||
|
||||
def create_renderable(self, total_checks_dict):
|
||||
services_num = len(total_checks_dict) # number of keys == number of services
|
||||
checks_num = sum(total_checks_dict.values())
|
||||
|
||||
plural_string = "checks"
|
||||
singular_string = "check"
|
||||
|
||||
check_noun = plural_string if checks_num > 1 else singular_string
|
||||
|
||||
# Create the progress bar
|
||||
self.overall_progress_bar = Progress(
|
||||
TextColumn("[bold]{task.description}"),
|
||||
BarColumn(bar_width=None),
|
||||
MofNCompleteColumn(),
|
||||
transient=False, # Optional: set True if you want the progress bar to disappear after completion
|
||||
)
|
||||
# Create the Services Completed task, to track the number of services completed
|
||||
self.service_progress_task_id = self.overall_progress_bar.add_task(
|
||||
"Services completed", total=services_num
|
||||
)
|
||||
# Create the Checks Completed task, to track the number of checks completed across all services
|
||||
self.check_progress_task_id = self.overall_progress_bar.add_task(
|
||||
"Checks executed", total=checks_num
|
||||
)
|
||||
|
||||
content = Text()
|
||||
content.append(
|
||||
f"Executing {checks_num} {check_noun} across {services_num} services, please wait...\n",
|
||||
style="bold",
|
||||
)
|
||||
|
||||
return [content, self.overall_progress_bar]
|
||||
|
||||
def increment_check_progress(self):
|
||||
self.overall_progress_bar.update(self.check_progress_task_id, advance=1)
|
||||
|
||||
def increment_service_progress(self):
|
||||
self.overall_progress_bar.update(self.service_progress_task_id, advance=1)
|
||||
|
||||
|
||||
class ResultsSection:
|
||||
def __init__(self, verbose=True):
|
||||
self.verbose = verbose
|
||||
self.table = Table(title="Service Check Results")
|
||||
self.table.add_column("Service", justify="left")
|
||||
|
||||
if self.verbose:
|
||||
self.serverities = ["critical", "high", "medium", "low"]
|
||||
# Add columns for each severity level when verbose, report on the count of fails per severity per service
|
||||
for severity in self.serverities:
|
||||
styled_header = (
|
||||
f"[{severity.lower()}]{severity.capitalize()}[/{severity.lower()}]"
|
||||
)
|
||||
self.table.add_column(styled_header, justify="center")
|
||||
|
||||
else:
|
||||
# Dynamically track the status's, report on the status counts for each service
|
||||
self.status_columns = set(["PASS", "FAIL"])
|
||||
self.service_findings = {} # Dictionary to store findings for each service
|
||||
|
||||
# Dictionary to map plain statuses to their stylized forms
|
||||
self.status_headers = {
|
||||
"FAIL": "[fail]Fail[/fail]",
|
||||
"PASS": "[pass]Pass[/pass]",
|
||||
}
|
||||
|
||||
# Add the initial columns with styling
|
||||
for status, header in self.status_headers.items():
|
||||
self.table.add_column(header, justify="center")
|
||||
|
||||
def add_results_for_service(self, service_name, service_findings):
|
||||
if self.verbose:
|
||||
# Count fails per severity
|
||||
severity_counts = {severity: 0 for severity in self.serverities}
|
||||
for finding in service_findings:
|
||||
if finding.status == "FAIL":
|
||||
severity_counts[finding.check_metadata.Severity] += 1
|
||||
|
||||
# Add row with severity counts
|
||||
row = [service_name] + [
|
||||
str(severity_counts[severity]) for severity in self.serverities
|
||||
]
|
||||
self.table.add_row(*row)
|
||||
else:
|
||||
# Update the dictionary with the new findings
|
||||
status_counts = {report.status: 0 for report in service_findings}
|
||||
for report in service_findings:
|
||||
status_counts[report.status] += 1
|
||||
self.service_findings[service_name] = status_counts
|
||||
|
||||
# Update status_columns and table columns
|
||||
self.status_columns.update(status_counts.keys())
|
||||
for status in self.status_columns:
|
||||
if status not in self.status_headers:
|
||||
# [{status.lower()}] is for the styling (defined in theme.yaml)
|
||||
# If new status, add it to status_headers and table
|
||||
styled_header = (
|
||||
f"[{status.lower()}]{status.capitalize()}[/{status.lower()}]"
|
||||
)
|
||||
self.status_headers[status] = styled_header
|
||||
self.table.add_column(styled_header, justify="center")
|
||||
|
||||
# Update the table with findings for all services
|
||||
self._update_table()
|
||||
|
||||
def _update_table(self):
|
||||
# Used for when verbose = false
|
||||
# Clear existing rows
|
||||
self.table.rows.clear()
|
||||
|
||||
# Add updated rows for all services
|
||||
for service, counts in self.service_findings.items():
|
||||
row = [service]
|
||||
for status in self.status_columns:
|
||||
count = counts.get(status, 0)
|
||||
percentage = (
|
||||
f"{(count / sum(counts.values()) * 100):.2f}%" if counts else "0%"
|
||||
)
|
||||
row.append(f"{count} ({percentage})")
|
||||
self.table.add_row(*row)
|
||||
|
||||
def __rich__(self):
|
||||
# This method allows the ResultsSection to be directly rendered by Rich
|
||||
if not self.table.rows:
|
||||
return Text("")
|
||||
return Padding(Align.center(self.table), (0, 2))
|
||||
|
||||
|
||||
# Create an instance of LiveDisplay to import elsewhere (ExecutionManager, the checks, the services)
|
||||
|
||||
live_display = LiveDisplay(vertical_overflow="visible")
|
||||
@@ -0,0 +1,16 @@
|
||||
[styles]
|
||||
info = yellow1
|
||||
warning = dark_orange
|
||||
fail = bold red
|
||||
pass = bold green
|
||||
banner_blue = dodger_blue3 bold
|
||||
banner_color = bold green
|
||||
orange_color = dark_orange
|
||||
critical = bold bright_red
|
||||
high = bold red
|
||||
medium = bold dark_orange
|
||||
low = bold yellow1
|
||||
|
||||
|
||||
|
||||
# style names must be lower case, start with a letter, and only contain letters or the characters ".", "-", "_".
|
||||
@@ -10,11 +10,8 @@ from prowler.config.config import aws_services_json_file
|
||||
from prowler.lib.check.check import list_modules, recover_checks_from_service
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file
|
||||
from prowler.providers.aws.config import (
|
||||
AWS_STS_GLOBAL_ENDPOINT_REGION,
|
||||
ROLE_SESSION_NAME,
|
||||
)
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Assume_Role, AWS_Audit_Info
|
||||
from prowler.providers.aws.config import AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info, AWSAssumeRole
|
||||
from prowler.providers.aws.lib.credentials.credentials import create_sts_session
|
||||
|
||||
|
||||
@@ -112,19 +109,13 @@ class AWS_Provider:
|
||||
|
||||
def assume_role(
|
||||
session: session.Session,
|
||||
assumed_role_info: AWS_Assume_Role,
|
||||
assumed_role_info: AWSAssumeRole,
|
||||
sts_endpoint_region: str = None,
|
||||
) -> dict:
|
||||
try:
|
||||
role_session_name = (
|
||||
assumed_role_info.role_session_name
|
||||
if assumed_role_info.role_session_name
|
||||
else ROLE_SESSION_NAME
|
||||
)
|
||||
|
||||
assume_role_arguments = {
|
||||
"RoleArn": assumed_role_info.role_arn,
|
||||
"RoleSessionName": role_session_name,
|
||||
"RoleSessionName": "ProwlerAsessmentSession",
|
||||
"DurationSeconds": assumed_role_info.session_duration,
|
||||
}
|
||||
|
||||
@@ -202,14 +193,10 @@ def get_aws_enabled_regions(audit_info: AWS_Audit_Info) -> set:
|
||||
ec2_client = audit_info.audit_session.client(service, region_name=default_region)
|
||||
|
||||
enabled_regions = set()
|
||||
try:
|
||||
# With AllRegions=False we only get the enabled regions for the account
|
||||
for region in ec2_client.describe_regions(AllRegions=False).get("Regions", []):
|
||||
enabled_regions.add(region.get("RegionName"))
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
# With AllRegions=False we only get the enabled regions for the account
|
||||
for region in ec2_client.describe_regions(AllRegions=False).get("Regions", []):
|
||||
enabled_regions.add(region.get("RegionName"))
|
||||
|
||||
return enabled_regions
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from typing import Any, Optional
|
||||
|
||||
from boto3 import client, session
|
||||
from botocore.config import Config
|
||||
from botocore.credentials import RefreshableCredentials
|
||||
from botocore.session import get_session
|
||||
from colorama import Fore, Style
|
||||
|
||||
from prowler.config.config import aws_services_json_file
|
||||
from prowler.lib.check.check import list_modules, recover_checks_from_service
|
||||
from prowler.lib.ui.live_display import live_display
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file
|
||||
from prowler.providers.aws.config import (
|
||||
AWS_STS_GLOBAL_ENDPOINT_REGION,
|
||||
BOTO3_USER_AGENT_EXTRA,
|
||||
)
|
||||
from prowler.providers.aws.models import (
|
||||
AWSOrganizationsInfo,
|
||||
AWSCredentials,
|
||||
AWSAssumeRole,
|
||||
AWSAssumeRoleConfiguration,
|
||||
AWSIdentityInfo,
|
||||
AWSSession,
|
||||
)
|
||||
from prowler.providers.aws.lib.arn.arn import parse_iam_credentials_arn
|
||||
from prowler.providers.aws.lib.credentials.credentials import (
|
||||
create_sts_session,
|
||||
validate_AWSCredentials,
|
||||
)
|
||||
from prowler.providers.aws.lib.organizations.organizations import (
|
||||
get_organizations_metadata,
|
||||
)
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
class AwsProvider(Provider):
|
||||
session: AWSSession = AWSSession(
|
||||
session=None, session_config=None, original_session=None
|
||||
)
|
||||
identity: AWSIdentityInfo = AWSIdentityInfo(
|
||||
account=None,
|
||||
account_arn=None,
|
||||
user_id=None,
|
||||
partition=None,
|
||||
identity_arn=None,
|
||||
profile=None,
|
||||
profile_region=None,
|
||||
audited_regions=[],
|
||||
)
|
||||
assumed_role: AWSAssumeRoleConfiguration = AWSAssumeRoleConfiguration(
|
||||
assumed_role_info=AWSAssumeRole(
|
||||
role_arn=None,
|
||||
session_duration=None,
|
||||
external_id=None,
|
||||
mfa_enabled=False,
|
||||
),
|
||||
assumed_role_credentials=AWSCredentials(
|
||||
aws_access_key_id=None,
|
||||
aws_session_token=None,
|
||||
aws_secret_access_key=None,
|
||||
expiration=None,
|
||||
),
|
||||
)
|
||||
organizations_metadata: AWSOrganizationsInfo = AWSOrganizationsInfo(
|
||||
account_details_email=None,
|
||||
account_details_name=None,
|
||||
account_details_arn=None,
|
||||
account_details_org=None,
|
||||
account_details_tags=None,
|
||||
)
|
||||
audit_resources: Optional[Any]
|
||||
audit_metadata: Optional[Any]
|
||||
audit_config: dict = {}
|
||||
mfa_enabled: bool = False
|
||||
ignore_unused_services: bool = False
|
||||
|
||||
def __init__(self, arguments: Namespace):
|
||||
logger.info("Setting AWS provider ...")
|
||||
# Parse input arguments
|
||||
# Assume Role Options
|
||||
input_role = getattr(arguments, "role", None)
|
||||
input_session_duration = getattr(arguments, "session_duration", None)
|
||||
input_external_id = getattr(arguments, "external_id", None)
|
||||
|
||||
# STS Endpoint Region
|
||||
sts_endpoint_region = getattr(arguments, "sts_endpoint_region", None)
|
||||
|
||||
# MFA Configuration (false by default)
|
||||
input_mfa = getattr(arguments, "mfa", None)
|
||||
|
||||
input_profile = getattr(arguments, "profile", None)
|
||||
input_regions = getattr(arguments, "region", None)
|
||||
organizations_role_arn = getattr(arguments, "organizations_role", None)
|
||||
|
||||
# Set the maximum retries for the standard retrier config
|
||||
aws_retries_max_attempts = getattr(arguments, "aws_retries_max_attempts", None)
|
||||
|
||||
# Set if unused services must be ignored
|
||||
ignore_unused_services = getattr(arguments, "ignore_unused_services", None)
|
||||
|
||||
# Set the maximum retries for the standard retrier config
|
||||
self.session.session_config = self.__set_session_config__(
|
||||
aws_retries_max_attempts
|
||||
)
|
||||
|
||||
# Set ignore unused services
|
||||
self.ignore_unused_services = ignore_unused_services
|
||||
|
||||
# Start populating AWS identity object
|
||||
self.identity.profile = input_profile
|
||||
self.identity.audited_regions = input_regions
|
||||
|
||||
# We need to create an original sessions using regular auth path (creds, profile, etc)
|
||||
logger.info("Generating original session ...")
|
||||
self.session.session = self.setup_session(input_mfa)
|
||||
|
||||
# After the session is created, validate it
|
||||
logger.info("Validating credentials ...")
|
||||
caller_identity = validate_AWSCredentials(
|
||||
self.session.session, input_regions, sts_endpoint_region
|
||||
)
|
||||
|
||||
logger.info("Credentials validated")
|
||||
logger.info(f"Original caller identity UserId: {caller_identity['UserId']}")
|
||||
logger.info(f"Original caller identity ARN: {caller_identity['Arn']}")
|
||||
# Set values of AWS identity object
|
||||
self.identity.account = caller_identity["Account"]
|
||||
self.identity.identity_arn = caller_identity["Arn"]
|
||||
self.identity.user_id = caller_identity["UserId"]
|
||||
self.identity.partition = parse_iam_credentials_arn(
|
||||
caller_identity["Arn"]
|
||||
).partition
|
||||
self.identity.account_arn = (
|
||||
f"arn:{self.identity.partition}:iam::{self.identity.account}:root"
|
||||
)
|
||||
|
||||
# save original session
|
||||
self.session.original_session = self.session.session
|
||||
# time for checking role assumption
|
||||
if input_role:
|
||||
# session will be the assumed one
|
||||
self.session.session = self.setup_assumed_session(
|
||||
input_role,
|
||||
input_external_id,
|
||||
input_mfa,
|
||||
input_session_duration,
|
||||
sts_endpoint_region,
|
||||
)
|
||||
logger.info("Audit session is the new session created assuming role")
|
||||
# check if organizations info is gonna be retrieved
|
||||
if organizations_role_arn:
|
||||
logger.info(
|
||||
f"Getting organizations metadata for account {organizations_role_arn}"
|
||||
)
|
||||
# session will be the assumed one with organizations permissions
|
||||
self.session.session = self.setup_assumed_session(
|
||||
organizations_role_arn,
|
||||
input_external_id,
|
||||
input_mfa,
|
||||
input_session_duration,
|
||||
sts_endpoint_region,
|
||||
)
|
||||
self.organizations_metadata = get_organizations_metadata(
|
||||
self.identity.account, self.assumed_role.assumed_role_credentials
|
||||
)
|
||||
logger.info("Organizations metadata retrieved")
|
||||
if self.session.session.region_name:
|
||||
self.identity.profile_region = self.session.session.region_name
|
||||
else:
|
||||
self.identity.profile_region = "us-east-1"
|
||||
|
||||
if not getattr(arguments, "only_logs", None):
|
||||
self.print_credentials()
|
||||
|
||||
# Parse Scan Tags
|
||||
if getattr(arguments, "resource_tags", None):
|
||||
input_resource_tags = arguments.resource_tags
|
||||
self.audit_resources = self.get_tagged_resources(input_resource_tags)
|
||||
|
||||
# Parse Input Resource ARNs
|
||||
self.audit_resources = getattr(arguments, "resource_arn", None)
|
||||
|
||||
def setup_session(self, input_mfa: bool):
|
||||
logger.info("Creating regular session ...")
|
||||
# Input MFA only if a role is not going to be assumed
|
||||
if input_mfa and not self.assumed_role.assumed_role_info.role_arn:
|
||||
mfa_ARN, mfa_TOTP = self.__input_role_mfa_token_and_code__()
|
||||
get_session_token_arguments = {
|
||||
"SerialNumber": mfa_ARN,
|
||||
"TokenCode": mfa_TOTP,
|
||||
}
|
||||
sts_client = client("sts")
|
||||
session_credentials = sts_client.get_session_token(
|
||||
**get_session_token_arguments
|
||||
)
|
||||
return session.Session(
|
||||
aws_access_key_id=session_credentials["Credentials"]["AccessKeyId"],
|
||||
aws_secret_access_key=session_credentials["Credentials"][
|
||||
"SecretAccessKey"
|
||||
],
|
||||
aws_session_token=session_credentials["Credentials"]["SessionToken"],
|
||||
profile_name=self.identity.profile,
|
||||
)
|
||||
else:
|
||||
return session.Session(
|
||||
profile_name=self.identity.profile,
|
||||
)
|
||||
|
||||
def setup_assumed_session(
|
||||
self,
|
||||
input_role: str,
|
||||
input_external_id: str,
|
||||
input_mfa: str,
|
||||
session_duration: int,
|
||||
sts_endpoint_region: str,
|
||||
):
|
||||
logger.info("Creating assumed session ...")
|
||||
# store information about the role is gonna be assumed
|
||||
self.assumed_role.assumed_role_info.role_arn = input_role
|
||||
self.assumed_role.assumed_role_info.session_duration = session_duration
|
||||
self.assumed_role.assumed_role_info.external_id = input_external_id
|
||||
self.assumed_role.assumed_role_info.mfa_enabled = input_mfa
|
||||
# Check if role arn is valid
|
||||
try:
|
||||
# this returns the arn already parsed into a dict to be used when it is needed to access its fields
|
||||
role_arn_parsed = parse_iam_credentials_arn(
|
||||
self.assumed_role.assumed_role_info.role_arn
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(f"{error.__class__.__name__} -- {error}")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
logger.info(f"Assuming role {self.assumed_role.assumed_role_info.role_arn}")
|
||||
# Assume the role
|
||||
assumed_role_response = self.__assume_role__(
|
||||
self.session.session,
|
||||
sts_endpoint_region,
|
||||
)
|
||||
logger.info("Role assumed")
|
||||
# Set the info needed to create a session with an assumed role
|
||||
self.assumed_role.assumed_role_credentials = AWSCredentials(
|
||||
aws_access_key_id=assumed_role_response["Credentials"]["AccessKeyId"],
|
||||
aws_session_token=assumed_role_response["Credentials"]["SessionToken"],
|
||||
aws_secret_access_key=assumed_role_response["Credentials"][
|
||||
"SecretAccessKey"
|
||||
],
|
||||
expiration=assumed_role_response["Credentials"]["Expiration"],
|
||||
)
|
||||
# Set identity parameters
|
||||
self.identity.account = role_arn_parsed.account_id
|
||||
self.identity.partition = role_arn_parsed.partition
|
||||
self.identity.account_arn = (
|
||||
f"arn:{self.identity.partition}:iam::{self.identity.account}:root"
|
||||
)
|
||||
# From botocore we can use RefreshableCredentials class, which has an attribute (refresh_using)
|
||||
# that needs to be a method without arguments that retrieves a new set of fresh credentials
|
||||
# asuming the role again. -> https://github.com/boto/botocore/blob/098cc255f81a25b852e1ecdeb7adebd94c7b1b73/botocore/credentials.py#L395
|
||||
assumed_refreshable_credentials = RefreshableCredentials(
|
||||
access_key=self.assumed_role.assumed_role_credentials.aws_access_key_id,
|
||||
secret_key=self.assumed_role.assumed_role_credentials.aws_secret_access_key,
|
||||
token=self.assumed_role.assumed_role_credentials.aws_session_token,
|
||||
expiry_time=self.assumed_role.assumed_role_credentials.expiration,
|
||||
refresh_using=self.refresh_credentials,
|
||||
method="sts-assume-role",
|
||||
)
|
||||
# Here we need the botocore session since it needs to use refreshable credentials
|
||||
assumed_botocore_session = get_session()
|
||||
assumed_botocore_session._credentials = assumed_refreshable_credentials
|
||||
assumed_botocore_session.set_config_variable(
|
||||
"region", self.identity.profile_region
|
||||
)
|
||||
return session.Session(
|
||||
profile_name=self.identity.profile,
|
||||
botocore_session=assumed_botocore_session,
|
||||
)
|
||||
|
||||
# Refresh credentials method using assume role
|
||||
# This method is called "adding ()" to the name, so it cannot accept arguments
|
||||
# https://github.com/boto/botocore/blob/098cc255f81a25b852e1ecdeb7adebd94c7b1b73/botocore/credentials.py#L570
|
||||
def refresh_credentials(self):
|
||||
live_display.print_aws_credentials(self.identity, self.assumed_role.assumed_role_info)
|
||||
|
||||
def generate_regional_clients(
|
||||
self, service: str, global_service: bool = False
|
||||
) -> dict:
|
||||
try:
|
||||
regional_clients = {}
|
||||
service_regions = self.get_available_aws_service_regions(service)
|
||||
# Check if it is global service to gather only one region
|
||||
if global_service:
|
||||
if service_regions:
|
||||
if self.identity.profile_region in service_regions:
|
||||
service_regions = [self.identity.profile_region]
|
||||
service_regions = service_regions[:1]
|
||||
for region in service_regions:
|
||||
regional_client = self.session.session.client(
|
||||
service, region_name=region, config=self.session.session_config
|
||||
)
|
||||
regional_client.region = region
|
||||
regional_clients[region] = regional_client
|
||||
return regional_clients
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def get_available_aws_service_regions(self, service: str) -> list:
|
||||
# Get json locally
|
||||
actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
with open_file(f"{actual_directory}/{aws_services_json_file}") as f:
|
||||
data = parse_json_file(f)
|
||||
# Check if it is a subservice
|
||||
json_regions = data["services"][service]["regions"][self.identity.partition]
|
||||
if (
|
||||
self.identity.audited_regions
|
||||
): # Check for input aws audit_info.audited_regions
|
||||
regions = list(
|
||||
set(json_regions).intersection(self.identity.audited_regions)
|
||||
) # Get common regions between input and json
|
||||
else: # Get all regions from json of the service and partition
|
||||
regions = json_regions
|
||||
return regions
|
||||
|
||||
def get_aws_available_regions():
|
||||
try:
|
||||
actual_directory = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
|
||||
with open_file(f"{actual_directory}/{aws_services_json_file}") as f:
|
||||
data = parse_json_file(f)
|
||||
|
||||
regions = set()
|
||||
for service in data["services"].values():
|
||||
for partition in service["regions"]:
|
||||
for item in service["regions"][partition]:
|
||||
regions.add(item)
|
||||
return list(regions)
|
||||
except Exception as error:
|
||||
logger.error(f"{error.__class__.__name__}: {error}")
|
||||
return []
|
||||
|
||||
def get_checks_from_input_arn(audit_resources: list, provider: str) -> set:
|
||||
"""get_checks_from_input_arn gets the list of checks from the input arns"""
|
||||
checks_from_arn = set()
|
||||
is_subservice_in_checks = False
|
||||
# Handle if there are audit resources so only their services are executed
|
||||
if audit_resources:
|
||||
services_without_subservices = ["guardduty", "kms", "s3", "elb", "efs"]
|
||||
service_list = set()
|
||||
sub_service_list = set()
|
||||
for resource in audit_resources:
|
||||
service = resource.split(":")[2]
|
||||
sub_service = resource.split(":")[5].split("/")[0].replace("-", "_")
|
||||
# WAF Services does not have checks
|
||||
if service != "wafv2" and service != "waf":
|
||||
# Parse services when they are different in the ARNs
|
||||
if service == "lambda":
|
||||
service = "awslambda"
|
||||
elif service == "elasticloadbalancing":
|
||||
service = "elb"
|
||||
elif service == "elasticfilesystem":
|
||||
service = "efs"
|
||||
elif service == "logs":
|
||||
service = "cloudwatch"
|
||||
# Check if Prowler has checks in service
|
||||
try:
|
||||
list_modules(provider, service)
|
||||
except ModuleNotFoundError:
|
||||
# Service is not supported
|
||||
pass
|
||||
else:
|
||||
service_list.add(service)
|
||||
|
||||
# Get subservices to execute only applicable checks
|
||||
if service not in services_without_subservices:
|
||||
# Parse some specific subservices
|
||||
if service == "ec2":
|
||||
if sub_service == "security_group":
|
||||
sub_service = "securitygroup"
|
||||
if sub_service == "network_acl":
|
||||
sub_service = "networkacl"
|
||||
if sub_service == "image":
|
||||
sub_service = "ami"
|
||||
if service == "rds":
|
||||
if sub_service == "cluster_snapshot":
|
||||
sub_service = "snapshot"
|
||||
sub_service_list.add(sub_service)
|
||||
else:
|
||||
sub_service_list.add(service)
|
||||
checks = recover_checks_from_service(service_list, provider)
|
||||
|
||||
# Filter only checks with audited subservices
|
||||
for check in checks:
|
||||
if any(sub_service in check for sub_service in sub_service_list):
|
||||
if not (sub_service == "policy" and "password_policy" in check):
|
||||
checks_from_arn.add(check)
|
||||
is_subservice_in_checks = True
|
||||
|
||||
if not is_subservice_in_checks:
|
||||
checks_from_arn = checks
|
||||
|
||||
# Return final checks list
|
||||
return sorted(checks_from_arn)
|
||||
|
||||
def get_regions_from_audit_resources(audit_resources: list) -> set:
|
||||
"""get_regions_from_audit_resources gets the regions from the audit resources arns"""
|
||||
audited_regions = set()
|
||||
for resource in audit_resources:
|
||||
region = resource.split(":")[3]
|
||||
if region:
|
||||
audited_regions.add(region)
|
||||
return audited_regions
|
||||
|
||||
def get_tagged_resources(self, input_resource_tags: list):
|
||||
"""
|
||||
get_tagged_resources returns a list of the resources that are going to be scanned based on the given input tags
|
||||
"""
|
||||
try:
|
||||
resource_tags = []
|
||||
tagged_resources = []
|
||||
for tag in input_resource_tags:
|
||||
key = tag.split("=")[0]
|
||||
value = tag.split("=")[1]
|
||||
resource_tags.append({"Key": key, "Values": [value]})
|
||||
# Get Resources with resource_tags for all regions
|
||||
for regional_client in self.generate_regional_clients(
|
||||
"resourcegroupstaggingapi"
|
||||
).values():
|
||||
try:
|
||||
get_resources_paginator = regional_client.get_paginator(
|
||||
"get_resources"
|
||||
)
|
||||
for page in get_resources_paginator.paginate(
|
||||
TagFilters=resource_tags
|
||||
):
|
||||
for resource in page["ResourceTagMappingList"]:
|
||||
tagged_resources.append(resource["ResourceARN"])
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
return tagged_resources
|
||||
|
||||
def get_default_region(self, service: str) -> str:
|
||||
"""get_default_region gets the default region based on the profile and audited service regions"""
|
||||
service_regions = self.get_available_aws_service_regions(service)
|
||||
default_region = (
|
||||
self.get_global_region()
|
||||
) # global region of the partition when all regions are audited and there is no profile region
|
||||
if self.identity.profile_region in service_regions:
|
||||
# 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 gets the global region based on the audited partition"""
|
||||
global_region = "us-east-1"
|
||||
if self.identity.partition == "aws-cn":
|
||||
global_region = "cn-north-1"
|
||||
elif self.identity.partition == "aws-us-gov":
|
||||
global_region = "us-gov-east-1"
|
||||
elif "aws-iso" in self.identity.partition:
|
||||
global_region = "aws-iso-global"
|
||||
return global_region
|
||||
|
||||
def __input_role_mfa_token_and_code__() -> tuple[str]:
|
||||
"""input_role_mfa_token_and_code ask for the AWS MFA ARN and TOTP and returns it."""
|
||||
mfa_ARN = input("Enter ARN of MFA: ")
|
||||
mfa_TOTP = input("Enter MFA code: ")
|
||||
return (mfa_ARN.strip(), mfa_TOTP.strip())
|
||||
|
||||
def __set_session_config__(self, aws_retries_max_attempts: bool):
|
||||
session_config = Config(
|
||||
retries={"max_attempts": 3, "mode": "standard"},
|
||||
user_agent_extra=BOTO3_USER_AGENT_EXTRA,
|
||||
)
|
||||
if aws_retries_max_attempts:
|
||||
# Create the new config
|
||||
config = Config(
|
||||
retries={
|
||||
"max_attempts": aws_retries_max_attempts,
|
||||
"mode": "standard",
|
||||
},
|
||||
)
|
||||
# Merge the new configuration
|
||||
session_config = self.session.session_config.merge(config)
|
||||
|
||||
return session_config
|
||||
|
||||
def __assume_role__(
|
||||
self,
|
||||
session,
|
||||
sts_endpoint_region: str,
|
||||
) -> dict:
|
||||
try:
|
||||
assume_role_arguments = {
|
||||
"RoleArn": self.assumed_role.assumed_role_info.role_arn,
|
||||
"RoleSessionName": "ProwlerAsessmentSession",
|
||||
"DurationSeconds": self.assumed_role.assumed_role_info.session_duration,
|
||||
}
|
||||
|
||||
# Set the info to assume the role from the partition, account and role name
|
||||
if self.assumed_role.assumed_role_info.external_id:
|
||||
assume_role_arguments[
|
||||
"ExternalId"
|
||||
] = self.assumed_role.assumed_role_info.external_id
|
||||
|
||||
if self.assumed_role.assumed_role_info.mfa_enabled:
|
||||
mfa_ARN, mfa_TOTP = self.__input_role_mfa_token_and_code__()
|
||||
assume_role_arguments["SerialNumber"] = mfa_ARN
|
||||
assume_role_arguments["TokenCode"] = mfa_TOTP
|
||||
|
||||
# Set the STS Endpoint Region
|
||||
if sts_endpoint_region is None:
|
||||
sts_endpoint_region = AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
|
||||
sts_client = create_sts_session(session, sts_endpoint_region)
|
||||
assumed_credentials = sts_client.assume_role(**assume_role_arguments)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
return assumed_credentials
|
||||
@@ -1,3 +1,2 @@
|
||||
AWS_STS_GLOBAL_ENDPOINT_REGION = "us-east-1"
|
||||
BOTO3_USER_AGENT_EXTRA = "APN_1826889"
|
||||
ROLE_SESSION_NAME = "ProwlerAssessmentSession"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from argparse import ArgumentTypeError, Namespace
|
||||
from re import fullmatch, search
|
||||
from re import search
|
||||
|
||||
from prowler.providers.aws.aws_provider import get_aws_available_regions
|
||||
from prowler.providers.aws.config import ROLE_SESSION_NAME
|
||||
from prowler.providers.aws.lib.arn.arn import arn_type
|
||||
|
||||
|
||||
@@ -28,19 +27,6 @@ def init_parser(self):
|
||||
help="ARN of the role to be assumed",
|
||||
# Pending ARN validation
|
||||
)
|
||||
aws_auth_subparser.add_argument(
|
||||
"--role-session-name",
|
||||
nargs="?",
|
||||
default=ROLE_SESSION_NAME,
|
||||
help="An identifier for the assumed role session. Defaults to ProwlerAssessmentSession",
|
||||
type=validate_role_session_name,
|
||||
)
|
||||
aws_auth_subparser.add_argument(
|
||||
"--sts-endpoint-region",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Specify the AWS STS endpoint region to use. Read more at https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html",
|
||||
)
|
||||
aws_auth_subparser.add_argument(
|
||||
"--mfa",
|
||||
action="store_true",
|
||||
@@ -133,14 +119,14 @@ def init_parser(self):
|
||||
default=None,
|
||||
help="Shodan API key used by check ec2_elastic_ip_shodan.",
|
||||
)
|
||||
# Allowlist
|
||||
allowlist_subparser = aws_parser.add_argument_group("Allowlist")
|
||||
allowlist_subparser.add_argument(
|
||||
# Mute List
|
||||
mutelist_subparser = aws_parser.add_argument_group("Mute List")
|
||||
mutelist_subparser.add_argument(
|
||||
"-w",
|
||||
"--allowlist-file",
|
||||
"--mutelist-file",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Path for allowlist yaml file. See example prowler/config/aws_allowlist.yaml for reference and format. It also accepts AWS DynamoDB Table or Lambda ARNs or S3 URIs, see more in https://docs.prowler.cloud/en/latest/tutorials/allowlist/",
|
||||
help="Path for mutelist yaml file. See example prowler/config/aws_mutelist.yaml for reference and format. It also accepts AWS DynamoDB Table or Lambda ARNs or S3 URIs, see more in https://docs.prowler.cloud/en/latest/tutorials/mutelist/",
|
||||
)
|
||||
|
||||
# Based Scans
|
||||
@@ -195,15 +181,10 @@ def validate_arguments(arguments: Namespace) -> tuple[bool, str]:
|
||||
|
||||
# Handle if session_duration is not the default value or external_id is set
|
||||
if (
|
||||
(arguments.session_duration and arguments.session_duration != 3600)
|
||||
or arguments.external_id
|
||||
or arguments.role_session_name != ROLE_SESSION_NAME
|
||||
):
|
||||
arguments.session_duration and arguments.session_duration != 3600
|
||||
) or arguments.external_id:
|
||||
if not arguments.role:
|
||||
return (
|
||||
False,
|
||||
"To use -I/--external-id, -T/--session-duration or --role-session-name options -R/--role option is needed",
|
||||
)
|
||||
return (False, "To use -I/-T options -R option is needed")
|
||||
|
||||
return (True, "")
|
||||
|
||||
@@ -216,16 +197,3 @@ def validate_bucket(bucket_name):
|
||||
raise ArgumentTypeError(
|
||||
"Bucket name must be valid (https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html)"
|
||||
)
|
||||
|
||||
|
||||
def validate_role_session_name(session_name):
|
||||
"""
|
||||
validates that the role session name is valid
|
||||
Documentation: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
"""
|
||||
if fullmatch("[\w+=,.@-]{2,64}", session_name):
|
||||
return session_name
|
||||
else:
|
||||
raise ArgumentTypeError(
|
||||
"Role Session Name must be 2-64 characters long and consist only of upper- and lower-case alphanumeric characters with no spaces. You can also include underscores or any of the following characters: =,.@-"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from boto3 import session
|
||||
from botocore.config import Config
|
||||
|
||||
from prowler.providers.aws.config import BOTO3_USER_AGENT_EXTRA
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Assume_Role, AWS_Audit_Info
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info, AWSAssumeRole
|
||||
|
||||
# Default Current Audit Info
|
||||
current_audit_info = AWS_Audit_Info(
|
||||
@@ -25,12 +25,11 @@ current_audit_info = AWS_Audit_Info(
|
||||
profile=None,
|
||||
profile_region=None,
|
||||
credentials=None,
|
||||
assumed_role_info=AWS_Assume_Role(
|
||||
assumed_role_info=AWSAssumeRole(
|
||||
role_arn=None,
|
||||
session_duration=None,
|
||||
external_id=None,
|
||||
mfa_enabled=None,
|
||||
role_session_name=None,
|
||||
),
|
||||
mfa_enabled=None,
|
||||
audit_resources=None,
|
||||
|
||||
@@ -7,7 +7,7 @@ from botocore.config import Config
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWS_Credentials:
|
||||
class AWSCredentials:
|
||||
aws_access_key_id: str
|
||||
aws_session_token: str
|
||||
aws_secret_access_key: str
|
||||
@@ -15,16 +15,15 @@ class AWS_Credentials:
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWS_Assume_Role:
|
||||
class AWSAssumeRole:
|
||||
role_arn: str
|
||||
session_duration: int
|
||||
external_id: str
|
||||
mfa_enabled: bool
|
||||
role_session_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWS_Organizations_Info:
|
||||
class AWSOrganizationsInfo:
|
||||
account_details_email: str
|
||||
account_details_name: str
|
||||
account_details_arn: str
|
||||
@@ -45,13 +44,13 @@ class AWS_Audit_Info:
|
||||
audited_partition: str
|
||||
profile: str
|
||||
profile_region: str
|
||||
credentials: AWS_Credentials
|
||||
credentials: AWSCredentials
|
||||
mfa_enabled: bool
|
||||
assumed_role_info: AWS_Assume_Role
|
||||
assumed_role_info: AWSAssumeRole
|
||||
audited_regions: list
|
||||
audit_resources: list
|
||||
organizations_metadata: AWS_Organizations_Info
|
||||
audit_metadata: Optional[Any] = None
|
||||
organizations_metadata: AWSOrganizationsInfo
|
||||
audit_metadata: Optional[Any]
|
||||
audit_config: Optional[dict] = None
|
||||
ignore_unused_services: bool = False
|
||||
enabled_regions: set = field(default_factory=set)
|
||||
|
||||
@@ -8,16 +8,12 @@ from prowler.providers.aws.config import AWS_STS_GLOBAL_ENDPOINT_REGION
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info
|
||||
|
||||
|
||||
def validate_aws_credentials(
|
||||
def validate_AWSCredentials(
|
||||
session: session, input_regions: list, sts_endpoint_region: str = None
|
||||
) -> dict:
|
||||
try:
|
||||
# For a valid STS GetCallerIdentity we have to use the right AWS Region
|
||||
# Check if the --sts-endpoint-region is set
|
||||
if sts_endpoint_region is not None:
|
||||
aws_region = sts_endpoint_region
|
||||
# If there is no region passed with -f/--region/--filter-region
|
||||
elif input_regions is None or len(input_regions) == 0:
|
||||
if input_regions is None or len(input_regions) == 0:
|
||||
# If you have a region configured in your AWS config or credentials file
|
||||
if session.region_name is not None:
|
||||
aws_region = session.region_name
|
||||
@@ -42,7 +38,7 @@ def validate_aws_credentials(
|
||||
return caller_identity
|
||||
|
||||
|
||||
def print_aws_credentials(audit_info: AWS_Audit_Info):
|
||||
def print_AWSCredentials(audit_info: AWS_Audit_Info):
|
||||
# Beautify audited regions, set "all" if there is no filter region
|
||||
regions = (
|
||||
", ".join(audit_info.audited_regions)
|
||||
|
||||
@@ -9,7 +9,7 @@ from schema import Optional, Schema
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.models import unroll_tags
|
||||
|
||||
allowlist_schema = Schema(
|
||||
mutelist_schema = Schema(
|
||||
{
|
||||
"Accounts": {
|
||||
str: {
|
||||
@@ -32,38 +32,38 @@ allowlist_schema = Schema(
|
||||
)
|
||||
|
||||
|
||||
def parse_allowlist_file(audit_info, allowlist_file):
|
||||
def parse_mutelist_file(audit_info, mutelist_file):
|
||||
try:
|
||||
# Check if file is a S3 URI
|
||||
if re.search("^s3://([^/]+)/(.*?([^/]+))$", allowlist_file):
|
||||
bucket = allowlist_file.split("/")[2]
|
||||
key = ("/").join(allowlist_file.split("/")[3:])
|
||||
if re.search("^s3://([^/]+)/(.*?([^/]+))$", mutelist_file):
|
||||
bucket = mutelist_file.split("/")[2]
|
||||
key = ("/").join(mutelist_file.split("/")[3:])
|
||||
s3_client = audit_info.audit_session.client("s3")
|
||||
allowlist = yaml.safe_load(
|
||||
mutelist = yaml.safe_load(
|
||||
s3_client.get_object(Bucket=bucket, Key=key)["Body"]
|
||||
)["Allowlist"]
|
||||
)["Mute List"]
|
||||
# Check if file is a Lambda Function ARN
|
||||
elif re.search(r"^arn:(\w+):lambda:", allowlist_file):
|
||||
lambda_region = allowlist_file.split(":")[3]
|
||||
elif re.search(r"^arn:(\w+):lambda:", mutelist_file):
|
||||
lambda_region = mutelist_file.split(":")[3]
|
||||
lambda_client = audit_info.audit_session.client(
|
||||
"lambda", region_name=lambda_region
|
||||
)
|
||||
lambda_response = lambda_client.invoke(
|
||||
FunctionName=allowlist_file, InvocationType="RequestResponse"
|
||||
FunctionName=mutelist_file, InvocationType="RequestResponse"
|
||||
)
|
||||
lambda_payload = lambda_response["Payload"].read()
|
||||
allowlist = yaml.safe_load(lambda_payload)["Allowlist"]
|
||||
mutelist = yaml.safe_load(lambda_payload)["Mute List"]
|
||||
# Check if file is a DynamoDB ARN
|
||||
elif re.search(
|
||||
r"^arn:aws(-cn|-us-gov)?:dynamodb:[a-z]{2}-[a-z-]+-[1-9]{1}:[0-9]{12}:table\/[a-zA-Z0-9._-]+$",
|
||||
allowlist_file,
|
||||
mutelist_file,
|
||||
):
|
||||
allowlist = {"Accounts": {}}
|
||||
table_region = allowlist_file.split(":")[3]
|
||||
mutelist = {"Accounts": {}}
|
||||
table_region = mutelist_file.split(":")[3]
|
||||
dynamodb_resource = audit_info.audit_session.resource(
|
||||
"dynamodb", region_name=table_region
|
||||
)
|
||||
dynamo_table = dynamodb_resource.Table(allowlist_file.split("/")[1])
|
||||
dynamo_table = dynamodb_resource.Table(mutelist_file.split("/")[1])
|
||||
response = dynamo_table.scan(
|
||||
FilterExpression=Attr("Accounts").is_in(
|
||||
[audit_info.audited_account, "*"]
|
||||
@@ -80,8 +80,8 @@ def parse_allowlist_file(audit_info, allowlist_file):
|
||||
)
|
||||
dynamodb_items.update(response["Items"])
|
||||
for item in dynamodb_items:
|
||||
# Create allowlist for every item
|
||||
allowlist["Accounts"][item["Accounts"]] = {
|
||||
# Create mutelist for every item
|
||||
mutelist["Accounts"][item["Accounts"]] = {
|
||||
"Checks": {
|
||||
item["Checks"]: {
|
||||
"Regions": item["Regions"],
|
||||
@@ -90,24 +90,24 @@ def parse_allowlist_file(audit_info, allowlist_file):
|
||||
}
|
||||
}
|
||||
if "Tags" in item:
|
||||
allowlist["Accounts"][item["Accounts"]]["Checks"][item["Checks"]][
|
||||
mutelist["Accounts"][item["Accounts"]]["Checks"][item["Checks"]][
|
||||
"Tags"
|
||||
] = item["Tags"]
|
||||
if "Exceptions" in item:
|
||||
allowlist["Accounts"][item["Accounts"]]["Checks"][item["Checks"]][
|
||||
mutelist["Accounts"][item["Accounts"]]["Checks"][item["Checks"]][
|
||||
"Exceptions"
|
||||
] = item["Exceptions"]
|
||||
else:
|
||||
with open(allowlist_file) as f:
|
||||
allowlist = yaml.safe_load(f)["Allowlist"]
|
||||
with open(mutelist_file) as f:
|
||||
mutelist = yaml.safe_load(f)["Mute List"]
|
||||
try:
|
||||
allowlist_schema.validate(allowlist)
|
||||
mutelist_schema.validate(mutelist)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__} -- Allowlist YAML is malformed - {error}[{error.__traceback__.tb_lineno}]"
|
||||
f"{error.__class__.__name__} -- Mute List YAML is malformed - {error}[{error.__traceback__.tb_lineno}]"
|
||||
)
|
||||
sys.exit(1)
|
||||
return allowlist
|
||||
return mutelist
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
@@ -115,27 +115,27 @@ def parse_allowlist_file(audit_info, allowlist_file):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def allowlist_findings(
|
||||
allowlist: dict,
|
||||
def mutelist_findings(
|
||||
mutelist: dict,
|
||||
audited_account: str,
|
||||
check_findings: [Any],
|
||||
):
|
||||
# Check if finding is allowlisted
|
||||
# Check if finding is muted
|
||||
for finding in check_findings:
|
||||
if is_allowlisted(
|
||||
allowlist,
|
||||
if is_muted(
|
||||
mutelist,
|
||||
audited_account,
|
||||
finding.check_metadata.CheckID,
|
||||
finding.region,
|
||||
finding.resource_id,
|
||||
unroll_tags(finding.resource_tags),
|
||||
):
|
||||
finding.status = "WARNING"
|
||||
finding.status = "MUTED"
|
||||
return check_findings
|
||||
|
||||
|
||||
def is_allowlisted(
|
||||
allowlist: dict,
|
||||
def is_muted(
|
||||
mutelist: dict,
|
||||
audited_account: str,
|
||||
check: str,
|
||||
finding_region: str,
|
||||
@@ -143,25 +143,30 @@ def is_allowlisted(
|
||||
finding_tags,
|
||||
):
|
||||
try:
|
||||
# By default is not allowlisted
|
||||
is_finding_allowlisted = False
|
||||
muted_checks = {}
|
||||
# By default is not muted
|
||||
is_finding_muted = False
|
||||
# First set account key from mutelist dict
|
||||
if audited_account in mutelist["Accounts"]:
|
||||
muted_checks = mutelist["Accounts"][audited_account]["Checks"]
|
||||
# If there is a *, it affects to all accounts
|
||||
# This cannot be elif since in the case of * and single accounts we
|
||||
# want to merge muted checks from * to the other accounts check list
|
||||
if "*" in mutelist["Accounts"]:
|
||||
checks_multi_account = mutelist["Accounts"]["*"]["Checks"]
|
||||
muted_checks.update(checks_multi_account)
|
||||
# Test if it is muted
|
||||
if is_muted_in_check(
|
||||
muted_checks,
|
||||
audited_account,
|
||||
check,
|
||||
finding_region,
|
||||
finding_resource,
|
||||
finding_tags,
|
||||
):
|
||||
is_finding_muted = True
|
||||
|
||||
# We always check all the accounts present in the allowlist
|
||||
# if one allowlists the finding we set the finding as allowlisted
|
||||
for account in allowlist["Accounts"]:
|
||||
if account == audited_account or account == "*":
|
||||
if is_allowlisted_in_check(
|
||||
allowlist["Accounts"][account]["Checks"],
|
||||
audited_account,
|
||||
check,
|
||||
finding_region,
|
||||
finding_resource,
|
||||
finding_tags,
|
||||
):
|
||||
is_finding_allowlisted = True
|
||||
break
|
||||
|
||||
return is_finding_allowlisted
|
||||
return is_finding_muted
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
@@ -169,8 +174,8 @@ def is_allowlisted(
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def is_allowlisted_in_check(
|
||||
allowlisted_checks,
|
||||
def is_muted_in_check(
|
||||
muted_checks,
|
||||
audited_account,
|
||||
check,
|
||||
finding_region,
|
||||
@@ -178,15 +183,15 @@ def is_allowlisted_in_check(
|
||||
finding_tags,
|
||||
):
|
||||
try:
|
||||
# Default value is not allowlisted
|
||||
is_check_allowlisted = False
|
||||
# Default value is not muted
|
||||
is_check_muted = False
|
||||
|
||||
for allowlisted_check, allowlisted_check_info in allowlisted_checks.items():
|
||||
for muted_check, muted_check_info in muted_checks.items():
|
||||
# map lambda to awslambda
|
||||
allowlisted_check = re.sub("^lambda", "awslambda", allowlisted_check)
|
||||
muted_check = re.sub("^lambda", "awslambda", muted_check)
|
||||
|
||||
# Check if the finding is excepted
|
||||
exceptions = allowlisted_check_info.get("Exceptions")
|
||||
exceptions = muted_check_info.get("Exceptions")
|
||||
if is_excepted(
|
||||
exceptions,
|
||||
audited_account,
|
||||
@@ -197,46 +202,36 @@ def is_allowlisted_in_check(
|
||||
# Break loop and return default value since is excepted
|
||||
break
|
||||
|
||||
allowlisted_regions = allowlisted_check_info.get("Regions")
|
||||
allowlisted_resources = allowlisted_check_info.get("Resources")
|
||||
allowlisted_tags = allowlisted_check_info.get("Tags", "*")
|
||||
# We need to set the allowlisted_tags if None, "" or [], so the falsy helps
|
||||
if not allowlisted_tags:
|
||||
allowlisted_tags = "*"
|
||||
|
||||
muted_regions = muted_check_info.get("Regions")
|
||||
muted_resources = muted_check_info.get("Resources")
|
||||
muted_tags = muted_check_info.get("Tags")
|
||||
# If there is a *, it affects to all checks
|
||||
if (
|
||||
"*" == allowlisted_check
|
||||
or check == allowlisted_check
|
||||
or re.search(allowlisted_check, check)
|
||||
"*" == muted_check
|
||||
or check == muted_check
|
||||
or re.search(muted_check, check)
|
||||
):
|
||||
allowlisted_in_check = True
|
||||
allowlisted_in_region = is_allowlisted_in_region(
|
||||
allowlisted_regions, finding_region
|
||||
)
|
||||
allowlisted_in_resource = is_allowlisted_in_resource(
|
||||
allowlisted_resources, finding_resource
|
||||
)
|
||||
allowlisted_in_tags = is_allowlisted_in_tags(
|
||||
allowlisted_tags, finding_tags
|
||||
muted_in_check = True
|
||||
muted_in_region = is_muted_in_region(muted_regions, finding_region)
|
||||
muted_in_resource = is_muted_in_resource(
|
||||
muted_resources, finding_resource
|
||||
)
|
||||
muted_in_tags = is_muted_in_tags(muted_tags, finding_tags)
|
||||
|
||||
# For a finding to be allowlisted requires the following set to True:
|
||||
# - allowlisted_in_check -> True
|
||||
# - allowlisted_in_region -> True
|
||||
# - allowlisted_in_tags -> True
|
||||
# - allowlisted_in_resource -> True
|
||||
# For a finding to be muted requires the following set to True:
|
||||
# - muted_in_check -> True
|
||||
# - muted_in_region -> True
|
||||
# - muted_in_tags -> True or muted_in_resource -> True
|
||||
# - excepted -> False
|
||||
|
||||
if (
|
||||
allowlisted_in_check
|
||||
and allowlisted_in_region
|
||||
and allowlisted_in_tags
|
||||
and allowlisted_in_resource
|
||||
muted_in_check
|
||||
and muted_in_region
|
||||
and (muted_in_tags or muted_in_resource)
|
||||
):
|
||||
is_check_allowlisted = True
|
||||
is_check_muted = True
|
||||
|
||||
return is_check_allowlisted
|
||||
return is_check_muted
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
@@ -244,12 +239,12 @@ def is_allowlisted_in_check(
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def is_allowlisted_in_region(
|
||||
allowlisted_regions,
|
||||
def is_muted_in_region(
|
||||
mutelist_regions,
|
||||
finding_region,
|
||||
):
|
||||
try:
|
||||
return __is_item_matched__(allowlisted_regions, finding_region)
|
||||
return __is_item_matched__(mutelist_regions, finding_region)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
@@ -257,9 +252,9 @@ def is_allowlisted_in_region(
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def is_allowlisted_in_tags(allowlisted_tags, finding_tags):
|
||||
def is_muted_in_tags(muted_tags, finding_tags):
|
||||
try:
|
||||
return __is_item_matched__(allowlisted_tags, finding_tags)
|
||||
return __is_item_matched__(muted_tags, finding_tags)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
@@ -267,9 +262,9 @@ def is_allowlisted_in_tags(allowlisted_tags, finding_tags):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def is_allowlisted_in_resource(allowlisted_resources, finding_resource):
|
||||
def is_muted_in_resource(muted_resources, finding_resource):
|
||||
try:
|
||||
return __is_item_matched__(allowlisted_resources, finding_resource)
|
||||
return __is_item_matched__(muted_resources, finding_resource)
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
@@ -310,17 +305,10 @@ def is_excepted(
|
||||
is_tag_excepted = __is_item_matched__(excepted_tags, finding_tags)
|
||||
|
||||
if (
|
||||
not is_account_excepted
|
||||
and not is_region_excepted
|
||||
and not is_resource_excepted
|
||||
and not is_tag_excepted
|
||||
):
|
||||
excepted = False
|
||||
elif (
|
||||
(is_account_excepted or not excepted_accounts)
|
||||
and (is_region_excepted or not excepted_regions)
|
||||
and (is_resource_excepted or not excepted_resources)
|
||||
and (is_tag_excepted or not excepted_tags)
|
||||
is_account_excepted
|
||||
and is_region_excepted
|
||||
and is_resource_excepted
|
||||
and is_tag_excepted
|
||||
):
|
||||
excepted = True
|
||||
return excepted
|
||||
@@ -3,12 +3,12 @@ import sys
|
||||
from boto3 import client
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Organizations_Info
|
||||
from prowler.providers.aws.lib.audit_info.models import AWSOrganizationsInfo
|
||||
|
||||
|
||||
def get_organizations_metadata(
|
||||
metadata_account: str, assumed_credentials: dict
|
||||
) -> AWS_Organizations_Info:
|
||||
) -> AWSOrganizationsInfo:
|
||||
try:
|
||||
organizations_client = client(
|
||||
"organizations",
|
||||
@@ -30,7 +30,7 @@ def get_organizations_metadata(
|
||||
account_details_tags = ""
|
||||
for tag in list_tags_for_resource["Tags"]:
|
||||
account_details_tags += tag["Key"] + ":" + tag["Value"] + ","
|
||||
organizations_info = AWS_Organizations_Info(
|
||||
organizations_info = AWSOrganizationsInfo(
|
||||
account_details_email=organizations_metadata["Account"]["Email"],
|
||||
account_details_name=organizations_metadata["Account"]["Name"],
|
||||
account_details_arn=organizations_metadata["Account"]["Arn"],
|
||||
|
||||
@@ -4,7 +4,7 @@ def is_condition_block_restrictive(
|
||||
"""
|
||||
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.
|
||||
|
||||
If argument is_cross_account_allowed is True it tests if the Condition block includes any of the operators allowlisted returning True if does, False if not.
|
||||
If argument is_cross_account_allowed is True it tests if the Condition block includes any of the operators mutelisted returning True if does, False if not.
|
||||
|
||||
|
||||
@param condition_statement: dict with an IAM Condition block, e.g.:
|
||||
@@ -71,6 +71,9 @@ def is_condition_block_restrictive(
|
||||
if is_condition_key_restrictive:
|
||||
is_condition_valid = True
|
||||
|
||||
if is_condition_key_restrictive:
|
||||
is_condition_valid = True
|
||||
|
||||
# value is a string
|
||||
elif isinstance(
|
||||
condition_statement[condition_operator][value],
|
||||
|
||||
@@ -211,13 +211,9 @@ def create_inventory_table(resources: list, resources_in_region: dict) -> dict:
|
||||
|
||||
def create_output(resources: list, audit_info: AWS_Audit_Info, args):
|
||||
json_output = []
|
||||
# Check if custom output filename was input, if not, set the default
|
||||
if not hasattr(args, "output_filename") or args.output_filename is None:
|
||||
output_file = (
|
||||
f"prowler-inventory-{audit_info.audited_account}-{output_file_timestamp}"
|
||||
)
|
||||
else:
|
||||
output_file = args.output_filename
|
||||
output_file = (
|
||||
f"prowler-inventory-{audit_info.audited_account}-{output_file_timestamp}"
|
||||
)
|
||||
|
||||
for item in sorted(resources, key=lambda d: d["arn"]):
|
||||
resource = {}
|
||||
@@ -279,8 +275,8 @@ def create_output(resources: list, audit_info: AWS_Audit_Info, args):
|
||||
f"\n{Fore.YELLOW}WARNING: Only resources that have or have had tags will appear (except for IAM and S3).\nSee more in https://docs.prowler.cloud/en/latest/tutorials/quick-inventory/#objections{Style.RESET_ALL}"
|
||||
)
|
||||
print("\nMore details in files:")
|
||||
print(f" - CSV: {args.output_directory}/{output_file + csv_file_suffix}")
|
||||
print(f" - JSON: {args.output_directory}/{output_file + json_file_suffix}")
|
||||
print(f" - CSV: {args.output_directory}/{output_file+csv_file_suffix}")
|
||||
print(f" - JSON: {args.output_directory}/{output_file+json_file_suffix}")
|
||||
|
||||
# Send output to S3 if needed (-B / -D)
|
||||
for mode in ["json", "csv"]:
|
||||
|
||||
@@ -27,7 +27,7 @@ def send_to_s3_bucket(
|
||||
else: # Compliance output mode
|
||||
filename = f"{output_filename}_{output_mode}{csv_file_suffix}"
|
||||
|
||||
logger.info(f"Sending output file {filename} to S3 bucket {output_bucket_name}")
|
||||
logger.info(f"Sending outputs to S3 bucket {output_bucket_name}")
|
||||
# File location
|
||||
file_name = output_directory + "/" + filename
|
||||
|
||||
|
||||
@@ -20,18 +20,16 @@ def prepare_security_hub_findings(
|
||||
security_hub_findings_per_region[region] = []
|
||||
|
||||
for finding in findings:
|
||||
# We don't send the INFO findings to AWS Security Hub
|
||||
if finding.status == "INFO":
|
||||
# We don't send the MANUAL findings to AWS Security Hub
|
||||
if finding.status == "MANUAL":
|
||||
continue
|
||||
|
||||
# We don't send findings to not enabled regions
|
||||
if finding.region not in enabled_regions:
|
||||
continue
|
||||
|
||||
# Handle quiet mode
|
||||
if (
|
||||
output_options.is_quiet or output_options.send_sh_only_fails
|
||||
) and finding.status != "FAIL":
|
||||
# Handle status filters, if any
|
||||
if not output_options.status or finding.status in output_options.status:
|
||||
continue
|
||||
|
||||
# Get the finding region
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from functools import wraps
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.ui.live_display import live_display
|
||||
from prowler.providers.aws.aws_provider import (
|
||||
generate_regional_clients,
|
||||
get_default_region,
|
||||
)
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info
|
||||
from prowler.providers.aws.aws_provider_new import AwsProvider
|
||||
|
||||
MAX_WORKERS = 10
|
||||
|
||||
@@ -19,18 +22,18 @@ class AWSService:
|
||||
- Also handles if the AWS Service is Global
|
||||
"""
|
||||
|
||||
def __init__(self, service: str, audit_info: AWS_Audit_Info, global_service=False):
|
||||
def __init__(self, service: str, provider: AwsProvider, global_service=False):
|
||||
# Audit Information
|
||||
self.audit_info = audit_info
|
||||
self.audited_account = audit_info.audited_account
|
||||
self.audited_account_arn = audit_info.audited_account_arn
|
||||
self.audited_partition = audit_info.audited_partition
|
||||
self.audit_resources = audit_info.audit_resources
|
||||
self.audited_checks = audit_info.audit_metadata.expected_checks
|
||||
self.audit_config = audit_info.audit_config
|
||||
self.provider = provider
|
||||
self.audited_account = provider.identity.account
|
||||
self.audited_account_arn = provider.identity.account_arn
|
||||
self.audited_partition = provider.identity.partition
|
||||
self.audit_resources = provider.audit_resources
|
||||
self.audited_checks = provider.audit_metadata.expected_checks
|
||||
self.audit_config = provider.audit_config
|
||||
|
||||
# AWS Session
|
||||
self.session = audit_info.audit_session
|
||||
self.session = provider.session.session
|
||||
|
||||
# We receive the service using __class__.__name__ or the service name in lowercase
|
||||
# e.g.: AccessAnalyzer --> we need a lowercase string, so service.lower()
|
||||
@@ -38,21 +41,33 @@ class AWSService:
|
||||
|
||||
# Generate Regional Clients
|
||||
if not global_service:
|
||||
self.regional_clients = generate_regional_clients(self.service, audit_info)
|
||||
self.regional_clients = provider.generate_regional_clients(
|
||||
self.service, global_service
|
||||
)
|
||||
|
||||
# Get a single region and client if the service needs it (e.g. AWS Global Service)
|
||||
# We cannot include this within an else because some services needs both the regional_clients
|
||||
# and a single client like S3
|
||||
self.region = get_default_region(self.service, audit_info)
|
||||
self.region = provider.get_default_region(self.service)
|
||||
self.client = self.session.client(self.service, self.region)
|
||||
|
||||
# Thread pool for __threading_call__
|
||||
self.thread_pool = ThreadPoolExecutor(max_workers=MAX_WORKERS)
|
||||
|
||||
self.live_display_enabled = False
|
||||
# Progress bar to add tasks to
|
||||
service_init_section = live_display.get_client_init_section()
|
||||
if service_init_section:
|
||||
# Only Flags is not set to True
|
||||
self.task_progress_bar = service_init_section.task_progress_bar
|
||||
self.progress_tasks = []
|
||||
# For us in other functions
|
||||
self.live_display_enabled = True
|
||||
|
||||
def __get_session__(self):
|
||||
return self.session
|
||||
|
||||
def __threading_call__(self, call, iterator=None):
|
||||
def __threading_call__(self, call, iterator=None, *args, **kwargs):
|
||||
# Use the provided iterator, or default to self.regional_clients
|
||||
items = iterator if iterator is not None else self.regional_clients.values()
|
||||
# Determine the total count for logging
|
||||
@@ -73,13 +88,58 @@ class AWSService:
|
||||
f"{self.service.upper()} - Starting threads for '{call_name}' function to process {item_count} items..."
|
||||
)
|
||||
|
||||
if self.live_display_enabled:
|
||||
# Setup the progress bar
|
||||
task_id = self.task_progress_bar.add_task(
|
||||
f"- {call_name}...", total=item_count, task_type="Service"
|
||||
)
|
||||
self.progress_tasks.append(task_id)
|
||||
|
||||
# Submit tasks to the thread pool
|
||||
futures = [self.thread_pool.submit(call, item) for item in items]
|
||||
futures = [
|
||||
self.thread_pool.submit(call, item, *args, **kwargs) for item in items
|
||||
]
|
||||
|
||||
# Wait for all tasks to complete
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
future.result() # Raises exceptions from the thread, if any
|
||||
if self.live_display_enabled:
|
||||
# Update the progress bar
|
||||
self.task_progress_bar.update(task_id, advance=1)
|
||||
except Exception:
|
||||
# Handle exceptions if necessary
|
||||
pass # Replace 'pass' with any additional exception handling logic. Currently handled within the called function
|
||||
|
||||
# Make the task disappear once completed
|
||||
# self.progress.remove_task(task_id)
|
||||
|
||||
@staticmethod
|
||||
def progress_decorator(func):
|
||||
"""
|
||||
Decorator to update the progress bar before and after a function call.
|
||||
To be used for methods within global services, which do not make use of the __threading_call__ function
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# Trim leading and trailing underscores from the call's name
|
||||
func_name = func.__name__.strip("_")
|
||||
# Add Capitalization
|
||||
func_name = " ".join([x.capitalize() for x in func_name.split("_")])
|
||||
|
||||
if self.live_display_enabled:
|
||||
task_id = self.task_progress_bar.add_task(
|
||||
f"- {func_name}...", total=1, task_type="Service"
|
||||
)
|
||||
self.progress_tasks.append(task_id)
|
||||
|
||||
result = func(self, *args, **kwargs) # Execute the function
|
||||
|
||||
if self.live_display_enabled:
|
||||
self.task_progress_bar.update(task_id, advance=1)
|
||||
# self.task_progress_bar.remove_task(task_id) # Uncomment if you want to remove the task on completion
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from boto3 import session
|
||||
from botocore.config import Config
|
||||
|
||||
@dataclass
|
||||
class AWSOrganizationsInfo:
|
||||
account_details_email: str
|
||||
account_details_name: str
|
||||
account_details_arn: str
|
||||
account_details_org: str
|
||||
account_details_tags: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWSCredentials:
|
||||
aws_access_key_id: str
|
||||
aws_session_token: str
|
||||
aws_secret_access_key: str
|
||||
expiration: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWSAssumeRole:
|
||||
role_arn: str
|
||||
session_duration: int
|
||||
external_id: str
|
||||
mfa_enabled: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWSAssumeRoleConfiguration:
|
||||
assumed_role_info: AWSAssumeRole
|
||||
assumed_role_credentials: AWSCredentials
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWSIdentityInfo:
|
||||
account: str
|
||||
account_arn: str
|
||||
user_id: str
|
||||
partition: str
|
||||
identity_arn: str
|
||||
profile: str
|
||||
profile_region: str
|
||||
audited_regions: list
|
||||
|
||||
|
||||
@dataclass
|
||||
class AWSSession:
|
||||
session: session.Session
|
||||
session_config: Config
|
||||
original_session: None
|
||||
@@ -1,6 +1,6 @@
|
||||
from prowler.providers.aws.lib.audit_info.audit_info import current_audit_info
|
||||
from prowler.providers.aws.services.accessanalyzer.accessanalyzer_service import (
|
||||
AccessAnalyzer,
|
||||
)
|
||||
from prowler.providers.common.common import get_global_provider
|
||||
|
||||
accessanalyzer_client = AccessAnalyzer(current_audit_info)
|
||||
accessanalyzer_client = AccessAnalyzer(get_global_provider())
|
||||
|
||||
@@ -31,11 +31,11 @@ class accessanalyzer_enabled(Check):
|
||||
)
|
||||
if (
|
||||
accessanalyzer_client.audit_config.get(
|
||||
"allowlist_non_default_regions", False
|
||||
"mute_non_default_regions", False
|
||||
)
|
||||
and not analyzer.region == accessanalyzer_client.region
|
||||
):
|
||||
report.status = "WARNING"
|
||||
report.status = "MUTED"
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ from prowler.providers.aws.lib.service.service import AWSService
|
||||
|
||||
################## AccessAnalyzer
|
||||
class AccessAnalyzer(AWSService):
|
||||
def __init__(self, audit_info):
|
||||
def __init__(self, provider):
|
||||
# Call AWSService's __init__
|
||||
super().__init__(__class__.__name__, audit_info)
|
||||
super().__init__(__class__.__name__, provider)
|
||||
self.analyzers = []
|
||||
self.__threading_call__(self.__list_analyzers__)
|
||||
self.__list_findings__()
|
||||
|
||||