Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c8ea5e3f | ||
|
|
b36152484d | ||
|
|
768ca3f0ce | ||
|
|
bedd05c075 | ||
|
|
721f73fdbe | ||
|
|
34c2128d88 | ||
|
|
14de3acdaa | ||
|
|
899b2f8eb6 | ||
|
|
27bb05fedc | ||
|
|
e1909b8ad9 | ||
|
|
0ed7a247b6 | ||
|
|
ee46bf3809 | ||
|
|
469254094b | ||
|
|
acac3fc693 | ||
|
|
022b7ef756 | ||
|
|
69d4f55734 | ||
|
|
a0bff4b859 | ||
|
|
23df599a03 | ||
|
|
c8d74ca350 | ||
|
|
8d6ba43ad0 | ||
|
|
44ca2f7a66 | ||
|
|
ec0be1c7fe | ||
|
|
fd732db91b | ||
|
|
67f45b7767 | ||
|
|
396e6a1c36 | ||
|
|
326c46defd | ||
|
|
7a1762be51 | ||
|
|
b466b476a3 | ||
|
|
e4652d4339 | ||
|
|
f1e4cd3938 | ||
|
|
e192a98079 | ||
|
|
833dc83922 | ||
|
|
ab1751c595 | ||
|
|
fff06f971e | ||
|
|
a138d2964e | ||
|
|
e6d7965453 | ||
|
|
ab714f0fc7 | ||
|
|
465b0f6a16 | ||
|
|
bd87351ea7 | ||
|
|
d79ec44e4c | ||
|
|
a2f84a12ea | ||
|
|
6fd71356ee | ||
|
|
a0a305d9b1 | ||
|
|
6396d90fa6 | ||
|
|
e324750ec2 | ||
|
|
5d99f020fa | ||
|
|
b82e928f58 | ||
|
|
da871897e6 | ||
|
|
81778f73e4 | ||
|
|
2623728518 | ||
|
|
97f1d1b476 | ||
|
|
2f6a837bc0 | ||
|
|
5e22c2d9a5 | ||
|
|
99bd637de4 | ||
|
|
b9177e5580 | ||
|
|
fc7ec184d9 | ||
|
|
7a6ca342af | ||
|
|
30b6e5e5c6 | ||
|
|
f8476decf7 | ||
|
|
49e238577c | ||
|
|
026fff79c6 | ||
|
|
36c3870c2f | ||
|
|
54c309dbda | ||
|
|
f00dd35f93 | ||
|
|
e040efb3c8 | ||
|
|
805d50586b | ||
|
|
a289a807c5 | ||
|
|
e9117f95ee | ||
|
|
82bd4e940f | ||
|
|
ad3b0b33f2 | ||
|
|
b2b664a5b0 | ||
|
|
571f3ebe1d | ||
|
|
c7f09df4e7 | ||
|
|
8758ecae97 | ||
|
|
f13c843ba6 | ||
|
|
e95f7dd540 | ||
|
|
693329b87e | ||
|
|
f1ad521f64 | ||
|
|
82fbba6513 | ||
|
|
66fba8e4cd | ||
|
|
417131fa36 | ||
|
|
9c9d270053 | ||
|
|
f7fab165ba | ||
|
|
93bdf43c95 | ||
|
|
b3866b5b71 | ||
|
|
2308084dee | ||
|
|
6eb5496c27 |
No files matched your search
@@ -1 +1 @@
|
||||
* @prowler-cloud/prowler-oss
|
||||
* @prowler-cloud/prowler-oss @prowler-cloud/prowler-dev
|
||||
@@ -9,6 +9,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
@@ -17,12 +18,14 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
- "dependencies"
|
||||
@@ -32,8 +35,8 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
- "github_actions"
|
||||
- "v3"
|
||||
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
prowler_version_major: ${{ steps.get-prowler-version.outputs.PROWLER_VERSION_MAJOR }}
|
||||
prowler_version: ${{ steps.update-prowler-version.outputs.PROWLER_VERSION }}
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
|
||||
@@ -89,12 +90,14 @@ jobs:
|
||||
esac
|
||||
|
||||
- name: Update Prowler version (release)
|
||||
id: update-prowler-version
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
PROWLER_VERSION="${{ github.event.release.tag_name }}"
|
||||
poetry version "${PROWLER_VERSION}"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_ENV}"
|
||||
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -153,7 +156,7 @@ jobs:
|
||||
echo "LATEST_COMMIT_HASH=${LATEST_COMMIT_HASH}" >> $GITHUB_ENV
|
||||
|
||||
- name: Dispatch event (latest)
|
||||
if: github.event_name == 'push' && ${{ needs.container-build-push.outputs.prowler_version_major == '3' }}
|
||||
if: github.event_name == 'push' && needs.container-build-push.outputs.prowler_version_major == '3'
|
||||
run: |
|
||||
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
@@ -162,10 +165,10 @@ jobs:
|
||||
--data '{"event_type":"dispatch","client_payload":{"version":"v3-latest", "tag": "${{ env.LATEST_COMMIT_HASH }}"}}'
|
||||
|
||||
- name: Dispatch event (release)
|
||||
if: github.event_name == 'release' && ${{ needs.container-build-push.outputs.prowler_version_major == '3' }}
|
||||
if: github.event_name == 'release' && needs.container-build-push.outputs.prowler_version_major == '3'
|
||||
run: |
|
||||
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--data '{"event_type":"dispatch","client_payload":{"version":"release", "tag":"${{ env.PROWLER_VERSION }}"}}'
|
||||
--data '{"event_type":"dispatch","client_payload":{"version":"release", "tag":"${{ needs.container-build-push.outputs.prowler_version }}"}}'
|
||||
@@ -11,8 +11,9 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.72.0
|
||||
uses: trufflesecurity/trufflehog@v3.73.0
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
head: HEAD
|
||||
extra_args: --only-verified
|
||||
@@ -10,4 +10,4 @@
|
||||
Want some swag as appreciation for your contribution?
|
||||
|
||||
# Prowler Developer Guide
|
||||
https://docs.prowler.cloud/en/latest/tutorials/developer-guide/
|
||||
https://docs.prowler.com/projects/prowler-open-source/en/latest/developer-guide/introduction/
|
||||
@@ -41,7 +41,21 @@
|
||||
|
||||
# Description
|
||||
|
||||
`Prowler` is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness.
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler SaaS</a>.
|
||||
|
||||
## Prowler CLI
|
||||
|
||||
```console
|
||||
prowler <provider>
|
||||
```
|
||||

|
||||
|
||||
## Prowler Dashboard
|
||||
|
||||
```console
|
||||
prowler dashboard
|
||||
```
|
||||

|
||||
|
||||
It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks.
|
||||
|
||||
@@ -52,14 +66,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|
||||
| Azure | 127 | 16 -> `prowler azure --list-services` | 2 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
|
||||
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
|
||||
|
||||
# 📖 Documentation
|
||||
|
||||
The full documentation can now be found at [https://docs.prowler.com](https://docs.prowler.com/projects/prowler-open-source/en/latest/)
|
||||
|
||||
## Looking for Prowler v2 documentation?
|
||||
For Prowler v2 Documentation, please go to https://github.com/prowler-cloud/prowler/tree/2.12.1.
|
||||
|
||||
# ⚙️ Install
|
||||
# 💻 Installation
|
||||
|
||||
## 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:
|
||||
@@ -74,9 +81,11 @@ More details at [https://docs.prowler.com](https://docs.prowler.com/projects/pro
|
||||
|
||||
The available versions of Prowler are the following:
|
||||
|
||||
- `latest`: in sync with master branch (bear in mind that it is not a stable version)
|
||||
- `latest`: in sync with `master` branch (bear in mind that it is not a stable version)
|
||||
- `v3-latest`: in sync with `v3` branch (bear in mind that it is not a stable version)
|
||||
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
|
||||
- `stable`: this tag always point to the latest release.
|
||||
- `v3-stable`: this tag always point to the latest release for v3.
|
||||
|
||||
The container images are available here:
|
||||
|
||||
@@ -97,200 +106,30 @@ python prowler.py -v
|
||||
|
||||
# 📐✏️ High level architecture
|
||||
|
||||
You can run Prowler from your workstation, an EC2 instance, Fargate or any other container, Codebuild, CloudShell and Cloud9.
|
||||
You can run Prowler from your workstation, a Kubernetes Job, a Google Compute Engine, an Azure VM, an EC2 instance, Fargate or any other container, CloudShell and many more.
|
||||
|
||||

|
||||

|
||||
|
||||
# 📝 Requirements
|
||||
# Deprecations from v3
|
||||
|
||||
Prowler has been written in Python using the [AWS SDK (Boto3)](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html#), [Azure SDK](https://azure.github.io/azure-sdk-for-python/) and [GCP API Python Client](https://github.com/googleapis/google-api-python-client/).
|
||||
## AWS
|
||||
## General
|
||||
- `Allowlist` now is called `Mutelist`.
|
||||
- The `--quiet` option has been deprecated, now use the `--status` flag to select the finding's status you want to get from PASS, FAIL or MANUAL.
|
||||
- All `INFO` finding's status has changed to `MANUAL`.
|
||||
- The CSV output format is common for all the providers.
|
||||
|
||||
Since Prowler uses AWS Credentials under the hood, you can follow any authentication method as described [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-precedence).
|
||||
Make sure you have properly configured your AWS-CLI with a valid Access Key and Region or declare AWS variables properly (or instance profile/role):
|
||||
|
||||
```console
|
||||
aws configure
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```console
|
||||
export AWS_ACCESS_KEY_ID="ASXXXXXXX"
|
||||
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
|
||||
export AWS_SESSION_TOKEN="XXXXXXXXX"
|
||||
```
|
||||
|
||||
Those credentials must be associated to a user or role with proper permissions to do all checks. To make sure, add the following AWS managed policies to the user or role being used:
|
||||
|
||||
- `arn:aws:iam::aws:policy/SecurityAudit`
|
||||
- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess`
|
||||
|
||||
> Moreover, some read-only additional permissions are needed for several checks, make sure you attach also the custom policy [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json) to the role you are using.
|
||||
|
||||
> If you want Prowler to send findings to [AWS Security Hub](https://aws.amazon.com/security-hub), make sure you also attach the custom policy [prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json).
|
||||
|
||||
## Azure
|
||||
|
||||
Prowler for Azure supports the following authentication types:
|
||||
|
||||
- Service principal authentication by environment variables (Enterprise Application)
|
||||
- Current az cli credentials stored
|
||||
- Interactive browser authentication
|
||||
- Managed identity authentication
|
||||
|
||||
### Service Principal authentication
|
||||
|
||||
To allow Prowler assume the service principal identity to start the scan, it is needed to configure the following environment variables:
|
||||
|
||||
```console
|
||||
export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXX"
|
||||
```
|
||||
|
||||
If you try to execute Prowler with the `--sp-env-auth` flag and those variables are empty or not exported, the execution is going to fail.
|
||||
### AZ CLI / Browser / Managed Identity authentication
|
||||
|
||||
The other three cases do not need additional configuration, `--az-cli-auth` and `--managed-identity-auth` are automated options, `--browser-auth` needs the user to authenticate using the default browser to start the scan. Also `--browser-auth` needs the tenant id to be specified with `--tenant-id`.
|
||||
|
||||
### Permissions
|
||||
|
||||
To use each one, you need to pass the proper flag to the execution. Prowler for Azure handles two types of permission scopes, which are:
|
||||
|
||||
- **Azure Active Directory permissions**: Used to retrieve metadata from the identity assumed by Prowler and future AAD checks (not mandatory to have access to execute the tool)
|
||||
- **Subscription scope permissions**: Required to launch the checks against your resources, mandatory to launch the tool.
|
||||
|
||||
|
||||
#### Azure Active Directory scope
|
||||
|
||||
Azure Active Directory (AAD) permissions required by the tool are the following:
|
||||
|
||||
- `Directory.Read.All`
|
||||
- `Policy.Read.All`
|
||||
|
||||
|
||||
#### Subscriptions scope
|
||||
|
||||
Regarding the subscription scope, Prowler by default scans all the subscriptions that is able to list, so it is required to add the following RBAC builtin roles per subscription to the entity that is going to be assumed by the tool:
|
||||
|
||||
- `Security Reader`
|
||||
- `Reader`
|
||||
|
||||
|
||||
## Google Cloud Platform
|
||||
|
||||
Prowler will follow the same credentials search as [Google authentication libraries](https://cloud.google.com/docs/authentication/application-default-credentials#search_order):
|
||||
|
||||
1. [GOOGLE_APPLICATION_CREDENTIALS environment variable](https://cloud.google.com/docs/authentication/application-default-credentials#GAC)
|
||||
2. [User credentials set up by using the Google Cloud CLI](https://cloud.google.com/docs/authentication/application-default-credentials#personal)
|
||||
3. [The attached service account, returned by the metadata server](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa)
|
||||
|
||||
Those credentials must be associated to a user or service account with proper permissions to do all checks. To make sure, add the `Viewer` role to the member associated with the credentials.
|
||||
|
||||
> By default, `prowler` will scan all accessible GCP Projects, use flag `--project-ids` to specify the projects to be scanned.
|
||||
|
||||
# 💻 Basic Usage
|
||||
|
||||
To run prowler, you will need to specify the provider (e.g aws or azure):
|
||||
|
||||
```console
|
||||
prowler <provider>
|
||||
```
|
||||
|
||||

|
||||
|
||||
> Running the `prowler` command without options will use your environment variable credentials.
|
||||
|
||||
By default, prowler will generate a CSV, a JSON and a HTML report, however you can generate JSON-ASFF (only for AWS Security Hub) report with `-M` or `--output-modes`:
|
||||
|
||||
```console
|
||||
prowler <provider> -M csv json json-asff html
|
||||
```
|
||||
|
||||
The html report will be located in the `output` directory as the other files and it will look like:
|
||||
|
||||

|
||||
|
||||
You can use `-l`/`--list-checks` or `--list-services` to list all available checks or services within the provider.
|
||||
|
||||
```console
|
||||
prowler <provider> --list-checks
|
||||
prowler <provider> --list-services
|
||||
```
|
||||
|
||||
For executing specific checks or services you can use options `-c`/`--checks` or `-s`/`--services`:
|
||||
|
||||
```console
|
||||
prowler aws --checks s3_bucket_public_access
|
||||
prowler aws --services s3 ec2
|
||||
```
|
||||
|
||||
Also, checks and services can be excluded with options `-e`/`--excluded-checks` or `--excluded-services`:
|
||||
|
||||
```console
|
||||
prowler aws --excluded-checks s3_bucket_public_access
|
||||
prowler aws --excluded-services s3 ec2
|
||||
```
|
||||
|
||||
You can always use `-h`/`--help` to access to the usage information and all the possible options:
|
||||
|
||||
```console
|
||||
prowler -h
|
||||
```
|
||||
|
||||
## Checks Configurations
|
||||
Several Prowler's checks have user configurable variables that can be modified in a common **configuration file**.
|
||||
This file can be found in the following path:
|
||||
```
|
||||
prowler/config/config.yaml
|
||||
```
|
||||
We have deprecated some of our outputs formats:
|
||||
- The HTML is replaced for the new Prowler Dashboard, run `prowler dashboard`.
|
||||
- The native JSON is replaced for the JSON [OCSF](https://schema.ocsf.io/) v1.1.0, common for all the providers.
|
||||
|
||||
## AWS
|
||||
- Deprecate the AWS flag --sts-endpoint-region since we use AWS STS regional tokens.
|
||||
- To send only FAILS to AWS Security Hub, now use either `--send-sh-only-fails` or `--security-hub --status FAIL`.
|
||||
|
||||
Use a custom AWS profile with `-p`/`--profile` and/or AWS regions which you want to audit with `-f`/`--filter-region`:
|
||||
|
||||
```console
|
||||
prowler aws --profile custom-profile -f us-east-1 eu-south-2
|
||||
```
|
||||
> By default, `prowler` will scan all AWS regions.
|
||||
# 📖 Documentation
|
||||
|
||||
## Azure
|
||||
|
||||
With Azure you need to specify which auth method is going to be used:
|
||||
|
||||
```console
|
||||
prowler azure [--sp-env-auth, --az-cli-auth, --browser-auth, --managed-identity-auth]
|
||||
```
|
||||
> By default, `prowler` will scan all Azure subscriptions.
|
||||
|
||||
## Google Cloud Platform
|
||||
|
||||
Optionally, you can provide the location of an application credential JSON file with the following argument:
|
||||
|
||||
```console
|
||||
prowler gcp --credentials-file path
|
||||
```
|
||||
> By default, `prowler` will scan all accessible GCP Projects, use flag `--project-ids` to specify the projects to be scanned.
|
||||
|
||||
## Kubernetes
|
||||
|
||||
For non in-cluster execution, you can provide the location of the KubeConfig file with the following argument:
|
||||
|
||||
```console
|
||||
prowler kubernetes --kubeconfig-file path
|
||||
```
|
||||
|
||||
For in-cluster execution, you can use the supplied yaml to run Prowler as a job:
|
||||
```console
|
||||
kubectl apply -f job.yaml
|
||||
kubectl apply -f prowler-role.yaml
|
||||
kubectl apply -f prowler-rolebinding.yaml
|
||||
kubectl get pods --> prowler-XXXXX
|
||||
kubectl logs prowler-XXXXX
|
||||
```
|
||||
|
||||
> By default, `prowler` will scan all namespaces in your active Kubernetes context, use flag `--context` to specify the context to be scanned and `--namespaces` to specify the namespaces to be scanned.
|
||||
Install, Usage, Tutorials and Developer Guide is at https://docs.prowler.com/
|
||||
|
||||
# 📃 License
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ sudo bash
|
||||
adduser prowler
|
||||
su prowler
|
||||
pip install prowler
|
||||
cd /tmp || exit
|
||||
cd /tmp
|
||||
prowler aws
|
||||
@@ -0,0 +1,2 @@
|
||||
DASHBOARD_PORT = 11666
|
||||
DASHBOARD_ARGS = {"debug": True, "port": DASHBOARD_PORT, "use_reloader": False}
|
||||
@@ -553,105 +553,6 @@ video {
|
||||
grid-template-columns: minmax(0, 16fr) repeat(11, minmax(0, 11fr));
|
||||
}
|
||||
|
||||
/* Styles for the table in the overview page */
|
||||
|
||||
.table-overview thead {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.table-overview tbody {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
.table-overview tbody tr {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Styles for thead */
|
||||
|
||||
.table-overview th {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(41 37 36 / var(--tw-bg-opacity));
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.table-overview td {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(41 37 36 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
/* Check ID */
|
||||
|
||||
.table-overview td:nth-child(1),
|
||||
.table-overview th:nth-child(1) {
|
||||
width: 52%;
|
||||
}
|
||||
|
||||
/* Severity */
|
||||
|
||||
.table-overview td:nth-child(2),
|
||||
.table-overview th:nth-child(2) {
|
||||
width: 8%;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* Status */
|
||||
|
||||
.table-overview td:nth-child(3),
|
||||
.table-overview th:nth-child(3) {
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
.table-overview td:nth-child(3) {
|
||||
font-weight: 700;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(185 28 28 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
/* Region */
|
||||
|
||||
.table-overview td:nth-child(4),
|
||||
.table-overview th:nth-child(4) {
|
||||
width: 9%;
|
||||
}
|
||||
|
||||
/* Service */
|
||||
|
||||
.table-overview td:nth-child(5),
|
||||
.table-overview th:nth-child(5) {
|
||||
width: 6%;
|
||||
}
|
||||
|
||||
/* Provider */
|
||||
|
||||
.table-overview td:nth-child(6),
|
||||
.table-overview th:nth-child(6) {
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
/* Account ID */
|
||||
|
||||
.table-overview td:nth-child(7),
|
||||
.table-overview th:nth-child(7) {
|
||||
width: 11%;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -1091,6 +992,19 @@ video {
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
/*Styles for previous-vext-container from table*/
|
||||
.previous-next-container {
|
||||
margin-top: 1rem;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/*Style for input in filter table*/
|
||||
.dash-table-container .dash-spreadsheet-container .dash-spreadsheet-inner input:not([type=radio]):not([type=checkbox]) {
|
||||
color: #FFF !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
|
||||
#_dash-app-content {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(231 229 228 / var(--tw-bg-opacity));
|
||||
|
||||
@@ -27,3 +27,6 @@ informational_color = "#3274d9"
|
||||
# Folder output path
|
||||
folder_path_overview = os.getcwd() + "/output"
|
||||
folder_path_compliance = os.getcwd() + "/output/compliance"
|
||||
|
||||
# Encoding
|
||||
encoding_format = "utf-8"
|
||||
@@ -0,0 +1,5 @@
|
||||
def init_dashboard_parser(self):
|
||||
"""Init the Dashboard CLI parser"""
|
||||
# If we don't set `help="Dashboard"` this won't be rendered
|
||||
# We don't want the dashboard to inherit from the common providers parser since it's a different component
|
||||
self.subparsers.add_parser("dashboard")
|
||||
@@ -5,7 +5,8 @@ def create_layout_overview(
|
||||
account_dropdown: html.Div,
|
||||
date_dropdown: html.Div,
|
||||
region_dropdown: html.Div,
|
||||
download_button: html.Button,
|
||||
download_button_csv: html.Button,
|
||||
download_button_xlsx: html.Button,
|
||||
severity_dropdown: html.Div,
|
||||
service_dropdown: html.Div,
|
||||
table_row_dropdown: html.Div,
|
||||
@@ -95,7 +96,8 @@ def create_layout_overview(
|
||||
)
|
||||
),
|
||||
table_row_dropdown,
|
||||
download_button,
|
||||
download_button_csv,
|
||||
download_button_xlsx,
|
||||
],
|
||||
className="flex justify-between items-center",
|
||||
),
|
||||
|
||||
@@ -15,6 +15,7 @@ from dash.dependencies import Input, Output
|
||||
|
||||
# Config import
|
||||
from dashboard.config import (
|
||||
encoding_format,
|
||||
fail_color,
|
||||
folder_path_compliance,
|
||||
info_color,
|
||||
@@ -37,7 +38,7 @@ warnings.filterwarnings("ignore")
|
||||
|
||||
csv_files = []
|
||||
for file in glob.glob(os.path.join(folder_path_compliance, "*.csv")):
|
||||
with open(file, "r", newline="") as csvfile:
|
||||
with open(file, "r", newline="", encoding=encoding_format) as csvfile:
|
||||
reader = csv.reader(csvfile)
|
||||
num_rows = sum(1 for row in reader)
|
||||
if num_rows > 1:
|
||||
|
||||
@@ -8,16 +8,16 @@ from itertools import product
|
||||
|
||||
# Third-party imports
|
||||
import dash
|
||||
import dash_bootstrap_components as dbc
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from dash import callback, ctx, dcc, html
|
||||
from dash import callback, ctx, dash_table, dcc, html
|
||||
from dash.dependencies import Input, Output
|
||||
|
||||
# Config import
|
||||
from dashboard.config import (
|
||||
critical_color,
|
||||
encoding_format,
|
||||
fail_color,
|
||||
folder_path_overview,
|
||||
high_color,
|
||||
@@ -51,7 +51,7 @@ warnings.filterwarnings("ignore")
|
||||
csv_files = []
|
||||
|
||||
for file in glob.glob(os.path.join(folder_path_overview, "*.csv")):
|
||||
with open(file, "r", newline="") as csvfile:
|
||||
with open(file, "r", newline="", encoding=encoding_format) as csvfile:
|
||||
reader = csv.reader(csvfile)
|
||||
num_rows = sum(1 for row in reader)
|
||||
if num_rows > 1:
|
||||
@@ -267,9 +267,15 @@ else:
|
||||
service_dropdown = create_service_dropdown(services)
|
||||
|
||||
# Create the download button
|
||||
download_button = html.Button(
|
||||
download_button_csv = html.Button(
|
||||
"Download this table as CSV",
|
||||
id="download_link",
|
||||
id="download_link_csv",
|
||||
n_clicks=0,
|
||||
className="border-solid border-2 border-prowler-stone-900/10 hover:border-solid hover:border-2 hover:border-prowler-stone-900/10 text-prowler-stone-900 inline-block px-4 py-2 text-xs font-bold uppercase transition-all rounded-lg text-gray-900 hover:bg-prowler-stone-900/10 flex justify-end w-fit",
|
||||
)
|
||||
download_button_xlsx = html.Button(
|
||||
"Download this table as XLSX",
|
||||
id="download_link_xlsx",
|
||||
n_clicks=0,
|
||||
className="border-solid border-2 border-prowler-stone-900/10 hover:border-solid hover:border-2 hover:border-prowler-stone-900/10 text-prowler-stone-900 inline-block px-4 py-2 text-xs font-bold uppercase transition-all rounded-lg text-gray-900 hover:bg-prowler-stone-900/10 flex justify-end w-fit",
|
||||
)
|
||||
@@ -292,7 +298,8 @@ else:
|
||||
account_dropdown,
|
||||
date_dropdown,
|
||||
region_dropdown,
|
||||
download_button,
|
||||
download_button_csv,
|
||||
download_button_xlsx,
|
||||
severity_dropdown,
|
||||
service_dropdown,
|
||||
table_row_dropdown,
|
||||
@@ -335,7 +342,8 @@ else:
|
||||
Input("cloud-account-filter", "value"),
|
||||
Input("region-filter", "value"),
|
||||
Input("report-date-filter", "value"),
|
||||
Input("download_link", "n_clicks"),
|
||||
Input("download_link_csv", "n_clicks"),
|
||||
Input("download_link_xlsx", "n_clicks"),
|
||||
Input("severity-filter", "value"),
|
||||
Input("service-filter", "value"),
|
||||
Input("table-rows", "value"),
|
||||
@@ -349,7 +357,8 @@ def filter_data(
|
||||
cloud_account_values,
|
||||
region_account_values,
|
||||
assessment_value,
|
||||
n_clicks,
|
||||
n_clicks_csv,
|
||||
n_clicks_xlsx,
|
||||
severity_values,
|
||||
service_values,
|
||||
table_row_values,
|
||||
@@ -360,7 +369,8 @@ def filter_data(
|
||||
k8s_clicks,
|
||||
):
|
||||
# Use n_clicks for vulture
|
||||
n_clicks = n_clicks
|
||||
n_clicks_csv = n_clicks_csv
|
||||
n_clicks_xlsx = n_clicks_xlsx
|
||||
# Filter the data
|
||||
filtered_data = data.copy()
|
||||
|
||||
@@ -905,13 +915,13 @@ def filter_data(
|
||||
|
||||
# Take the values from the table_row_values
|
||||
if table_row_values == -1:
|
||||
if len(table_data) >= 25:
|
||||
table_row_values = 25
|
||||
if len(table_data) < 25:
|
||||
table_row_values = len(table_data)
|
||||
else:
|
||||
table_row_values = "Full"
|
||||
table_row_values = 25
|
||||
|
||||
if len(table_data) < 25:
|
||||
table_row_values = "Full"
|
||||
table_row_values = len(table_data)
|
||||
|
||||
if len(table_data) >= 25:
|
||||
table_row_options.append(25)
|
||||
@@ -921,25 +931,151 @@ def filter_data(
|
||||
table_row_options.append(75)
|
||||
if len(table_data) >= 100:
|
||||
table_row_options.append(100)
|
||||
table_row_options.append("Full")
|
||||
table_row_options.append(len(table_data))
|
||||
|
||||
if table_row_values == "Full":
|
||||
table = dbc.Table.from_dataframe(
|
||||
table_data,
|
||||
striped=True,
|
||||
bordered=False,
|
||||
hover=True,
|
||||
className="table-overview",
|
||||
)
|
||||
else:
|
||||
table_data = table_data[:table_row_values]
|
||||
table = dbc.Table.from_dataframe(
|
||||
table_data,
|
||||
striped=True,
|
||||
bordered=False,
|
||||
hover=True,
|
||||
className="table-overview",
|
||||
)
|
||||
table_data["Severity"] = table_data["Severity"].str.capitalize()
|
||||
|
||||
table = dash_table.DataTable(
|
||||
data=table_data.to_dict("records"),
|
||||
style_data={
|
||||
"whiteSpace": "normal",
|
||||
"height": "auto",
|
||||
"color": "black",
|
||||
"fontFamily": "sans-serif",
|
||||
},
|
||||
columns=[
|
||||
{
|
||||
"name": "Check ID - Resource UID",
|
||||
"id": "Check ID",
|
||||
"deletable": False,
|
||||
},
|
||||
{
|
||||
"name": "Severity",
|
||||
"id": "Severity",
|
||||
"deletable": False,
|
||||
},
|
||||
{"name": "Status", "id": "Status", "deletable": False},
|
||||
{"name": "Region", "id": "Region", "deletable": False},
|
||||
{"name": "Service", "id": "Service", "deletable": False},
|
||||
{"name": "Provider", "id": "Provider", "deletable": False},
|
||||
{"name": "Account ID", "id": "Account ID", "deletable": False},
|
||||
],
|
||||
style_table={"table-layout": "fixed"},
|
||||
style_cell={"textAlign": "left", "layout": "fixed"},
|
||||
style_header={
|
||||
"fontWeight": "bold",
|
||||
"layout": "fixed",
|
||||
"backgroundColor": "rgb(41,37,36)",
|
||||
"fontFamily": "sans-serif",
|
||||
},
|
||||
page_size=table_row_values,
|
||||
style_data_conditional=[
|
||||
{
|
||||
"if": {"row_index": "odd"},
|
||||
"backgroundColor": "rgb(200, 200, 200)",
|
||||
"width": "100%",
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"filter_query": '{Status} = "FAIL"', # matching rows of a hidden column with the id, `id`
|
||||
"column_id": "Status",
|
||||
},
|
||||
"backgroundColor": fail_color,
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"filter_query": '{Status} = "PASS"', # matching rows of a hidden column with the id, `id`
|
||||
"column_id": "Status",
|
||||
},
|
||||
"backgroundColor": pass_color,
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"filter_query": '{Status} = "MANUAL"', # matching rows of a hidden column with the id, `id`
|
||||
"column_id": "Status",
|
||||
},
|
||||
"backgroundColor": manual_color,
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"filter_query": '{Status} = "INFO"', # matching rows of a hidden column with the id, `id`
|
||||
"column_id": "Status",
|
||||
},
|
||||
"backgroundColor": info_color,
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"filter_query": '{Status} = "MUTED (FAIL)"', # matching rows of a hidden column with the id, `id`
|
||||
"column_id": "Status",
|
||||
},
|
||||
"backgroundColor": muted_fail_color,
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"filter_query": '{Status} = "MUTED (PASS)"', # matching rows of a hidden column with the id, `id`
|
||||
"column_id": "Status",
|
||||
},
|
||||
"backgroundColor": muted_pass_color,
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"filter_query": '{Status} = "MUTED (MANUAL)"', # matching rows of a hidden column with the id, `id`
|
||||
"column_id": "Status",
|
||||
},
|
||||
"backgroundColor": muted_manual_color,
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"column_id": "Severity",
|
||||
},
|
||||
"text-transform": "capitalize",
|
||||
},
|
||||
],
|
||||
style_cell_conditional=[
|
||||
{"if": {"column_id": "Check ID + Resource UID"}, "max-width": "58%"},
|
||||
{
|
||||
"if": {"column_id": "Severity"},
|
||||
"max-width": "8%",
|
||||
"text-align": "center",
|
||||
},
|
||||
{
|
||||
"if": {"column_id": "Status"},
|
||||
"max-width": "7%",
|
||||
"text-align": "center",
|
||||
},
|
||||
{
|
||||
"if": {"column_id": "Region"},
|
||||
"max-width": "9%",
|
||||
"text-align": "center",
|
||||
},
|
||||
{
|
||||
"if": {"column_id": "Service"},
|
||||
"max-width": "6%",
|
||||
"text-align": "center",
|
||||
},
|
||||
{
|
||||
"if": {"column_id": "Provider"},
|
||||
"max-width": "7%",
|
||||
"text-align": "center",
|
||||
},
|
||||
{
|
||||
"if": {"column_id": "Account ID"},
|
||||
"max-width": "11%",
|
||||
"text-align": "center",
|
||||
},
|
||||
],
|
||||
id="table-overview",
|
||||
sort_action="native",
|
||||
sort_mode="single",
|
||||
style_as_list_view=True,
|
||||
filter_action="native",
|
||||
filter_options={"placeholder_text": "🔍"},
|
||||
style_filter={
|
||||
"background-color": "#3e403f",
|
||||
"color": "white",
|
||||
"fontFamily": "sans-serif",
|
||||
},
|
||||
)
|
||||
|
||||
# Status Graphic
|
||||
status_graph = [
|
||||
@@ -980,7 +1116,7 @@ def filter_data(
|
||||
|
||||
# Table
|
||||
table_card = [
|
||||
html.Div([table], className="grid grid-cols-auto"),
|
||||
html.Div([table], className="grid grid-cols-auto w-full"),
|
||||
]
|
||||
|
||||
# Create Provider Cards
|
||||
@@ -1007,10 +1143,22 @@ def filter_data(
|
||||
),
|
||||
)
|
||||
]
|
||||
if ctx.triggered_id == "download_link":
|
||||
csv_data = dcc.send_data_frame(
|
||||
table_data.to_csv, "prowler-dashboard-export.csv"
|
||||
)
|
||||
if (
|
||||
ctx.triggered_id == "download_link_csv"
|
||||
or ctx.triggered_id == "download_link_xlsx"
|
||||
):
|
||||
# Cut the data to the wanted rows
|
||||
table_data = table_data.head(table_row_values)
|
||||
if ctx.triggered_id == "download_link_csv":
|
||||
csv_data = dcc.send_data_frame(
|
||||
table_data.to_csv, "prowler-dashboard-export.csv", index=False
|
||||
)
|
||||
if ctx.triggered_id == "download_link_xlsx":
|
||||
csv_data = dcc.send_data_frame(
|
||||
table_data.to_excel,
|
||||
"prowler-dashboard-export.xlsx",
|
||||
index=False,
|
||||
)
|
||||
return (
|
||||
status_graph,
|
||||
two_pie_chart,
|
||||
|
||||
@@ -20,73 +20,6 @@
|
||||
.custom-grid-large {
|
||||
grid-template-columns: minmax(0, 10fr) repeat(11, minmax(0, 11fr));
|
||||
}
|
||||
|
||||
/* Styles for the table in the overview page */
|
||||
.table-overview thead {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.table-overview tbody {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.table-overview tbody tr {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
/* Styles for thead */
|
||||
.table-overview th {
|
||||
@apply bg-prowler-stone-900 text-sm py-3 font-bold;
|
||||
}
|
||||
|
||||
.table-overview td {
|
||||
@apply text-prowler-stone-900 bg-prowler-white text-sm py-2 font-bold;
|
||||
}
|
||||
|
||||
/* Check ID */
|
||||
.table-overview td:nth-child(1),
|
||||
.table-overview th:nth-child(1) {
|
||||
@apply w-[52%];
|
||||
}
|
||||
/* Severity */
|
||||
.table-overview td:nth-child(2),
|
||||
.table-overview th:nth-child(2) {
|
||||
@apply w-[8%] capitalize;
|
||||
}
|
||||
/* Status */
|
||||
.table-overview td:nth-child(3),
|
||||
.table-overview th:nth-child(3) {
|
||||
@apply w-[7%];
|
||||
}
|
||||
.table-overview td:nth-child(3) {
|
||||
@apply font-bold text-prowler-error;
|
||||
}
|
||||
/* Region */
|
||||
.table-overview td:nth-child(4),
|
||||
.table-overview th:nth-child(4) {
|
||||
@apply w-[9%];
|
||||
}
|
||||
/* Service */
|
||||
.table-overview td:nth-child(5),
|
||||
.table-overview th:nth-child(5) {
|
||||
@apply w-[6%];
|
||||
}
|
||||
/* Provider */
|
||||
.table-overview td:nth-child(6),
|
||||
.table-overview th:nth-child(6) {
|
||||
@apply w-[7%];
|
||||
}
|
||||
/* Account ID */
|
||||
.table-overview td:nth-child(7),
|
||||
.table-overview th:nth-child(7) {
|
||||
@apply w-[11%];
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles for the accordion in the compliance page */
|
||||
#_dash-app-content .accordion .accordion-header .accordion-button {
|
||||
@apply text-prowler-stone-900 inline-block px-4 text-xs font-bold uppercase transition-all rounded-lg bg-prowler-stone-300 hover:bg-prowler-stone-900/10;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Audit Info
|
||||
|
||||
In each Prowler provider we have a Python object called `audit_info` which is in charge of keeping the credentials, the configuration and the state of each audit, and it's passed to each service during the `__init__`.
|
||||
|
||||
- AWS: https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/lib/audit_info/models.py#L34-L54
|
||||
- GCP: https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/lib/audit_info/models.py#L7-L30
|
||||
- Azure: https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/lib/audit_info/models.py#L17-L31
|
||||
|
||||
This `audit_info` object is shared during the Prowler execution and for that reason is important to mock it in each test to isolate them. See the [testing guide](./unit-testing.md) for more information.
|
||||
@@ -5,9 +5,15 @@ Here you can find how to create new checks for Prowler.
|
||||
**To create a check is required to have a Prowler provider service already created, so if the service is not present or the attribute you want to audit is not retrieved by the service, please refer to the [Service](./services.md) documentation.**
|
||||
|
||||
## Introduction
|
||||
|
||||
The checks are the fundamental piece of Prowler. A check is a simply piece of code that ensures if something is configured against cybersecurity best practices. Then the check generates a finding with the result and includes the check's metadata to give the user more contextual information about the result, the risk and how to remediate it.
|
||||
|
||||
To create a new check for a supported Prowler provider, you will need to create a folder with the check name inside the specific service for the selected provider.
|
||||
|
||||
We are going to use the `ec2_ami_public` check form the `AWS` provider as an example. So the folder name will `prowler/providers/aws/services/ec2/ec2_ami_public` (following the format `prowler/providers/<provider>/services/<service>/<check_name>`), with the name of check following the pattern: `service_subservice/resource_action`.
|
||||
We are going to use the `ec2_ami_public` check from the `AWS` provider as an example. So the folder name will be `prowler/providers/aws/services/ec2/ec2_ami_public` (following the format `prowler/providers/<provider>/services/<service>/<check_name>`), with the name of check following the pattern: `service_subservice_resource_action`.
|
||||
|
||||
???+ note
|
||||
A subservice is an specific component of a service that is gonna be audited. Sometimes it could be the shortened name of the class attribute that is gonna be accessed in the check.
|
||||
|
||||
Inside that folder, we need to create three files:
|
||||
|
||||
@@ -101,7 +107,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.
|
||||
- `FAIL` --> If the check is failing against the configured value.
|
||||
- `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 `.`
|
||||
@@ -111,7 +117,7 @@ All the checks MUST fill the `report.status` and `report.status_extended` with t
|
||||
|
||||
All the checks MUST fill the `report.region` with the following criteria:
|
||||
|
||||
- If the audited resource is regional use the `region` attribute within the resource object.
|
||||
- If the audited resource is regional use the `region` (the name changes depending on the provider: `location` in Azure and GCP and `namespace` in K8s) attribute within the resource object.
|
||||
- If the audited resource is global use the `service_client.region` within the service client object.
|
||||
|
||||
### Resource ID, Name and ARN
|
||||
@@ -140,7 +146,7 @@ All the checks MUST fill the `report.resource_id` and `report.resource_arn` with
|
||||
### Python Model
|
||||
The following is the Python model for the check's class.
|
||||
|
||||
As per August 5th 2023 the `Check_Metadata_Model` can be found [here](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py#L59-L80).
|
||||
As per April 11th 2024 the `Check_Metadata_Model` can be found [here](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py#L36-L82).
|
||||
|
||||
```python
|
||||
class Check(ABC, Check_Metadata_Model):
|
||||
@@ -243,11 +249,11 @@ Each Prowler check has metadata associated which is stored at the same level of
|
||||
# Code holds different methods to remediate the FAIL finding
|
||||
"Code": {
|
||||
# CLI holds the command in the provider native CLI to remediate it
|
||||
"CLI": "https://docs.bridgecrew.io/docs/public_8#cli-command",
|
||||
"CLI": "https://docs.prowler.com/checks/public_8#cli-command",
|
||||
# NativeIaC holds the native IaC code to remediate it, use "https://docs.bridgecrew.io/docs"
|
||||
"NativeIaC": "",
|
||||
# Other holds the other commands, scripts or code to remediate it, use "https://www.trendmicro.com/cloudoneconformity"
|
||||
"Other": "https://docs.bridgecrew.io/docs/public_8#aws-console",
|
||||
"Other": "https://docs.prowler.com/checks/public_8#aws-console",
|
||||
# Terraform holds the Terraform code to remediate it, use "https://docs.bridgecrew.io/docs"
|
||||
"Terraform": ""
|
||||
},
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
|
||||
# Create a new Provider for Prowler
|
||||
|
||||
Here you can find how to create a new Provider in Prowler to give support for making all security checks needed and make your cloud safer!
|
||||
|
||||
## Introduction
|
||||
|
||||
Providers are the foundation on which Prowler is built, a simple definition for a cloud provider could be "third-party company that offers a platform where any IT resource you need is available at any time upon request". The most well-known cloud providers are Amazon Web Services, Azure from Microsoft and Google Cloud which are already supported by Prowler.
|
||||
|
||||
To create a new provider that is not supported now by Prowler and add your security checks you must create a new folder to store all the related files within it (services, checks, etc.). It must be store in route `prowler/providers/<new_provider_name>/`.
|
||||
|
||||
Inside that folder, you MUST create the following files and folders:
|
||||
|
||||
- A `lib` folder: to store all extra functions.
|
||||
- A `services` folder: to store all [services](./services.md) to audit.
|
||||
- An empty `__init__.py`: to make Python treat this service folder as a package.
|
||||
- A `<new_provider_name>_provider.py`, containing all the provider's logic necessary to get authenticated in the provider, configurations and extra data useful for final report.
|
||||
- A `models.py`, containing all the models necessary for the new provider.
|
||||
|
||||
## Provider
|
||||
|
||||
The structure for Prowler's providers is set up in such a way that they can be utilized through a generic service specific to each provider. This is achieved by passing the required parameters to the constructor, which in turn initializes all the necessary session values.
|
||||
|
||||
### Base Class
|
||||
|
||||
All the providers in Prowler inherits from the same [base class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/common/provider.py). It is an [abstract base class](https://docs.python.org/3/library/abc.html) that defines the interface for all provider classes. The code of the class is the next:
|
||||
|
||||
```python title="Provider Base Class"
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
class Provider(ABC):
|
||||
"""
|
||||
The Provider class is an abstract base class that defines the interface for all provider classes in the auditing system.
|
||||
|
||||
Attributes:
|
||||
type (property): The type of the provider.
|
||||
identity (property): The identity of the provider for auditing.
|
||||
session (property): The session of the provider for auditing.
|
||||
audit_config (property): The audit configuration of the provider.
|
||||
output_options (property): The output configuration of the provider for auditing.
|
||||
|
||||
Methods:
|
||||
print_credentials(): Displays the provider's credentials used for auditing in the command-line interface.
|
||||
setup_session(): Sets up the session for the provider.
|
||||
get_output_mapping(): Returns the output mapping between the provider and the generic model.
|
||||
validate_arguments(): Validates the arguments for the provider.
|
||||
get_checks_to_execute_by_audit_resources(): Returns a set of checks based on the input resources to scan.
|
||||
|
||||
Note:
|
||||
This is an abstract base class and should not be instantiated directly. Each provider should implement its own
|
||||
version of the Provider class by inheriting from this base class and implementing the required methods and properties.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def type(self) -> str:
|
||||
"""
|
||||
type method stores the provider's type.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def identity(self) -> str:
|
||||
"""
|
||||
identity method stores the provider's identity to audit.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def setup_session(self) -> Any:
|
||||
"""
|
||||
setup_session sets up the session for the provider.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def session(self) -> str:
|
||||
"""
|
||||
session method stores the provider's session to audit.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def audit_config(self) -> str:
|
||||
"""
|
||||
audit_config method stores the provider's audit configuration.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def print_credentials(self) -> None:
|
||||
"""
|
||||
print_credentials is used to display in the CLI the provider's credentials used to audit.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def output_options(self) -> str:
|
||||
"""
|
||||
output_options method returns the provider's audit output configuration.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@output_options.setter
|
||||
@abstractmethod
|
||||
def output_options(self, value: str) -> Any:
|
||||
"""
|
||||
output_options.setter sets the provider's audit output configuration.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_output_mapping(self) -> dict:
|
||||
"""
|
||||
get_output_mapping returns the output mapping between the provider and the generic model.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_arguments(self) -> None:
|
||||
"""
|
||||
validate_arguments validates the arguments for the provider.
|
||||
|
||||
This method can be overridden in each provider if needed.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_checks_to_execute_by_audit_resources(self) -> set:
|
||||
"""
|
||||
get_checks_to_execute_by_audit_resources returns a set of checks based on the input resources to scan.
|
||||
|
||||
This is a fallback that returns None if the service has not implemented this function.
|
||||
"""
|
||||
return set()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def mutelist(self):
|
||||
"""
|
||||
mutelist method returns the provider's mutelist.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@mutelist.setter
|
||||
@abstractmethod
|
||||
def mutelist(self, path: str):
|
||||
"""
|
||||
mutelist.setter sets the provider's mutelist.
|
||||
|
||||
This method needs to be created in each provider.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
```
|
||||
|
||||
### Provider Class
|
||||
|
||||
Due to the complexity and differences of each provider use the rest of the providers as a template for the implementation.
|
||||
|
||||
- [AWS](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/aws_provider.py)
|
||||
- [GCP](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/gcp_provider.py)
|
||||
- [Azure](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/azure_provider.py)
|
||||
- [Kubernetes](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/kubernetes_provider.py)
|
||||
|
||||
To facilitate understanding here is a pseudocode of how the most basic provider could be with examples.
|
||||
|
||||
```python title="Provider Example Class"
|
||||
|
||||
# Library imports to authenticate in the Provider
|
||||
|
||||
from prowler.config.config import load_and_validate_config_file
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.mutelist.mutelist import parse_mutelist_file
|
||||
from prowler.lib.utils.utils import print_boxes
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.<new_provider_name>.models import (
|
||||
# All providers models needed
|
||||
ProvierSessionModel,
|
||||
ProvierIdentityModel,
|
||||
ProvierOutputOptionsModel
|
||||
)
|
||||
|
||||
class NewProvider(Provider):
|
||||
# All properties from the class, some of this are properties in the base class
|
||||
_type: str = "<provider_name>"
|
||||
_session: <ProvierSessionModel>
|
||||
_identity: <ProvierIdentityModel>
|
||||
_audit_config: dict
|
||||
_output_options: ProvierOutputOptionsModel
|
||||
_mutelist: dict
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
def __init__(self, arguments):
|
||||
"""
|
||||
Initializes the NewProvider instance.
|
||||
Args:
|
||||
arguments (dict): A dictionary containing configuration arguments.
|
||||
"""
|
||||
logger.info("Setting <NewProviderName> provider ...")
|
||||
# First get from arguments the necesary from the cloud acount (subscriptions or projects or whatever the provider use for storing services)
|
||||
|
||||
# Set the session with the method enforced by parent class
|
||||
self._session = self.setup_session(credentials_file)
|
||||
|
||||
# Set the Identity class normaly the provider class give by Python provider library
|
||||
self._identity = <ProvierIdentityModel>()
|
||||
|
||||
# Set the provider configuration
|
||||
self._audit_config = load_and_validate_config_file(
|
||||
self._type, arguments.config_file
|
||||
)
|
||||
|
||||
# All enforced properties by the parent class
|
||||
@property
|
||||
def identity(self):
|
||||
return self._identity
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def audit_config(self):
|
||||
return self._audit_config
|
||||
|
||||
@property
|
||||
def output_options(self):
|
||||
return self._output_options
|
||||
|
||||
def setup_session(self, <all_needed_for_auth>):
|
||||
"""
|
||||
Sets up the Provider session.
|
||||
|
||||
Args:
|
||||
<all_needed_for_auth> Can include all necessary arguments to setup the session
|
||||
|
||||
Returns:
|
||||
Credentials necesary to communicate with the provider.
|
||||
"""
|
||||
pass
|
||||
|
||||
"""
|
||||
This method is enforced by parent class and is used to print all relevant
|
||||
information during the prowler execution as a header of execution.
|
||||
Normally the Account ID, User name or stuff like this is displayed in colors using the colorama module (Fore).
|
||||
"""
|
||||
def print_credentials(self):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
```
|
||||
@@ -4,33 +4,36 @@ Here you can find how to create a new service, or to complement an existing one,
|
||||
|
||||
## Introduction
|
||||
|
||||
To create a new service, you will need to create a folder inside the specific provider, i.e. `prowler/providers/<provider>/services/<service>/`.
|
||||
In Prowler, a service is basically a solution that is offered by a cloud provider i.e. [ec2](https://aws.amazon.com/ec2/). Essentially it is a class that stores all the necessary stuff that we will need later in the checks to audit some aspects of our Cloud account.
|
||||
|
||||
To create a new service, you will need to create a folder inside the specific provider, i.e. `prowler/providers/<provider>/services/<new_service_name>/`.
|
||||
|
||||
Inside that folder, you MUST create the following files:
|
||||
|
||||
- An empty `__init__.py`: to make Python treat this service folder as a package.
|
||||
- A `<service>_service.py`, containing all the service's logic and API calls.
|
||||
- A `<service>_client_.py`, containing the initialization of the service's class we have just created so the checks's checks can use it.
|
||||
- A `<new_service_name>_service.py`, containing all the service's logic and API calls.
|
||||
- A `<new_service_name>_client_.py`, containing the initialization of the service's class we have just created so the checks's checks can use it.
|
||||
|
||||
## Service
|
||||
|
||||
The Prowler's service structure is the following and the way to initialise it is just by importing the service client in a check.
|
||||
|
||||
## Service Base Class
|
||||
### Service Base Class
|
||||
|
||||
All the Prowler provider's services inherits from a base class depending on the provider used.
|
||||
|
||||
- [AWS Service Base Class](https://github.com/prowler-cloud/prowler/blob/22f8855ad7dad2e976dabff78611b643e234beaf/prowler/providers/aws/lib/service/service.py)
|
||||
- [GCP Service Base Class](https://github.com/prowler-cloud/prowler/blob/22f8855ad7dad2e976dabff78611b643e234beaf/prowler/providers/gcp/lib/service/service.py)
|
||||
- [Azure Service Base Class](https://github.com/prowler-cloud/prowler/blob/22f8855ad7dad2e976dabff78611b643e234beaf/prowler/providers/azure/lib/service/service.py)
|
||||
- [AWS Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/lib/service/service.py)
|
||||
- [GCP Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/lib/service/service.py)
|
||||
- [Azure Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/lib/service/service.py)
|
||||
- [Kubernetes Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/lib/service/service.py)
|
||||
|
||||
Each class is used to initialize the credentials and the API's clients to be used in the service. If some threading is used it must be coded there.
|
||||
|
||||
## Service Class
|
||||
### Service Class
|
||||
|
||||
Due to the complexity and differencies of each provider API we are going to use an example service to guide you in how can it be created.
|
||||
Due to the complexity and differences of each provider API we are going to use an example service to guide you in how can it be created.
|
||||
|
||||
The following is the `<service>_service.py` file:
|
||||
The following is the `<new_service_name>_service.py` file:
|
||||
|
||||
```python title="Service Class"
|
||||
from datetime import datetime
|
||||
@@ -55,12 +58,12 @@ from prowler.providers.<provider>.lib.service.service import ServiceParentClass
|
||||
# Create a class for the Service
|
||||
################## <Service>
|
||||
class <Service>(ServiceParentClass):
|
||||
def __init__(self, audit_info):
|
||||
def __init__(self, provider):
|
||||
# Call Service Parent Class __init__
|
||||
# We use the __class__.__name__ to get it automatically
|
||||
# from the Service Class name but you can pass a custom
|
||||
# string if the provider's API service name is different
|
||||
super().__init__(__class__.__name__, audit_info)
|
||||
super().__init__(__class__.__name__, provider)
|
||||
|
||||
# Create an empty dictionary of items to be gathered,
|
||||
# using the unique ID as the dictionary key
|
||||
@@ -175,10 +178,12 @@ class <Service>(ServiceParentClass):
|
||||
f"{<item>.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
```
|
||||
???+note
|
||||
To avoid fake findings, when Prowler can't retrieve the items, because an Access Denied or similar error, we set that items value as `None`.
|
||||
|
||||
### Service Models
|
||||
#### Service Models
|
||||
|
||||
For each class object we need to model we use the Pydantic's [BaseModel](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel) to take advantage of the data validation.
|
||||
Service models are classes that are used in the service to design all that we need to store in each class object extrated from API calls. We use the Pydantic's [BaseModel](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel) to take advantage of the data validation.
|
||||
|
||||
```python title="Service Model"
|
||||
# In each service class we have to create some classes using
|
||||
@@ -202,7 +207,7 @@ class <Item>(BaseModel):
|
||||
tags: Optional[list]
|
||||
"""<Items>[].tags"""
|
||||
```
|
||||
### Service Objects
|
||||
#### Service Objects
|
||||
In the service each group of resources should be created as a Python [dictionary](https://docs.python.org/3/tutorial/datastructures.html#dictionaries). This is because we are performing lookups all the time and the Python dictionary lookup has [O(1) complexity](https://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions).
|
||||
|
||||
We MUST set as the dictionary key a unique ID, like the resource Unique ID or ARN.
|
||||
@@ -213,17 +218,17 @@ self.vpcs = {}
|
||||
self.vpcs["vpc-01234567890abcdef"] = VPC_Object_Class()
|
||||
```
|
||||
|
||||
## Service Client
|
||||
### Service Client
|
||||
|
||||
Each Prowler service requires a service client to use the service in the checks.
|
||||
|
||||
The following is the `<service>_client.py` containing the initialization of the service's class we have just created so the service's checks can use them:
|
||||
The following is the `<new_service_name>_client.py` containing the initialization of the service's class we have just created so the service's checks can use them:
|
||||
|
||||
```python
|
||||
from prowler.providers.<provider>.lib.audit_info.audit_info import audit_info
|
||||
from prowler.providers.<provider>.services.<service>.<service>_service import <Service>
|
||||
from prowler.providers.common.common import get_global_provider
|
||||
from prowler.providers.<provider>.services.<new_service_name>.<new_service_name>_service import <Service>
|
||||
|
||||
<service>_client = <Service>(audit_info)
|
||||
<new_service_name>_client = <Service>(get_global_provider())
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
@@ -62,50 +62,6 @@ For the AWS provider we have ways to test a Prowler check based on the following
|
||||
|
||||
In the following section we are going to explain all of the above scenarios with examples. The main difference between those scenarios comes from if the [Moto](https://github.com/getmoto/moto) library covers the AWS API calls made by the service. You can check the covered API calls [here](https://github.com/getmoto/moto/blob/master/IMPLEMENTATION_COVERAGE.md).
|
||||
|
||||
An important point for the AWS testing is that in each check we MUST have a unique `audit_info` which is the key object during the AWS execution to isolate the test execution.
|
||||
|
||||
Check the [Audit Info](./audit-info.md) section to get more details.
|
||||
|
||||
```python
|
||||
# We need to import the AWS_Audit_Info and the Audit_Metadata
|
||||
# to set the audit_info to call AWS APIs
|
||||
from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
AWS_ACCOUNT_NUMBER = "123456789012"
|
||||
|
||||
def set_mocked_audit_info(self):
|
||||
audit_info = AWS_Audit_Info(
|
||||
session_config=None,
|
||||
original_session=None,
|
||||
audit_session=session.Session(
|
||||
profile_name=None,
|
||||
botocore_session=None,
|
||||
),
|
||||
audit_config=None,
|
||||
audited_account=AWS_ACCOUNT_NUMBER,
|
||||
audited_account_arn=f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root",
|
||||
audited_user_id=None,
|
||||
audited_partition="aws",
|
||||
audited_identity_arn=None,
|
||||
profile=None,
|
||||
profile_region=None,
|
||||
credentials=None,
|
||||
assumed_role_info=None,
|
||||
audited_regions=["us-east-1", "eu-west-1"],
|
||||
organizations_metadata=None,
|
||||
audit_resources=None,
|
||||
mfa_enabled=False,
|
||||
audit_metadata=Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=[],
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
),
|
||||
)
|
||||
|
||||
return audit_info
|
||||
```
|
||||
### Checks
|
||||
|
||||
For the AWS tests examples we are going to use the tests for the `iam_password_policy_uppercase` check.
|
||||
@@ -148,29 +104,29 @@ class Test_iam_password_policy_uppercase:
|
||||
# policy we want to set to False the RequireUppercaseCharacters
|
||||
iam_client.update_account_password_policy(RequireUppercaseCharacters=False)
|
||||
|
||||
# We set a mocked audit_info for AWS not to share the same audit state
|
||||
# between checks
|
||||
current_audit_info = self.set_mocked_audit_info()
|
||||
# The aws_provider is mocked using set_mocked_aws_provider to use it as the return of the get_global_provider method.
|
||||
# this mocked provider is defined in fixtures
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
# The Prowler service import MUST be made within the decorated
|
||||
# code not to make real API calls to the AWS service.
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
# Prowler for AWS uses a shared object called `current_audit_info` where it stores
|
||||
# the audit's state, credentials and configuration.
|
||||
# Prowler for AWS uses a shared object called aws_provider where it stores
|
||||
# the info related with the provider
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.lib.audit_info.audit_info.current_audit_info",
|
||||
new=current_audit_info,
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
# We have to mock also the iam_client from the check to enforce that the iam_client used is the one
|
||||
# created within this check because patch != import, and if you execute tests in parallel some objects
|
||||
# can be already initialised hence the check won't be isolated
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_password_policy_uppercase.iam_password_policy_uppercase.iam_client",
|
||||
new=IAM(current_audit_info),
|
||||
new=IAM(aws_provider),
|
||||
):
|
||||
# We import the check within the two mocks not to initialise the iam_client with some shared information from
|
||||
# the current_audit_info or the IAM service.
|
||||
# the aws_provider or the IAM service.
|
||||
from prowler.providers.aws.services.iam.iam_password_policy_uppercase.iam_password_policy_uppercase import (
|
||||
iam_password_policy_uppercase,
|
||||
)
|
||||
@@ -235,9 +191,8 @@ class Test_iam_password_policy_uppercase:
|
||||
expiration=True,
|
||||
)
|
||||
|
||||
# We set a mocked audit_info for AWS not to share the same audit state
|
||||
# between checks
|
||||
current_audit_info = self.set_mocked_audit_info()
|
||||
# We set a mocked aws_provider to unify providers, this way will isolate each test not to step on other tests configuration
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
# In this scenario we have to mock also the IAM service and the iam_client from the check to enforce # that the iam_client used is the one created within this check because patch != import, and if you # execute tests in parallel some objects can be already initialised hence the check won't be isolated.
|
||||
# In this case we don't use the Moto decorator, we use the mocked IAM client for both objects
|
||||
@@ -249,7 +204,7 @@ class Test_iam_password_policy_uppercase:
|
||||
new=mocked_iam_client,
|
||||
):
|
||||
# We import the check within the two mocks not to initialise the iam_client with some shared information from
|
||||
# the current_audit_info or the IAM service.
|
||||
# the aws_provider or the IAM service.
|
||||
from prowler.providers.aws.services.iam.iam_password_policy_uppercase.iam_password_policy_uppercase import (
|
||||
iam_password_policy_uppercase,
|
||||
)
|
||||
@@ -333,19 +288,48 @@ Note that this does not use Moto, to keep it simple, but if you use any `moto`-d
|
||||
|
||||
#### Mocking more than one service
|
||||
|
||||
Since we are mocking the provider, it can be customized setting multiple attributes to the provider:
|
||||
```python
|
||||
def set_mocked_aws_provider(
|
||||
audited_regions: list[str] = [],
|
||||
audited_account: str = AWS_ACCOUNT_NUMBER,
|
||||
audited_account_arn: str = AWS_ACCOUNT_ARN,
|
||||
audited_partition: str = AWS_COMMERCIAL_PARTITION,
|
||||
expected_checks: list[str] = [],
|
||||
profile_region: str = None,
|
||||
audit_config: dict = {},
|
||||
fixer_config: dict = {},
|
||||
scan_unused_services: bool = True,
|
||||
audit_session: session.Session = session.Session(
|
||||
profile_name=None,
|
||||
botocore_session=None,
|
||||
),
|
||||
original_session: session.Session = None,
|
||||
enabled_regions: set = None,
|
||||
arguments: Namespace = Namespace(),
|
||||
create_default_organization: bool = True,
|
||||
) -> AwsProvider:
|
||||
```
|
||||
|
||||
If the test your are creating belongs to a check that uses more than one provider service, you should mock each of the services used. For example, the check `cloudtrail_logs_s3_bucket_access_logging_enabled` requires the CloudTrail and the S3 client, hence the service's mock part of the test will be as follows:
|
||||
|
||||
|
||||
```python
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.lib.audit_info.audit_info.current_audit_info",
|
||||
new=mock_audit_info,
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
return_value=set_mocked_aws_provider(
|
||||
[AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1]
|
||||
),
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.cloudtrail.cloudtrail_logs_s3_bucket_access_logging_enabled.cloudtrail_logs_s3_bucket_access_logging_enabled.cloudtrail_client",
|
||||
new=Cloudtrail(mock_audit_info),
|
||||
new=Cloudtrail(
|
||||
set_mocked_aws_provider([AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1])
|
||||
),
|
||||
), mock.patch(
|
||||
"prowler.providers.aws.services.cloudtrail.cloudtrail_logs_s3_bucket_access_logging_enabled.cloudtrail_logs_s3_bucket_access_logging_enabled.s3_client",
|
||||
new=S3(mock_audit_info),
|
||||
new=S3(
|
||||
set_mocked_aws_provider([AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1])
|
||||
),
|
||||
):
|
||||
```
|
||||
|
||||
@@ -363,10 +347,10 @@ from prowler.providers.<provider>.services.<service>.<service>_client import <se
|
||||
```
|
||||
2. `<service>_client.py`:
|
||||
```python
|
||||
from prowler.providers.<provider>.lib.audit_info.audit_info import audit_info
|
||||
from prowler.providers.common.common import get_global_provider
|
||||
from prowler.providers.<provider>.services.<service>.<service>_service import <SERVICE>
|
||||
|
||||
<service>_client = <SERVICE>(audit_info)
|
||||
<service>_client = <SERVICE>(mocked_provider)
|
||||
```
|
||||
|
||||
Due to the above import path it's not the same to patch the following objects because if you run a bunch of tests, either in parallel or not, some clients can be already instantiated by another check, hence your test execution will be using another test's service instance:
|
||||
@@ -384,19 +368,20 @@ A useful read about this topic can be found in the following article: https://st
|
||||
|
||||
Mocking a service client using the following code ...
|
||||
|
||||
Once the needed attributes are set for the mocked provider, you can use the mocked provider:
|
||||
```python title="Mocking the service_client"
|
||||
with mock.patch(
|
||||
"prowler.providers.<provider>.lib.audit_info.audit_info.audit_info",
|
||||
new=audit_info,
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
new=set_mocked_aws_provider([<region>]),
|
||||
), mock.patch(
|
||||
"prowler.providers.<provider>.services.<service>.<check>.<check>.<service>_client",
|
||||
new=<SERVICE>(audit_info),
|
||||
new=<SERVICE>(set_mocked_aws_provider([<region>])),
|
||||
):
|
||||
```
|
||||
will cause that the service will be initialised twice:
|
||||
|
||||
1. When the `<SERVICE>(audit_info)` is mocked out using `mock.patch` to have the object ready for the patching.
|
||||
2. At the `<service>_client.py` when we are patching it since the `mock.patch` needs to go to that object an initialise it, hence the `<SERVICE>(audit_info)` will be called again.
|
||||
1. When the `<SERVICE>(set_mocked_aws_provider([<region>]))` is mocked out using `mock.patch` to have the object ready for the patching.
|
||||
2. At the `<service>_client.py` when we are patching it since the `mock.patch` needs to go to that object an initialise it, hence the `<SERVICE>(set_mocked_aws_provider([<region>]))` will be called again.
|
||||
|
||||
Then, when we import the `<service>_client.py` at `<check>.py`, since we are mocking where the object is used, Python will use the mocked one.
|
||||
|
||||
@@ -408,17 +393,17 @@ Mocking a service client using the following code ...
|
||||
|
||||
```python title="Mocking the service and the service_client"
|
||||
with mock.patch(
|
||||
"prowler.providers.<provider>.lib.audit_info.audit_info.audit_info",
|
||||
new=audit_info,
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
new=set_mocked_aws_provider([<region>]),
|
||||
), mock.patch(
|
||||
"prowler.providers.<provider>.services.<service>.<SERVICE>",
|
||||
new=<SERVICE>(audit_info),
|
||||
new=<SERVICE>(set_mocked_aws_provider([<region>])),
|
||||
) as service_client, mock.patch(
|
||||
"prowler.providers.<provider>.services.<service>.<service>_client.<service>_client",
|
||||
new=service_client,
|
||||
):
|
||||
```
|
||||
will cause that the service will be initialised once, just when the `<SERVICE>(audit_info)` is mocked out using `mock.patch`.
|
||||
will cause that the service will be initialised once, just when the `set_mocked_aws_provider([<region>])` is mocked out using `mock.patch`.
|
||||
|
||||
Then, at the check_level when Python tries to import the client with `from prowler.providers.<provider>.services.<service>.<service>_client`, since it is already mocked out, the execution will continue using the `service_client` without getting into the `<service>_client.py`.
|
||||
|
||||
@@ -437,74 +422,97 @@ Please refer to the [AWS checks tests](./unit-testing.md#checks) for more inform
|
||||
|
||||
For the GCP Provider we don't have any library to mock out the API calls we use. So in this scenario we inject the objects in the service client using [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock).
|
||||
|
||||
The following code shows how to use MagicMock to create the service objects for a GCP check test.
|
||||
The following code shows how to use MagicMock to create the service objects for a GCP check test. It is a real example adapted for informative purposes.
|
||||
|
||||
```python
|
||||
# We need to import the unittest.mock to allow us to patch some objects
|
||||
# not to use shared ones between test, hence to isolate the test
|
||||
from re import search
|
||||
from unittest import mock
|
||||
|
||||
# GCP Constants
|
||||
GCP_PROJECT_ID = "123456789012"
|
||||
# Import some constant values needed in every check
|
||||
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider
|
||||
|
||||
# We are going to create a test for the compute_firewall_rdp_access_from_the_internet_allowed check
|
||||
class Test_compute_firewall_rdp_access_from_the_internet_allowed:
|
||||
# We are going to create a test for the compute_project_os_login_enabled check
|
||||
class Test_compute_project_os_login_enabled:
|
||||
|
||||
# We name the tests with test_<service>_<check_name>_<test_action>
|
||||
def test_compute_compute_firewall_rdp_access_from_the_internet_allowed_one_compliant_rule_with_valid_port(self):
|
||||
def test_one_compliant_project(self):
|
||||
# Import the service resource model to create the mocked object
|
||||
from prowler.providers.gcp.services.compute.compute_service import Project
|
||||
# Create the custom Project object to be tested
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
enable_oslogin=True,
|
||||
)
|
||||
# Mocked client with MagicMock
|
||||
compute_client = mock.MagicMock
|
||||
|
||||
# Assign GCP client configuration
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.region = "global"
|
||||
compute_client.projects = [project]
|
||||
|
||||
# Import the service resource model to create the mocked object
|
||||
from prowler.providers.gcp.services.compute.compute_service import Firewall
|
||||
|
||||
# Create the custom Firewall object to be tested
|
||||
firewall = Firewall(
|
||||
name="test",
|
||||
id="1234567890",
|
||||
source_ranges=["0.0.0.0/0"],
|
||||
direction="INGRESS",
|
||||
allowed_rules=[{"IPProtocol": "tcp", "ports": ["443"]}],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
)
|
||||
compute_client.firewalls = [firewall]
|
||||
|
||||
# In this scenario we have to mock also the Compute service and the compute_client from the check to enforce that the compute_client used is the one created within this check because patch != import, and if you execute tests in parallel some objects can be already initialised hence the check won't be isolated.
|
||||
# In this case we don't use the Moto decorator, we use the mocked Compute client for both objects
|
||||
# In this scenario we have to mock the app_client from the check to enforce that the compute_client used is the one created above
|
||||
# And also is mocked the return value of get_global_provider function to return our GCP mocked provider defined in fixtures
|
||||
with mock.patch(
|
||||
"prowler.providers.gcp.services.compute.compute_service.Compute",
|
||||
new=defender_client,
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
), mock.patch(
|
||||
"prowler.providers.gcp.services.compute.compute_client.compute_client",
|
||||
new=defender_client,
|
||||
"prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled.compute_client",
|
||||
new=compute_client,
|
||||
):
|
||||
|
||||
# We import the check within the two mocks not to initialise the iam_client with some shared information from
|
||||
# the current_audit_info or the Compute service.
|
||||
from prowler.providers.gcp.services.compute.compute_firewall_rdp_access_from_the_internet_allowed.compute_firewall_rdp_access_from_the_internet_allowed import (
|
||||
compute_firewall_rdp_access_from_the_internet_allowed,
|
||||
# We import the check within the two mocks
|
||||
from prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled import (
|
||||
compute_project_os_login_enabled,
|
||||
)
|
||||
|
||||
# Once imported, we only need to instantiate the check's class
|
||||
check = compute_firewall_rdp_access_from_the_internet_allowed()
|
||||
|
||||
check = compute_project_os_login_enabled()
|
||||
# And then, call the execute() function to run the check
|
||||
# against the IAM client we've set up.
|
||||
# against the Compute client we've set up.
|
||||
result = check.execute()
|
||||
|
||||
# Last but not least, we need to assert all the fields
|
||||
# from the check's results
|
||||
# Assert the expected results
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == f"Firewall {firewall.name} does not expose port 3389 (RDP) to the internet."
|
||||
assert result[0].resource_name = firewall.name
|
||||
assert result[0].resource_id == firewall.id
|
||||
assert result[0].project_id = GCP_PROJECT_ID
|
||||
assert result[0].location = compute_client.region
|
||||
assert search(
|
||||
f"Project {project.id} has OS Login enabled",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == project.id
|
||||
assert result[0].location == "global"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
# Complementary test to make more coverage for different scenarios
|
||||
def test_one_non_compliant_project(self):
|
||||
from prowler.providers.gcp.services.compute.compute_service import Project
|
||||
|
||||
project = Project(
|
||||
id=GCP_PROJECT_ID,
|
||||
enable_oslogin=False,
|
||||
)
|
||||
|
||||
compute_client = mock.MagicMock
|
||||
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||
compute_client.projects = [project]
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
), mock.patch(
|
||||
"prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled.compute_client",
|
||||
new=compute_client,
|
||||
):
|
||||
from prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled import (
|
||||
compute_project_os_login_enabled,
|
||||
)
|
||||
|
||||
check = compute_project_os_login_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert search(
|
||||
f"Project {project.id} does not have OS Login enabled",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == project.id
|
||||
assert result[0].location == "global"
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
```
|
||||
|
||||
### Services
|
||||
@@ -517,80 +525,186 @@ Coming soon ...
|
||||
|
||||
For the Azure Provider we don't have any library to mock out the API calls we use. So in this scenario we inject the objects in the service client using [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock).
|
||||
|
||||
The following code shows how to use MagicMock to create the service objects for a Azure check test.
|
||||
The following code shows how to use MagicMock to create the service objects for a Azure check test. It is a real example adapted for informative purposes.
|
||||
|
||||
```python
|
||||
```python title="app_ensure_http_is_redirected_to_https_test.py"
|
||||
# We need to import the unittest.mock to allow us to patch some objects
|
||||
# not to use shared ones between test, hence to isolate the test
|
||||
from unittest import mock
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
# Azure Constants
|
||||
from tests.providers.azure.azure_fixtures import AZURE_SUBSCRIPTION
|
||||
# Import some constans values needed in almost every check
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
|
||||
|
||||
# We are going to create a test for the Test_defender_ensure_defender_for_arm_is_on check
|
||||
class Test_defender_ensure_defender_for_arm_is_on:
|
||||
# We are going to create a test for the app_ensure_http_is_redirected_to_https check
|
||||
class Test_app_ensure_http_is_redirected_to_https:
|
||||
|
||||
# We name the tests with test_<service>_<check_name>_<test_action>
|
||||
def test_defender_defender_ensure_defender_for_arm_is_on_arm_pricing_tier_not_standard(self):
|
||||
resource_id = str(uuid4())
|
||||
|
||||
def test_app_http_to_https_disabled(self):
|
||||
resource_id = f"/subscriptions/{uuid4()}"
|
||||
# Mocked client with MagicMock
|
||||
defender_client = mock.MagicMock
|
||||
app_client = mock.MagicMock
|
||||
|
||||
# Import the service resource model to create the mocked object
|
||||
from prowler.providers.azure.services.defender.defender_service import Defender_Pricing
|
||||
|
||||
# Create the custom Defender object to be tested
|
||||
defender_client.pricings = {
|
||||
AZURE_SUBSCRIPTION: {
|
||||
"Arm": Defender_Pricing(
|
||||
resource_id=resource_id,
|
||||
pricing_tier="Not Standard",
|
||||
free_trial_remaining_time=0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# In this scenario we have to mock also the Defender service and the defender_client from the check to enforce that the defender_client used is the one created within this check because patch != import, and if you execute tests in parallel some objects can be already initialised hence the check won't be isolated.
|
||||
# In this case we don't use the Moto decorator, we use the mocked Defender client for both objects
|
||||
# In this scenario we have to mock the app_client from the check to enforce that the app_client used is the one created above
|
||||
# And also is mocked the return value of get_global_provider function to return our Azure mocked provider defined in fixtures
|
||||
with mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_service.Defender",
|
||||
new=defender_client,
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
), mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_client.defender_client",
|
||||
new=defender_client,
|
||||
"prowler.providers.azure.services.app.app_ensure_http_is_redirected_to_https.app_ensure_http_is_redirected_to_https.app_client",
|
||||
new=app_client,
|
||||
):
|
||||
|
||||
# We import the check within the two mocks not to initialise the iam_client with some shared information from
|
||||
# the current_audit_info or the Defender service.
|
||||
from prowler.providers.azure.services.defender.defender_ensure_defender_for_arm_is_on.defender_ensure_defender_for_arm_is_on import (
|
||||
defender_ensure_defender_for_arm_is_on,
|
||||
# We import the check within the two mocks
|
||||
from prowler.providers.azure.services.app.app_ensure_http_is_redirected_to_https.app_ensure_http_is_redirected_to_https import (
|
||||
app_ensure_http_is_redirected_to_https,
|
||||
)
|
||||
# Import the service resource model to create the mocked object
|
||||
from prowler.providers.azure.services.app.app_service import WebApp
|
||||
|
||||
# Create the custom App object to be tested
|
||||
app_client.apps = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
"app_id-1": WebApp(
|
||||
resource_id=resource_id,
|
||||
auth_enabled=True,
|
||||
configurations=mock.MagicMock(),
|
||||
client_cert_mode="Ignore",
|
||||
https_only=False,
|
||||
identity=None,
|
||||
location="West Europe",
|
||||
)
|
||||
}
|
||||
}
|
||||
# Once imported, we only need to instantiate the check's class
|
||||
check = defender_ensure_defender_for_arm_is_on()
|
||||
|
||||
check = app_ensure_http_is_redirected_to_https()
|
||||
# And then, call the execute() function to run the check
|
||||
# against the IAM client we've set up.
|
||||
# against the App client we've set up.
|
||||
result = check.execute()
|
||||
|
||||
# Last but not least, we need to assert all the fields
|
||||
# from the check's results
|
||||
# Assert the expected results
|
||||
assert len(result) == 1
|
||||
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"HTTP is not redirected to HTTPS for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION
|
||||
assert result[0].resource_name == "Defender plan ARM"
|
||||
assert result[0].resource_name == "app_id-1"
|
||||
assert result[0].resource_id == resource_id
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].location == "West Europe"
|
||||
|
||||
# Complementary test to make more coverage for different scenarios
|
||||
def test_app_http_to_https_enabled(self):
|
||||
resource_id = f"/subscriptions/{uuid4()}"
|
||||
app_client = mock.MagicMock
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.common.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
), mock.patch(
|
||||
"prowler.providers.azure.services.app.app_ensure_http_is_redirected_to_https.app_ensure_http_is_redirected_to_https.app_client",
|
||||
new=app_client,
|
||||
):
|
||||
from prowler.providers.azure.services.app.app_ensure_http_is_redirected_to_https.app_ensure_http_is_redirected_to_https import (
|
||||
app_ensure_http_is_redirected_to_https,
|
||||
)
|
||||
from prowler.providers.azure.services.app.app_service import WebApp
|
||||
|
||||
app_client.apps = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
"app_id-1": WebApp(
|
||||
resource_id=resource_id,
|
||||
auth_enabled=True,
|
||||
configurations=mock.MagicMock(),
|
||||
client_cert_mode="Ignore",
|
||||
https_only=True,
|
||||
identity=None,
|
||||
location="West Europe",
|
||||
)
|
||||
}
|
||||
}
|
||||
check = app_ensure_http_is_redirected_to_https()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"HTTP is redirected to HTTPS for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_ID}'."
|
||||
)
|
||||
assert result[0].resource_name == "app_id-1"
|
||||
assert result[0].resource_id == resource_id
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].location == "West Europe"
|
||||
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
Coming soon ...
|
||||
For testing Azure services, we have to follow the same logic as with the Azure checks. We still mock all the API calls, but in this case, every method that uses an API call to set up an attribute is mocked with the [patch](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) decorator at the beginning of the class. Remember that every method of a service MUST be tested.
|
||||
|
||||
The following code shows a real example of a testing class, but it has more comments than usual for educational purposes.
|
||||
|
||||
```python title="AppInsights Service Test"
|
||||
# We need to import the unittest.mock.patch to allow us to patch some objects
|
||||
# not to use shared ones between test, hence to isolate the test
|
||||
from unittest.mock import patch
|
||||
# Import the models needed from the service file
|
||||
from prowler.providers.azure.services.appinsights.appinsights_service import (
|
||||
AppInsights,
|
||||
Component,
|
||||
)
|
||||
# Import some constans values needed in almost every check
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
set_mocked_azure_provider,
|
||||
)
|
||||
|
||||
# Function to mock the service function __get_components__, this function task is to return a possible value that real function could returns
|
||||
def mock_appinsights_get_components(_):
|
||||
return {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
"app_id-1": Component(
|
||||
resource_id="/subscriptions/resource_id",
|
||||
resource_name="AppInsightsTest",
|
||||
location="westeurope",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# Patch decorator to use the mocked function instead the function with the real API call
|
||||
@patch(
|
||||
"prowler.providers.azure.services.appinsights.appinsights_service.AppInsights.__get_components__",
|
||||
new=mock_appinsights_get_components,
|
||||
)
|
||||
class Test_AppInsights_Service:
|
||||
# Mandatory test for every service, this method test the instance of the client is correct
|
||||
def test__get_client__(self):
|
||||
app_insights = AppInsights(set_mocked_azure_provider())
|
||||
assert (
|
||||
app_insights.clients[AZURE_SUBSCRIPTION_ID].__class__.__name__
|
||||
== "ApplicationInsightsManagementClient"
|
||||
)
|
||||
# Second typical method that test if subscriptions is defined inside the client object
|
||||
def test__get_subscriptions__(self):
|
||||
app_insights = AppInsights(set_mocked_azure_provider())
|
||||
assert app_insights.subscriptions.__class__.__name__ == "dict"
|
||||
# Test for the function __get_components__, inside this client is used the mocked function
|
||||
def test__get_components__(self):
|
||||
appinsights = AppInsights(set_mocked_azure_provider())
|
||||
assert len(appinsights.components) == 1
|
||||
assert (
|
||||
appinsights.components[AZURE_SUBSCRIPTION_ID]["app_id-1"].resource_id
|
||||
== "/subscriptions/resource_id"
|
||||
)
|
||||
assert (
|
||||
appinsights.components[AZURE_SUBSCRIPTION_ID]["app_id-1"].resource_name
|
||||
== "AppInsightsTest"
|
||||
)
|
||||
assert (
|
||||
appinsights.components[AZURE_SUBSCRIPTION_ID]["app_id-1"].location
|
||||
== "westeurope"
|
||||
)
|
||||
```
|
||||
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 631 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 240 KiB |
@@ -1,8 +1,20 @@
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure and Google Cloud security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler SaaS</a>.
|
||||
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler SaaS</a>.
|
||||
|
||||

|
||||
## Prowler CLI
|
||||
|
||||
Prowler offers hundreds of controls covering more than 25 standards and compliance frameworks like CIS, PCI-DSS, ISO27001, GDPR, HIPAA, FFIEC, SOC2, AWS FTR, ENS and custom security frameworks.
|
||||
```console
|
||||
prowler <provider>
|
||||
```
|
||||

|
||||
|
||||
## Prowler Dashboard
|
||||
|
||||
```console
|
||||
prowler dashboard
|
||||
```
|
||||

|
||||
|
||||
It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks.
|
||||
|
||||
## Quick Start
|
||||
### Installation
|
||||
@@ -124,12 +136,12 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
adduser prowler
|
||||
su prowler
|
||||
pip install prowler
|
||||
cd /tmp || exit
|
||||
cd /tmp
|
||||
prowler aws
|
||||
```
|
||||
|
||||
???+ note
|
||||
To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/home/cloudshell-user/output/prowler-output-123456789012-20221220191331.csv`
|
||||
To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/tmp/output/prowler-output-123456789012-20221220191331.csv`
|
||||
|
||||
=== "Azure CloudShell"
|
||||
|
||||
@@ -148,9 +160,11 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
|
||||
|
||||
The available versions of Prowler are the following:
|
||||
|
||||
- `latest`: in sync with master branch (bear in mind that it is not a stable version)
|
||||
- `latest`: in sync with `master` branch (bear in mind that it is not a stable version)
|
||||
- `v3-latest`: in sync with `v3` branch (bear in mind that it is not a stable version)
|
||||
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
|
||||
- `stable`: this tag always point to the latest release.
|
||||
- `v3-stable`: this tag always point to the latest release for v3.
|
||||
|
||||
The container images are available here:
|
||||
|
||||
@@ -159,9 +173,27 @@ The container images are available here:
|
||||
|
||||
## High level architecture
|
||||
|
||||
You can run Prowler from your workstation, an EC2 instance, Fargate or any other container, Codebuild, CloudShell, Cloud9 and many more.
|
||||
You can run Prowler from your workstation, a Kubernetes Job, a Google Compute Engine, an Azure VM, an EC2 instance, Fargate or any other container, CloudShell and many more.
|
||||
|
||||

|
||||
|
||||
## Deprecations from v3
|
||||
|
||||
### General
|
||||
- `Allowlist` now is called `Mutelist`.
|
||||
- The `--quiet` option has been deprecated, now use the `--status` flag to select the finding's status you want to get from PASS, FAIL or MANUAL.
|
||||
- All `INFO` finding's status has changed to `MANUAL`.
|
||||
- The CSV output format is common for all the providers.
|
||||
|
||||
We have deprecated some of our outputs formats:
|
||||
|
||||
- The HTML is replaced for the new Prowler Dashboard, run `prowler dashboard`.
|
||||
- The native JSON is replaced for the JSON [OCSF](https://schema.ocsf.io/) v1.1.0, common for all the providers.
|
||||
|
||||
### AWS
|
||||
- Deprecate the AWS flag --sts-endpoint-region since we use AWS STS regional tokens.
|
||||
- To send only FAILS to AWS Security Hub, now use either `--send-sh-only-fails` or `--security-hub --status FAIL`.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure` or `kubernetes`):
|
||||
@@ -232,7 +264,7 @@ prowler aws --profile custom-profile -f us-east-1 eu-south-2
|
||||
???+ note
|
||||
By default, `prowler` will scan all AWS regions.
|
||||
|
||||
See more details about AWS Authentication in [Requirements](getting-started/requirements.md)
|
||||
See more details about AWS Authentication in [Requirements](getting-started/requirements.md#aws)
|
||||
|
||||
### Azure
|
||||
|
||||
@@ -252,7 +284,7 @@ prowler azure --browser-auth --tenant-id "XXXXXXXX"
|
||||
prowler azure --managed-identity-auth
|
||||
```
|
||||
|
||||
See more details about Azure Authentication in [Requirements](getting-started/requirements.md)
|
||||
See more details about Azure Authentication in [Requirements](getting-started/requirements.md#azure)
|
||||
|
||||
Prowler by default scans all the subscriptions that is allowed to scan, if you want to scan a single subscription or various specific subscriptions you can use the following flag (using az cli auth as example):
|
||||
```console
|
||||
@@ -279,9 +311,9 @@ Prowler by default scans all the GCP Projects that is allowed to scan, if you wa
|
||||
prowler gcp --project-ids <Project ID 1> <Project ID 2> ... <Project ID N>
|
||||
```
|
||||
|
||||
See more details about GCP Authentication in [Requirements](getting-started/requirements.md)
|
||||
See more details about GCP Authentication in [Requirements](getting-started/requirements.md#google-cloud)
|
||||
|
||||
## Kubernetes
|
||||
### Kubernetes
|
||||
|
||||
Prowler allows you to scan your Kubernetes Cluster either from within the cluster or from outside the cluster.
|
||||
|
||||
@@ -293,9 +325,9 @@ prowler kubernetes --kubeconfig-file path
|
||||
|
||||
For in-cluster execution, you can use the supplied yaml to run Prowler as a job:
|
||||
```console
|
||||
kubectl apply -f job.yaml
|
||||
kubectl apply -f prowler-role.yaml
|
||||
kubectl apply -f prowler-rolebinding.yaml
|
||||
kubectl apply -f kubernetes/job.yaml
|
||||
kubectl apply -f kubernetes/prowler-role.yaml
|
||||
kubectl apply -f kubernetes/prowler-rolebinding.yaml
|
||||
kubectl get pods --> prowler-XXXXX
|
||||
kubectl logs prowler-XXXXX
|
||||
```
|
||||
|
||||
@@ -27,7 +27,7 @@ Those credentials must be associated to a user or role with proper permissions t
|
||||
|
||||
Prowler can use your custom AWS Profile with:
|
||||
```console
|
||||
prowler <provider> -p/--profile <profile_name>
|
||||
prowler aws -p/--profile <profile_name>
|
||||
```
|
||||
|
||||
## Multi-Factor Authentication
|
||||
|
||||
@@ -7,7 +7,7 @@ sudo bash
|
||||
adduser prowler
|
||||
su prowler
|
||||
pip install prowler
|
||||
cd /tmp || exit
|
||||
cd /tmp
|
||||
prowler aws
|
||||
```
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
To save your report in an S3 bucket, use `-B`/`--output-bucket`.
|
||||
|
||||
```sh
|
||||
prowler <provider> -B my-bucket
|
||||
prowler aws -B my-bucket
|
||||
```
|
||||
|
||||
If you can use a custom folder and/or filename, use `-o`/`--output-directory` and/or `-F`/`--output-filename`.
|
||||
|
||||
```sh
|
||||
prowler <provider> \
|
||||
prowler aws \
|
||||
-B my-bucket \
|
||||
--output-directory test-folder \
|
||||
--output-filename output-filename
|
||||
@@ -18,8 +18,11 @@ prowler <provider> \
|
||||
By default Prowler sends HTML, JSON and CSV output formats, if you want to send a custom output format or a single one of the defaults you can specify it with the `-M`/`--output-modes` flag.
|
||||
|
||||
```sh
|
||||
prowler <provider> -M csv -B my-bucket
|
||||
prowler aws -M csv -B my-bucket
|
||||
```
|
||||
|
||||
|
||||
???+ note
|
||||
In the case you do not want to use the assumed role credentials but the initial credentials to put the reports into the S3 bucket, use `-D`/`--output-bucket-no-assume` instead of `-B`/`--output-bucket`. Make sure that the used credentials have `s3:PutObject` permissions in the S3 path where the reports are going to be uploaded.
|
||||
In the case you do not want to use the assumed role credentials but the initial credentials to put the reports into the S3 bucket, use `-D`/`--output-bucket-no-assume` instead of `-B`/`--output-bucket`.
|
||||
???+ warning
|
||||
Make sure that the used credentials have `s3:PutObject` permissions in the S3 path where the reports are going to be uploaded.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Threat Detection
|
||||
|
||||
Prowler allows you to do threat detection in AWS based on the CloudTrail log records. To run checks related with threat detection use:
|
||||
```
|
||||
prowler aws --category threat-detection
|
||||
```
|
||||
This comand will run these checks:
|
||||
|
||||
* `cloudtrail_threat_detection_privilege_escalation`
|
||||
* `cloudtrail_threat_detection_enumeration`
|
||||
|
||||
???+ note
|
||||
Threat Detection checks will be only executed using `--category threat-detection` flag due to preformance.
|
||||
|
||||
## Config File
|
||||
|
||||
If you want to manage the behavior of the Threat Detection checks you can edit `config.yaml` file from `/prowler/config`. In this file you can edit the following attributes related with Threat Detection:
|
||||
|
||||
* `threat_detection_privilege_escalation_threshold`: determines the percentage of actions found to decide if it is an privilege_scalation attack event, by default is 0.1 (10%)
|
||||
* `threat_detection_privilege_escalation_minutes`: it is the past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
|
||||
* `threat_detection_privilege_escalation_actions`: these are the default actions related with priviledge scalation.
|
||||
* `threat_detection_enumeration_threshold`: determines the percentage of actions found to decide if it is an enumeration attack event, by default is 0.1 (10%)
|
||||
* `threat_detection_enumeration_minutes`: it is the past minutes to search from now for enumeration attacks, by default is 1440 minutes (24 hours)
|
||||
* `threat_detection_enumeration_actions`: these are the default actions related with enumeration attacks.
|
||||
@@ -1,11 +1,11 @@
|
||||
# Check mapping between Prowler v3 and v2
|
||||
# Check mapping between Prowler v4/v3 and v2
|
||||
|
||||
Prowler v3 comes with different identifiers but we maintained the same checks that were implemented in v2. The reason for this change is because in previous versions of Prowler, check names were mostly based on CIS Benchmark for AWS. In v3 all checks are independent from any security framework and they have its own name and ID.
|
||||
Prowler v3 comes with different identifiers but we maintained the same checks that were implemented in v2. The reason for this change is because in previous versions of Prowler, check names were mostly based on CIS Benchmark for AWS. In v4 and v3 all checks are independent from any security framework and they have its own name and ID.
|
||||
|
||||
If you need more information about how new compliance implementation works in Prowler v3 see [Compliance](../compliance.md) section.
|
||||
If you need more information about how new compliance implementation works in Prowler v4 and v3 see [Compliance](../compliance.md) section.
|
||||
|
||||
```
|
||||
checks_v3_to_v2_mapping = {
|
||||
checks_v4_v3_to_v2_mapping = {
|
||||
"accessanalyzer_enabled_without_findings": "extra769",
|
||||
"account_maintain_current_contact_details": "check117",
|
||||
"account_security_contact_information_is_registered": "check118",
|
||||
|
||||
@@ -10,7 +10,7 @@ Prowler can analyze your environment based on a specific compliance framework an
|
||||
```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.
|
||||
Standard results will be shown and additionally the framework information as the sample below for CIS AWS 2.0. For details a CSV file has been generated as well.
|
||||
|
||||
<img src="../img/compliance/compliance-cis-sample1.png"/>
|
||||
|
||||
|
||||
@@ -4,13 +4,18 @@ Prowler allows you to run your own local dashboards using the csv outputs provid
|
||||
```sh
|
||||
prowler dashboard
|
||||
```
|
||||
|
||||
To run Prowler local dashboard with docker, use:
|
||||
???+ note
|
||||
You can expose the `dashboard` server in another address using the `HOST` environment variable.
|
||||
|
||||
To run Prowler local dashboard with Docker, use:
|
||||
|
||||
```sh
|
||||
docker run toniblyx/prowler:latest dashboard
|
||||
docker run --env HOST=0.0.0.0 --publish 127.0.0.1:11666:11666 toniblyx/prowler:latest dashboard
|
||||
```
|
||||
|
||||
???+ note
|
||||
**Remember that the `dashboard` server is not authenticated, if you expose it to the internet, you are running it at your own risk.**
|
||||
|
||||
The banner and additional info about the dashboard will be shown on your console:
|
||||
<img src="../img/dashboard/dashboard-banner.png">
|
||||
|
||||
@@ -21,8 +26,9 @@ The overview page provides a full impression of your findings obtained from Prow
|
||||
<img src="../img/dashboard/dashboard-overview.png">
|
||||
|
||||
In this page you can do multiple functions:
|
||||
|
||||
* Apply filters (Assessment Date / Account / Region)
|
||||
* See wich files has been scaned to generate the dashboard placing your mouse on the `?` icon:
|
||||
* See wich files has been scanned to generate the dashboard placing your mouse on the `?` icon:
|
||||
<img src="../img/dashboard/dashboard-files-scanned.png">
|
||||
* Download the `Top 25 Failed Findings by Severity` table using the button `DOWNLOAD THIS TABLE AS CSV`
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Prowler Fixer
|
||||
Prowler allows you to fix some of the failed findings it identifies. You can use the `--fixer` flag to run the fixes that are available for the checks that failed.
|
||||
|
||||
```sh
|
||||
prowler <provider> -c <check_to_fix_1> <check_to_fix_2> ... --fixer
|
||||
```
|
||||
|
||||
<img src="../img/fixer.png">
|
||||
|
||||
???+ note
|
||||
You can see all the available fixes for each provider with the `--list-fixers` flag.
|
||||
|
||||
```sh
|
||||
prowler <provider> --list-fixer
|
||||
```
|
||||
|
||||
## Writing a Fixer
|
||||
To write a fixer, you need to create a file called `<check_id>_fixer.py` inside the check folder, with a function called `fixer` that receives either the region or the resource to be fixed as a parameter, and returns a boolean value indicating if the fix was successful or not.
|
||||
|
||||
For example, the regional fixer for the `ec2_ebs_default_encryption` check, which enables EBS encryption by default in a region, would look like this:
|
||||
```python
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
|
||||
|
||||
|
||||
def fixer(region):
|
||||
"""
|
||||
Enable EBS encryption by default in a region. NOTE: Custom KMS keys for EBS Default Encryption may be overwritten.
|
||||
Requires the ec2:EnableEbsEncryptionByDefault permission:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "ec2:EnableEbsEncryptionByDefault",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
region (str): AWS region
|
||||
Returns:
|
||||
bool: True if EBS encryption by default is enabled, False otherwise
|
||||
"""
|
||||
try:
|
||||
regional_client = ec2_client.regional_clients[region]
|
||||
return regional_client.enable_ebs_encryption_by_default()[
|
||||
"EbsEncryptionByDefault"
|
||||
]
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
```
|
||||
On the other hand, the fixer for the `s3_account_level_public_access_blocks` check, which enables the account-level public access blocks for S3, would look like this:
|
||||
```python
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.s3.s3control_client import s3control_client
|
||||
|
||||
|
||||
def fixer(resource_id: str) -> bool:
|
||||
"""
|
||||
Enable S3 Block Public Access for the account. NOTE: By blocking all S3 public access you may break public S3 buckets.
|
||||
Requires the s3:PutAccountPublicAccessBlock permission:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:PutAccountPublicAccessBlock",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Returns:
|
||||
bool: True if S3 Block Public Access is enabled, False otherwise
|
||||
"""
|
||||
try:
|
||||
s3control_client.client.put_public_access_block(
|
||||
AccountId=resource_id,
|
||||
PublicAccessBlockConfiguration={
|
||||
"BlockPublicAcls": True,
|
||||
"IgnorePublicAcls": True,
|
||||
"BlockPublicPolicy": True,
|
||||
"RestrictPublicBuckets": True,
|
||||
},
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
```
|
||||
|
||||
## Fixer Config file
|
||||
For some fixers, you can have configurable parameters depending on your use case. You can either use the default config file in `prowler/config/fixer_config.yaml` or create a custom config file and pass it to the fixer with the `--fixer-config` flag. The config file should be a YAML file with the following structure:
|
||||
```yaml
|
||||
# Fixer configuration file
|
||||
aws:
|
||||
# ec2_ebs_default_encryption
|
||||
# No configuration needed for this check
|
||||
|
||||
# s3_account_level_public_access_blocks
|
||||
# No configuration needed for this check
|
||||
|
||||
# iam_password_policy_* checks:
|
||||
iam_password_policy:
|
||||
MinimumPasswordLength: 14
|
||||
RequireSymbols: True
|
||||
RequireNumbers: True
|
||||
RequireUppercaseCharacters: True
|
||||
RequireLowercaseCharacters: True
|
||||
AllowUsersToChangePassword: True
|
||||
MaxPasswordAge: 90
|
||||
PasswordReusePrevention: 24
|
||||
HardExpiry: False
|
||||
|
||||
# accessanalyzer_enabled
|
||||
accessanalyzer_enabled:
|
||||
AnalyzerName: "DefaultAnalyzer"
|
||||
AnalyzerType: "ACCOUNT_UNUSED_ACCESS"
|
||||
|
||||
# guardduty_is_enabled
|
||||
# No configuration needed for this check
|
||||
|
||||
# securityhub_enabled
|
||||
securityhub_enabled:
|
||||
EnableDefaultStandards: True
|
||||
|
||||
# cloudtrail_multi_region_enabled
|
||||
cloudtrail_multi_region_enabled:
|
||||
TrailName: "DefaultTrail"
|
||||
S3BucketName: "my-cloudtrail-bucket"
|
||||
IsMultiRegionTrail: True
|
||||
EnableLogFileValidation: True
|
||||
# CloudWatchLogsLogGroupArn: "arn:aws:logs:us-east-1:123456789012:log-group:my-cloudtrail-log-group"
|
||||
# CloudWatchLogsRoleArn: "arn:aws:iam::123456789012:role/my-cloudtrail-role"
|
||||
# KmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
|
||||
|
||||
# kms_cmk_rotation_enabled
|
||||
# No configuration needed for this check
|
||||
|
||||
# ec2_ebs_snapshot_account_block_public_access
|
||||
ec2_ebs_snapshot_account_block_public_access:
|
||||
State: "block-all-sharing"
|
||||
|
||||
# ec2_instance_account_imdsv2_enabled
|
||||
# No configuration needed for this check
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# GCP Projects
|
||||
|
||||
By default, Prowler is multi-project, which means that is going to scan all the Google Cloud projects that the authenticated user has access to. If you want to scan a specific project(s), you can use the `--project-ids` argument.
|
||||
|
||||
```console
|
||||
prowler gcp --project-ids project-id1 project-id2
|
||||
```
|
||||
|
||||
???+ note
|
||||
You can use asterisk `*` to scan projects that match a pattern. For example, `prowler gcp --project-ids "prowler*"` will scan all the projects that start with `prowler`.
|
||||
|
||||
???+ note
|
||||
If you want to know the projects that you have access to, you can use the following command:
|
||||
|
||||
```console
|
||||
prowler gcp --list-project-ids
|
||||
```
|
||||
|
||||
### Exclude Projects
|
||||
|
||||
If you want to exclude some projects from the scan, you can use the `--exclude-project-ids` argument.
|
||||
|
||||
```console
|
||||
prowler gcp --exclude-project-ids project-id1 project-id2
|
||||
```
|
||||
|
||||
???+ note
|
||||
You can use asterisk `*` to exclude projects that match a pattern. For example, `prowler gcp --exclude-project-ids "sys*"` will exclude all the projects that start with `sys`.
|
||||
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 67 KiB |
@@ -1,28 +1,36 @@
|
||||
# Reporting
|
||||
|
||||
By default, Prowler will generate a CSV, JSON, JSON-OCSF and a HTML report, however you could generate a JSON-ASFF (used by AWS Security Hub) report with `-M` or `--output-modes`:
|
||||
By default, Prowler will generate the CSV and JSON-[OCSF](https://schema.ocsf.io/) report.
|
||||
|
||||
```console
|
||||
prowler <provider> -M csv json json-ocsf json-asff html
|
||||
prowler <provider> -M csv json-ocsf json-asff
|
||||
```
|
||||
|
||||
If you want to generate the JSON-ASFF (used by AWS Security Hub) report you can set it using the `-M/--output-modes/--output-formats`, like:
|
||||
|
||||
```console
|
||||
prowler <provider> --output-formats json-asff
|
||||
```
|
||||
|
||||
By default, all the compliance outputs will be generated when Prowler is executed. Compliance outputs will be placed inside the `/output/compliance` directory.
|
||||
|
||||
## Custom Output Flags
|
||||
By default, Prowler creates a file inside the `output` directory named `prowler-output-ACCOUNT_NUM-OUTPUT_DATE.format`.
|
||||
By default, Prowler creates a file inside the `output` directory named: `prowler-output-ACCOUNT_NUM-OUTPUT_DATE.format`.
|
||||
|
||||
However, both the output file name and directory can be personalised:
|
||||
|
||||
- Custom output report name: you can use the flag `-F`/`--output-filename`
|
||||
```console
|
||||
prowler <provider> -M csv json json-asff html -F <custom_report_name>
|
||||
prowler <provider> -M csv json-ocsf json-asff -F <custom_report_name>
|
||||
```
|
||||
- Custom output directory: you can use the flag `-o`/`--output-directory`
|
||||
```console
|
||||
prowler <provider> -M csv json json-asff html -o <custom_report_directory>
|
||||
prowler <provider> -M csv json-ocsf json-asff -o <custom_report_directory>
|
||||
```
|
||||
???+ note
|
||||
Both flags can be used simultaneously to provide a custom directory and filename.
|
||||
```console
|
||||
prowler <provider> -M csv json json-asff html \
|
||||
prowler <provider> -M csv json-ocsf json-asff \
|
||||
-F <custom_report_name> -o <custom_report_directory>
|
||||
```
|
||||
## Output timestamp format
|
||||
@@ -33,448 +41,188 @@ By default, the timestamp format of the output files is ISO 8601. This can be ch
|
||||
Prowler supports natively the following output formats:
|
||||
|
||||
- CSV
|
||||
- JSON
|
||||
- JSON-OCSF
|
||||
- JSON-ASFF
|
||||
- HTML
|
||||
|
||||
Hereunder is the structure for each of the supported report formats by Prowler:
|
||||
|
||||
### HTML
|
||||

|
||||
|
||||
### CSV
|
||||
|
||||
CSV format has a set of common columns for all the providers, and then provider specific columns.
|
||||
The common columns are the following:
|
||||
The CSV format has a common format for all the providers. The following are the available columns:
|
||||
|
||||
- ASSESSMENT_START_TIME
|
||||
- FINDING_UNIQUE_ID
|
||||
- AUTH_METHOD
|
||||
- TIMESTAMP
|
||||
- ACCOUNT_UID
|
||||
- ACCOUNT_NAME
|
||||
- ACCOUNT_EMAIL
|
||||
- ACCOUNT_ORGANIZATION_UID
|
||||
- ACCOUNT_ORGANIZATION_NAME
|
||||
- ACCOUNT_TAGS
|
||||
- FINDING_UID
|
||||
- PROVIDER
|
||||
- CHECK_ID
|
||||
- CHECK_TITLE
|
||||
- CHECK_TYPE
|
||||
- STATUS
|
||||
- STATUS_EXTENDED
|
||||
- MUTED
|
||||
- SERVICE_NAME
|
||||
- SUBSERVICE_NAME
|
||||
- SEVERITY
|
||||
- RESOURCE_TYPE
|
||||
- RESOURCE_UID
|
||||
- RESOURCE_NAME
|
||||
- RESOURCE_DETAILS
|
||||
- RESOURCE_TAGS
|
||||
- PARTITION
|
||||
- REGION
|
||||
- DESCRIPTION
|
||||
- RISK
|
||||
- RELATED_URL
|
||||
- REMEDIATION_RECOMMENDATION_TEXT
|
||||
- REMEDIATION_RECOMMENDATION_URL
|
||||
- REMEDIATION_RECOMMENDATION_CODE_NATIVEIAC
|
||||
- REMEDIATION_RECOMMENDATION_CODE_TERRAFORM
|
||||
- REMEDIATION_RECOMMENDATION_CODE_CLI
|
||||
- REMEDIATION_RECOMMENDATION_CODE_OTHER
|
||||
- REMEDIATION_CODE_NATIVEIAC
|
||||
- REMEDIATION_CODE_TERRAFORM
|
||||
- REMEDIATION_CODE_CLI
|
||||
- REMEDIATION_CODE_OTHER
|
||||
- COMPLIANCE
|
||||
- CATEGORIES
|
||||
- DEPENDS_ON
|
||||
- RELATED_TO
|
||||
- NOTES
|
||||
|
||||
And then by the provider specific columns:
|
||||
|
||||
#### AWS
|
||||
|
||||
- PROFILE
|
||||
- ACCOUNT_ID
|
||||
- ACCOUNT_NAME
|
||||
- ACCOUNT_EMAIL
|
||||
- ACCOUNT_ARN
|
||||
- ACCOUNT_ORG
|
||||
- ACCOUNT_TAGS
|
||||
- REGION
|
||||
- RESOURCE_ID
|
||||
- RESOURCE_ARN
|
||||
|
||||
#### AZURE
|
||||
|
||||
- TENANT_DOMAIN
|
||||
- SUBSCRIPTION
|
||||
- RESOURCE_ID
|
||||
- RESOURCE_NAME
|
||||
|
||||
#### GCP
|
||||
|
||||
- PROJECT_ID
|
||||
- LOCATION
|
||||
- RESOURCE_ID
|
||||
- RESOURCE_NAME
|
||||
|
||||
#### KUBERNETES
|
||||
|
||||
- NAMESPACE
|
||||
- RESOURCE_ID
|
||||
- RESOURCE_NAME
|
||||
- PROWLER_VERSION
|
||||
|
||||
???+ note
|
||||
Since Prowler v4 the CSV column delimiter is the semicolon (`;`)
|
||||
|
||||
### JSON
|
||||
|
||||
The following code is an example output of the JSON format:
|
||||
|
||||
```json
|
||||
[{
|
||||
"AssessmentStartTime": "2022-12-01T14:16:57.354413",
|
||||
"FindingUniqueId": "",
|
||||
"Provider": "aws",
|
||||
"Profile": "dev",
|
||||
"AccountId": "ACCOUNT_ID",
|
||||
"OrganizationsInfo": null,
|
||||
"Region": "eu-west-1",
|
||||
"CheckID": "rds_instance_minor_version_upgrade_enabled",
|
||||
"CheckTitle": "Ensure RDS instances have minor version upgrade enabled.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "rds",
|
||||
"SubServiceName": "",
|
||||
"Status": "PASS",
|
||||
"StatusExtended": "RDS Instance rds-instance-id has minor version upgrade enabled.",
|
||||
"Severity": "low",
|
||||
"ResourceId": "rds-instance-id",
|
||||
"ResourceArn": "",
|
||||
"ResourceTags": {
|
||||
"test": "test",
|
||||
"enironment": "dev"
|
||||
},
|
||||
"ResourceType": "AwsRdsDbInstance",
|
||||
"ResourceDetails": "",
|
||||
"Description": "Ensure RDS instances have minor version upgrade enabled.",
|
||||
"Risk": "Auto Minor Version Upgrade is a feature that you can enable to have your database automatically upgraded when a new minor database engine version is available. Minor version upgrades often patch security vulnerabilities and fix bugs and therefore should be applied.",
|
||||
"RelatedUrl": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#cloudformation",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#terraform",
|
||||
"CLI": "aws rds modify-db-instance --db-instance-identifier <db_instance_id> --auto-minor-version-upgrade --apply-immediately",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-auto-minor-version-upgrade.html"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable auto minor version upgrade for all databases and environments.",
|
||||
"Url": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Notes": "",
|
||||
"Compliance": {
|
||||
"CIS-1.4": [
|
||||
"1.20"
|
||||
],
|
||||
"CIS-1.5": [
|
||||
"1.20"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"AssessmentStartTime": "2022-12-01T14:16:57.354413",
|
||||
"FindingUniqueId": "",
|
||||
"Provider": "aws",
|
||||
"Profile": "dev",
|
||||
"AccountId": "ACCOUNT_ID",
|
||||
"OrganizationsInfo": null,
|
||||
"Region": "eu-west-1",
|
||||
"CheckID": "rds_instance_minor_version_upgrade_enabled",
|
||||
"CheckTitle": "Ensure RDS instances have minor version upgrade enabled.",
|
||||
"CheckType": [],
|
||||
"ServiceName": "rds",
|
||||
"SubServiceName": "",
|
||||
"Status": "PASS",
|
||||
"StatusExtended": "RDS Instance rds-instance-id has minor version upgrade enabled.",
|
||||
"Severity": "low",
|
||||
"ResourceId": "rds-instance-id",
|
||||
"ResourceArn": "",
|
||||
"ResourceType": "AwsRdsDbInstance",
|
||||
"ResourceTags": {},
|
||||
"Description": "Ensure RDS instances have minor version upgrade enabled.",
|
||||
"Risk": "Auto Minor Version Upgrade is a feature that you can enable to have your database automatically upgraded when a new minor database engine version is available. Minor version upgrades often patch security vulnerabilities and fix bugs and therefore should be applied.",
|
||||
"RelatedUrl": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#cloudformation",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/ensure-aws-db-instance-gets-all-minor-upgrades-automatically#terraform",
|
||||
"CLI": "aws rds modify-db-instance --db-instance-identifier <db_instance_id> --auto-minor-version-upgrade --apply-immediately",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/RDS/rds-auto-minor-version-upgrade.html"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable auto minor version upgrade for all databases and environments.",
|
||||
"Url": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/"
|
||||
}
|
||||
},
|
||||
"Categories": [],
|
||||
"Notes": "",
|
||||
"Compliance": {}
|
||||
}]
|
||||
```
|
||||
Since Prowler v3 the CSV column delimiter is the semicolon (`;`)
|
||||
|
||||
|
||||
### JSON-OCSF
|
||||
|
||||
Based on [Open Cybersecurity Schema Framework Security Finding v1.0.0-rc.3](https://schema.ocsf.io/1.0.0-rc.3/classes/security_finding?extensions=)
|
||||
The JSON-OCSF output format implements the [Detection Finding](https://schema.ocsf.io/1.1.0/classes/detection_finding) from the [OCSF v1.1.0](https://schema.ocsf.io/1.1.0)
|
||||
|
||||
```json
|
||||
[{
|
||||
"finding": {
|
||||
"title": "Check if ACM Certificates are about to expire in specific days or less",
|
||||
"desc": "Check if ACM Certificates are about to expire in specific days or less",
|
||||
"supporting_data": {
|
||||
"Risk": "Expired certificates can impact service availability.",
|
||||
"Notes": ""
|
||||
"metadata": {
|
||||
"event_code": "cloudtrail_multi_region_enabled",
|
||||
"product": {
|
||||
"name": "Prowler",
|
||||
"vendor_name": "Prowler",
|
||||
"version": "4.1.0"
|
||||
},
|
||||
"remediation": {
|
||||
"kb_articles": [
|
||||
"https://docs.aws.amazon.com/config/latest/developerguide/acm-certificate-expiration-check.html"
|
||||
],
|
||||
"desc": "Monitor certificate expiration and take automated action to renew; replace or remove. Having shorter TTL for any security artifact is a general recommendation; but requires additional automation in place. If not longer required delete certificate. Use AWS config using the managed rule: acm-certificate-expiration-check."
|
||||
},
|
||||
"types": [
|
||||
"Data Protection"
|
||||
],
|
||||
"src_url": "https://docs.aws.amazon.com/config/latest/developerguide/acm-certificate-expiration-check.html",
|
||||
"uid": "prowler-aws-acm_certificates_expiration_check-012345678912-eu-west-1-*.xxxxxxxxxxxxxx",
|
||||
"related_events": []
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"severity_id": 4,
|
||||
"severity": "High",
|
||||
"status": "New",
|
||||
"status_code": "FAIL",
|
||||
"status_detail": "No CloudTrail trails enabled and logging were found.",
|
||||
"status_id": 1,
|
||||
"activity_name": "Create",
|
||||
"activity_id": 1,
|
||||
"finding_info": {
|
||||
"created_time": "2024-04-08T11:33:51.870861",
|
||||
"desc": "Ensure CloudTrail is enabled in all regions",
|
||||
"product_uid": "prowler",
|
||||
"title": "Ensure CloudTrail is enabled in all regions",
|
||||
"uid": "prowler-aws-cloudtrail_multi_region_enabled-123456789012-ap-northeast-1-123456789012"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"cloud_partition": "aws",
|
||||
"region": "ap-northeast-1",
|
||||
"group": {
|
||||
"name": "acm"
|
||||
"name": "cloudtrail"
|
||||
},
|
||||
"labels": [],
|
||||
"name": "123456789012",
|
||||
"type": "AwsCloudTrailTrail",
|
||||
"uid": "arn:aws:cloudtrail:ap-northeast-1:123456789012:trail",
|
||||
"data": {
|
||||
"details": ""
|
||||
},
|
||||
"region": "eu-west-1",
|
||||
"name": "xxxxxxxxxxxxxx",
|
||||
"uid": "arn:aws:acm:eu-west-1:012345678912:certificate/xxxxxxxxxxxxxx",
|
||||
"labels": [
|
||||
{
|
||||
"Key": "project",
|
||||
"Value": "prowler-pro"
|
||||
},
|
||||
{
|
||||
"Key": "environment",
|
||||
"Value": "dev"
|
||||
},
|
||||
{
|
||||
"Key": "terraform",
|
||||
"Value": "true"
|
||||
},
|
||||
{
|
||||
"Key": "terraform_state",
|
||||
"Value": "aws"
|
||||
}
|
||||
],
|
||||
"type": "AwsCertificateManagerCertificate",
|
||||
"details": ""
|
||||
}
|
||||
],
|
||||
"status_detail": "ACM Certificate for xxxxxxxxxxxxxx expires in 111 days.",
|
||||
"compliance": {
|
||||
"status": "Success",
|
||||
"requirements": [
|
||||
"CISA: ['your-data-2']",
|
||||
"SOC2: ['cc_6_7']",
|
||||
"MITRE-ATTACK: ['T1040']",
|
||||
"GDPR: ['article_32']",
|
||||
"HIPAA: ['164_308_a_4_ii_a', '164_312_e_1']",
|
||||
"AWS-Well-Architected-Framework-Security-Pillar: ['SEC09-BP01']",
|
||||
"NIST-800-171-Revision-2: ['3_13_1', '3_13_2', '3_13_8', '3_13_11']",
|
||||
"NIST-800-53-Revision-4: ['ac_4', 'ac_17_2', 'sc_12']",
|
||||
"NIST-800-53-Revision-5: ['sc_7_12', 'sc_7_16']",
|
||||
"NIST-CSF-1.1: ['ac_5', 'ds_2']",
|
||||
"RBI-Cyber-Security-Framework: ['annex_i_1_3']",
|
||||
"FFIEC: ['d3-pc-im-b-1']",
|
||||
"FedRamp-Moderate-Revision-4: ['ac-4', 'ac-17-2', 'sc-12']",
|
||||
"FedRAMP-Low-Revision-4: ['ac-17', 'sc-12']"
|
||||
],
|
||||
"status_detail": "ACM Certificate for xxxxxxxxxxxxxx expires in 111 days."
|
||||
},
|
||||
"message": "ACM Certificate for xxxxxxxxxxxxxx expires in 111 days.",
|
||||
"severity_id": 4,
|
||||
"severity": "High",
|
||||
"category_name": "Findings",
|
||||
"category_uid": 2,
|
||||
"class_name": "DetectionFinding",
|
||||
"class_uid": 2004,
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "",
|
||||
"uid": "012345678912"
|
||||
"name": "test-account",
|
||||
"type": "AWS_Account",
|
||||
"type_id": 10,
|
||||
"uid": "123456789012"
|
||||
},
|
||||
"region": "eu-west-1",
|
||||
"org": {
|
||||
"uid": "",
|
||||
"name": ""
|
||||
"name": "",
|
||||
"uid": ""
|
||||
},
|
||||
"provider": "aws",
|
||||
"project_uid": ""
|
||||
"region": "ap-northeast-1"
|
||||
},
|
||||
"time": "2023-06-30 10:28:55.297615",
|
||||
"metadata": {
|
||||
"original_time": "2023-06-30T10:28:55.297615",
|
||||
"profiles": [
|
||||
"dev"
|
||||
],
|
||||
"product": {
|
||||
"language": "en",
|
||||
"name": "Prowler",
|
||||
"version": "3.6.1",
|
||||
"vendor_name": "Prowler/ProwlerPro",
|
||||
"feature": {
|
||||
"name": "acm_certificates_expiration_check",
|
||||
"uid": "acm_certificates_expiration_check",
|
||||
"version": "3.6.1"
|
||||
}
|
||||
},
|
||||
"version": "1.0.0-rc.3"
|
||||
"event_time": "2024-04-08T11:33:51.870861",
|
||||
"remediation": {
|
||||
"desc": "Ensure Logging is set to ON on all regions (even if they are not being used at the moment.",
|
||||
"references": [
|
||||
"aws cloudtrail create-trail --name <trail_name> --bucket-name <s3_bucket_for_cloudtrail> --is-multi-region-trail aws cloudtrail update-trail --name <trail_name> --is-multi-region-trail ",
|
||||
"https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrailconcepts.html#cloudtrail-concepts-management-events"
|
||||
]
|
||||
},
|
||||
"state_id": 0,
|
||||
"state": "New",
|
||||
"status_id": 1,
|
||||
"status": "Success",
|
||||
"type_uid": 200101,
|
||||
"type_name": "Security Finding: Create",
|
||||
"impact_id": 0,
|
||||
"impact": "Unknown",
|
||||
"confidence_id": 0,
|
||||
"confidence": "Unknown",
|
||||
"activity_id": 1,
|
||||
"activity_name": "Create",
|
||||
"category_uid": 2,
|
||||
"category_name": "Findings",
|
||||
"class_uid": 2001,
|
||||
"class_name": "Security Finding"
|
||||
},{
|
||||
"finding": {
|
||||
"title": "Check if ACM Certificates are about to expire in specific days or less",
|
||||
"desc": "Check if ACM Certificates are about to expire in specific days or less",
|
||||
"supporting_data": {
|
||||
"Risk": "Expired certificates can impact service availability.",
|
||||
"Notes": ""
|
||||
},
|
||||
"remediation": {
|
||||
"kb_articles": [
|
||||
"https://docs.aws.amazon.com/config/latest/developerguide/acm-certificate-expiration-check.html"
|
||||
"type_uid": 200401,
|
||||
"type_name": "Create",
|
||||
"unmapped": {
|
||||
"check_type": "Software and Configuration Checks,Industry and Regulatory Standards,CIS AWS Foundations Benchmark",
|
||||
"related_url": "",
|
||||
"categories": "forensics-ready",
|
||||
"depends_on": "",
|
||||
"related_to": "",
|
||||
"notes": "",
|
||||
"compliance": {
|
||||
"CISA": [
|
||||
"your-systems-3",
|
||||
"your-data-2"
|
||||
],
|
||||
"desc": "Monitor certificate expiration and take automated action to renew; replace or remove. Having shorter TTL for any security artifact is a general recommendation; but requires additional automation in place. If not longer required delete certificate. Use AWS config using the managed rule: acm-certificate-expiration-check."
|
||||
},
|
||||
"types": [
|
||||
"Data Protection"
|
||||
],
|
||||
"src_url": "https://docs.aws.amazon.com/config/latest/developerguide/acm-certificate-expiration-check.html",
|
||||
"uid": "prowler-aws-acm_certificates_expiration_check-012345678912-eu-west-1-xxxxxxxxxxxxx",
|
||||
"related_events": []
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"group": {
|
||||
"name": "acm"
|
||||
},
|
||||
"region": "eu-west-1",
|
||||
"name": "xxxxxxxxxxxxx",
|
||||
"uid": "arn:aws:acm:eu-west-1:012345678912:certificate/3ea965a0-368d-4d13-95eb-5042a994edc4",
|
||||
"labels": [
|
||||
{
|
||||
"Key": "name",
|
||||
"Value": "prowler-pro-saas-dev-acm-internal-wildcard"
|
||||
},
|
||||
{
|
||||
"Key": "project",
|
||||
"Value": "prowler-pro-saas"
|
||||
},
|
||||
{
|
||||
"Key": "environment",
|
||||
"Value": "dev"
|
||||
},
|
||||
{
|
||||
"Key": "terraform",
|
||||
"Value": "true"
|
||||
},
|
||||
{
|
||||
"Key": "terraform_state",
|
||||
"Value": "aws/saas/base"
|
||||
}
|
||||
"SOC2": [
|
||||
"cc_2_1",
|
||||
"cc_7_2",
|
||||
"cc_a_1_2"
|
||||
],
|
||||
"CIS-1.4": [
|
||||
"3.1"
|
||||
],
|
||||
"CIS-1.5": [
|
||||
"3.1"
|
||||
],
|
||||
"GDPR": [
|
||||
"article_25",
|
||||
"article_30"
|
||||
],
|
||||
"AWS-Foundational-Security-Best-Practices": [
|
||||
"cloudtrail"
|
||||
],
|
||||
"ISO27001-2013": [
|
||||
"A.12.4"
|
||||
],
|
||||
"HIPAA": [
|
||||
"164_308_a_1_ii_d",
|
||||
"164_308_a_3_ii_a",
|
||||
"164_308_a_6_ii",
|
||||
"164_312_b",
|
||||
"164_312_e_2_i"
|
||||
],
|
||||
"type": "AwsCertificateManagerCertificate",
|
||||
"details": ""
|
||||
}
|
||||
],
|
||||
"status_detail": "ACM Certificate for xxxxxxxxxxxxx expires in 119 days.",
|
||||
"compliance": {
|
||||
"status": "Success",
|
||||
"requirements": [
|
||||
"CISA: ['your-data-2']",
|
||||
"SOC2: ['cc_6_7']",
|
||||
"MITRE-ATTACK: ['T1040']",
|
||||
"GDPR: ['article_32']",
|
||||
"HIPAA: ['164_308_a_4_ii_a', '164_312_e_1']",
|
||||
"AWS-Well-Architected-Framework-Security-Pillar: ['SEC09-BP01']",
|
||||
"NIST-800-171-Revision-2: ['3_13_1', '3_13_2', '3_13_8', '3_13_11']",
|
||||
"NIST-800-53-Revision-4: ['ac_4', 'ac_17_2', 'sc_12']",
|
||||
"NIST-800-53-Revision-5: ['sc_7_12', 'sc_7_16']",
|
||||
"NIST-CSF-1.1: ['ac_5', 'ds_2']",
|
||||
"RBI-Cyber-Security-Framework: ['annex_i_1_3']",
|
||||
"FFIEC: ['d3-pc-im-b-1']",
|
||||
"FedRamp-Moderate-Revision-4: ['ac-4', 'ac-17-2', 'sc-12']",
|
||||
"FedRAMP-Low-Revision-4: ['ac-17', 'sc-12']"
|
||||
],
|
||||
"status_detail": "ACM Certificate for xxxxxxxxxxxxx expires in 119 days."
|
||||
},
|
||||
"message": "ACM Certificate for xxxxxxxxxxxxx expires in 119 days.",
|
||||
"severity_id": 4,
|
||||
"severity": "High",
|
||||
"cloud": {
|
||||
"account": {
|
||||
"name": "",
|
||||
"uid": "012345678912"
|
||||
},
|
||||
"region": "eu-west-1",
|
||||
"org": {
|
||||
"uid": "",
|
||||
"name": ""
|
||||
},
|
||||
"provider": "aws",
|
||||
"project_uid": ""
|
||||
},
|
||||
"time": "2023-06-30 10:28:55.297615",
|
||||
"metadata": {
|
||||
"original_time": "2023-06-30T10:28:55.297615",
|
||||
"profiles": [
|
||||
"dev"
|
||||
],
|
||||
"product": {
|
||||
"language": "en",
|
||||
"name": "Prowler",
|
||||
"version": "3.6.1",
|
||||
"vendor_name": "Prowler/ProwlerPro",
|
||||
"feature": {
|
||||
"name": "acm_certificates_expiration_check",
|
||||
"uid": "acm_certificates_expiration_check",
|
||||
"version": "3.6.1"
|
||||
}
|
||||
},
|
||||
"version": "1.0.0-rc.3"
|
||||
},
|
||||
"state_id": 0,
|
||||
"state": "New",
|
||||
"status_id": 1,
|
||||
"status": "Success",
|
||||
"type_uid": 200101,
|
||||
"type_name": "Security Finding: Create",
|
||||
"impact_id": 0,
|
||||
"impact": "Unknown",
|
||||
"confidence_id": 0,
|
||||
"confidence": "Unknown",
|
||||
"activity_id": 1,
|
||||
"activity_name": "Create",
|
||||
"category_uid": 2,
|
||||
"category_name": "Findings",
|
||||
"class_uid": 2001,
|
||||
"class_name": "Security Finding"
|
||||
}]
|
||||
```
|
||||
|
||||
???+ note
|
||||
Each finding is a `json` object.
|
||||
Each finding is a `json` object within a list.
|
||||
|
||||
### JSON-ASFF
|
||||
|
||||
???+ note
|
||||
Only available when using `--security-hub` or `--output-formats json-asff`
|
||||
|
||||
The following code is an example output of the [JSON-ASFF](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format-syntax.html) format:
|
||||
|
||||
```json
|
||||
@@ -540,70 +288,84 @@ The following code is an example output of the [JSON-ASFF](https://docs.aws.amaz
|
||||
"Url": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/"
|
||||
}
|
||||
}
|
||||
},{
|
||||
"SchemaVersion": "2018-10-08",
|
||||
"Id": "prowler-rds_instance_minor_version_upgrade_enabled-ACCOUNT_ID-eu-west-1-06d21d75e",
|
||||
"ProductArn": "arn:aws:securityhub:eu-west-1::product/prowler/prowler",
|
||||
"RecordState": "ACTIVE",
|
||||
"ProductFields": {
|
||||
"ProviderName": "Prowler",
|
||||
"ProviderVersion": "3.0-beta-21Nov2022",
|
||||
"ProwlerResourceName": "rds-instance-id"
|
||||
},
|
||||
"GeneratorId": "prowler-rds_instance_minor_version_upgrade_enabled",
|
||||
"AwsAccountId": "ACCOUNT_ID",
|
||||
"Types": [],
|
||||
"FirstObservedAt": "2022-12-01T13:16:57Z",
|
||||
"UpdatedAt": "2022-12-01T13:16:57Z",
|
||||
"CreatedAt": "2022-12-01T13:16:57Z",
|
||||
"Severity": {
|
||||
"Label": "LOW"
|
||||
},
|
||||
"Title": "Ensure RDS instances have minor version upgrade enabled.",
|
||||
"Description": "Ensure RDS instances have minor version upgrade enabled.",
|
||||
"Resources": [
|
||||
{
|
||||
"Type": "AwsRdsDbInstance",
|
||||
"Id": "rds-instance-id",
|
||||
"Partition": "aws",
|
||||
"Region": "eu-west-1"
|
||||
}
|
||||
],
|
||||
"Compliance": {
|
||||
"Status": "PASSED",
|
||||
"RelatedRequirements": [
|
||||
"CISA your-systems-2 booting-up-thing-to-do-first-3",
|
||||
"CIS-1.5 2.3.2",
|
||||
"AWS-Foundational-Security-Best-Practices rds",
|
||||
"RBI-Cyber-Security-Framework annex_i_6",
|
||||
"FFIEC d3-cc-pm-b-1 d3-cc-pm-b-3"
|
||||
],
|
||||
"AssociatedStandards": [
|
||||
{
|
||||
"StandardsId": "CISA"
|
||||
},
|
||||
{
|
||||
"StandardsId": "CIS-1.5"
|
||||
},
|
||||
{
|
||||
"StandardsId": "AWS-Foundational-Security-Best-Practices"
|
||||
},
|
||||
{
|
||||
"StandardsId": "RBI-Cyber-Security-Framework"
|
||||
},
|
||||
{
|
||||
"StandardsId": "FFIEC"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Remediation": {
|
||||
"Recommendation": {
|
||||
"Text": "Enable auto minor version upgrade for all databases and environments.",
|
||||
"Url": "https://aws.amazon.com/blogs/database/best-practices-for-upgrading-amazon-rds-to-major-and-minor-versions-of-postgresql/"
|
||||
}
|
||||
}
|
||||
}]
|
||||
```
|
||||
|
||||
???+ note
|
||||
Each finding is a `json` object within a list.
|
||||
|
||||
|
||||
## V4 Deprecations
|
||||
|
||||
Some deprecations have been made to unify formats and improve outputs.
|
||||
|
||||
### HTML
|
||||
|
||||
HTML output format has been deprecated in favor of the new dashboard, use it with `prowler dashboard`. You can read more about it at [here](dashboard.md).
|
||||
|
||||
### JSON
|
||||
|
||||
Native JSON format has been deprecated in favor of JSON [OCSF](https://schema.ocsf.io/) `v1.1.0`.
|
||||
|
||||
The following is the mapping between the native JSON and the Detection Finding from the JSON-OCSF:
|
||||
|
||||
| Native JSON Prowler v3 | JSON-OCSF v.1.1.0 |
|
||||
| --- |---|
|
||||
| AssessmentStartTime | event_time |
|
||||
| FindingUniqueId | finding_info.uid |
|
||||
| Provider | cloud.account.type |
|
||||
| CheckID | metadata.event_code |
|
||||
| CheckTitle | finding_info.title |
|
||||
| CheckType | unmapped.check_type |
|
||||
| ServiceName | resources.group.name |
|
||||
| SubServiceName | _Not mapped yet_ |
|
||||
| Status | status_code |
|
||||
| StatusExtended | status_detail |
|
||||
| Severity | severity |
|
||||
| ResourceType | resources.type |
|
||||
| ResourceDetails | resources.data.details |
|
||||
| Description | finding_info.desc |
|
||||
| Risk | risk_details |
|
||||
| RelatedUrl | unmapped.related_url |
|
||||
| Remediation.Recommendation.Text | remediation.desc |
|
||||
| Remediation.Recommendation.Url | remediation.references |
|
||||
| Remediation.Code.NativeIaC | remediation.references |
|
||||
| Remediation.Code.Terraform | remediation.references |
|
||||
| Remediation.Code.CLI | remediation.references |
|
||||
| Remediation.Code.Other | remediation.references |
|
||||
| Compliance | unmapped.compliance |
|
||||
| Categories | unmapped.categories |
|
||||
| DependsOn | unmapped.depends_on |
|
||||
| RelatedTo | unmapped.related_to |
|
||||
| Notes | unmapped.notes |
|
||||
| Profile | _Not mapped yet_ |
|
||||
| AccountId | cloud.account.uid |
|
||||
| OrganizationsInfo.account_name | cloud.account.name |
|
||||
| OrganizationsInfo.account_email | _Not mapped yet_ |
|
||||
| OrganizationsInfo.account_arn | _Not mapped yet_ |
|
||||
| OrganizationsInfo.account_org | cloud.org.name |
|
||||
| OrganizationsInfo.account_tags | cloud.account.labels |
|
||||
| Region | resources.region |
|
||||
| ResourceId | resources.name |
|
||||
| ResourceArn | resources.uid |
|
||||
| ResourceTags | resources.labels |
|
||||
|
||||
|
||||
### CSV Columns
|
||||
|
||||
In Prowler v3 each provider had some specific columns, different from the rest. These are the cases that have changed in Prowler v4:
|
||||
|
||||
| Provider | v3 | v4 |
|
||||
| --- |---|---|
|
||||
| AWS | PROFILE | AUTH_METHOD |
|
||||
| AWS | ACCOUNT_ID| ACCOUNT_UID |
|
||||
| AWS | ACCOUNT_ORGANIZATION_ARN | ACCOUNT_ORGANIZATION_UID |
|
||||
| AWS | ACCOUNT_ORG | ACCOUNT_ORGANIZATION_NAME |
|
||||
| AWS | FINDING_UNIQUE_ID | FINDING_UID |
|
||||
| AWS | ASSESSMENT_START_TIME | TIMESTAMP |
|
||||
| AZURE | TENANT_DOMAIN | ACCOUNT_ORGANIZATION_NAME |
|
||||
| AZURE | SUBSCRIPTION | ACCOUNT_UID |
|
||||
| GCP | PROJECT_ID | ACCOUNT_UID |
|
||||
| GCP | LOCATION | REGION |
|
||||
| AWS / AZURE / GCP | RESOURCE_ID | RESOURCE_NAME |
|
||||
| AWS / AZURE / GCP | RESOURCE_ARN | RESOURCE_UID |
|
||||
@@ -16,8 +16,23 @@ theme:
|
||||
- navigation.sections
|
||||
- navigation.top
|
||||
palette:
|
||||
primary: black
|
||||
accent: green
|
||||
# Palette toggle for light mode
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: black
|
||||
accent: green
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
# Palette toggle for dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: black
|
||||
accent: green
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
|
||||
|
||||
plugins:
|
||||
- search
|
||||
@@ -38,6 +53,7 @@ nav:
|
||||
- Reporting: tutorials/reporting.md
|
||||
- Compliance: tutorials/compliance.md
|
||||
- Dashboard: tutorials/dashboard.md
|
||||
- Fixer: tutorials/fixer.md
|
||||
- Quick Inventory: tutorials/quick-inventory.md
|
||||
- Slack Integration: tutorials/integrations.md
|
||||
- Configuration File: tutorials/configuration_file.md
|
||||
@@ -58,19 +74,21 @@ nav:
|
||||
- Scan Multiple AWS Accounts: tutorials/aws/multiaccount.md
|
||||
- Send reports to AWS S3: tutorials/aws/s3.md
|
||||
- AWS CloudShell: tutorials/aws/cloudshell.md
|
||||
- Checks v2 to v3 Mapping: tutorials/aws/v2_to_v3_checks_mapping.md
|
||||
- Checks v2 to v3 and v4 Mapping: tutorials/aws/v2_to_v3_checks_mapping.md
|
||||
- Tag-based Scan: tutorials/aws/tag-based-scan.md
|
||||
- Resource ARNs based Scan: tutorials/aws/resource-arn-based-scan.md
|
||||
- Boto3 Configuration: tutorials/aws/boto3-configuration.md
|
||||
- Threat Detection: tutorials/aws/threat-detection.md
|
||||
- Azure:
|
||||
- Authentication: tutorials/azure/authentication.md
|
||||
- Non default clouds: tutorials/azure/use-non-default-cloud.md
|
||||
- Subscriptions: tutorials/azure/subscriptions.md
|
||||
- Google Cloud:
|
||||
- Authentication: tutorials/gcp/authentication.md
|
||||
- Projects: tutorials/gcp/projects.md
|
||||
- Developer Guide:
|
||||
- Introduction: developer-guide/introduction.md
|
||||
- Audit Info: developer-guide/audit-info.md
|
||||
- Provider: developer-guide/provider.md
|
||||
- Services: developer-guide/services.md
|
||||
- Checks: developer-guide/checks.md
|
||||
- Documentation: developer-guide/documentation.md
|
||||
|
||||
@@ -53,16 +53,18 @@ from prowler.providers.common.quick_inventory import run_provider_quick_inventor
|
||||
|
||||
|
||||
def prowler():
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "dashboard":
|
||||
from dashboard.__main__ import dashboard
|
||||
|
||||
sys.exit(dashboard.run(debug=True, port=11666, use_reloader=False))
|
||||
# Parse Arguments
|
||||
parser = ProwlerArgumentParser()
|
||||
args = parser.parse()
|
||||
|
||||
# Save Arguments
|
||||
provider = args.provider
|
||||
if provider == "dashboard":
|
||||
from dashboard import DASHBOARD_ARGS
|
||||
from dashboard.__main__ import dashboard
|
||||
|
||||
sys.exit(dashboard.run(**DASHBOARD_ARGS))
|
||||
|
||||
checks = args.check
|
||||
excluded_checks = args.excluded_check
|
||||
excluded_services = args.excluded_service
|
||||
@@ -213,7 +215,7 @@ def prowler():
|
||||
checks_to_execute,
|
||||
global_provider,
|
||||
custom_checks_metadata,
|
||||
getattr(args, "mutelist_file", None),
|
||||
global_provider.mutelist_file_path,
|
||||
args.config_file,
|
||||
)
|
||||
else:
|
||||
@@ -226,7 +228,15 @@ def prowler():
|
||||
print(f"{Style.BRIGHT}\nRunning Prowler Fixer, please wait...{Style.RESET_ALL}")
|
||||
# Check if there are any FAIL findings
|
||||
if any("FAIL" in finding.status for finding in findings):
|
||||
run_fixer(findings)
|
||||
fixed_findings = run_fixer(findings)
|
||||
if not fixed_findings:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.RED}\nThere were findings to fix, but the fixer failed or it is not implemented for those findings yet. {Style.RESET_ALL}\n"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.GREEN}\n{fixed_findings} findings fixed!{Style.RESET_ALL}\n"
|
||||
)
|
||||
else:
|
||||
print(f"{Style.BRIGHT}{Fore.GREEN}\nNo findings to fix!{Style.RESET_ALL}\n")
|
||||
sys.exit()
|
||||
@@ -240,7 +250,7 @@ def prowler():
|
||||
environ["SLACK_API_TOKEN"],
|
||||
environ["SLACK_CHANNEL_ID"],
|
||||
stats,
|
||||
provider,
|
||||
global_provider,
|
||||
)
|
||||
else:
|
||||
logger.critical(
|
||||
@@ -335,8 +345,10 @@ def prowler():
|
||||
global_provider,
|
||||
global_provider.output_options,
|
||||
)
|
||||
# Only display compliance table if there are findings and it is a default execution
|
||||
if findings and default_execution:
|
||||
# Only display compliance table if there are findings (not all MANUAL) and it is a default execution
|
||||
if (
|
||||
findings and not all(finding.status == "MANUAL" for finding in findings)
|
||||
) and default_execution:
|
||||
compliance_overview = False
|
||||
if not compliance_framework:
|
||||
compliance_framework = get_available_compliance_frameworks(provider)
|
||||
@@ -364,7 +376,11 @@ def prowler():
|
||||
remove_custom_checks_module(checks_folder, provider)
|
||||
|
||||
# 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:
|
||||
if (
|
||||
not args.ignore_exit_code_3
|
||||
and stats["total_fail"] > 0
|
||||
and not stats["all_fails_are_muted"]
|
||||
):
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from prowler.providers.common.common import get_global_provider
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "4.0.0"
|
||||
prowler_version = "4.1.0"
|
||||
square_logo_img = "https://user-images.githubusercontent.com/38561120/235905862-9ece5bd7-9aa3-4e48-807a-3a9035eb8bfb.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
azure_logo = "https://user-images.githubusercontent.com/38561120/235927375-b23e2e0f-8932-49ec-b59c-d89f61c8041d.png"
|
||||
@@ -61,16 +61,20 @@ json_ocsf_file_suffix = ".ocsf.json"
|
||||
default_config_file_path = (
|
||||
f"{pathlib.Path(os.path.dirname(os.path.realpath(__file__)))}/config.yaml"
|
||||
)
|
||||
default_fixer_config_file_path = (
|
||||
f"{pathlib.Path(os.path.dirname(os.path.realpath(__file__)))}/fixer_config.yaml"
|
||||
)
|
||||
|
||||
|
||||
def get_default_mute_file_path(provider: str):
|
||||
"""
|
||||
get_default_mute_file_path returns the default mute file path for the provider
|
||||
"""
|
||||
# TODO: crate default mutelist file for kubernetes, azure and gcp
|
||||
if provider == "aws":
|
||||
return f"{pathlib.Path(os.path.dirname(os.path.realpath(__file__)))}/{provider}_mutelist.yaml"
|
||||
return None
|
||||
# TODO: create default mutelist file for kubernetes, azure and gcp
|
||||
mutelist_path = f"{pathlib.Path(os.path.dirname(os.path.realpath(__file__)))}/{provider}_mutelist.yaml"
|
||||
if not os.path.isfile(mutelist_path):
|
||||
mutelist_path = None
|
||||
return mutelist_path
|
||||
|
||||
|
||||
def check_current_version():
|
||||
@@ -117,12 +121,17 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict:
|
||||
# Not to introduce a breaking change we have to allow the old format config file without any provider keys
|
||||
# and a new format with a key for each provider to include their configuration values within
|
||||
# Check if the new format is passed
|
||||
if "aws" in config_file or "gcp" in config_file or "azure" in config_file:
|
||||
if (
|
||||
"aws" in config_file
|
||||
or "gcp" in config_file
|
||||
or "azure" in config_file
|
||||
or "kubernetes" in config_file
|
||||
):
|
||||
config = config_file.get(provider, {})
|
||||
else:
|
||||
config = config_file if config_file else {}
|
||||
# Not to break Azure and GCP does not support neither use the old config format
|
||||
if provider in ["azure", "gcp"]:
|
||||
# Not to break Azure, K8s and GCP does not support neither use the old config format
|
||||
if provider in ["azure", "gcp", "kubernetes"]:
|
||||
config = {}
|
||||
|
||||
return config
|
||||
@@ -132,3 +141,22 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict:
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_and_validate_fixer_config_file(
|
||||
provider: str, fixer_config_file_path: str
|
||||
) -> dict:
|
||||
"""
|
||||
load_and_validate_fixer_config_file reads the Prowler fixer config file in YAML format from the default location or the file passed with the --fixer-config flag
|
||||
"""
|
||||
try:
|
||||
with open(fixer_config_file_path) as f:
|
||||
fixer_config_file = yaml.safe_load(f)
|
||||
|
||||
return fixer_config_file.get(provider, {})
|
||||
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -52,18 +52,27 @@ aws:
|
||||
# aws.awslambda_function_using_supported_runtimes
|
||||
obsolete_lambda_runtimes:
|
||||
[
|
||||
"java8",
|
||||
"go1.x",
|
||||
"provided",
|
||||
"python3.6",
|
||||
"python2.7",
|
||||
"python3.7",
|
||||
"nodejs4.3",
|
||||
"nodejs4.3-edge",
|
||||
"nodejs6.10",
|
||||
"nodejs",
|
||||
"nodejs8.10",
|
||||
"nodejs10.x",
|
||||
"nodejs12.x",
|
||||
"nodejs14.x",
|
||||
"dotnet5.0",
|
||||
"dotnetcore1.0",
|
||||
"dotnetcore2.0",
|
||||
"dotnetcore2.1",
|
||||
"dotnetcore3.1",
|
||||
"ruby2.5",
|
||||
"ruby2.7",
|
||||
]
|
||||
|
||||
# AWS Organizations
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Fixer configuration file
|
||||
aws:
|
||||
# ec2_ebs_default_encryption
|
||||
# No configuration needed for this check
|
||||
|
||||
# s3_account_level_public_access_blocks
|
||||
# No configuration needed for this check
|
||||
|
||||
# iam_password_policy_* checks:
|
||||
iam_password_policy:
|
||||
MinimumPasswordLength: 14
|
||||
RequireSymbols: True
|
||||
RequireNumbers: True
|
||||
RequireUppercaseCharacters: True
|
||||
RequireLowercaseCharacters: True
|
||||
AllowUsersToChangePassword: True
|
||||
MaxPasswordAge: 90
|
||||
PasswordReusePrevention: 24
|
||||
HardExpiry: False
|
||||
|
||||
# accessanalyzer_enabled
|
||||
accessanalyzer_enabled:
|
||||
AnalyzerName: "DefaultAnalyzer"
|
||||
AnalyzerType: "ACCOUNT_UNUSED_ACCESS"
|
||||
|
||||
# guardduty_is_enabled
|
||||
# No configuration needed for this check
|
||||
|
||||
# securityhub_enabled
|
||||
securityhub_enabled:
|
||||
EnableDefaultStandards: True
|
||||
|
||||
# cloudtrail_multi_region_enabled
|
||||
cloudtrail_multi_region_enabled:
|
||||
TrailName: "DefaultTrail"
|
||||
S3BucketName: "my-cloudtrail-bucket"
|
||||
IsMultiRegionTrail: True
|
||||
EnableLogFileValidation: True
|
||||
# CloudWatchLogsLogGroupArn: "arn:aws:logs:us-east-1:123456789012:log-group:my-cloudtrail-log-group"
|
||||
# CloudWatchLogsRoleArn: "arn:aws:iam::123456789012:role/my-cloudtrail-role"
|
||||
# KmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
|
||||
|
||||
# kms_cmk_rotation_enabled
|
||||
# No configuration needed for this check
|
||||
|
||||
#ec2_ebs_snapshot_account_block_public_access
|
||||
ec2_ebs_snapshot_account_block_public_access:
|
||||
State: "block-all-sharing"
|
||||
|
||||
#ec2_instance_account_imdsv2_enabled
|
||||
# No configuration needed for this check
|
||||
@@ -7,7 +7,6 @@ import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from pkgutil import walk_packages
|
||||
from resource import setrlimit
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
@@ -23,7 +22,6 @@ from prowler.lib.logger import logger
|
||||
from prowler.lib.mutelist.mutelist import mutelist_findings
|
||||
from prowler.lib.outputs.outputs import report
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
|
||||
from prowler.providers.common.common import get_global_provider
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
|
||||
@@ -208,7 +206,7 @@ def list_services(provider: str) -> set:
|
||||
|
||||
def list_fixers(provider: str) -> set:
|
||||
available_fixers = set()
|
||||
checks = recover_checks_from_provider(provider)
|
||||
checks = recover_checks_from_provider(provider, include_fixers=True)
|
||||
# Build list of check's metadata files
|
||||
for check_info in checks:
|
||||
# Build check path name
|
||||
@@ -375,7 +373,9 @@ def parse_checks_from_compliance_framework(
|
||||
return checks_to_execute
|
||||
|
||||
|
||||
def recover_checks_from_provider(provider: str, service: str = None) -> list[tuple]:
|
||||
def recover_checks_from_provider(
|
||||
provider: str, service: str = None, include_fixers: bool = False
|
||||
) -> list[tuple]:
|
||||
"""
|
||||
Recover all checks from the selected provider and service
|
||||
|
||||
@@ -388,7 +388,11 @@ def recover_checks_from_provider(provider: str, service: str = None) -> list[tup
|
||||
# Format: "prowler.providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
check_module_name = module_name.name
|
||||
# We need to exclude common shared libraries in services
|
||||
if check_module_name.count(".") == 6 and "lib" not in check_module_name:
|
||||
if (
|
||||
check_module_name.count(".") == 6
|
||||
and "lib" not in check_module_name
|
||||
and (not check_module_name.endswith("_fixer") or include_fixers)
|
||||
):
|
||||
check_path = module_name.module_finder.path
|
||||
# Check name is the last part of the check_module_name
|
||||
check_name = check_module_name.split(".")[-1]
|
||||
@@ -463,15 +467,18 @@ def run_check(check: Check, output_options) -> list:
|
||||
return findings
|
||||
|
||||
|
||||
def run_fixer(check_findings: list):
|
||||
def run_fixer(check_findings: list) -> int:
|
||||
"""
|
||||
Run the fixer for the check if it exists and there are any FAIL findings
|
||||
Args:
|
||||
check_findings (list): list of findings
|
||||
Returns:
|
||||
int: number of fixed findings
|
||||
"""
|
||||
try:
|
||||
# Map findings to each check
|
||||
findings_dict = {}
|
||||
fixed_findings = 0
|
||||
for finding in check_findings:
|
||||
if finding.check_metadata.CheckID not in findings_dict:
|
||||
findings_dict[finding.check_metadata.CheckID] = []
|
||||
@@ -484,7 +491,7 @@ def run_fixer(check_findings: list):
|
||||
check_module_path = f"prowler.providers.{findings[0].check_metadata.Provider}.services.{findings[0].check_metadata.ServiceName}.{check}.{check}_fixer"
|
||||
lib = import_check(check_module_path)
|
||||
fixer = getattr(lib, "fixer")
|
||||
except AttributeError:
|
||||
except ModuleNotFoundError:
|
||||
logger.error(f"Fixer method not implemented for check {check}")
|
||||
else:
|
||||
print(
|
||||
@@ -492,10 +499,44 @@ def run_fixer(check_findings: list):
|
||||
)
|
||||
for finding in findings:
|
||||
if finding.status == "FAIL":
|
||||
print(
|
||||
f"\t{orange_color}FIXING{Style.RESET_ALL} {finding.region}... {(Fore.GREEN + 'DONE') if fixer(finding.region) else (Fore.RED + 'ERROR')}{Style.RESET_ALL}"
|
||||
)
|
||||
print()
|
||||
# Check what type of fixer is:
|
||||
# - If it is a fixer for a specific resource and region
|
||||
# - If it is a fixer for a specific region
|
||||
# - If it is a fixer for a specific resource
|
||||
if (
|
||||
"region" in fixer.__code__.co_varnames
|
||||
and "resource_id" in fixer.__code__.co_varnames
|
||||
):
|
||||
print(
|
||||
f"\t{orange_color}FIXING{Style.RESET_ALL} {finding.resource_id} in {finding.region}... "
|
||||
)
|
||||
if fixer(
|
||||
resource_id=finding.resource_id,
|
||||
region=finding.region,
|
||||
):
|
||||
fixed_findings += 1
|
||||
print(f"\t{Fore.GREEN}DONE{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"\t{Fore.RED}ERROR{Style.RESET_ALL}")
|
||||
elif "region" in fixer.__code__.co_varnames:
|
||||
print(
|
||||
f"\t{orange_color}FIXING{Style.RESET_ALL} {finding.region}... "
|
||||
)
|
||||
if fixer(region=finding.region):
|
||||
fixed_findings += 1
|
||||
print(f"\t{Fore.GREEN}DONE{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"\t{Fore.RED}ERROR{Style.RESET_ALL}")
|
||||
else:
|
||||
print(
|
||||
f"\t{orange_color}FIXING{Style.RESET_ALL} Resource {finding.resource_id}... "
|
||||
)
|
||||
if fixer(resource_id=finding.resource_id):
|
||||
fixed_findings += 1
|
||||
print(f"\t\t{Fore.GREEN}DONE{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"\t\t{Fore.RED}ERROR{Style.RESET_ALL}")
|
||||
return fixed_findings
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
@@ -515,9 +556,6 @@ def execute_checks(
|
||||
services_executed = set()
|
||||
checks_executed = set()
|
||||
|
||||
# TODO: why is this here?
|
||||
global_provider = get_global_provider()
|
||||
|
||||
# Initialize the Audit Metadata
|
||||
# TODO: this should be done in the provider class
|
||||
global_provider.audit_metadata = Audit_Metadata(
|
||||
@@ -529,7 +567,7 @@ def execute_checks(
|
||||
|
||||
if os.name != "nt":
|
||||
try:
|
||||
from resource import RLIMIT_NOFILE, getrlimit
|
||||
from resource import RLIMIT_NOFILE, getrlimit, setrlimit
|
||||
|
||||
# Check ulimit for the maximum system open files
|
||||
soft, hard = getrlimit(RLIMIT_NOFILE)
|
||||
@@ -693,6 +731,11 @@ def execute(
|
||||
)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider"
|
||||
)
|
||||
check_findings = []
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
@@ -2,13 +2,14 @@ import argparse
|
||||
import sys
|
||||
from argparse import RawTextHelpFormatter
|
||||
|
||||
from dashboard.lib.arguments.arguments import init_dashboard_parser
|
||||
from prowler.config.config import (
|
||||
available_compliance_frameworks,
|
||||
check_current_version,
|
||||
default_config_file_path,
|
||||
default_fixer_config_file_path,
|
||||
default_output_directory,
|
||||
finding_statuses,
|
||||
get_default_mute_file_path,
|
||||
valid_severities,
|
||||
)
|
||||
from prowler.providers.common.arguments import (
|
||||
@@ -24,10 +25,17 @@ class ProwlerArgumentParser:
|
||||
self.parser = argparse.ArgumentParser(
|
||||
prog="prowler",
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,dashboard} ...",
|
||||
epilog="""
|
||||
Available components:
|
||||
dashboard Prowler local dashboard
|
||||
Available Cloud Providers:
|
||||
{aws,azure,gcp,kubernetes}
|
||||
aws AWS Provider
|
||||
azure Azure Provider
|
||||
gcp GCP Provider
|
||||
kubernetes Kubernetes Provider
|
||||
|
||||
Available components:
|
||||
dashboard Local dashboard
|
||||
|
||||
To see the different available options on a specific component, run:
|
||||
prowler {provider|dashboard} -h|--help
|
||||
@@ -40,15 +48,14 @@ Detailed documentation at https://docs.prowler.com
|
||||
"--version",
|
||||
"-v",
|
||||
action="store_true",
|
||||
help="Show Prowler version",
|
||||
help="show Prowler version",
|
||||
)
|
||||
# Common arguments parser
|
||||
self.common_providers_parser = argparse.ArgumentParser(add_help=False)
|
||||
|
||||
# Providers Parser
|
||||
self.subparsers = self.parser.add_subparsers(
|
||||
title="Available cloud providers",
|
||||
dest="provider",
|
||||
title="Available Cloud Providers", dest="provider", help=argparse.SUPPRESS
|
||||
)
|
||||
|
||||
self.__init_outputs_parser__()
|
||||
@@ -64,6 +71,9 @@ Detailed documentation at https://docs.prowler.com
|
||||
# Init Providers Arguments
|
||||
init_providers_parser(self)
|
||||
|
||||
# Dahboard Parser
|
||||
init_dashboard_parser(self)
|
||||
|
||||
def parse(self, args=None) -> argparse.Namespace:
|
||||
"""
|
||||
parse is a wrapper to call parse_args() and do some validation
|
||||
@@ -97,11 +107,11 @@ Detailed documentation at https://docs.prowler.com
|
||||
# A provider is always required
|
||||
if not args.provider:
|
||||
self.parser.error(
|
||||
"A provider is required to see its specific help options."
|
||||
"A provider/component is required to see its specific help options."
|
||||
)
|
||||
|
||||
# Only Logging Configuration
|
||||
if args.only_logs or args.list_checks_json:
|
||||
if args.provider != "dashboard" and (args.only_logs or args.list_checks_json):
|
||||
args.no_banner = True
|
||||
|
||||
# Extra validation for provider arguments
|
||||
@@ -315,13 +325,11 @@ Detailed documentation at https://docs.prowler.com
|
||||
|
||||
def __init_mutelist_parser__(self):
|
||||
mutelist_subparser = self.common_providers_parser.add_argument_group("Mutelist")
|
||||
provider = sys.argv[1] if len(sys.argv) > 1 else "aws"
|
||||
mutelist_subparser.add_argument(
|
||||
"--mutelist-file",
|
||||
"-w",
|
||||
nargs="?",
|
||||
default=get_default_mute_file_path(provider),
|
||||
help="Path for mutelist yaml file. See example prowler/config/<provider>_mutelist.yaml for reference and format. For AWS provider, it also accepts AWS DynamoDB Table, Lambda ARNs or S3 URIs, see more in https://docs.prowler.cloud/en/latest/tutorials/mutelist/",
|
||||
help="Path for mutelist YAML file. See example prowler/config/<provider>_mutelist.yaml for reference and format. For AWS provider, it also accepts AWS DynamoDB Table, Lambda ARNs or S3 URIs, see more in https://docs.prowler.cloud/en/latest/tutorials/mutelist/",
|
||||
)
|
||||
|
||||
def __init_config_parser__(self):
|
||||
@@ -332,6 +340,12 @@ Detailed documentation at https://docs.prowler.com
|
||||
default=default_config_file_path,
|
||||
help="Set configuration file path",
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"--fixer-config",
|
||||
nargs="?",
|
||||
default=default_fixer_config_file_path,
|
||||
help="Set configuration fixer file path",
|
||||
)
|
||||
|
||||
def __init_custom_checks_metadata_parser__(self):
|
||||
# CustomChecksMetadata
|
||||
|
||||
@@ -40,7 +40,7 @@ class FindingOutput(BaseModel):
|
||||
# Optional since depends on permissions
|
||||
account_organization_name: Optional[str]
|
||||
# Optional since depends on permissions
|
||||
account_tags: Optional[str]
|
||||
account_tags: Optional[list[str]]
|
||||
finding_uid: str
|
||||
provider: str
|
||||
check_id: str
|
||||
@@ -69,7 +69,7 @@ class FindingOutput(BaseModel):
|
||||
remediation_code_terraform: str
|
||||
remediation_code_cli: str
|
||||
remediation_code_other: str
|
||||
compliance: str
|
||||
compliance: dict
|
||||
categories: str
|
||||
depends_on: str
|
||||
related_to: str
|
||||
|
||||
@@ -50,6 +50,7 @@ def write_compliance_row_aws_well_architected_framework(
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.compliance.cis_aws import generate_compliance_row_cis_aws
|
||||
from prowler.lib.outputs.compliance.cis_azure import generate_compliance_row_cis_azure
|
||||
@@ -67,3 +71,121 @@ def write_compliance_row_cis(
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
def get_cis_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
sections = {}
|
||||
cis_compliance_table = {
|
||||
"Provider": [],
|
||||
"Section": [],
|
||||
"Level 1": [],
|
||||
"Level 2": [],
|
||||
"Muted": [],
|
||||
}
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
for index, finding in enumerate(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},
|
||||
"Muted": 0,
|
||||
}
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
fail_count.append(index)
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
pass_count.append(index)
|
||||
if "Level 1" in attribute.Profile:
|
||||
if not finding.muted:
|
||||
if finding.status == "FAIL":
|
||||
sections[section]["Level 1"]["FAIL"] += 1
|
||||
else:
|
||||
sections[section]["Level 1"]["PASS"] += 1
|
||||
elif "Level 2" in attribute.Profile:
|
||||
if not finding.muted:
|
||||
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}"
|
||||
)
|
||||
cis_compliance_table["Muted"].append(
|
||||
f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}"
|
||||
)
|
||||
if (
|
||||
len(fail_count) + len(pass_count) + len(muted_count) > 1
|
||||
): # If there are no resources, don't print the compliance table
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{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"
|
||||
)
|
||||
@@ -29,6 +29,7 @@ def generate_compliance_row_cis_aws(
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_AWS_CIS)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ def generate_compliance_row_cis_azure(
|
||||
ResourceId=finding.resource_id,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_AZURE_CIS)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ def generate_compliance_row_cis_gcp(
|
||||
ResourceId=finding.resource_id,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_GCP_CIS)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ def generate_compliance_row_cis_kubernetes(
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
csv_header = generate_csv_fields(Check_Output_CSV_KUBERNETES_CIS)
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
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.cis import get_cis_table, write_compliance_row_cis
|
||||
from prowler.lib.outputs.compliance.ens_rd2022_aws import (
|
||||
get_ens_rd2022_aws_table,
|
||||
write_compliance_row_ens_rd2022_aws,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.generic import write_compliance_row_generic
|
||||
from prowler.lib.outputs.compliance.generic import (
|
||||
get_generic_compliance_table,
|
||||
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 (
|
||||
get_mitre_attack_table,
|
||||
write_compliance_row_mitre_attack_aws,
|
||||
)
|
||||
|
||||
@@ -154,312 +155,41 @@ def display_compliance_table(
|
||||
):
|
||||
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":
|
||||
if attribute.Tipo != "recomendacion":
|
||||
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
|
||||
): # If there are no resources, don't print the compliance table
|
||||
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"
|
||||
)
|
||||
get_ens_rd2022_aws_table(
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance_framework,
|
||||
output_filename,
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
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 "Level 1" in attribute.Profile:
|
||||
if finding.status == "FAIL":
|
||||
sections[section]["Level 1"]["FAIL"] += 1
|
||||
else:
|
||||
sections[section]["Level 1"]["PASS"] += 1
|
||||
elif "Level 2" in attribute.Profile:
|
||||
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
|
||||
): # If there are no resources, don't print the compliance table
|
||||
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"
|
||||
)
|
||||
get_cis_table(
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance_framework,
|
||||
output_filename,
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
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
|
||||
): # If there are no resources, don't print the compliance table
|
||||
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"
|
||||
)
|
||||
get_mitre_attack_table(
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance_framework,
|
||||
output_filename,
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
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
|
||||
): # If there are no resources, don't print the compliance table
|
||||
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"
|
||||
)
|
||||
get_generic_compliance_table(
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance_framework,
|
||||
output_filename,
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color, timestamp
|
||||
from prowler.lib.outputs.compliance.models import Check_Output_CSV_ENS_RD2022
|
||||
from prowler.lib.outputs.csv.csv import generate_csv_fields
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
@@ -43,6 +46,130 @@ def write_compliance_row_ens_rd2022_aws(
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
|
||||
|
||||
def get_ens_rd2022_aws_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
marcos = {}
|
||||
ens_compliance_table = {
|
||||
"Proveedor": [],
|
||||
"Marco/Categoria": [],
|
||||
"Estado": [],
|
||||
"Alto": [],
|
||||
"Medio": [],
|
||||
"Bajo": [],
|
||||
"Opcional": [],
|
||||
"Muted": [],
|
||||
}
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
for index, finding in enumerate(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,
|
||||
"Muted": 0,
|
||||
}
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
marcos[marco_categoria]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL":
|
||||
if (
|
||||
attribute.Tipo != "recomendacion"
|
||||
and index not in fail_count
|
||||
):
|
||||
fail_count.append(index)
|
||||
marcos[marco_categoria][
|
||||
"Estado"
|
||||
] = f"{Fore.RED}NO CUMPLE{Style.RESET_ALL}"
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
pass_count.append(index)
|
||||
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}"
|
||||
)
|
||||
ens_compliance_table["Muted"].append(
|
||||
f"{orange_color}{marcos[marco]['Muted']}{Style.RESET_ALL}"
|
||||
)
|
||||
if (
|
||||
len(fail_count) + len(pass_count) + len(muted_count) > 1
|
||||
): # If there are no resources, don't print the compliance table
|
||||
print(
|
||||
f"\nEstado de Cumplimiento de {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL}:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) NO CUMPLE{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) CUMPLE{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{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"
|
||||
)
|
||||
@@ -1,6 +1,9 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color, timestamp
|
||||
from prowler.lib.outputs.compliance.models import Check_Output_CSV_Generic_Compliance
|
||||
from prowler.lib.outputs.csv.csv import generate_csv_fields
|
||||
from prowler.lib.utils.utils import outputs_unix_timestamp
|
||||
@@ -45,5 +48,58 @@ def write_compliance_row_generic(
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
|
||||
|
||||
def get_generic_compliance_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
for index, finding in enumerate(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.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
fail_count.append(index)
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
pass_count.append(index)
|
||||
if (
|
||||
len(fail_count) + len(pass_count) + len(muted_count) > 1
|
||||
): # If there are no resources, don't print the compliance table
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{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"
|
||||
)
|
||||
@@ -46,6 +46,7 @@ def write_compliance_row_iso27001_2013_aws(
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
@@ -1,6 +1,9 @@
|
||||
from csv import DictWriter
|
||||
|
||||
from prowler.config.config import timestamp
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color, timestamp
|
||||
from prowler.lib.outputs.compliance.models import Check_Output_MITRE_ATTACK
|
||||
from prowler.lib.outputs.csv.csv import generate_csv_fields
|
||||
from prowler.lib.outputs.utils import unroll_list
|
||||
@@ -62,6 +65,101 @@ def write_compliance_row_mitre_attack_aws(
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_id,
|
||||
CheckId=finding.check_metadata.CheckID,
|
||||
Muted=finding.muted,
|
||||
)
|
||||
|
||||
csv_writer.writerow(compliance_row.__dict__)
|
||||
|
||||
|
||||
def get_mitre_attack_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
tactics = {}
|
||||
mitre_compliance_table = {
|
||||
"Provider": [],
|
||||
"Tactic": [],
|
||||
"Status": [],
|
||||
"Muted": [],
|
||||
}
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
for index, finding in enumerate(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, "Muted": 0}
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
tactics[tactic]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL":
|
||||
if index not in fail_count:
|
||||
fail_count.append(index)
|
||||
tactics[tactic]["FAIL"] += 1
|
||||
elif finding.status == "PASS":
|
||||
if index not in pass_count:
|
||||
pass_count.append(index)
|
||||
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 tactics[tactic]["Muted"] > 0:
|
||||
mitre_compliance_table["Muted"].append(
|
||||
f"{orange_color}{tactics[tactic]['Muted']}{Style.RESET_ALL}"
|
||||
)
|
||||
if (
|
||||
len(fail_count) + len(pass_count) + len(muted_count) > 1
|
||||
): # If there are no resources, don't print the compliance table
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{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"
|
||||
)
|
||||
@@ -29,6 +29,7 @@ class Check_Output_MITRE_ATTACK(BaseModel):
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_ENS_RD2022(BaseModel):
|
||||
@@ -56,6 +57,7 @@ class Check_Output_CSV_ENS_RD2022(BaseModel):
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_AWS_CIS(BaseModel):
|
||||
@@ -84,6 +86,7 @@ class Check_Output_CSV_AWS_CIS(BaseModel):
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_AZURE_CIS(BaseModel):
|
||||
@@ -113,6 +116,7 @@ class Check_Output_CSV_AZURE_CIS(BaseModel):
|
||||
ResourceId: str
|
||||
ResourceName: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_GCP_CIS(BaseModel):
|
||||
@@ -142,6 +146,7 @@ class Check_Output_CSV_GCP_CIS(BaseModel):
|
||||
ResourceId: str
|
||||
ResourceName: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_KUBERNETES_CIS(BaseModel):
|
||||
@@ -171,6 +176,7 @@ class Check_Output_CSV_KUBERNETES_CIS(BaseModel):
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_Generic_Compliance(BaseModel):
|
||||
@@ -194,6 +200,7 @@ class Check_Output_CSV_Generic_Compliance(BaseModel):
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_AWS_Well_Architected(BaseModel):
|
||||
@@ -219,6 +226,7 @@ class Check_Output_CSV_AWS_Well_Architected(BaseModel):
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
|
||||
|
||||
class Check_Output_CSV_AWS_ISO27001_2013(BaseModel):
|
||||
@@ -239,3 +247,4 @@ class Check_Output_CSV_AWS_ISO27001_2013(BaseModel):
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
@@ -16,7 +16,9 @@ from prowler.lib.utils.utils import hash_sha512
|
||||
def generate_json_asff_status(status: str, muted: bool = False) -> str:
|
||||
json_asff_status = ""
|
||||
if muted:
|
||||
json_asff_status = "MUTED"
|
||||
# Per AWS Security Hub "MUTED" is not a valid status
|
||||
# https://docs.aws.amazon.com/securityhub/1.0/APIReference/API_Compliance.html
|
||||
json_asff_status = "WARNING"
|
||||
else:
|
||||
if status == "PASS":
|
||||
json_asff_status = "PASSED"
|
||||
|
||||
@@ -81,6 +81,7 @@ def fill_json_ocsf(finding_output: FindingOutput) -> DetectionFinding:
|
||||
status=finding_status.name,
|
||||
status_code=finding_output.status,
|
||||
status_detail=finding_output.status_extended,
|
||||
risk_details=finding_output.risk,
|
||||
resources=[
|
||||
ResourceDetails(
|
||||
# TODO: Check labels for other providers
|
||||
@@ -96,9 +97,11 @@ def fill_json_ocsf(finding_output: FindingOutput) -> DetectionFinding:
|
||||
# TODO: this should be included only if using the Cloud profile
|
||||
cloud_partition=finding_output.partition,
|
||||
region=finding_output.region,
|
||||
data={"details": finding_output.resource_details},
|
||||
)
|
||||
],
|
||||
metadata=Metadata(
|
||||
event_code=finding_output.check_id,
|
||||
product=Product(
|
||||
name="Prowler",
|
||||
vendor_name="Prowler",
|
||||
@@ -107,6 +110,15 @@ def fill_json_ocsf(finding_output: FindingOutput) -> DetectionFinding:
|
||||
),
|
||||
type_uid=DetectionFindingTypeID.Create,
|
||||
type_name=DetectionFindingTypeID.Create.name,
|
||||
unmapped={
|
||||
"check_type": finding_output.check_type,
|
||||
"related_url": finding_output.related_url,
|
||||
"categories": finding_output.categories,
|
||||
"depends_on": finding_output.depends_on,
|
||||
"related_to": finding_output.related_to,
|
||||
"notes": finding_output.notes,
|
||||
"compliance": finding_output.compliance,
|
||||
},
|
||||
)
|
||||
|
||||
if finding_output.provider == "kubernetes":
|
||||
@@ -125,6 +137,7 @@ def fill_json_ocsf(finding_output: FindingOutput) -> DetectionFinding:
|
||||
type_id=cloud_account_type.value,
|
||||
type=cloud_account_type.name,
|
||||
uid=finding_output.account_uid,
|
||||
labels=finding_output.account_tags,
|
||||
),
|
||||
org=Organization(
|
||||
uid=finding_output.account_organization_uid,
|
||||
|
||||
@@ -20,7 +20,7 @@ from prowler.lib.outputs.csv.csv import generate_csv_fields
|
||||
from prowler.lib.outputs.file_descriptors import fill_file_descriptors
|
||||
from prowler.lib.outputs.json_asff.json_asff import fill_json_asff
|
||||
from prowler.lib.outputs.json_ocsf.json_ocsf import fill_json_ocsf
|
||||
from prowler.lib.outputs.utils import unroll_dict
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_list
|
||||
|
||||
|
||||
def stdout_report(finding, color, verbose, status, fix):
|
||||
@@ -125,23 +125,12 @@ def report(check_findings, provider):
|
||||
csv_data = {}
|
||||
csv_data.update(provider_data_mapping)
|
||||
csv_data.update(common_finding_data)
|
||||
csv_data["compliance"] = unroll_dict(
|
||||
get_check_compliance(finding, provider.type, output_options)
|
||||
csv_data["compliance"] = get_check_compliance(
|
||||
finding, provider.type, output_options
|
||||
)
|
||||
finding_output = generate_provider_output(
|
||||
provider, finding, csv_data
|
||||
)
|
||||
|
||||
# CSV
|
||||
if "csv" in file_descriptors:
|
||||
csv_writer = DictWriter(
|
||||
file_descriptors["csv"],
|
||||
fieldnames=generate_csv_fields(FindingOutput),
|
||||
delimiter=";",
|
||||
)
|
||||
|
||||
csv_writer.writerow(finding_output.dict())
|
||||
|
||||
# JSON
|
||||
if "json-ocsf" in file_descriptors:
|
||||
detection_finding = fill_json_ocsf(finding_output)
|
||||
@@ -151,6 +140,22 @@ def report(check_findings, provider):
|
||||
)
|
||||
file_descriptors["json-ocsf"].write(",")
|
||||
|
||||
# CSV
|
||||
if "csv" in file_descriptors:
|
||||
finding_output.compliance = unroll_dict(
|
||||
finding_output.compliance
|
||||
)
|
||||
finding_output.account_tags = unroll_list(
|
||||
finding_output.account_tags, ","
|
||||
)
|
||||
csv_writer = DictWriter(
|
||||
file_descriptors["csv"],
|
||||
fieldnames=generate_csv_fields(FindingOutput),
|
||||
delimiter=";",
|
||||
)
|
||||
|
||||
csv_writer.writerow(finding_output.dict())
|
||||
|
||||
else: # No service resources in the whole account
|
||||
color = set_report_color("MANUAL")
|
||||
if output_options.verbose:
|
||||
@@ -200,6 +205,7 @@ def extract_findings_statistics(findings: list) -> dict:
|
||||
total_fail = 0
|
||||
resources = set()
|
||||
findings_count = 0
|
||||
all_fails_are_muted = True
|
||||
|
||||
for finding in findings:
|
||||
# Save the resource_id
|
||||
@@ -210,10 +216,13 @@ def extract_findings_statistics(findings: list) -> dict:
|
||||
if finding.status == "FAIL":
|
||||
total_fail += 1
|
||||
findings_count += 1
|
||||
if not finding.muted and all_fails_are_muted:
|
||||
all_fails_are_muted = False
|
||||
|
||||
stats["total_pass"] = total_pass
|
||||
stats["total_fail"] = total_fail
|
||||
stats["resources_count"] = len(resources)
|
||||
stats["findings_count"] = findings_count
|
||||
stats["all_fails_are_muted"] = all_fails_are_muted
|
||||
|
||||
return stats
|
||||
@@ -40,11 +40,13 @@ def display_summary_table(
|
||||
entity_type = "Context"
|
||||
audited_entities = provider.identity.context
|
||||
|
||||
if findings:
|
||||
# Check if there are findings and that they are not all MANUAL
|
||||
if findings and not all(finding.status == "MANUAL" for finding in findings):
|
||||
current = {
|
||||
"Service": "",
|
||||
"Provider": "",
|
||||
"Total": 0,
|
||||
"Pass": 0,
|
||||
"Critical": 0,
|
||||
"High": 0,
|
||||
"Medium": 0,
|
||||
@@ -70,9 +72,9 @@ def display_summary_table(
|
||||
):
|
||||
add_service_to_table(findings_table, current)
|
||||
|
||||
current["Total"] = current["Muted"] = current["Critical"] = current[
|
||||
"High"
|
||||
] = current["Medium"] = current["Low"] = 0
|
||||
current["Total"] = current["Pass"] = current["Muted"] = current[
|
||||
"Critical"
|
||||
] = current["High"] = current["Medium"] = current["Low"] = 0
|
||||
|
||||
current["Service"] = finding.check_metadata.ServiceName
|
||||
current["Provider"] = finding.check_metadata.Provider
|
||||
@@ -83,6 +85,7 @@ def display_summary_table(
|
||||
current["Muted"] += 1
|
||||
if finding.status == "PASS":
|
||||
pass_count += 1
|
||||
current["Pass"] += 1
|
||||
elif finding.status == "FAIL":
|
||||
fail_count += 1
|
||||
if finding.check_metadata.Severity == "critical":
|
||||
@@ -155,7 +158,7 @@ def add_service_to_table(findings_table, current):
|
||||
)
|
||||
current["Status"] = f"{Fore.RED}FAIL ({total_fails}){Style.RESET_ALL}"
|
||||
else:
|
||||
current["Status"] = f"{Fore.GREEN}PASS ({current['Total']}){Style.RESET_ALL}"
|
||||
current["Status"] = f"{Fore.GREEN}PASS ({current['Pass']}){Style.RESET_ALL}"
|
||||
|
||||
findings_table["Provider"].append(current["Provider"])
|
||||
findings_table["Service"].append(current["Service"])
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
def unroll_list(listed_items: list):
|
||||
def unroll_list(listed_items: list, separator: str = "|"):
|
||||
unrolled_items = ""
|
||||
separator = "|"
|
||||
if listed_items:
|
||||
for item in listed_items:
|
||||
if not unrolled_items:
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import grp
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
|
||||
try:
|
||||
import grp
|
||||
import pwd
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
@@ -10,10 +10,13 @@ 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, load_and_validate_config_file
|
||||
from prowler.config.config import (
|
||||
aws_services_json_file,
|
||||
load_and_validate_config_file,
|
||||
load_and_validate_fixer_config_file,
|
||||
)
|
||||
from prowler.lib.check.check import list_modules, recover_checks_from_service
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.mutelist.mutelist import parse_mutelist_file
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
|
||||
from prowler.providers.aws.config import (
|
||||
AWS_STS_GLOBAL_ENDPOINT_REGION,
|
||||
@@ -50,7 +53,6 @@ class AwsProvider(Provider):
|
||||
_audit_config: dict
|
||||
_scan_unused_services: bool = False
|
||||
_enabled_regions: set = set()
|
||||
_mutelist: dict = {}
|
||||
_output_options: AWSOutputOptions
|
||||
# TODO: this is not optional, enforce for all providers
|
||||
audit_metadata: Audit_Metadata
|
||||
@@ -224,12 +226,18 @@ class AwsProvider(Provider):
|
||||
# Set ignore unused services
|
||||
self._scan_unused_services = scan_unused_services
|
||||
|
||||
# TODO: move this to the providers, pending for AWS, GCP, AZURE and K8s
|
||||
# Audit Config
|
||||
self._audit_config = {}
|
||||
if hasattr(arguments, "config_file"):
|
||||
self._audit_config = load_and_validate_config_file(
|
||||
self._type, arguments.config_file
|
||||
)
|
||||
self._fixer_config = {}
|
||||
if hasattr(arguments, "fixer_config"):
|
||||
self._fixer_config = load_and_validate_fixer_config_file(
|
||||
self._type, arguments.fixer_config
|
||||
)
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
@@ -259,6 +267,10 @@ class AwsProvider(Provider):
|
||||
def audit_config(self):
|
||||
return self._audit_config
|
||||
|
||||
@property
|
||||
def fixer_config(self):
|
||||
return self._fixer_config
|
||||
|
||||
@property
|
||||
def output_options(self):
|
||||
return self._output_options
|
||||
@@ -270,20 +282,6 @@ class AwsProvider(Provider):
|
||||
arguments, bulk_checks_metadata, self._identity
|
||||
)
|
||||
|
||||
@property
|
||||
def mutelist(self):
|
||||
return self._mutelist
|
||||
|
||||
@mutelist.setter
|
||||
def mutelist(self, mutelist_path):
|
||||
if mutelist_path:
|
||||
mutelist = parse_mutelist_file(
|
||||
mutelist_path, self._session.current_session, self._identity.account
|
||||
)
|
||||
else:
|
||||
mutelist = {}
|
||||
self._mutelist = mutelist
|
||||
|
||||
@property
|
||||
def get_output_mapping(self):
|
||||
return {
|
||||
@@ -632,7 +630,7 @@ class AwsProvider(Provider):
|
||||
audited_regions.add(region)
|
||||
return audited_regions
|
||||
|
||||
def get_tagged_resources(self, input_resource_tags: list[str]):
|
||||
def get_tagged_resources(self, input_resource_tags: list[str]) -> list[str]:
|
||||
"""
|
||||
Returns a list of the resources that are going to be scanned based on the given input tags.
|
||||
|
||||
@@ -791,18 +789,25 @@ class AwsProvider(Provider):
|
||||
|
||||
def get_aws_enabled_regions(self, current_session: Session) -> set:
|
||||
"""get_aws_enabled_regions returns a set of enabled AWS regions"""
|
||||
try:
|
||||
# EC2 Client to check enabled regions
|
||||
service = "ec2"
|
||||
default_region = self.get_default_region(service)
|
||||
ec2_client = current_session.client(service, region_name=default_region)
|
||||
|
||||
# EC2 Client to check enabled regions
|
||||
service = "ec2"
|
||||
default_region = self.get_default_region(service)
|
||||
ec2_client = current_session.client(service, region_name=default_region)
|
||||
enabled_regions = set()
|
||||
# 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"))
|
||||
|
||||
enabled_regions = set()
|
||||
# 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
|
||||
return enabled_regions
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return set()
|
||||
|
||||
# TODO: review this function
|
||||
# Maybe this should be done within the AwsProvider and not in __main__.py
|
||||
|
||||
@@ -1254,6 +1254,7 @@
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-west-3",
|
||||
"us-east-1",
|
||||
@@ -1277,6 +1278,7 @@
|
||||
"braket": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"eu-west-2",
|
||||
"us-east-1",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
@@ -2269,6 +2271,7 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
@@ -2278,6 +2281,7 @@
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -2304,6 +2308,7 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
@@ -2313,6 +2318,7 @@
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -2341,6 +2347,7 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
@@ -2350,6 +2357,7 @@
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -4214,6 +4222,7 @@
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
@@ -4644,6 +4653,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
@@ -5219,16 +5229,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"iot-roborunner": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"eu-central-1",
|
||||
"us-east-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"iot1click-devices": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -5579,6 +5579,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
@@ -6169,7 +6170,10 @@
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lightsail": {
|
||||
@@ -6521,7 +6525,9 @@
|
||||
"aws": [
|
||||
"us-east-1"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-cn": [
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
@@ -7194,6 +7200,7 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -8667,16 +8674,21 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
@@ -8734,18 +8746,29 @@
|
||||
"s3control": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -9437,6 +9460,7 @@
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -9963,6 +9987,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
@@ -10228,6 +10253,24 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"timestream-influxdb": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-west-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"timestream-write": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -10494,6 +10537,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
@@ -10502,6 +10546,7 @@
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"sa-east-1",
|
||||
@@ -10511,7 +10556,10 @@
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"vmwarecloudonaws": {
|
||||
|
||||
@@ -30,9 +30,9 @@ def get_organizations_metadata(
|
||||
def parse_organizations_metadata(metadata: dict, tags: dict) -> AWSOrganizationsInfo:
|
||||
try:
|
||||
# Convert Tags dictionary to String
|
||||
account_details_tags = ""
|
||||
account_details_tags = []
|
||||
for tag in tags.get("Tags", {}):
|
||||
account_details_tags += f"{tag['Key']}:{tag['Value']},"
|
||||
account_details_tags.append(f"{tag['Key']}:{tag['Value']}")
|
||||
|
||||
account_details = metadata.get("Account", {})
|
||||
|
||||
@@ -46,7 +46,7 @@ def parse_organizations_metadata(metadata: dict, tags: dict) -> AWSOrganizations
|
||||
organization_account_arn=aws_account_arn.arn,
|
||||
organization_arn=aws_organization_arn,
|
||||
organization_id=aws_organization_id,
|
||||
account_tags=account_details_tags.rstrip(","),
|
||||
account_tags=account_details_tags,
|
||||
)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
|
||||
@@ -10,24 +10,25 @@ def send_to_s3_bucket(
|
||||
output_filename, output_directory, output_mode, output_bucket_name, audit_session
|
||||
):
|
||||
try:
|
||||
filename = ""
|
||||
# Get only last part of the path
|
||||
if output_mode == "csv":
|
||||
filename = f"{output_filename}{csv_file_suffix}"
|
||||
elif output_mode == "json-asff":
|
||||
filename = f"{output_filename}{json_asff_file_suffix}"
|
||||
elif output_mode == "json-ocsf":
|
||||
filename = f"{output_filename}{json_ocsf_file_suffix}"
|
||||
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}")
|
||||
# File location
|
||||
file_name = output_directory + "/" + filename
|
||||
|
||||
# S3 Object name
|
||||
bucket_directory = get_s3_object_path(output_directory)
|
||||
object_name = bucket_directory + "/" + output_mode + "/" + filename
|
||||
filename = ""
|
||||
# Get only last part of the path
|
||||
if output_mode in ["csv", "json-asff", "json-ocsf"]:
|
||||
if output_mode == "csv":
|
||||
filename = f"{output_filename}{csv_file_suffix}"
|
||||
elif output_mode == "json-asff":
|
||||
filename = f"{output_filename}{json_asff_file_suffix}"
|
||||
elif output_mode == "json-ocsf":
|
||||
filename = f"{output_filename}{json_ocsf_file_suffix}"
|
||||
file_name = output_directory + "/" + filename
|
||||
object_name = bucket_directory + "/" + output_mode + "/" + filename
|
||||
else: # Compliance output mode
|
||||
filename = f"{output_filename}_{output_mode}{csv_file_suffix}"
|
||||
file_name = output_directory + "/compliance/" + filename
|
||||
object_name = bucket_directory + "/compliance/" + filename
|
||||
|
||||
logger.info(f"Sending output file {filename} to S3 bucket {output_bucket_name}")
|
||||
|
||||
s3_client = audit_session.client("s3")
|
||||
s3_client.upload_file(file_name, output_bucket_name, object_name)
|
||||
|
||||
@@ -196,7 +196,6 @@ def __send_findings_to_security_hub__(
|
||||
for findings in list_chunked:
|
||||
batch_import = security_hub_client.batch_import_findings(Findings=findings)
|
||||
if batch_import["FailedCount"] > 0:
|
||||
print(batch_import["FailedCount"])
|
||||
failed_import = batch_import["FailedFindings"][0]
|
||||
logger.error(
|
||||
f"Failed to send findings to AWS Security Hub -- {failed_import['ErrorCode']} -- {failed_import['ErrorMessage']}"
|
||||
|
||||
@@ -31,6 +31,7 @@ class AWSService:
|
||||
self.audit_resources = provider.audit_resources
|
||||
self.audited_checks = provider.audit_metadata.expected_checks
|
||||
self.audit_config = provider.audit_config
|
||||
self.fixer_config = provider.fixer_config
|
||||
|
||||
# AWS Session
|
||||
self.session = provider.session.current_session
|
||||
|
||||
@@ -16,7 +16,7 @@ class AWSOrganizationsInfo:
|
||||
organization_account_arn: str
|
||||
organization_arn: str
|
||||
organization_id: str
|
||||
account_tags: str
|
||||
account_tags: list[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.aws.services.accessanalyzer.accessanalyzer_client import (
|
||||
accessanalyzer_client,
|
||||
)
|
||||
|
||||
|
||||
def fixer(region):
|
||||
"""
|
||||
Enable Access Analyzer in a region. Requires the access-analyzer:CreateAnalyzer permission:
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "access-analyzer:CreateAnalyzer",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Args:
|
||||
region (str): AWS region
|
||||
Returns:
|
||||
bool: True if Access Analyzer is enabled, False otherwise
|
||||
"""
|
||||
try:
|
||||
regional_client = accessanalyzer_client.regional_clients[region]
|
||||
regional_client.create_analyzer(
|
||||
analyzerName=accessanalyzer_client.fixer_config.get(
|
||||
"accessanalyzer_enabled", {}
|
||||
).get("AnalyzerName", "DefaultAnalyzer"),
|
||||
type=accessanalyzer_client.fixer_config.get(
|
||||
"accessanalyzer_enabled", {}
|
||||
).get("AnalyzerType", "ACCOUNT_UNUSED_ACCESS"),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -17,7 +17,7 @@
|
||||
"Code": {
|
||||
"CLI": "No command available.",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.bridgecrew.io/docs/iam_18-maintain-contact-details#aws-console",
|
||||
"Other": "https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.bridgecrew.io/docs/iam_18-maintain-contact-details#aws-console",
|
||||
"Other": "https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
|
||||
@@ -6,22 +6,26 @@ class account_maintain_different_contact_details_to_security_billing_and_operati
|
||||
Check
|
||||
):
|
||||
def execute(self):
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = account_client.region
|
||||
report.resource_id = account_client.audited_account
|
||||
report.resource_arn = account_client.audited_account_arn
|
||||
findings = []
|
||||
if account_client.contact_base:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = account_client.region
|
||||
report.resource_id = account_client.audited_account
|
||||
report.resource_arn = account_client.audited_account_arn
|
||||
|
||||
if (
|
||||
len(account_client.contact_phone_numbers)
|
||||
== account_client.number_of_contacts
|
||||
and len(account_client.contact_names) == account_client.number_of_contacts
|
||||
# This is because the primary contact has no email field
|
||||
and len(account_client.contact_emails)
|
||||
== account_client.number_of_contacts - 1
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = "SECURITY, BILLING and OPERATIONS contacts found and they are different between each other and between ROOT contact."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact."
|
||||
return [report]
|
||||
if (
|
||||
len(account_client.contact_phone_numbers)
|
||||
== account_client.number_of_contacts
|
||||
and len(account_client.contact_names)
|
||||
== account_client.number_of_contacts
|
||||
# This is because the primary contact has no email field
|
||||
and len(account_client.contact_emails)
|
||||
== account_client.number_of_contacts - 1
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = "SECURITY, BILLING and OPERATIONS contacts found and they are different between each other and between ROOT contact."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -17,7 +17,7 @@
|
||||
"Code": {
|
||||
"CLI": "No command available.",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.bridgecrew.io/docs/iam_19#aws-console",
|
||||
"Other": "https://docs.prowler.com/checks/aws/iam-policies/iam_19#aws-console",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"Code": {
|
||||
"CLI": "No command available.",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.bridgecrew.io/docs/iam_15",
|
||||
"Other": "https://docs.prowler.com/checks/aws/iam-policies/iam_15",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
|
||||
@@ -18,28 +18,29 @@ class Account(AWSService):
|
||||
self.contacts_security = self.__get_alternate_contact__("SECURITY")
|
||||
self.contacts_operations = self.__get_alternate_contact__("OPERATIONS")
|
||||
|
||||
# Set of contact phone numbers
|
||||
self.contact_phone_numbers = {
|
||||
self.contact_base.phone_number,
|
||||
self.contacts_billing.phone_number,
|
||||
self.contacts_security.phone_number,
|
||||
self.contacts_operations.phone_number,
|
||||
}
|
||||
if self.contact_base:
|
||||
# Set of contact phone numbers
|
||||
self.contact_phone_numbers = {
|
||||
self.contact_base.phone_number,
|
||||
self.contacts_billing.phone_number,
|
||||
self.contacts_security.phone_number,
|
||||
self.contacts_operations.phone_number,
|
||||
}
|
||||
|
||||
# Set of contact names
|
||||
self.contact_names = {
|
||||
self.contact_base.name,
|
||||
self.contacts_billing.name,
|
||||
self.contacts_security.name,
|
||||
self.contacts_operations.name,
|
||||
}
|
||||
# Set of contact names
|
||||
self.contact_names = {
|
||||
self.contact_base.name,
|
||||
self.contacts_billing.name,
|
||||
self.contacts_security.name,
|
||||
self.contacts_operations.name,
|
||||
}
|
||||
|
||||
# Set of contact emails
|
||||
self.contact_emails = {
|
||||
self.contacts_billing.email,
|
||||
self.contacts_security.email,
|
||||
self.contacts_operations.email,
|
||||
}
|
||||
# Set of contact emails
|
||||
self.contact_emails = {
|
||||
self.contacts_billing.email,
|
||||
self.contacts_security.email,
|
||||
self.contacts_operations.email,
|
||||
}
|
||||
|
||||
def __get_contact_information__(self):
|
||||
try:
|
||||
@@ -53,10 +54,16 @@ class Account(AWSService):
|
||||
phone_number=primary_account_contact.get("PhoneNumber"),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return Contact(type="PRIMARY")
|
||||
if error.response["Error"]["Code"] == "AccessDeniedException":
|
||||
logger.error(
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
logger.error(
|
||||
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return Contact(type="PRIMARY")
|
||||
|
||||
def __get_alternate_contact__(self, contact_type: str):
|
||||
try:
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/public_6-api-gateway-authorizer-set#cloudformation",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/public-policies/public_6-api-gateway-authorizer-set#cloudformation",
|
||||
"Other": "",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/public_6-api-gateway-authorizer-set#terraform"
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/public-policies/public_6-api-gateway-authorizer-set#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Implement Amazon Cognito or a Lambda function to control access to your API.",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/ensure-api-gateway-stage-have-logging-level-defined-as-appropiate#terraform"
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/logging-policies/ensure-api-gateway-stage-have-logging-level-defined-as-appropiate#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Monitoring is an important part of maintaining the reliability, availability and performance of API Gateway and your AWS solutions. You should collect monitoring data from all of the parts of your AWS solution. CloudTrail provides a record of actions taken by a user, role, or an AWS service in API Gateway. Using the information collected by CloudTrail, you can determine the request that was made to API Gateway, the IP address from which the request was made, who made the request, etc.",
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.bridgecrew.io/docs/bc_aws_logging_30#aws-console",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/bc_aws_logging_30#cloudformation"
|
||||
"Other": "https://docs.prowler.com/checks/aws/logging-policies/bc_aws_logging_30#aws-console",
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/logging-policies/bc_aws_logging_30#cloudformation"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Monitoring is an important part of maintaining the reliability, availability and performance of API Gateway and your AWS solutions. You should collect monitoring data from all of the parts of your AWS solution. CloudTrail provides a record of actions taken by a user, role, or an AWS service in API Gateway. Using the information collected by CloudTrail, you can determine the request that was made to API Gateway, the IP address from which the request was made, who made the request, etc.",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"CLI": "aws athena update-work-group --region <REGION> --work-group <workgroup_name> --configuration-updates ResultConfigurationUpdates={EncryptionConfiguration={EncryptionOption=SSE_S3|SSE_KMS|CSE_KMS}}",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Athena/encryption-enabled.html",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/ensure-that-athena-workgroup-is-encrypted#terraform"
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-that-athena-workgroup-is-encrypted#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable Encryption. Use a CMK where possible. It will provide additional management and privacy benefits.",
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws athena update-work-group --region <REGION> --work-group <workgroup_name> --configuration-updates EnforceWorkGroupConfiguration=True",
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/bc_aws_general_33#cloudformation",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_33#cloudformation",
|
||||
"Other": "",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/bc_aws_general_33#terraform"
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_33#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Ensure that workgroup configuration is enforced so it cannot be overriden by client-side settings.",
|
||||
@@ -29,4 +29,4 @@
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/secretsmanager/latest/userguide/lambda-functions.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "https://docs.bridgecrew.io/docs/bc_aws_secrets_3#cli-command",
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/bc_aws_secrets_3#cloudformation",
|
||||
"CLI": "https://docs.prowler.com/checks/aws/secrets-policies/bc_aws_secrets_3#cli-command",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/secrets-policies/bc_aws_secrets_3#cloudformation",
|
||||
"Other": "",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/bc_aws_secrets_3#terraform"
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/secrets-policies/bc_aws_secrets_3#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use Secrets Manager to securely provide database credentials to Lambda functions and secure the databases as well as use the credentials to connect and query them without hardcoding the secrets in code or passing them through environmental variables.",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from botocore.client import ClientError
|
||||
from pydantic import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
@@ -37,6 +38,8 @@ class Backup(AWSService):
|
||||
self.audit_resources,
|
||||
)
|
||||
):
|
||||
if self.backup_vaults is None:
|
||||
self.backup_vaults = []
|
||||
self.backup_vaults.append(
|
||||
BackupVault(
|
||||
arn=configuration.get("BackupVaultArn"),
|
||||
@@ -55,7 +58,13 @@ class Backup(AWSService):
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
except ClientError as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
if error.response["Error"]["Code"] == "AccessDeniedException":
|
||||
if not self.backup_vaults:
|
||||
self.backup_vaults = None
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
|
||||
@@ -5,24 +5,24 @@ from prowler.providers.aws.services.backup.backup_client import backup_client
|
||||
class backup_vaults_encrypted(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
|
||||
for backup_vault in backup_client.backup_vaults:
|
||||
# By default we assume that the result is fail
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Backup Vault {backup_vault.name} is not encrypted."
|
||||
)
|
||||
report.resource_arn = backup_vault.arn
|
||||
report.resource_id = backup_vault.name
|
||||
report.region = backup_vault.region
|
||||
# if it is encrypted we only change the status and the status extended
|
||||
if backup_vault.encryption:
|
||||
report.status = "PASS"
|
||||
if backup_client.backup_vaults:
|
||||
for backup_vault in backup_client.backup_vaults:
|
||||
# By default we assume that the result is fail
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Backup Vault {backup_vault.name} is encrypted."
|
||||
f"Backup Vault {backup_vault.name} is not encrypted."
|
||||
)
|
||||
# then we store the finding
|
||||
findings.append(report)
|
||||
report.resource_arn = backup_vault.arn
|
||||
report.resource_id = backup_vault.name
|
||||
report.region = backup_vault.region
|
||||
# if it is encrypted we only change the status and the status extended
|
||||
if backup_vault.encryption:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Backup Vault {backup_vault.name} is encrypted."
|
||||
)
|
||||
# then we store the finding
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -5,18 +5,19 @@ from prowler.providers.aws.services.backup.backup_client import backup_client
|
||||
class backup_vaults_exist(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "No Backup Vault exist."
|
||||
report.resource_arn = backup_client.backup_vault_arn_template
|
||||
report.resource_id = backup_client.audited_account
|
||||
report.region = backup_client.region
|
||||
if backup_client.backup_vaults:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"At least one backup vault exists: {backup_client.backup_vaults[0].name}."
|
||||
report.resource_arn = backup_client.backup_vaults[0].arn
|
||||
report.resource_id = backup_client.backup_vaults[0].name
|
||||
report.region = backup_client.backup_vaults[0].region
|
||||
if backup_client.backup_vaults is not None:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "No Backup Vault exist."
|
||||
report.resource_arn = backup_client.backup_vault_arn_template
|
||||
report.resource_id = backup_client.audited_account
|
||||
report.region = backup_client.region
|
||||
if backup_client.backup_vaults:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"At least one backup vault exists: {backup_client.backup_vaults[0].name}."
|
||||
report.resource_arn = backup_client.backup_vaults[0].arn
|
||||
report.resource_id = backup_client.backup_vaults[0].name
|
||||
report.region = backup_client.backup_vaults[0].region
|
||||
|
||||
findings.append(report)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -13,7 +13,7 @@
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "https://docs.bridgecrew.io/docs/bc_aws_secrets_2#cli-command",
|
||||
"CLI": "https://docs.prowler.com/checks/aws/secrets-policies/bc_aws_secrets_2#cli-command",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/security-policy.html",
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/networking_32#cloudformation",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/networking-policies/networking_32#cloudformation",
|
||||
"Other": "",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/networking_32#terraform"
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/networking-policies/networking_32#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use HTTPS everywhere possible. It will enforce privacy and protect against account hijacking and other threats.",
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "https://docs.bridgecrew.io/docs/logging_20#cli-command",
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/logging_20#cloudformation",
|
||||
"CLI": "https://docs.prowler.com/checks/aws/logging-policies/logging_20#cli-command",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/logging-policies/logging_20#cloudformation",
|
||||
"Other": "",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/logging_20#terraform"
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/logging-policies/logging_20#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Real-time monitoring can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms. Enable logging for services with defined log rotation. These logs are useful for Incident Response and forensics investigation among other use cases.",
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"RelatedUrl": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/secure-connections-supported-viewer-protocols-ciphers.html",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "https://docs.bridgecrew.io/docs/networking_33#cli-command",
|
||||
"CLI": "https://docs.prowler.com/checks/aws/networking-policies/networking_33#cli-command",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.bridgecrew.io/docs/networking_33#aws-cloudfront-console",
|
||||
"Other": "https://docs.prowler.com/checks/aws/networking-policies/networking_33#aws-cloudfront-console",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-integrated-with-waf.html",
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/bc_aws_general_27#cloudformation",
|
||||
"Other": "https://docs.bridgecrew.io/docs/bc_aws_general_27#cloudfront-console",
|
||||
"Terraform": "https://docs.bridgecrew.io/docs/bc_aws_general_27#terraform"
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_27#cloudformation",
|
||||
"Other": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_27#cloudfront-console",
|
||||
"Terraform": "https://docs.prowler.com/checks/aws/general-policies/bc_aws_general_27#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use AWS WAF to protect your service from common web exploits. These could affect availability and performance; compromise security; or consume excessive resources.",
|
||||
|
||||
@@ -8,28 +8,29 @@ from prowler.providers.aws.services.s3.s3_client import s3_client
|
||||
class cloudtrail_bucket_requires_mfa_delete(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.is_logging:
|
||||
trail_bucket_is_in_account = False
|
||||
trail_bucket = trail.s3_bucket
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Trail {trail.name} bucket ({trail_bucket}) does not have MFA delete enabled."
|
||||
for bucket in s3_client.buckets:
|
||||
if trail_bucket == bucket.name:
|
||||
trail_bucket_is_in_account = True
|
||||
if bucket.mfa_delete:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Trail {trail.name} bucket ({trail_bucket}) has MFA delete enabled."
|
||||
# check if trail bucket is a cross account bucket
|
||||
if not trail_bucket_is_in_account:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = f"Trail {trail.name} bucket ({trail_bucket}) is a cross-account bucket in another account out of Prowler's permissions scope, please check it manually."
|
||||
if cloudtrail_client.trails is not None:
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.is_logging:
|
||||
trail_bucket_is_in_account = False
|
||||
trail_bucket = trail.s3_bucket
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Trail {trail.name} bucket ({trail_bucket}) does not have MFA delete enabled."
|
||||
for bucket in s3_client.buckets:
|
||||
if trail_bucket == bucket.name:
|
||||
trail_bucket_is_in_account = True
|
||||
if bucket.mfa_delete:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Trail {trail.name} bucket ({trail_bucket}) has MFA delete enabled."
|
||||
# check if trail bucket is a cross account bucket
|
||||
if not trail_bucket_is_in_account:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = f"Trail {trail.name} bucket ({trail_bucket}) is a cross-account bucket in another account out of Prowler's permissions scope, please check it manually."
|
||||
|
||||
findings.append(report)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -19,7 +19,7 @@
|
||||
"Code": {
|
||||
"CLI": "aws cloudtrail update-trail --name <trail_name> --cloudwatch-logs-log-group- arn <cloudtrail_log_group_arn> --cloudwatch-logs-role-arn <cloudtrail_cloudwatchLogs_role_arn>",
|
||||
"NativeIaC": "",
|
||||
"Other": "https://docs.bridgecrew.io/docs/logging_4#aws-console",
|
||||
"Other": "https://docs.prowler.com/checks/aws/logging-policies/logging_4#aws-console",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
|
||||
@@ -11,37 +11,38 @@ maximum_time_without_logging = 1
|
||||
class cloudtrail_cloudwatch_logging_enabled(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.name:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "PASS"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = (
|
||||
f"Multiregion trail {trail.name} has been logging the last 24h."
|
||||
)
|
||||
else:
|
||||
report.status_extended = f"Single region trail {trail.name} has been logging the last 24h."
|
||||
if trail.latest_cloudwatch_delivery_time:
|
||||
last_log_delivery = (
|
||||
datetime.now().replace(tzinfo=timezone.utc)
|
||||
- trail.latest_cloudwatch_delivery_time
|
||||
)
|
||||
if last_log_delivery > timedelta(days=maximum_time_without_logging):
|
||||
if cloudtrail_client.trails is not None:
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.name:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "PASS"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = f"Multiregion trail {trail.name} has been logging the last 24h."
|
||||
else:
|
||||
report.status_extended = f"Single region trail {trail.name} has been logging the last 24h."
|
||||
if trail.latest_cloudwatch_delivery_time:
|
||||
last_log_delivery = (
|
||||
datetime.now().replace(tzinfo=timezone.utc)
|
||||
- trail.latest_cloudwatch_delivery_time
|
||||
)
|
||||
if last_log_delivery > timedelta(
|
||||
days=maximum_time_without_logging
|
||||
):
|
||||
report.status = "FAIL"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = f"Multiregion trail {trail.name} is not logging in the last 24h."
|
||||
else:
|
||||
report.status_extended = f"Single region trail {trail.name} is not logging in the last 24h."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = f"Multiregion trail {trail.name} is not logging in the last 24h."
|
||||
report.status_extended = f"Multiregion trail {trail.name} is not logging in the last 24h or not configured to deliver logs."
|
||||
else:
|
||||
report.status_extended = f"Single region trail {trail.name} is not logging in the last 24h."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = f"Multiregion trail {trail.name} is not logging in the last 24h or not configured to deliver logs."
|
||||
else:
|
||||
report.status_extended = f"Single region trail {trail.name} is not logging in the last 24h or not configured to deliver logs."
|
||||
findings.append(report)
|
||||
report.status_extended = f"Single region trail {trail.name} is not logging in the last 24h or not configured to deliver logs."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -7,19 +7,18 @@ from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
|
||||
class cloudtrail_insights_exist(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.is_logging:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Trail {trail.name} does not have insight selectors and it is logging."
|
||||
if trail.has_insight_selectors:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Trail {trail.name} has insight selectors and it is logging."
|
||||
)
|
||||
findings.append(report)
|
||||
if cloudtrail_client.trails is not None:
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.is_logging:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Trail {trail.name} does not have insight selectors and it is logging."
|
||||
if trail.has_insight_selectors:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Trail {trail.name} has insight selectors and it is logging."
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -18,7 +18,7 @@
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws cloudtrail update-trail --name <trail_name> --kms-id <cloudtrail_kms_key> aws kms put-key-policy --key-id <cloudtrail_kms_key> --policy <cloudtrail_kms_key_policy>",
|
||||
"NativeIaC": "https://docs.bridgecrew.io/docs/logging_7#fix---buildtime",
|
||||
"NativeIaC": "https://docs.prowler.com/checks/aws/logging-policies/logging_7#fix---buildtime",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
},
|
||||
|
||||
@@ -7,32 +7,29 @@ from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
|
||||
class cloudtrail_kms_encryption_enabled(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.name:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "FAIL"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = (
|
||||
f"Multiregion trail {trail.name} has encryption disabled."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
f"Single region trail {trail.name} has encryption disabled."
|
||||
)
|
||||
if trail.kms_key:
|
||||
report.status = "PASS"
|
||||
if cloudtrail_client.trails is not None:
|
||||
for trail in cloudtrail_client.trails.values():
|
||||
if trail.name:
|
||||
report = Check_Report_AWS(self.metadata())
|
||||
report.region = trail.region
|
||||
report.resource_id = trail.name
|
||||
report.resource_arn = trail.arn
|
||||
report.resource_tags = trail.tags
|
||||
report.status = "FAIL"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = (
|
||||
f"Multiregion trail {trail.name} has encryption enabled."
|
||||
f"Multiregion trail {trail.name} has encryption disabled."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
f"Single region trail {trail.name} has encryption enabled."
|
||||
f"Single region trail {trail.name} has encryption disabled."
|
||||
)
|
||||
findings.append(report)
|
||||
if trail.kms_key:
|
||||
report.status = "PASS"
|
||||
if trail.is_multiregion:
|
||||
report.status_extended = f"Multiregion trail {trail.name} has encryption enabled."
|
||||
else:
|
||||
report.status_extended = f"Single region trail {trail.name} has encryption enabled."
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||