mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
Merge branch 'master' into PRWLR-6707-create-a-way-of-reporting-for-prowler-threatscore
This commit is contained in:
@@ -141,3 +141,9 @@ SOCIAL_GITHUB_OAUTH_CLIENT_SECRET=""
|
||||
# Single Sign-On (SSO)
|
||||
SAML_PUBLIC_CERT=""
|
||||
SAML_PRIVATE_KEY=""
|
||||
|
||||
# Lighthouse tracing
|
||||
LANGSMITH_TRACING=false
|
||||
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
||||
LANGSMITH_API_KEY=""
|
||||
LANGCHAIN_PROJECT=""
|
||||
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
- name: Build and push container image (latest)
|
||||
# Comment the following line for testing
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
# Set push: false for testing
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Build and push container image (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
push: true
|
||||
|
||||
@@ -48,12 +48,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/api-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -28,6 +28,10 @@ env:
|
||||
VALKEY_DB: 0
|
||||
API_WORKING_DIR: ./api
|
||||
IMAGE_NAME: prowler-api
|
||||
IGNORE_FILES: |
|
||||
api/docs/**
|
||||
api/README.md
|
||||
api/CHANGELOG.md
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -78,12 +82,7 @@ jobs:
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
with:
|
||||
files: api/**
|
||||
files_ignore: |
|
||||
api/.github/**
|
||||
api/docs/**
|
||||
api/permissions/**
|
||||
api/README.md
|
||||
api/mkdocs.yml
|
||||
files_ignore: ${{ env.IGNORE_FILES }}
|
||||
|
||||
- name: Replace @master with current branch in pyproject.toml
|
||||
working-directory: ./api
|
||||
@@ -193,7 +192,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
@@ -202,10 +201,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
with:
|
||||
files: api/**
|
||||
files_ignore: ${{ env.IGNORE_FILES }}
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- name: Build Container
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.API_WORKING_DIR }}
|
||||
push: false
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@b06f6d72a3791308bb7ba59c2b8cb7a083bd17e4 # v3.88.26
|
||||
uses: trufflesecurity/trufflehog@90694bf9af66e7536abc5824e7a87246dbf933cb # v3.88.35
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
|
||||
- name: Build and push container image (latest)
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
|
||||
- name: Build and push container image (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
# Use local context to get changes
|
||||
# https://github.com/docker/build-push-action#path-context
|
||||
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/sdk-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
# Codecov
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
pip install boto3
|
||||
|
||||
- name: Configure AWS Credentials -- DEV
|
||||
uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
aws-region: ${{ env.AWS_REGION_DEV }}
|
||||
role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }}
|
||||
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
- name: Build and push container image (latest)
|
||||
# Comment the following line for testing
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
build-args: |
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
- name: Build and push container image (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.WORKING_DIRECTORY }}
|
||||
build-args: |
|
||||
|
||||
@@ -48,12 +48,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/ui-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
|
||||
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- name: Build Container
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: ${{ env.UI_WORKING_DIR }}
|
||||
# Always build using `prod` target
|
||||
|
||||
@@ -87,7 +87,7 @@ prowler dashboard
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 567 | 82 | 36 | 10 |
|
||||
| GCP | 79 | 13 | 9 | 3 |
|
||||
| GCP | 79 | 13 | 10 | 3 |
|
||||
| Azure | 142 | 18 | 10 | 3 |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 |
|
||||
| GitHub | 16 | 2 | 1 | 0 |
|
||||
|
||||
+53
-42
@@ -5,57 +5,69 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
## [v1.9.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
|
||||
- Added SSO with SAML support [(#7822)](https://github.com/prowler-cloud/prowler/pull/7822).
|
||||
- Support GCP Service Account key. [(#7824)](https://github.com/prowler-cloud/prowler/pull/7824)
|
||||
- Added new `GET /compliance-overviews` endpoints to retrieve compliance metadata and specific requirements statuses [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877).
|
||||
- SSO with SAML support [(#7822)](https://github.com/prowler-cloud/prowler/pull/7822)
|
||||
- Support GCP Service Account key [(#7824)](https://github.com/prowler-cloud/prowler/pull/7824)
|
||||
- `GET /compliance-overviews` endpoints to retrieve compliance metadata and specific requirements statuses [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877)
|
||||
|
||||
### Changed
|
||||
- Reworked `GET /compliance-overviews` to return proper requirement metrics [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877).
|
||||
- Reworked `GET /compliance-overviews` to return proper requirement metrics [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.5] (Prowler v5.7.5)
|
||||
|
||||
### Fixed
|
||||
- Normalize provider UID to ensure safe and unique export directory paths [(#8007)](https://github.com/prowler-cloud/prowler/pull/8007).
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.4] (Prowler v5.7.4)
|
||||
|
||||
### Removed
|
||||
- Reverted RLS transaction handling and DB custom backend [(#7994)](https://github.com/prowler-cloud/prowler/pull/7994)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.3] (Prowler v5.7.3)
|
||||
|
||||
### Added
|
||||
- Database backend to handle already closed connections [(#7935)](https://github.com/prowler-cloud/prowler/pull/7935).
|
||||
- Database backend to handle already closed connections [(#7935)](https://github.com/prowler-cloud/prowler/pull/7935)
|
||||
|
||||
### Changed
|
||||
- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
|
||||
### Fixed
|
||||
- Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916).
|
||||
- Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932).
|
||||
|
||||
- Transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916)
|
||||
- Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.2] (Prowler v5.7.2)
|
||||
|
||||
### Fixed
|
||||
- Fixed task lookup to use task_kwargs instead of task_args for scan report resolution. [(#7830)](https://github.com/prowler-cloud/prowler/pull/7830)
|
||||
- Fixed Kubernetes UID validation to allow valid context names [(#7871)](https://github.com/prowler-cloud/prowler/pull/7871)
|
||||
- Fixed the connection status verification before launching a scan [(#7831)](https://github.com/prowler-cloud/prowler/pull/7831)
|
||||
- Fixed a race condition when creating background tasks [(#7876)](https://github.com/prowler-cloud/prowler/pull/7876).
|
||||
- Fixed an error when modifying or retrieving tenants due to missing user UUID in transaction context [(#7890)](https://github.com/prowler-cloud/prowler/pull/7890).
|
||||
- Task lookup to use task_kwargs instead of task_args for scan report resolution [(#7830)](https://github.com/prowler-cloud/prowler/pull/7830)
|
||||
- Kubernetes UID validation to allow valid context names [(#7871)](https://github.com/prowler-cloud/prowler/pull/7871)
|
||||
- Connection status verification before launching a scan [(#7831)](https://github.com/prowler-cloud/prowler/pull/7831)
|
||||
- Race condition when creating background tasks [(#7876)](https://github.com/prowler-cloud/prowler/pull/7876)
|
||||
- Error when modifying or retrieving tenants due to missing user UUID in transaction context [(#7890)](https://github.com/prowler-cloud/prowler/pull/7890)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.1] (Prowler v5.7.1)
|
||||
|
||||
### Fixed
|
||||
- Added database index to improve performance on finding lookup [(#7800)](https://github.com/prowler-cloud/prowler/pull/7800).
|
||||
- Added database index to improve performance on finding lookup [(#7800)](https://github.com/prowler-cloud/prowler/pull/7800)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.0] (Prowler v5.7.0)
|
||||
|
||||
### Added
|
||||
- Added huge improvements to `/findings/metadata` and resource related filters for findings [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690).
|
||||
- Added improvements to `/overviews` endpoints [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690).
|
||||
- Added new queue to perform backfill background tasks [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690).
|
||||
- Added new endpoints to retrieve latest findings and metadata [(#7743)](https://github.com/prowler-cloud/prowler/pull/7743).
|
||||
- Added export support for Prowler ThreatScore in M365 [(7783)](https://github.com/prowler-cloud/prowler/pull/7783)
|
||||
- Huge improvements to `/findings/metadata` and resource related filters for findings [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690)
|
||||
- Improvements to `/overviews` endpoints [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690)
|
||||
- Queue to perform backfill background tasks [(#7690)](https://github.com/prowler-cloud/prowler/pull/7690)
|
||||
- New endpoints to retrieve latest findings and metadata [(#7743)](https://github.com/prowler-cloud/prowler/pull/7743)
|
||||
- Export support for Prowler ThreatScore in M365 [(7783)](https://github.com/prowler-cloud/prowler/pull/7783)
|
||||
|
||||
---
|
||||
|
||||
@@ -63,9 +75,9 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
### Added
|
||||
|
||||
- Added M365 as a new provider [(#7563)](https://github.com/prowler-cloud/prowler/pull/7563).
|
||||
- Added a `compliance/` folder and ZIP‐export functionality for all compliance reports.[(#7653)](https://github.com/prowler-cloud/prowler/pull/7653).
|
||||
- Added a new API endpoint to fetch and download any specific compliance file by name [(#7653)](https://github.com/prowler-cloud/prowler/pull/7653).
|
||||
- M365 as a new provider [(#7563)](https://github.com/prowler-cloud/prowler/pull/7563)
|
||||
- `compliance/` folder and ZIP‐export functionality for all compliance reports [(#7653)](https://github.com/prowler-cloud/prowler/pull/7653)
|
||||
- API endpoint to fetch and download any specific compliance file by name [(#7653)](https://github.com/prowler-cloud/prowler/pull/7653)
|
||||
|
||||
---
|
||||
|
||||
@@ -73,43 +85,42 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
### Added
|
||||
|
||||
- Support for developing new integrations [(#7167)](https://github.com/prowler-cloud/prowler/pull/7167).
|
||||
- HTTP Security Headers [(#7289)](https://github.com/prowler-cloud/prowler/pull/7289).
|
||||
- New endpoint to get the compliance overviews metadata [(#7333)](https://github.com/prowler-cloud/prowler/pull/7333).
|
||||
- Support for muted findings [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378).
|
||||
- Added missing fields to API findings and resources [(#7318)](https://github.com/prowler-cloud/prowler/pull/7318).
|
||||
- Support for developing new integrations [(#7167)](https://github.com/prowler-cloud/prowler/pull/7167)
|
||||
- HTTP Security Headers [(#7289)](https://github.com/prowler-cloud/prowler/pull/7289)
|
||||
- New endpoint to get the compliance overviews metadata [(#7333)](https://github.com/prowler-cloud/prowler/pull/7333)
|
||||
- Support for muted findings [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378)
|
||||
- Missing fields to API findings and resources [(#7318)](https://github.com/prowler-cloud/prowler/pull/7318)
|
||||
|
||||
---
|
||||
|
||||
## [v1.5.4] (Prowler v5.4.4)
|
||||
|
||||
### Fixed
|
||||
- Fixed a bug with periodic tasks when trying to delete a provider ([#7466])(https://github.com/prowler-cloud/prowler/pull/7466).
|
||||
- Bug with periodic tasks when trying to delete a provider [(#7466)](https://github.com/prowler-cloud/prowler/pull/7466)
|
||||
|
||||
---
|
||||
|
||||
## [v1.5.3] (Prowler v5.4.3)
|
||||
|
||||
### Fixed
|
||||
- Added duplicated scheduled scans handling ([#7401])(https://github.com/prowler-cloud/prowler/pull/7401).
|
||||
- Added environment variable to configure the deletion task batch size ([#7423])(https://github.com/prowler-cloud/prowler/pull/7423).
|
||||
- Duplicated scheduled scans handling [(#7401)](https://github.com/prowler-cloud/prowler/pull/7401)
|
||||
- Environment variable to configure the deletion task batch size [(#7423)](https://github.com/prowler-cloud/prowler/pull/7423)
|
||||
|
||||
---
|
||||
|
||||
## [v1.5.2] (Prowler v5.4.2)
|
||||
|
||||
### Changed
|
||||
- Refactored deletion logic and implemented retry mechanism for deletion tasks [(#7349)](https://github.com/prowler-cloud/prowler/pull/7349).
|
||||
- Refactored deletion logic and implemented retry mechanism for deletion tasks [(#7349)](https://github.com/prowler-cloud/prowler/pull/7349)
|
||||
|
||||
---
|
||||
|
||||
## [v1.5.1] (Prowler v5.4.1)
|
||||
|
||||
### Fixed
|
||||
- Added a handled response in case local files are missing [(#7183)](https://github.com/prowler-cloud/prowler/pull/7183).
|
||||
- Fixed a race condition when deleting export files after the S3 upload [(#7172)](https://github.com/prowler-cloud/prowler/pull/7172).
|
||||
- Handled exception when a provider has no secret in test connection [(#7283)](https://github.com/prowler-cloud/prowler/pull/7283).
|
||||
|
||||
- Handle response in case local files are missing [(#7183)](https://github.com/prowler-cloud/prowler/pull/7183)
|
||||
- Race condition when deleting export files after the S3 upload [(#7172)](https://github.com/prowler-cloud/prowler/pull/7172)
|
||||
- Handle exception when a provider has no secret in test connection [(#7283)](https://github.com/prowler-cloud/prowler/pull/7283)
|
||||
|
||||
---
|
||||
|
||||
@@ -117,20 +128,20 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
### Added
|
||||
- Social login integration with Google and GitHub [(#6906)](https://github.com/prowler-cloud/prowler/pull/6906)
|
||||
- Add API scan report system, now all scans launched from the API will generate a compressed file with the report in OCSF, CSV and HTML formats [(#6878)](https://github.com/prowler-cloud/prowler/pull/6878).
|
||||
- API scan report system, now all scans launched from the API will generate a compressed file with the report in OCSF, CSV and HTML formats [(#6878)](https://github.com/prowler-cloud/prowler/pull/6878)
|
||||
- Configurable Sentry integration [(#6874)](https://github.com/prowler-cloud/prowler/pull/6874)
|
||||
|
||||
### Changed
|
||||
- Optimized `GET /findings` endpoint to improve response time and size [(#7019)](https://github.com/prowler-cloud/prowler/pull/7019).
|
||||
- Optimized `GET /findings` endpoint to improve response time and size [(#7019)](https://github.com/prowler-cloud/prowler/pull/7019)
|
||||
|
||||
---
|
||||
|
||||
## [v1.4.0] (Prowler v5.3.0)
|
||||
|
||||
### Changed
|
||||
- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700).
|
||||
- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800).
|
||||
- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863).
|
||||
- Increased the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869).
|
||||
- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700)
|
||||
- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800)
|
||||
- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863)
|
||||
- Increased the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869)
|
||||
|
||||
---
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
apply_migrations() {
|
||||
echo "Applying database migrations..."
|
||||
|
||||
# Fix Inconsistent migration history after adding sites app
|
||||
poetry run python manage.py check_and_fix_socialaccount_sites_migration --database admin
|
||||
|
||||
poetry run python manage.py migrate --database admin
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.filters import SearchFilter
|
||||
@@ -46,9 +47,11 @@ class BaseViewSet(ModelViewSet):
|
||||
|
||||
|
||||
class BaseRLSViewSet(BaseViewSet):
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# Ideally, this logic would be in the `.setup()` method but DRF view sets don't call it
|
||||
# https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.setup
|
||||
if request.auth is None:
|
||||
@@ -58,19 +61,9 @@ class BaseRLSViewSet(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
self.request.tenant_id = tenant_id
|
||||
|
||||
self._rls_cm = rls_transaction(tenant_id)
|
||||
self._rls_cm.__enter__()
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
if hasattr(self, "_rls_cm"):
|
||||
self._rls_cm.__exit__(None, None, None)
|
||||
del self._rls_cm
|
||||
|
||||
return response
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
@@ -80,7 +73,8 @@ class BaseRLSViewSet(BaseViewSet):
|
||||
|
||||
class BaseTenantViewset(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
tenant = super().dispatch(request, *args, **kwargs)
|
||||
with transaction.atomic():
|
||||
tenant = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
try:
|
||||
# If the request is a POST, create the admin role
|
||||
@@ -115,8 +109,6 @@ class BaseTenantViewset(BaseViewSet):
|
||||
pass # Tenant might not exist, handle gracefully
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
@@ -125,27 +117,19 @@ class BaseTenantViewset(BaseViewSet):
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
user_id = str(request.user.id)
|
||||
|
||||
self._rls_cm = rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR)
|
||||
self._rls_cm.__enter__()
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
if hasattr(self, "_rls_cm"):
|
||||
self._rls_cm.__exit__(None, None, None)
|
||||
del self._rls_cm
|
||||
|
||||
return response
|
||||
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseUserViewset(BaseViewSet):
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# TODO refactor after improving RLS on users
|
||||
if request.stream is not None and request.stream.method == "POST":
|
||||
return
|
||||
return super().initial(request, *args, **kwargs)
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
@@ -153,16 +137,6 @@ class BaseUserViewset(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
self.request.tenant_id = tenant_id
|
||||
|
||||
self._rls_cm = rls_transaction(tenant_id)
|
||||
self._rls_cm.__enter__()
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
if hasattr(self, "_rls_cm"):
|
||||
self._rls_cm.__exit__(None, None, None)
|
||||
del self._rls_cm
|
||||
|
||||
return response
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
@@ -196,6 +196,10 @@ def generate_compliance_overview_template(prowler_compliance: dict):
|
||||
requirement_dict = {
|
||||
"name": requirement.Name or requirement.Id,
|
||||
"description": requirement.Description,
|
||||
"tactics": getattr(requirement, "Tactics", []),
|
||||
"subtechniques": getattr(requirement, "SubTechniques", []),
|
||||
"platforms": getattr(requirement, "Platforms", []),
|
||||
"technique_url": getattr(requirement, "TechniqueURL", ""),
|
||||
"attributes": [
|
||||
dict(attribute) for attribute in requirement.Attributes
|
||||
],
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
|
||||
|
||||
def table_exists(table_name):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_name = %s
|
||||
)
|
||||
""",
|
||||
[table_name],
|
||||
)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Fix migration inconsistency between socialaccount and sites"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
help="Specifies the database to operate on.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
db = options["database"]
|
||||
connection = connections[db]
|
||||
recorder = MigrationRecorder(connection)
|
||||
|
||||
applied = set(recorder.applied_migrations())
|
||||
|
||||
has_social = ("socialaccount", "0001_initial") in applied
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'django_site'
|
||||
);
|
||||
"""
|
||||
)
|
||||
site_table_exists = cursor.fetchone()[0]
|
||||
|
||||
if has_social and not site_table_exists:
|
||||
self.stdout.write(
|
||||
f"Detected inconsistency in '{db}'. Creating 'django_site' table manually..."
|
||||
)
|
||||
|
||||
with transaction.atomic(using=db):
|
||||
with connection.schema_editor() as schema_editor:
|
||||
schema_editor.create_model(Site)
|
||||
|
||||
recorder.record_applied("sites", "0001_initial")
|
||||
recorder.record_applied("sites", "0002_alter_domain_unique")
|
||||
|
||||
self.stdout.write(
|
||||
"Fixed: 'django_site' table created and migrations registered."
|
||||
)
|
||||
|
||||
# Ensure the relationship table also exists
|
||||
if not table_exists("socialaccount_socialapp_sites"):
|
||||
self.stdout.write(
|
||||
"Detected missing 'socialaccount_socialapp_sites' table. Creating manually..."
|
||||
)
|
||||
with connection.schema_editor() as schema_editor:
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
|
||||
schema_editor.create_model(
|
||||
SocialApp._meta.get_field("sites").remote_field.through
|
||||
)
|
||||
self.stdout.write(
|
||||
"Fixed: 'socialaccount_socialapp_sites' table created."
|
||||
)
|
||||
@@ -167,6 +167,8 @@ paths:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- framework_description
|
||||
- name
|
||||
- framework
|
||||
- version
|
||||
- description
|
||||
@@ -5100,7 +5102,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this saml configurations.
|
||||
description: A UUID string identifying this saml configuration.
|
||||
required: true
|
||||
tags:
|
||||
- SAML
|
||||
@@ -5124,7 +5126,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this saml configurations.
|
||||
description: A UUID string identifying this saml configuration.
|
||||
required: true
|
||||
tags:
|
||||
- SAML
|
||||
@@ -5160,7 +5162,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this saml configurations.
|
||||
description: A UUID string identifying this saml configuration.
|
||||
required: true
|
||||
tags:
|
||||
- SAML
|
||||
@@ -7165,6 +7167,10 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
framework_description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
framework:
|
||||
type: string
|
||||
version:
|
||||
@@ -7174,6 +7180,8 @@ components:
|
||||
attributes: {}
|
||||
required:
|
||||
- id
|
||||
- framework_description
|
||||
- name
|
||||
- framework
|
||||
- version
|
||||
- description
|
||||
|
||||
@@ -218,6 +218,10 @@ class TestCompliance:
|
||||
Description="Description of requirement 1",
|
||||
Attributes=[],
|
||||
Checks=["check1", "check2"],
|
||||
Tactics=["tactic1"],
|
||||
SubTechniques=["subtechnique1"],
|
||||
Platforms=["platform1"],
|
||||
TechniqueURL="https://example.com",
|
||||
)
|
||||
requirement2 = MagicMock(
|
||||
Id="requirement2",
|
||||
@@ -225,6 +229,10 @@ class TestCompliance:
|
||||
Description="Description of requirement 2",
|
||||
Attributes=[],
|
||||
Checks=[],
|
||||
Tactics=[],
|
||||
SubTechniques=[],
|
||||
Platforms=[],
|
||||
TechniqueURL="",
|
||||
)
|
||||
compliance1 = MagicMock(
|
||||
Requirements=[requirement1, requirement2],
|
||||
@@ -247,6 +255,10 @@ class TestCompliance:
|
||||
"requirement1": {
|
||||
"name": "Requirement 1",
|
||||
"description": "Description of requirement 1",
|
||||
"tactics": ["tactic1"],
|
||||
"subtechniques": ["subtechnique1"],
|
||||
"platforms": ["platform1"],
|
||||
"technique_url": "https://example.com",
|
||||
"attributes": [],
|
||||
"checks": {"check1": None, "check2": None},
|
||||
"checks_status": {
|
||||
@@ -260,6 +272,10 @@ class TestCompliance:
|
||||
"requirement2": {
|
||||
"name": "Requirement 2",
|
||||
"description": "Description of requirement 2",
|
||||
"tactics": [],
|
||||
"subtechniques": [],
|
||||
"platforms": [],
|
||||
"technique_url": "",
|
||||
"attributes": [],
|
||||
"checks": {},
|
||||
"checks_status": {
|
||||
|
||||
@@ -4779,13 +4779,13 @@ class TestComplianceOverviewViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 2 # Two compliance frameworks
|
||||
assert len(data) == 3 # Three compliance frameworks
|
||||
|
||||
# Check that we get aggregated data for each compliance framework
|
||||
framework_ids = [item["id"] for item in data]
|
||||
assert "aws_account_security_onboarding_aws" in framework_ids
|
||||
assert "cis_1.4_aws" in framework_ids
|
||||
|
||||
assert "mitre_attack_aws" in framework_ids
|
||||
# Check structure of response
|
||||
for item in data:
|
||||
assert "id" in item
|
||||
@@ -4901,6 +4901,35 @@ class TestComplianceOverviewViewSet:
|
||||
assert "attributes" in attributes
|
||||
assert "metadata" in attributes["attributes"]
|
||||
assert "check_ids" in attributes["attributes"]
|
||||
assert "technique_details" not in attributes["attributes"]
|
||||
|
||||
def test_compliance_overview_attributes_technique_details(
|
||||
self, authenticated_client
|
||||
):
|
||||
response = authenticated_client.get(
|
||||
reverse("complianceoverview-attributes"),
|
||||
{"filter[compliance_id]": "mitre_attack_aws"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert len(data) > 0
|
||||
|
||||
# Check structure of attributes response
|
||||
for item in data:
|
||||
assert "id" in item
|
||||
assert "attributes" in item
|
||||
attributes = item["attributes"]
|
||||
assert "framework" in attributes
|
||||
assert "version" in attributes
|
||||
assert "description" in attributes
|
||||
assert "attributes" in attributes
|
||||
assert "metadata" in attributes["attributes"]
|
||||
assert "check_ids" in attributes["attributes"]
|
||||
assert "technique_details" in attributes["attributes"]
|
||||
assert "tactics" in attributes["attributes"]["technique_details"]
|
||||
assert "subtechniques" in attributes["attributes"]["technique_details"]
|
||||
assert "platforms" in attributes["attributes"]["technique_details"]
|
||||
assert "technique_url" in attributes["attributes"]["technique_details"]
|
||||
|
||||
def test_compliance_overview_attributes_missing_compliance_id(
|
||||
self, authenticated_client
|
||||
|
||||
@@ -1722,6 +1722,8 @@ class ComplianceOverviewDetailSerializer(serializers.Serializer):
|
||||
|
||||
class ComplianceOverviewAttributesSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
framework_description = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
framework = serializers.CharField()
|
||||
version = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
|
||||
@@ -2992,13 +2992,31 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
|
||||
|
||||
metadata = requirement.get("attributes", [])
|
||||
|
||||
base_attributes = {
|
||||
"metadata": metadata,
|
||||
"check_ids": check_ids,
|
||||
}
|
||||
|
||||
# Add technique details for MITRE-ATTACK framework
|
||||
if "mitre_attack" in compliance_id:
|
||||
base_attributes["technique_details"] = {
|
||||
"tactics": requirement.get("tactics", []),
|
||||
"subtechniques": requirement.get("subtechniques", []),
|
||||
"platforms": requirement.get("platforms", []),
|
||||
"technique_url": requirement.get("technique_url", ""),
|
||||
}
|
||||
|
||||
attribute_data.append(
|
||||
{
|
||||
"id": requirement_id,
|
||||
"framework_description": compliance_framework.get(
|
||||
"description", ""
|
||||
),
|
||||
"name": requirement.get("name", ""),
|
||||
"framework": compliance_framework.get("framework", ""),
|
||||
"version": compliance_framework.get("version", ""),
|
||||
"description": requirement.get("description", ""),
|
||||
"attributes": {"metadata": metadata, "check_ids": check_ids},
|
||||
"attributes": base_attributes,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import warnings
|
||||
|
||||
from celery import Celery, Task
|
||||
from config.env import env
|
||||
|
||||
# Suppress specific warnings from django-rest-auth: https://github.com/iMerica/dj-rest-auth/issues/684
|
||||
warnings.filterwarnings(
|
||||
"ignore", category=UserWarning, module="dj_rest_auth.registration.serializers"
|
||||
)
|
||||
|
||||
BROKER_VISIBILITY_TIMEOUT = env.int("DJANGO_BROKER_VISIBILITY_TIMEOUT", default=86400)
|
||||
|
||||
celery_app = Celery("tasks")
|
||||
|
||||
@@ -131,7 +131,6 @@ DJANGO_GUID = {
|
||||
}
|
||||
|
||||
DATABASE_ROUTERS = ["api.db_router.MainRouter"]
|
||||
POSTGRES_EXTRA_DB_BACKEND_BASE = "database_backend"
|
||||
|
||||
|
||||
# Password validation
|
||||
|
||||
@@ -880,6 +880,22 @@ def compliance_requirements_overviews_fixture(scans_fixture, tenants_fixture):
|
||||
total_checks=3,
|
||||
)
|
||||
|
||||
# Create another compliance framework for testing MITRE ATT&CK
|
||||
requirement_overview7 = ComplianceRequirementOverview.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan1,
|
||||
compliance_id="mitre_attack_aws",
|
||||
framework="MITRE-ATTACK",
|
||||
version="1.0",
|
||||
description="MITRE ATT&CK",
|
||||
region="eu-west-1",
|
||||
requirement_id="mitre_requirement1",
|
||||
requirement_status=StatusChoices.FAIL,
|
||||
passed_checks=0,
|
||||
failed_checks=0,
|
||||
total_checks=0,
|
||||
)
|
||||
|
||||
return (
|
||||
requirement_overview1,
|
||||
requirement_overview2,
|
||||
@@ -887,6 +903,7 @@ def compliance_requirements_overviews_fixture(scans_fixture, tenants_fixture):
|
||||
requirement_overview4,
|
||||
requirement_overview5,
|
||||
requirement_overview6,
|
||||
requirement_overview7,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import django.db
|
||||
from django.db.backends.postgresql.base import (
|
||||
DatabaseWrapper as BuiltinPostgresDatabaseWrapper,
|
||||
)
|
||||
from psycopg2 import InterfaceError
|
||||
|
||||
|
||||
class DatabaseWrapper(BuiltinPostgresDatabaseWrapper):
|
||||
def create_cursor(self, name=None):
|
||||
try:
|
||||
return super().create_cursor(name=name)
|
||||
except InterfaceError:
|
||||
django.db.close_old_connections()
|
||||
django.db.connection.connect()
|
||||
return super().create_cursor(name=name)
|
||||
@@ -3,6 +3,12 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
# Suppress specific warnings from django-rest-auth: https://github.com/iMerica/dj-rest-auth/issues/684
|
||||
warnings.filterwarnings(
|
||||
"ignore", category=UserWarning, module="dj_rest_auth.registration.serializers"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
import boto3
|
||||
@@ -238,15 +239,18 @@ def _generate_output_directory(
|
||||
'/tmp/tenant-1234/aws/scan-5678/prowler-output-2023-02-15T12:34:56',
|
||||
'/tmp/tenant-1234/aws/scan-5678/compliance/prowler-output-2023-02-15T12:34:56'
|
||||
"""
|
||||
# Sanitize the prowler provider name to ensure it is a valid directory name
|
||||
prowler_provider_sanitized = re.sub(r"[^\w\-]", "-", prowler_provider)
|
||||
|
||||
path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/prowler-output-"
|
||||
f"{prowler_provider}-{output_file_timestamp}"
|
||||
f"{prowler_provider_sanitized}-{output_file_timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
compliance_path = (
|
||||
f"{output_directory}/{tenant_id}/{scan_id}/compliance/prowler-output-"
|
||||
f"{prowler_provider}-{output_file_timestamp}"
|
||||
f"{prowler_provider_sanitized}-{output_file_timestamp}"
|
||||
)
|
||||
os.makedirs("/".join(compliance_path.split("/")[:-1]), exist_ok=True)
|
||||
|
||||
|
||||
@@ -145,3 +145,22 @@ class TestOutputs:
|
||||
|
||||
assert path.endswith(f"{provider}-{output_file_timestamp}")
|
||||
assert compliance.endswith(f"{provider}-{output_file_timestamp}")
|
||||
|
||||
def test_generate_output_directory_invalid_character(self, tmpdir):
|
||||
from prowler.config.config import output_file_timestamp
|
||||
|
||||
base_tmp = Path(str(tmpdir.mkdir("generate_output")))
|
||||
base_dir = str(base_tmp)
|
||||
tenant_id = "t1"
|
||||
scan_id = "s1"
|
||||
provider = "aws/test@check"
|
||||
|
||||
path, compliance = _generate_output_directory(
|
||||
base_dir, provider, tenant_id, scan_id
|
||||
)
|
||||
|
||||
assert os.path.isdir(os.path.dirname(path))
|
||||
assert os.path.isdir(os.path.dirname(compliance))
|
||||
|
||||
assert path.endswith(f"aws-test-check-{output_file_timestamp}")
|
||||
assert compliance.endswith(f"aws-test-check-{output_file_timestamp}")
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,117 @@
|
||||
# Prowler Multicloud CIS Benchmarks PowerBI Template
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install Microsoft PowerBI Desktop
|
||||
|
||||
This report requires the Microsoft PowerBI Desktop software which can be downloaded for free from Microsoft.
|
||||
2. Run compliance scans in Prowler
|
||||
|
||||
The report uses compliance csv outputs from Prowler. Compliance scans be run using either [Prowler CLI](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-cli) or [Prowler Cloud/App](https://cloud.prowler.com/sign-in)
|
||||
1. Prowler CLI -> Run a Prowler scan using the --compliance option
|
||||
2. Prowler Cloud/App -> Navigate to the compliance section to download csv outputs
|
||||

|
||||
|
||||
|
||||
The template supports the following CIS Benchmarks only:
|
||||
|
||||
| Compliance Framework | Version |
|
||||
| ---------------------------------------------- | ------- |
|
||||
| CIS Amazon Web Services Foundations Benchmark | v4.0.1 |
|
||||
| CIS Google Cloud Platform Foundation Benchmark | v3.0.0 |
|
||||
| CIS Microsoft Azure Foundations Benchmark | v3.0.0 |
|
||||
| CIS Kubernetes Benchmark | v1.10.0 |
|
||||
|
||||
Ensure you run or download the correct benchmark versions.
|
||||
3. Create a local directory to store Prowler csvoutputs
|
||||
|
||||
Once downloaded, place your csv outputs in a directory on your local machine. If you rename the files, they must maintain the provider in the filename.
|
||||
|
||||
To use time-series capabilities such as "compliance percent over time" you'll need scans from multiple dates.
|
||||
4. Download and run the PowerBI template file (.pbit)
|
||||
|
||||
Running the .pbit file will open PowerBI Desktop and prompt you for the full filepath to the local directory
|
||||
5. Enter the full filepath to the directory created in step 3
|
||||
|
||||
Provide the full filepath from the root directory.
|
||||
|
||||
Ensure that the filepath is not wrapped in quotation marks (""). If you use Window's "copy as path" feature, it will automatically include quotation marks.
|
||||
6. Save the report as a PowerBI file (.pbix)
|
||||
|
||||
Once the filepath is entered, the template will automatically ingest and populate the report. You can then save this file as a new PowerBI report. If you'd like to generate another report, simply re-run the template file (.pbit) from step 4.
|
||||
|
||||
## Validation
|
||||
|
||||
After setting up your dashboard, you may want to validate the Prowler csv files were ingested correctly. To do this, navigate to the "Configuration" tab.
|
||||
|
||||
The "loaded CIS Benchmarks" table shows the supported benchmarks and versions. This is defined by the template file and not editable by the user. All benchmarks will be loaded regardless of which providers you provided csv outputs for.
|
||||
|
||||
The "Prowler CSV Folder" shows the path to the local directory you provided.
|
||||
|
||||
The "Loaded Prowler Exports" table shows the ingested csv files from the local directory. It will mark files that are treated as the latest assessment with a green checkmark.
|
||||
|
||||

|
||||
|
||||
## Report Sections
|
||||
|
||||
The PowerBI Report is broken into three main report pages
|
||||
|
||||
| Report Page | Description |
|
||||
| ----------- | ----------------------------------------------------------------------------------- |
|
||||
| Overview | Provides general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes |
|
||||
| Benchmark | Provides overview of a single CIS Benchmark |
|
||||
| Requirement | Drill-through page to view details of a single requirement |
|
||||
|
||||
|
||||
### Overview Page
|
||||
|
||||
The overview page is a general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes.
|
||||
|
||||

|
||||
|
||||
The page has the following components:
|
||||
|
||||
| Component | Description |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| CIS Benchmark Overview | Table with benchmark name, Version, and overall compliance percentage |
|
||||
| Provider by Requirement Status | Bar chart showing benchmark requirements by status by provider |
|
||||
| Compliance Percent Heatmap | Heatmap showing compliance percent by benchmark and profile level |
|
||||
| Profile level by Requirement Status | Bar chart showing requirements by status and profile level |
|
||||
| Compliance Percent Over Time by Provider | Line chart showing overall compliance perecentage over time by provider. |
|
||||
|
||||
### Benchmark Page
|
||||
|
||||
The benchmark page provides an overview of a single CIS Benchmark. You can select the benchmark from the dropdown as well as scope down to specific profile levels or regions.
|
||||
|
||||

|
||||
|
||||
The page has the following components:
|
||||
|
||||
| Component | Description |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Compliance Percent Heatmap | Heatmap showing compliance percent by region and profile level |
|
||||
| Benchmark Section by Requirement Status | Bar chart showing benchmark requirements by bennchmark section and status |
|
||||
| Compliance percent Over Time by Region | Line chart showing overall compliance percentage over time by region |
|
||||
| Benchmark Requirements | Table showing requirement section, requirement number, reuqirement title, number of resources tested, status, and number of failing checks |
|
||||
|
||||
### Requirement Page
|
||||
|
||||
The requirement page is a drill-through page to view details of a single requirement. To populate the requirement page right click on a requiement from the "Benchmark Requirements" table on the benchmark page and select "Drill through" -> "Requirement".
|
||||
|
||||

|
||||
|
||||
The requirement page has the following components:
|
||||
|
||||
| Component | Description |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Title | Title of the requirement |
|
||||
| Rationale | Rationale of the requirement |
|
||||
| Remediation | Remedation guidance for the requirement |
|
||||
| Region by Check Status | Bar chart showing Prowler checks by region and status |
|
||||
| Resource Checks for Benchmark Requirements | Table showing Resource ID, Resource Name, Status, Description, and Prowler Checkl |
|
||||
|
||||
## Walkthrough Video
|
||||
[](https://www.youtube.com/watch?v=lfKFkTqBxjU)
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_3_levels
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
data["REQUIREMENTS_DESCRIPTION"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_DESCRIPTION"] = data["REQUIREMENTS_DESCRIPTION"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
].apply(lambda x: x[:80] + "..." if len(str(x)) > 80 else x)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SUBSECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION"
|
||||
].apply(lambda x: x[:150] + "..." if len(str(x)) > 150 else x)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_3_levels(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
)
|
||||
@@ -83,7 +83,18 @@ def load_csv_files(csv_files):
|
||||
"""Load CSV files into a single pandas DataFrame."""
|
||||
dfs = []
|
||||
for file in csv_files:
|
||||
df = pd.read_csv(file, sep=";", on_bad_lines="skip")
|
||||
account_columns = ["ACCOUNT_ID", "ACCOUNT_UID", "SUBSCRIPTION"]
|
||||
|
||||
df_sample = pd.read_csv(file, sep=";", on_bad_lines="skip", nrows=1)
|
||||
|
||||
dtype_dict = {}
|
||||
for col in account_columns:
|
||||
if col in df_sample.columns:
|
||||
dtype_dict[col] = str
|
||||
|
||||
# Read the full file with proper dtypes
|
||||
df = pd.read_csv(file, sep=";", on_bad_lines="skip", dtype=dtype_dict)
|
||||
|
||||
if "CHECK_ID" in df.columns:
|
||||
if "TIMESTAMP" in df.columns or df["PROVIDER"].unique() == "aws":
|
||||
dfs.append(df.astype(str))
|
||||
@@ -120,7 +131,6 @@ if data is None:
|
||||
]
|
||||
)
|
||||
else:
|
||||
|
||||
# This handles the case where we are using v3 outputs
|
||||
if "ASSESSMENT_START_TIME" in data.columns:
|
||||
data["ASSESSMENT_START_TIME"] = data["ASSESSMENT_START_TIME"].str.replace(
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Prowler Multicloud CIS Benchmarks PowerBI Template
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install Microsoft PowerBI Desktop
|
||||
|
||||
This report requires the Microsoft PowerBI Desktop software which can be downloaded for free from Microsoft.
|
||||
2. Run compliance scans in Prowler
|
||||
|
||||
The report uses compliance csv outputs from Prowler. Compliance scans be run using either [Prowler CLI](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-cli) or [Prowler Cloud/App](https://cloud.prowler.com/sign-in)
|
||||
1. Prowler CLI -> Run a Prowler scan using the --compliance option
|
||||
2. Prowler Cloud/App -> Navigate to the compliance section to download csv outputs
|
||||

|
||||
|
||||
|
||||
The template supports the following CIS Benchmarks only:
|
||||
|
||||
| Compliance Framework | Version |
|
||||
| ---------------------------------------------- | ------- |
|
||||
| CIS Amazon Web Services Foundations Benchmark | v4.0.1 |
|
||||
| CIS Google Cloud Platform Foundation Benchmark | v3.0.0 |
|
||||
| CIS Microsoft Azure Foundations Benchmark | v3.0.0 |
|
||||
| CIS Kubernetes Benchmark | v1.10.0 |
|
||||
|
||||
Ensure you run or download the correct benchmark versions.
|
||||
3. Create a local directory to store Prowler csvoutputs
|
||||
|
||||
Once downloaded, place your csv outputs in a directory on your local machine. If you rename the files, they must maintain the provider in the filename.
|
||||
|
||||
To use time-series capabilities such as "compliance percent over time" you'll need scans from multiple dates.
|
||||
4. Download and run the PowerBI template file (.pbit)
|
||||
|
||||
Running the .pbit file will open PowerBI Desktop and prompt you for the full filepath to the local directory
|
||||
5. Enter the full filepath to the directory created in step 3
|
||||
|
||||
Provide the full filepath from the root directory.
|
||||
|
||||
Ensure that the filepath is not wrapped in quotation marks (""). If you use Window's "copy as path" feature, it will automatically include quotation marks.
|
||||
6. Save the report as a PowerBI file (.pbix)
|
||||
|
||||
Once the filepath is entered, the template will automatically ingest and populate the report. You can then save this file as a new PowerBI report. If you'd like to generate another report, simply re-run the template file (.pbit) from step 4.
|
||||
|
||||
## Validation
|
||||
|
||||
After setting up your dashboard, you may want to validate the Prowler csv files were ingested correctly. To do this, navigate to the "Configuration" tab.
|
||||
|
||||
The "loaded CIS Benchmarks" table shows the supported benchmarks and versions. This is defined by the template file and not editable by the user. All benchmarks will be loaded regardless of which providers you provided csv outputs for.
|
||||
|
||||
The "Prowler CSV Folder" shows the path to the local directory you provided.
|
||||
|
||||
The "Loaded Prowler Exports" table shows the ingested csv files from the local directory. It will mark files that are treated as the latest assessment with a green checkmark.
|
||||
|
||||

|
||||
|
||||
## Report Sections
|
||||
|
||||
The PowerBI Report is broken into three main report pages
|
||||
|
||||
| Report Page | Description |
|
||||
| ----------- | ----------------------------------------------------------------------------------- |
|
||||
| Overview | Provides general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes |
|
||||
| Benchmark | Provides overview of a single CIS Benchmark |
|
||||
| Requirement | Drill-through page to view details of a single requirement |
|
||||
|
||||
|
||||
### Overview Page
|
||||
|
||||
The overview page is a general CIS Benchmark overview across both AWS, Azure, GCP, and Kubernetes.
|
||||
|
||||

|
||||
|
||||
The page has the following components:
|
||||
|
||||
| Component | Description |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| CIS Benchmark Overview | Table with benchmark name, Version, and overall compliance percentage |
|
||||
| Provider by Requirement Status | Bar chart showing benchmark requirements by status by provider |
|
||||
| Compliance Percent Heatmap | Heatmap showing compliance percent by benchmark and profile level |
|
||||
| Profile level by Requirement Status | Bar chart showing requirements by status and profile level |
|
||||
| Compliance Percent Over Time by Provider | Line chart showing overall compliance perecentage over time by provider. |
|
||||
|
||||
### Benchmark Page
|
||||
|
||||
The benchmark page provides an overview of a single CIS Benchmark. You can select the benchmark from the dropdown as well as scope down to specific profile levels or regions.
|
||||
|
||||

|
||||
|
||||
The page has the following components:
|
||||
|
||||
| Component | Description |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Compliance Percent Heatmap | Heatmap showing compliance percent by region and profile level |
|
||||
| Benchmark Section by Requirement Status | Bar chart showing benchmark requirements by bennchmark section and status |
|
||||
| Compliance percent Over Time by Region | Line chart showing overall compliance percentage over time by region |
|
||||
| Benchmark Requirements | Table showing requirement section, requirement number, reuqirement title, number of resources tested, status, and number of failing checks |
|
||||
|
||||
### Requirement Page
|
||||
|
||||
The requirement page is a drill-through page to view details of a single requirement. To populate the requirement page right click on a requiement from the "Benchmark Requirements" table on the benchmark page and select "Drill through" -> "Requirement".
|
||||
|
||||

|
||||
|
||||
The requirement page has the following components:
|
||||
|
||||
| Component | Description |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Title | Title of the requirement |
|
||||
| Rationale | Rationale of the requirement |
|
||||
| Remediation | Remedation guidance for the requirement |
|
||||
| Region by Check Status | Bar chart showing Prowler checks by region and status |
|
||||
| Resource Checks for Benchmark Requirements | Table showing Resource ID, Resource Name, Status, Description, and Prowler Checkl |
|
||||
|
||||
## Walkthrough Video
|
||||
[](https://www.youtube.com/watch?v=lfKFkTqBxjU)
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Getting Started with GCP on Prowler Cloud/App
|
||||
|
||||
<iframe width="560" height="380" src="https://www.youtube-nocookie.com/embed/v1as8vTFlMg" title="Prowler Cloud Onboarding GCP" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
|
||||
|
||||
Set up your GCP project to enable security scanning using Prowler Cloud/App.
|
||||
|
||||
## Requirements
|
||||
|
||||
+122
-111
@@ -5,153 +5,164 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
## [v5.8.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Add CIS 1.11 compliance framework for Kubernetes. [(#7790)](https://github.com/prowler-cloud/prowler/pull/7790)
|
||||
- Support `HTTPS_PROXY` and `K8S_SKIP_TLS_VERIFY` in Kubernetes. [(#7720)](https://github.com/prowler-cloud/prowler/pull/7720)
|
||||
- Add Weight for Prowler ThreatScore scoring. [(#7795)](https://github.com/prowler-cloud/prowler/pull/7795)
|
||||
- Add new check `entra_users_mfa_capable` for M365 provider. [(#7734)](https://github.com/prowler-cloud/prowler/pull/7734)
|
||||
- Add new check `admincenter_organization_customer_lockbox_enabled` for M365 provider. [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732)
|
||||
- Add new check `admincenter_external_calendar_sharing_disabled` for M365 provider. [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733)
|
||||
- Add a level for Prowler ThreatScore in the accordion in Dashboard. [(#7739)](https://github.com/prowler-cloud/prowler/pull/7739)
|
||||
- Add CIS 4.0 compliance framework for GCP. [(7785)](https://github.com/prowler-cloud/prowler/pull/7785)
|
||||
- Add `repository_has_codeowners_file` check for GitHub provider. [(#7752)](https://github.com/prowler-cloud/prowler/pull/7752)
|
||||
- Add `repository_default_branch_requires_signed_commits` check for GitHub provider. [(#7777)](https://github.com/prowler-cloud/prowler/pull/7777)
|
||||
- Add `repository_inactive_not_archived` check for GitHub provider. [(#7786)](https://github.com/prowler-cloud/prowler/pull/7786)
|
||||
- Add `repository_dependency_scanning_enabled` check for GitHub provider. [(#7771)](https://github.com/prowler-cloud/prowler/pull/7771)
|
||||
- Add `repository_secret_scanning_enabled` check for GitHub provider. [(#7759)](https://github.com/prowler-cloud/prowler/pull/7759)
|
||||
- Add `repository_default_branch_requires_codeowners_review` check for GitHub provider. [(#7753)](https://github.com/prowler-cloud/prowler/pull/7753)
|
||||
- Add NIS 2 compliance framework for AWS. [(#7839)](https://github.com/prowler-cloud/prowler/pull/7839)
|
||||
- Add NIS 2 compliance framework for Azure. [(#7857)](https://github.com/prowler-cloud/prowler/pull/7857)
|
||||
- Add search bar in Dashboard Overview page. [(#7804)](https://github.com/prowler-cloud/prowler/pull/7804)
|
||||
- Add `storage_account_key_access_disabled` check for Azure provider. [(#7974)](https://github.com/prowler-cloud/prowler/pull/7974)
|
||||
- Add `storage_ensure_file_shares_soft_delete_is_enabled` check for Azure provider. [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966)
|
||||
- CIS 1.11 compliance framework for Kubernetes [(#7790)](https://github.com/prowler-cloud/prowler/pull/7790)
|
||||
- Support `HTTPS_PROXY` and `K8S_SKIP_TLS_VERIFY` in Kubernetes [(#7720)](https://github.com/prowler-cloud/prowler/pull/7720)
|
||||
- Weight for Prowler ThreatScore scoring [(#7795)](https://github.com/prowler-cloud/prowler/pull/7795)
|
||||
- New check `entra_users_mfa_capable` for M365 provider [(#7734)](https://github.com/prowler-cloud/prowler/pull/7734)
|
||||
- New check `admincenter_organization_customer_lockbox_enabled` for M365 provider [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732)
|
||||
- New check `admincenter_external_calendar_sharing_disabled` for M365 provider [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733)
|
||||
- a level for Prowler ThreatScore in the accordion in Dashboard [(#7739)](https://github.com/prowler-cloud/prowler/pull/7739)
|
||||
- CIS 4.0 compliance framework for GCP [(7785)](https://github.com/prowler-cloud/prowler/pull/7785)
|
||||
- `repository_has_codeowners_file` check for GitHub provider [(#7752)](https://github.com/prowler-cloud/prowler/pull/7752)
|
||||
- `repository_default_branch_requires_signed_commits` check for GitHub provider [(#7777)](https://github.com/prowler-cloud/prowler/pull/7777)
|
||||
- `repository_inactive_not_archived` check for GitHub provider [(#7786)](https://github.com/prowler-cloud/prowler/pull/7786)
|
||||
- `repository_dependency_scanning_enabled` check for GitHub provider [(#7771)](https://github.com/prowler-cloud/prowler/pull/7771)
|
||||
- `repository_secret_scanning_enabled` check for GitHub provider [(#7759)](https://github.com/prowler-cloud/prowler/pull/7759)
|
||||
- `repository_default_branch_requires_codeowners_review` check for GitHub provider [(#7753)](https://github.com/prowler-cloud/prowler/pull/7753)
|
||||
- NIS 2 compliance framework for AWS [(#7839)](https://github.com/prowler-cloud/prowler/pull/7839)
|
||||
- NIS 2 compliance framework for Azure [(#7857)](https://github.com/prowler-cloud/prowler/pull/7857)
|
||||
- Search bar in Dashboard Overview page [(#7804)](https://github.com/prowler-cloud/prowler/pull/7804)
|
||||
- NIS 2 compliance framework for GCP [(#7912)](https://github.com/prowler-cloud/prowler/pull/7912)
|
||||
- `storage_account_key_access_disabled` check for Azure provider [(#7974)](https://github.com/prowler-cloud/prowler/pull/7974)
|
||||
- `storage_ensure_file_shares_soft_delete_is_enabled` check for Azure provider [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966)
|
||||
- Make `validate_mutelist` method static inside `Mutelist` class [(#7811)](https://github.com/prowler-cloud/prowler/pull/7811)
|
||||
|
||||
### Fixed
|
||||
- Add github provider to `usage` section of `prowler -h`: [(#7906)](https://github.com/prowler-cloud/prowler/pull/7906)
|
||||
- Github provider to `usage` section of `prowler -h`: [(#7906)](https://github.com/prowler-cloud/prowler/pull/7906)
|
||||
|
||||
---
|
||||
## [v5.7.5] (Prowler v5.7.5)
|
||||
|
||||
### Fixed
|
||||
- `apiserver_strong_ciphers_only` check for K8S provider [(#7952)](https://github.com/prowler-cloud/prowler/pull/7952)
|
||||
- Handle `0` at the start and end of account uids in Prowler Dashboard [(#7955)](https://github.com/prowler-cloud/prowler/pull/7955)
|
||||
- Typo in PCI 4.0 for K8S provider [(#7971)](https://github.com/prowler-cloud/prowler/pull/7971)
|
||||
- AWS root credentials checks always verify if root credentials are enabled [(#7967)](https://github.com/prowler-cloud/prowler/pull/7967)
|
||||
|
||||
---
|
||||
|
||||
## [v5.7.3] (Prowler v5.7.3)
|
||||
|
||||
### Fixed
|
||||
- Automatically encrypt password in Microsoft365 provider. [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
- Remove last encrypted password appearances. [(#7825)](https://github.com/prowler-cloud/prowler/pull/7825)
|
||||
- Automatically encrypt password in Microsoft365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
- Remove last encrypted password appearances [(#7825)](https://github.com/prowler-cloud/prowler/pull/7825)
|
||||
|
||||
---
|
||||
|
||||
## [v5.7.2] (Prowler v5.7.2)
|
||||
|
||||
### Fixed
|
||||
- Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
|
||||
- Fix `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license. [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
|
||||
- Fix `m365_powershell` to close the PowerShell sessions in msgraph services. [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
|
||||
- Fix `defender_ensure_notify_alerts_severity_is_high`check to accept high or lower severity. [(#7862)](https://github.com/prowler-cloud/prowler/pull/7862)
|
||||
- Replace `Directory.Read.All` permission with `Domain.Read.All` which is more restrictive. [(#7888)](https://github.com/prowler-cloud/prowler/pull/7888)
|
||||
- Split calls to list Azure Functions attributes. [(#7778)](https://github.com/prowler-cloud/prowler/pull/7778)
|
||||
- `m365_powershell test_credentials` to use sanitized credentials [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
|
||||
- `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
|
||||
- `m365_powershell` to close the PowerShell sessions in msgraph services [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
|
||||
- `defender_ensure_notify_alerts_severity_is_high`check to accept high or lower severity [(#7862)](https://github.com/prowler-cloud/prowler/pull/7862)
|
||||
- Replace `Directory.Read.All` permission with `Domain.Read.All` which is more restrictive [(#7888)](https://github.com/prowler-cloud/prowler/pull/7888)
|
||||
- Split calls to list Azure Functions attributes [(#7778)](https://github.com/prowler-cloud/prowler/pull/7778)
|
||||
|
||||
---
|
||||
|
||||
## [v5.7.0] (Prowler v5.7.0)
|
||||
|
||||
### Added
|
||||
- Update the compliance list supported for each provider from docs. [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
|
||||
- Allow setting cluster name in in-cluster mode in Kubernetes. [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695)
|
||||
- Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
- Add GitHub provider. [(#5787)](https://github.com/prowler-cloud/prowler/pull/5787)
|
||||
- Add `repository_default_branch_requires_multiple_approvals` check for GitHub provider. [(#6160)](https://github.com/prowler-cloud/prowler/pull/6160)
|
||||
- Add `repository_default_branch_protection_enabled` check for GitHub provider. [(#6161)](https://github.com/prowler-cloud/prowler/pull/6161)
|
||||
- Add `repository_default_branch_requires_linear_history` check for GitHub provider. [(#6162)](https://github.com/prowler-cloud/prowler/pull/6162)
|
||||
- Add `repository_default_branch_disallows_force_push` check for GitHub provider. [(#6197)](https://github.com/prowler-cloud/prowler/pull/6197)
|
||||
- Add `repository_default_branch_deletion_disabled` check for GitHub provider. [(#6200)](https://github.com/prowler-cloud/prowler/pull/6200)
|
||||
- Add `repository_default_branch_status_checks_required` check for GitHub provider. [(#6204)](https://github.com/prowler-cloud/prowler/pull/6204)
|
||||
- Add `repository_default_branch_protection_applies_to_admins` check for GitHub provider. [(#6205)](https://github.com/prowler-cloud/prowler/pull/6205)
|
||||
- Add `repository_branch_delete_on_merge_enabled` check for GitHub provider. [(#6209)](https://github.com/prowler-cloud/prowler/pull/6209)
|
||||
- Add `repository_default_branch_requires_conversation_resolution` check for GitHub provider. [(#6208)](https://github.com/prowler-cloud/prowler/pull/6208)
|
||||
- Add `organization_members_mfa_required` check for GitHub provider. [(#6304)](https://github.com/prowler-cloud/prowler/pull/6304)
|
||||
- Add GitHub provider documentation and CIS v1.0.0 compliance. [(#6116)](https://github.com/prowler-cloud/prowler/pull/6116)
|
||||
- Add CIS 5.0 compliance framework for AWS. [(7766)](https://github.com/prowler-cloud/prowler/pull/7766)
|
||||
- Update the compliance list supported for each provider from docs [(#7694)](https://github.com/prowler-cloud/prowler/pull/7694)
|
||||
- Allow setting cluster name in in-cluster mode in Kubernetes [(#7695)](https://github.com/prowler-cloud/prowler/pull/7695)
|
||||
- Prowler ThreatScore for M365 provider [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
- GitHub provider [(#5787)](https://github.com/prowler-cloud/prowler/pull/5787)
|
||||
- `repository_default_branch_requires_multiple_approvals` check for GitHub provider [(#6160)](https://github.com/prowler-cloud/prowler/pull/6160)
|
||||
- `repository_default_branch_protection_enabled` check for GitHub provider [(#6161)](https://github.com/prowler-cloud/prowler/pull/6161)
|
||||
- `repository_default_branch_requires_linear_history` check for GitHub provider [(#6162)](https://github.com/prowler-cloud/prowler/pull/6162)
|
||||
- `repository_default_branch_disallows_force_push` check for GitHub provider [(#6197)](https://github.com/prowler-cloud/prowler/pull/6197)
|
||||
- `repository_default_branch_deletion_disabled` check for GitHub provider [(#6200)](https://github.com/prowler-cloud/prowler/pull/6200)
|
||||
- `repository_default_branch_status_checks_required` check for GitHub provider [(#6204)](https://github.com/prowler-cloud/prowler/pull/6204)
|
||||
- `repository_default_branch_protection_applies_to_admins` check for GitHub provider [(#6205)](https://github.com/prowler-cloud/prowler/pull/6205)
|
||||
- `repository_branch_delete_on_merge_enabled` check for GitHub provider [(#6209)](https://github.com/prowler-cloud/prowler/pull/6209)
|
||||
- `repository_default_branch_requires_conversation_resolution` check for GitHub provider [(#6208)](https://github.com/prowler-cloud/prowler/pull/6208)
|
||||
- `organization_members_mfa_required` check for GitHub provider [(#6304)](https://github.com/prowler-cloud/prowler/pull/6304)
|
||||
- GitHub provider documentation and CIS v1.0.0 compliance [(#6116)](https://github.com/prowler-cloud/prowler/pull/6116)
|
||||
- CIS 5.0 compliance framework for AWS [(7766)](https://github.com/prowler-cloud/prowler/pull/7766)
|
||||
|
||||
### Fixed
|
||||
- Update CIS 4.0 for M365 provider. [(#7699)](https://github.com/prowler-cloud/prowler/pull/7699)
|
||||
- Update CIS 4.0 for M365 provider [(#7699)](https://github.com/prowler-cloud/prowler/pull/7699)
|
||||
- Update and upgrade CIS for all the providers [(#7738)](https://github.com/prowler-cloud/prowler/pull/7738)
|
||||
- Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible`. [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750)
|
||||
- Change severity logic for `ec2_securitygroup_allow_ingress_from_internet_to_all_ports` check. [(#7764)](https://github.com/prowler-cloud/prowler/pull/7764)
|
||||
- Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible` [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750)
|
||||
- Change severity logic for `ec2_securitygroup_allow_ingress_from_internet_to_all_ports` check [(#7764)](https://github.com/prowler-cloud/prowler/pull/7764)
|
||||
|
||||
---
|
||||
|
||||
## [v5.6.0] (Prowler v5.6.0)
|
||||
|
||||
### Added
|
||||
- Add SOC2 compliance framework to Azure. [(#7489)](https://github.com/prowler-cloud/prowler/pull/7489)
|
||||
- Add check for unused Service Accounts in GCP. [(#7419)](https://github.com/prowler-cloud/prowler/pull/7419)
|
||||
- Add Powershell to Microsoft365. [(#7331)](https://github.com/prowler-cloud/prowler/pull/7331)
|
||||
- Add service Defender to Microsoft365 with one check for Common Attachments filter enabled in Malware Policies. [(#7425)](https://github.com/prowler-cloud/prowler/pull/7425)
|
||||
- Add check for Outbound Antispam Policy well configured in service Defender for M365. [(#7480)](https://github.com/prowler-cloud/prowler/pull/7480)
|
||||
- Add check for Antiphishing Policy well configured in service Defender in M365. [(#7453)](https://github.com/prowler-cloud/prowler/pull/7453)
|
||||
- Add check for Notifications for Internal users enabled in Malware Policies from service Defender in M365. [(#7435)](https://github.com/prowler-cloud/prowler/pull/7435)
|
||||
- Add support CLOUDSDK_AUTH_ACCESS_TOKEN in GCP. [(#7495)](https://github.com/prowler-cloud/prowler/pull/7495)
|
||||
- Add service Exchange to Microsoft365 with one check for Organizations Mailbox Auditing enabled. [(#7408)](https://github.com/prowler-cloud/prowler/pull/7408)
|
||||
- Add check for Bypass Disable in every Mailbox for service Defender in M365. [(#7418)](https://github.com/prowler-cloud/prowler/pull/7418)
|
||||
- Add new check `teams_external_domains_restricted`. [(#7557)](https://github.com/prowler-cloud/prowler/pull/7557)
|
||||
- Add new check `teams_email_sending_to_channel_disabled`. [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533)
|
||||
- Add new check for External Mails Tagged for service Exchange in M365. [(#7580)](https://github.com/prowler-cloud/prowler/pull/7580)
|
||||
- Add new check for WhiteList not used in Transport Rules for service Defender in M365. [(#7569)](https://github.com/prowler-cloud/prowler/pull/7569)
|
||||
- Add check for Inbound Antispam Policy with no allowed domains from service Defender in M365. [(#7500)](https://github.com/prowler-cloud/prowler/pull/7500)
|
||||
- Add new check `teams_meeting_anonymous_user_join_disabled`. [(#7565)](https://github.com/prowler-cloud/prowler/pull/7565)
|
||||
- Add new check `teams_unmanaged_communication_disabled`. [(#7561)](https://github.com/prowler-cloud/prowler/pull/7561)
|
||||
- Add new check `teams_external_users_cannot_start_conversations`. [(#7562)](https://github.com/prowler-cloud/prowler/pull/7562)
|
||||
- Add new check for AllowList not used in the Connection Filter Policy from service Defender in M365. [(#7492)](https://github.com/prowler-cloud/prowler/pull/7492)
|
||||
- Add new check for SafeList not enabled in the Connection Filter Policy from service Defender in M365. [(#7492)](https://github.com/prowler-cloud/prowler/pull/7492)
|
||||
- Add new check for DKIM enabled for service Defender in M365. [(#7485)](https://github.com/prowler-cloud/prowler/pull/7485)
|
||||
- Add new check `teams_meeting_anonymous_user_start_disabled`. [(#7567)](https://github.com/prowler-cloud/prowler/pull/7567)
|
||||
- Add new check `teams_meeting_external_lobby_bypass_disabled`. [(#7568)](https://github.com/prowler-cloud/prowler/pull/7568)
|
||||
- Add new check `teams_meeting_dial_in_lobby_bypass_disabled`. [(#7571)](https://github.com/prowler-cloud/prowler/pull/7571)
|
||||
- Add new check `teams_meeting_external_control_disabled`. [(#7604)](https://github.com/prowler-cloud/prowler/pull/7604)
|
||||
- Add new check `teams_meeting_external_chat_disabled`. [(#7605)](https://github.com/prowler-cloud/prowler/pull/7605)
|
||||
- Add new check `teams_meeting_recording_disabled`. [(#7607)](https://github.com/prowler-cloud/prowler/pull/7607)
|
||||
- Add new check `teams_meeting_presenters_restricted`. [(#7613)](https://github.com/prowler-cloud/prowler/pull/7613)
|
||||
- Add new check `teams_security_reporting_enabled`. [(#7614)](https://github.com/prowler-cloud/prowler/pull/7614)
|
||||
- Add new check `defender_chat_report_policy_configured`. [(#7614)](https://github.com/prowler-cloud/prowler/pull/7614)
|
||||
- Add new check `teams_meeting_chat_anonymous_users_disabled`. [(#7579)](https://github.com/prowler-cloud/prowler/pull/7579)
|
||||
- Add Prowler Threat Score Compliance Framework. [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
|
||||
- Add documentation for M365 provider. [(#7622)](https://github.com/prowler-cloud/prowler/pull/7622)
|
||||
- Add support for m365 provider in Prowler Dashboard. [(#7633)](https://github.com/prowler-cloud/prowler/pull/7633)
|
||||
- Add new check for Modern Authentication enabled for Exchange Online in M365. [(#7636)](https://github.com/prowler-cloud/prowler/pull/7636)
|
||||
- Add new check `sharepoint_onedrive_sync_restricted_unmanaged_devices`. [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)
|
||||
- Add new check for Additional Storage restricted for Exchange in M365. [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638)
|
||||
- Add new check for Roles Assignment Policy with no AddIns for Exchange in M365. [(#7644)](https://github.com/prowler-cloud/prowler/pull/7644)
|
||||
- Add new check for Auditing Mailbox on E3 users is enabled for Exchange in M365. [(#7642)](https://github.com/prowler-cloud/prowler/pull/7642)
|
||||
- Add new check for SMTP Auth disabled for Exchange in M365. [(#7640)](https://github.com/prowler-cloud/prowler/pull/7640)
|
||||
- Add new check for MailTips full enabled for Exchange in M365. [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637)
|
||||
- Add new check for Comprehensive Attachments Filter Applied for Defender in M365. [(#7661)](https://github.com/prowler-cloud/prowler/pull/7661)
|
||||
- Modified check `exchange_mailbox_properties_auditing_enabled` to make it configurable. [(#7662)](https://github.com/prowler-cloud/prowler/pull/7662)
|
||||
- Add snapshots to m365 documentation. [(#7673)](https://github.com/prowler-cloud/prowler/pull/7673)
|
||||
- Add support for static credentials for sending findings to Amazon S3 and AWS Security Hub. [(#7322)](https://github.com/prowler-cloud/prowler/pull/7322)
|
||||
- Add Prowler ThreatScore for M365 provider. [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
- Add Microsoft User and User Credential auth to reports [(#7681)](https://github.com/prowler-cloud/prowler/pull/7681)
|
||||
- SOC2 compliance framework to Azure [(#7489)](https://github.com/prowler-cloud/prowler/pull/7489)
|
||||
- Check for unused Service Accounts in GCP [(#7419)](https://github.com/prowler-cloud/prowler/pull/7419)
|
||||
- Powershell to Microsoft365 [(#7331)](https://github.com/prowler-cloud/prowler/pull/7331)
|
||||
- Service Defender to Microsoft365 with one check for Common Attachments filter enabled in Malware Policies [(#7425)](https://github.com/prowler-cloud/prowler/pull/7425)
|
||||
- Check for Outbound Antispam Policy well configured in service Defender for M365 [(#7480)](https://github.com/prowler-cloud/prowler/pull/7480)
|
||||
- Check for Antiphishing Policy well configured in service Defender in M365 [(#7453)](https://github.com/prowler-cloud/prowler/pull/7453)
|
||||
- Check for Notifications for Internal users enabled in Malware Policies from service Defender in M365 [(#7435)](https://github.com/prowler-cloud/prowler/pull/7435)
|
||||
- Support CLOUDSDK_AUTH_ACCESS_TOKEN in GCP [(#7495)](https://github.com/prowler-cloud/prowler/pull/7495)
|
||||
- Service Exchange to Microsoft365 with one check for Organizations Mailbox Auditing enabled [(#7408)](https://github.com/prowler-cloud/prowler/pull/7408)
|
||||
- Check for Bypass Disable in every Mailbox for service Defender in M365 [(#7418)](https://github.com/prowler-cloud/prowler/pull/7418)
|
||||
- New check `teams_external_domains_restricted` [(#7557)](https://github.com/prowler-cloud/prowler/pull/7557)
|
||||
- New check `teams_email_sending_to_channel_disabled` [(#7533)](https://github.com/prowler-cloud/prowler/pull/7533)
|
||||
- New check for External Mails Tagged for service Exchange in M365 [(#7580)](https://github.com/prowler-cloud/prowler/pull/7580)
|
||||
- New check for WhiteList not used in Transport Rules for service Defender in M365 [(#7569)](https://github.com/prowler-cloud/prowler/pull/7569)
|
||||
- Check for Inbound Antispam Policy with no allowed domains from service Defender in M365 [(#7500)](https://github.com/prowler-cloud/prowler/pull/7500)
|
||||
- New check `teams_meeting_anonymous_user_join_disabled` [(#7565)](https://github.com/prowler-cloud/prowler/pull/7565)
|
||||
- New check `teams_unmanaged_communication_disabled` [(#7561)](https://github.com/prowler-cloud/prowler/pull/7561)
|
||||
- New check `teams_external_users_cannot_start_conversations` [(#7562)](https://github.com/prowler-cloud/prowler/pull/7562)
|
||||
- New check for AllowList not used in the Connection Filter Policy from service Defender in M365 [(#7492)](https://github.com/prowler-cloud/prowler/pull/7492)
|
||||
- New check for SafeList not enabled in the Connection Filter Policy from service Defender in M365 [(#7492)](https://github.com/prowler-cloud/prowler/pull/7492)
|
||||
- New check for DKIM enabled for service Defender in M365 [(#7485)](https://github.com/prowler-cloud/prowler/pull/7485)
|
||||
- New check `teams_meeting_anonymous_user_start_disabled` [(#7567)](https://github.com/prowler-cloud/prowler/pull/7567)
|
||||
- New check `teams_meeting_external_lobby_bypass_disabled` [(#7568)](https://github.com/prowler-cloud/prowler/pull/7568)
|
||||
- New check `teams_meeting_dial_in_lobby_bypass_disabled` [(#7571)](https://github.com/prowler-cloud/prowler/pull/7571)
|
||||
- New check `teams_meeting_external_control_disabled` [(#7604)](https://github.com/prowler-cloud/prowler/pull/7604)
|
||||
- New check `teams_meeting_external_chat_disabled` [(#7605)](https://github.com/prowler-cloud/prowler/pull/7605)
|
||||
- New check `teams_meeting_recording_disabled` [(#7607)](https://github.com/prowler-cloud/prowler/pull/7607)
|
||||
- New check `teams_meeting_presenters_restricted` [(#7613)](https://github.com/prowler-cloud/prowler/pull/7613)
|
||||
- New check `teams_security_reporting_enabled` [(#7614)](https://github.com/prowler-cloud/prowler/pull/7614)
|
||||
- New check `defender_chat_report_policy_configured` [(#7614)](https://github.com/prowler-cloud/prowler/pull/7614)
|
||||
- New check `teams_meeting_chat_anonymous_users_disabled` [(#7579)](https://github.com/prowler-cloud/prowler/pull/7579)
|
||||
- Prowler Threat Score Compliance Framework [(#7603)](https://github.com/prowler-cloud/prowler/pull/7603)
|
||||
- Documentation for M365 provider [(#7622)](https://github.com/prowler-cloud/prowler/pull/7622)
|
||||
- Support for m365 provider in Prowler Dashboard [(#7633)](https://github.com/prowler-cloud/prowler/pull/7633)
|
||||
- New check for Modern Authentication enabled for Exchange Online in M365 [(#7636)](https://github.com/prowler-cloud/prowler/pull/7636)
|
||||
- New check `sharepoint_onedrive_sync_restricted_unmanaged_devices` [(#7589)](https://github.com/prowler-cloud/prowler/pull/7589)
|
||||
- New check for Additional Storage restricted for Exchange in M365 [(#7638)](https://github.com/prowler-cloud/prowler/pull/7638)
|
||||
- New check for Roles Assignment Policy with no AddIns for Exchange in M365 [(#7644)](https://github.com/prowler-cloud/prowler/pull/7644)
|
||||
- New check for Auditing Mailbox on E3 users is enabled for Exchange in M365 [(#7642)](https://github.com/prowler-cloud/prowler/pull/7642)
|
||||
- New check for SMTP Auth disabled for Exchange in M365 [(#7640)](https://github.com/prowler-cloud/prowler/pull/7640)
|
||||
- New check for MailTips full enabled for Exchange in M365 [(#7637)](https://github.com/prowler-cloud/prowler/pull/7637)
|
||||
- New check for Comprehensive Attachments Filter Applied for Defender in M365 [(#7661)](https://github.com/prowler-cloud/prowler/pull/7661)
|
||||
- Modified check `exchange_mailbox_properties_auditing_enabled` to make it configurable [(#7662)](https://github.com/prowler-cloud/prowler/pull/7662)
|
||||
- snapshots to m365 documentation [(#7673)](https://github.com/prowler-cloud/prowler/pull/7673)
|
||||
- support for static credentials for sending findings to Amazon S3 and AWS Security Hub [(#7322)](https://github.com/prowler-cloud/prowler/pull/7322)
|
||||
- Prowler ThreatScore for M365 provider [(#7692)](https://github.com/prowler-cloud/prowler/pull/7692)
|
||||
- Microsoft User and User Credential auth to reports [(#7681)](https://github.com/prowler-cloud/prowler/pull/7681)
|
||||
|
||||
### Fixed
|
||||
- Fix package name location in pyproject.toml while replicating for prowler-cloud. [(#7531)](https://github.com/prowler-cloud/prowler/pull/7531)
|
||||
- Remove cache in PyPI release action. [(#7532)](https://github.com/prowler-cloud/prowler/pull/7532)
|
||||
- Add the correct values for logger.info inside iam service. [(#7526)](https://github.com/prowler-cloud/prowler/pull/7526)
|
||||
- Update S3 bucket naming validation to accept dots. [(#7545)](https://github.com/prowler-cloud/prowler/pull/7545)
|
||||
- Handle new FlowLog model properties in Azure. [(#7546)](https://github.com/prowler-cloud/prowler/pull/7546)
|
||||
- Improve compliance and dashboard. [(#7596)](https://github.com/prowler-cloud/prowler/pull/7596)
|
||||
- Remove invalid parameter `create_file_descriptor`. [(#7600)](https://github.com/prowler-cloud/prowler/pull/7600)
|
||||
- Remove first empty line in HTML output. [(#7606)](https://github.com/prowler-cloud/prowler/pull/7606)
|
||||
- Remove empty files in Prowler. [(#7627)](https://github.com/prowler-cloud/prowler/pull/7627)
|
||||
- Ensure that ContentType in upload_file matches the uploaded file's format. [(#7635)](https://github.com/prowler-cloud/prowler/pull/7635)
|
||||
- Fix incorrect check inside 4.4.1 requirement for Azure CIS 2.0. [(#7656)](https://github.com/prowler-cloud/prowler/pull/7656)
|
||||
- Remove muted findings on compliance page from Prowler Dashboard. [(#7683)](https://github.com/prowler-cloud/prowler/pull/7683)
|
||||
- Remove duplicated findings on compliance page from Prowler Dashboard. [(#7686)](https://github.com/prowler-cloud/prowler/pull/7686)
|
||||
- Fix incorrect values for Prowler Threatscore compliance LevelOfRisk inside requirements. [(#7667)](https://github.com/prowler-cloud/prowler/pull/7667)
|
||||
- Package name location in pyproject.toml while replicating for prowler-cloud [(#7531)](https://github.com/prowler-cloud/prowler/pull/7531)
|
||||
- Remove cache in PyPI release action [(#7532)](https://github.com/prowler-cloud/prowler/pull/7532)
|
||||
- The correct values for logger.info inside iam service [(#7526)](https://github.com/prowler-cloud/prowler/pull/7526)
|
||||
- Update S3 bucket naming validation to accept dots [(#7545)](https://github.com/prowler-cloud/prowler/pull/7545)
|
||||
- Handle new FlowLog model properties in Azure [(#7546)](https://github.com/prowler-cloud/prowler/pull/7546)
|
||||
- Improve compliance and dashboard [(#7596)](https://github.com/prowler-cloud/prowler/pull/7596)
|
||||
- Remove invalid parameter `create_file_descriptor` [(#7600)](https://github.com/prowler-cloud/prowler/pull/7600)
|
||||
- Remove first empty line in HTML output [(#7606)](https://github.com/prowler-cloud/prowler/pull/7606)
|
||||
- Remove empty files in Prowler [(#7627)](https://github.com/prowler-cloud/prowler/pull/7627)
|
||||
- Ensure that ContentType in upload_file matches the uploaded file's format [(#7635)](https://github.com/prowler-cloud/prowler/pull/7635)
|
||||
- Incorrect check inside 4.4.1 requirement for Azure CIS 2.0 [(#7656)](https://github.com/prowler-cloud/prowler/pull/7656)
|
||||
- Remove muted findings on compliance page from Prowler Dashboard [(#7683)](https://github.com/prowler-cloud/prowler/pull/7683)
|
||||
- Remove duplicated findings on compliance page from Prowler Dashboard [(#7686)](https://github.com/prowler-cloud/prowler/pull/7686)
|
||||
- Incorrect values for Prowler Threatscore compliance LevelOfRisk inside requirements [(#7667)](https://github.com/prowler-cloud/prowler/pull/7667)
|
||||
|
||||
---
|
||||
|
||||
## [v5.5.1] (Prowler v5.5.1)
|
||||
|
||||
### Fixed
|
||||
- Add default name to contacts in Azure Defender. [(#7483)](https://github.com/prowler-cloud/prowler/pull/7483)
|
||||
- Handle projects without ID in GCP. [(#7496)](https://github.com/prowler-cloud/prowler/pull/7496)
|
||||
- Restore packages location in PyProject. [(#7510)](https://github.com/prowler-cloud/prowler/pull/7510)
|
||||
- Default name to contacts in Azure Defender [(#7483)](https://github.com/prowler-cloud/prowler/pull/7483)
|
||||
- Handle projects without ID in GCP [(#7496)](https://github.com/prowler-cloud/prowler/pull/7496)
|
||||
- Restore packages location in PyProject [(#7510)](https://github.com/prowler-cloud/prowler/pull/7510)
|
||||
|
||||
---
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Framework": "PCI",
|
||||
"Version": "4.0",
|
||||
"Provider": "Core",
|
||||
"Provider": "Kubernetes",
|
||||
"Description": "The Payment Card Industry Data Security Standard (PCI DSS) is a proprietary information security standard. It's administered by the PCI Security Standards Council, which was founded by American Express, Discover Financial Services, JCB International, MasterCard Worldwide, and Visa Inc. PCI DSS applies to entities that store, process, or transmit cardholder data (CHD) or sensitive authentication data (SAD). This includes, but isn't limited to, merchants, processors, acquirers, issuers, and service providers. The PCI DSS is mandated by the card brands and administered by the Payment Card Industry Security Standards Council.",
|
||||
"Requirements": [
|
||||
{
|
||||
|
||||
@@ -98,7 +98,6 @@ class Mutelist(ABC):
|
||||
mutelist_file_path: Property that returns the mutelist file path.
|
||||
is_finding_muted: Abstract method to check if a finding is muted.
|
||||
get_mutelist_file_from_local_file: Retrieves the mutelist file from a local file.
|
||||
validate_mutelist: Validates the mutelist against a schema.
|
||||
is_muted: Checks if a finding is muted for the audited account, check, region, resource, and tags.
|
||||
is_muted_in_check: Checks if a check is muted.
|
||||
is_excepted: Checks if the account, region, resource, and tags are excepted based on the exceptions.
|
||||
@@ -119,7 +118,7 @@ class Mutelist(ABC):
|
||||
self._mutelist = mutelist_content
|
||||
|
||||
if self._mutelist:
|
||||
self.validate_mutelist()
|
||||
self._mutelist = Mutelist.validate_mutelist(self._mutelist)
|
||||
|
||||
@property
|
||||
def mutelist(self) -> dict:
|
||||
@@ -142,17 +141,6 @@ class Mutelist(ABC):
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
)
|
||||
|
||||
def validate_mutelist(self) -> bool:
|
||||
try:
|
||||
validate(self._mutelist, schema=mutelist_schema)
|
||||
return True
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- Mutelist YAML is malformed - {error}[{error.__traceback__.tb_lineno}]"
|
||||
)
|
||||
self._mutelist = {}
|
||||
return False
|
||||
|
||||
def is_muted(
|
||||
self,
|
||||
audited_account: str,
|
||||
@@ -449,3 +437,23 @@ class Mutelist(ABC):
|
||||
f"{error.__class__.__name__} -- {error}[{error.__traceback__.tb_lineno}]"
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_mutelist(mutelist: dict) -> dict:
|
||||
"""
|
||||
Validate the mutelist against the schema.
|
||||
|
||||
Args:
|
||||
mutelist (dict): The mutelist to be validated.
|
||||
|
||||
Returns:
|
||||
dict: The mutelist itself.
|
||||
"""
|
||||
try:
|
||||
validate(mutelist, schema=mutelist_schema)
|
||||
return mutelist
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- Mutelist YAML is malformed - {error}[{error.__traceback__.tb_lineno}]"
|
||||
)
|
||||
return {}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -101,6 +102,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -147,6 +149,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -193,6 +196,7 @@
|
||||
"aws": [
|
||||
"ap-south-1",
|
||||
"ap-southeast-2",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -343,6 +347,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -445,6 +450,7 @@
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"mx-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -466,6 +472,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -512,6 +519,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -817,17 +825,24 @@
|
||||
"aps": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-7",
|
||||
"ca-central-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
"eu-south-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"me-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -842,6 +857,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -888,6 +904,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1017,6 +1034,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1063,6 +1081,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1120,6 +1139,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1177,6 +1197,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1255,6 +1276,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1713,6 +1735,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1842,6 +1865,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -1888,6 +1912,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -2025,6 +2050,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -2094,6 +2120,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -2661,6 +2688,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -2994,6 +3022,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3188,6 +3217,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3249,6 +3279,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3295,6 +3326,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3459,6 +3491,11 @@
|
||||
"dsql": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-3",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2"
|
||||
@@ -3472,6 +3509,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3518,6 +3556,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3564,6 +3603,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3610,6 +3650,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3656,6 +3697,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3712,6 +3754,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3758,6 +3801,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3804,6 +3848,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3850,6 +3895,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3893,6 +3939,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -3992,6 +4039,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -4038,6 +4086,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -4084,6 +4133,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -4230,6 +4280,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -4276,6 +4327,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -4322,6 +4374,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -4385,6 +4438,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -4480,6 +4534,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -5178,6 +5233,7 @@
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
]
|
||||
}
|
||||
@@ -5187,6 +5243,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -5322,6 +5379,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -5992,6 +6050,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -6113,6 +6172,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -6205,6 +6265,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -6553,6 +6614,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -6827,6 +6889,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -7437,6 +7500,7 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -7475,8 +7539,10 @@
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-south-2",
|
||||
@@ -7701,6 +7767,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -7751,8 +7818,11 @@
|
||||
"eu-central-1",
|
||||
"eu-north-1",
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
@@ -7779,6 +7849,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -7828,6 +7899,7 @@
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
"ap-south-1",
|
||||
"ap-south-2",
|
||||
"ap-southeast-1",
|
||||
"ap-southeast-2",
|
||||
"ca-central-1",
|
||||
@@ -7851,19 +7923,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"opsworks": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
"ap-southeast-1",
|
||||
"eu-central-1",
|
||||
"eu-west-1",
|
||||
"us-east-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
"opsworkschefautomate": {
|
||||
"regions": {
|
||||
"aws": [
|
||||
@@ -7903,6 +7962,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8070,6 +8130,7 @@
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"mx-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -8161,6 +8222,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8381,6 +8443,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8549,6 +8612,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8595,6 +8659,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8641,6 +8706,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8718,6 +8784,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -8960,6 +9027,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9006,6 +9074,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9069,6 +9138,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9153,6 +9223,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9199,6 +9270,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9245,6 +9317,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9305,6 +9378,8 @@
|
||||
"ap-southeast-2",
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ap-southeast-5",
|
||||
"ap-southeast-7",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
@@ -9318,6 +9393,7 @@
|
||||
"il-central-1",
|
||||
"me-central-1",
|
||||
"me-south-1",
|
||||
"mx-central-1",
|
||||
"sa-east-1",
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
@@ -9339,6 +9415,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9421,6 +9498,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -9773,6 +9851,7 @@
|
||||
"ap-southeast-3",
|
||||
"ap-southeast-4",
|
||||
"ca-central-1",
|
||||
"ca-west-1",
|
||||
"eu-central-1",
|
||||
"eu-central-2",
|
||||
"eu-north-1",
|
||||
@@ -9790,7 +9869,10 @@
|
||||
"us-west-1",
|
||||
"us-west-2"
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-cn": [
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-us-gov": []
|
||||
}
|
||||
},
|
||||
@@ -9854,6 +9936,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10029,6 +10112,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10157,6 +10241,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10471,6 +10556,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10541,6 +10627,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10587,6 +10674,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10785,6 +10873,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10877,6 +10966,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10923,6 +11013,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -10980,6 +11071,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -11026,6 +11118,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -11299,6 +11392,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -11371,6 +11465,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -11537,6 +11632,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -11619,6 +11715,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
@@ -11973,6 +12070,7 @@
|
||||
"aws": [
|
||||
"af-south-1",
|
||||
"ap-east-1",
|
||||
"ap-east-2",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-northeast-3",
|
||||
|
||||
@@ -41,7 +41,7 @@ class AWSMutelist(Mutelist):
|
||||
else:
|
||||
self.get_mutelist_file_from_local_file(mutelist_path)
|
||||
if self._mutelist:
|
||||
self.validate_mutelist()
|
||||
self._mutelist = self.validate_mutelist(self._mutelist)
|
||||
|
||||
def is_finding_muted(
|
||||
self,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use the credential report to check the user and ensure the access_key_1_active and access_key_2_active fields are set to FALSE.",
|
||||
"Text": "Use the credential report to check the user and ensure the access_key_1_active and access_key_2_active fields are set to FALSE. If using AWS Organizations, consider enabling Centralized Root Management and removing individual root credentials.",
|
||||
"Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_getting-report.html"
|
||||
}
|
||||
},
|
||||
|
||||
+55
-29
@@ -5,39 +5,65 @@ from prowler.providers.aws.services.iam.iam_client import iam_client
|
||||
class iam_no_root_access_key(Check):
|
||||
def execute(self) -> Check_Report_AWS:
|
||||
findings = []
|
||||
# Check if the root credentials are managed by AWS Organizations
|
||||
if (
|
||||
iam_client.organization_features is not None
|
||||
and "RootCredentialsManagement" not in iam_client.organization_features
|
||||
):
|
||||
|
||||
if iam_client.credential_report:
|
||||
for user in iam_client.credential_report:
|
||||
if user["user"] == "<root_account>":
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=user)
|
||||
report.region = iam_client.region
|
||||
report.resource_id = user["user"]
|
||||
report.resource_arn = user["arn"]
|
||||
if (
|
||||
user["access_key_1_active"] == "false"
|
||||
and user["access_key_2_active"] == "false"
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
"Root account does not have access keys."
|
||||
password_enabled = user["password_enabled"] == "true"
|
||||
access_key_1_active = user["access_key_1_active"] == "true"
|
||||
access_key_2_active = user["access_key_2_active"] == "true"
|
||||
|
||||
# Only report if root actually has credentials
|
||||
if password_enabled or access_key_1_active or access_key_2_active:
|
||||
report = Check_Report_AWS(
|
||||
metadata=self.metadata(), resource=user
|
||||
)
|
||||
elif (
|
||||
user["access_key_1_active"] == "true"
|
||||
and user["access_key_2_active"] == "true"
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"Root account has two active access keys."
|
||||
report.region = iam_client.region
|
||||
report.resource_id = user["user"]
|
||||
report.resource_arn = user["arn"]
|
||||
|
||||
# Check if organization manages root credentials
|
||||
org_managed = (
|
||||
iam_client.organization_features is not None
|
||||
and "RootCredentialsManagement"
|
||||
in iam_client.organization_features
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"Root account has one active access key."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
if not access_key_1_active and not access_key_2_active:
|
||||
report.status = "PASS"
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has password configured but no access keys. "
|
||||
"Consider removing individual root credentials since organizational "
|
||||
"root management is active."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
"Root account does not have access keys."
|
||||
)
|
||||
elif access_key_1_active and access_key_2_active:
|
||||
report.status = "FAIL"
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has two active access keys "
|
||||
"despite organizational root management being enabled."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
"Root account has two active access keys."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has one active access key "
|
||||
"despite organizational root management being enabled."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
"Root account has one active access key."
|
||||
)
|
||||
findings.append(report)
|
||||
break
|
||||
|
||||
return findings
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Using IAM console navigate to Dashboard and expand Activate MFA on your root account.",
|
||||
"Text": "Using IAM console navigate to Dashboard and expand Activate MFA on your root account. If using AWS Organizations, consider enabling Centralized Root Management and removing individual root credentials.",
|
||||
"Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html#id_root-user_manage_mfa"
|
||||
}
|
||||
},
|
||||
|
||||
+69
-32
@@ -7,40 +7,77 @@ class iam_root_hardware_mfa_enabled(Check):
|
||||
findings = []
|
||||
# This check is only available in Commercial Partition
|
||||
if iam_client.audited_partition == "aws":
|
||||
# Check if the root credentials are managed by AWS Organizations
|
||||
if (
|
||||
iam_client.organization_features is not None
|
||||
and "RootCredentialsManagement" not in iam_client.organization_features
|
||||
):
|
||||
if iam_client.account_summary:
|
||||
virtual_mfa = False
|
||||
report = Check_Report_AWS(
|
||||
metadata=self.metadata(),
|
||||
resource=iam_client.account_summary,
|
||||
)
|
||||
report.region = iam_client.region
|
||||
report.resource_id = "<root_account>"
|
||||
report.resource_arn = iam_client.mfa_arn_template
|
||||
if iam_client.credential_report:
|
||||
for user in iam_client.credential_report:
|
||||
if user["user"] == "<root_account>":
|
||||
password_enabled = user["password_enabled"] == "true"
|
||||
access_key_1_active = user["access_key_1_active"] == "true"
|
||||
access_key_2_active = user["access_key_2_active"] == "true"
|
||||
|
||||
if (
|
||||
iam_client.account_summary["SummaryMap"]["AccountMFAEnabled"]
|
||||
> 0
|
||||
):
|
||||
for mfa in iam_client.virtual_mfa_devices:
|
||||
# If the ARN of the associated IAM user of the Virtual MFA device is "arn:aws:iam::[aws-account-id]:root", your AWS root account is not using a hardware-based MFA device for MFA protection.
|
||||
if "root" in mfa.get("User", {}).get("Arn", ""):
|
||||
virtual_mfa = True
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "Root account has a virtual MFA instead of a hardware MFA device enabled."
|
||||
if not virtual_mfa:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
"Root account has a hardware MFA device enabled."
|
||||
# Only report if root actually has credentials
|
||||
if (
|
||||
password_enabled
|
||||
or access_key_1_active
|
||||
or access_key_2_active
|
||||
) and iam_client.account_summary:
|
||||
virtual_mfa = False
|
||||
report = Check_Report_AWS(
|
||||
metadata=self.metadata(),
|
||||
resource=user,
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = "MFA is not enabled for root account."
|
||||
report.region = iam_client.region
|
||||
report.resource_id = user["user"]
|
||||
report.resource_arn = iam_client.mfa_arn_template
|
||||
|
||||
findings.append(report)
|
||||
# Check if organization manages root credentials
|
||||
org_managed = (
|
||||
iam_client.organization_features is not None
|
||||
and "RootCredentialsManagement"
|
||||
in iam_client.organization_features
|
||||
)
|
||||
|
||||
if (
|
||||
iam_client.account_summary["SummaryMap"][
|
||||
"AccountMFAEnabled"
|
||||
]
|
||||
> 0
|
||||
):
|
||||
for mfa in iam_client.virtual_mfa_devices:
|
||||
# If the ARN of the associated IAM user of the Virtual MFA device is "arn:aws:iam::[aws-account-id]:root", your AWS root account is not using a hardware-based MFA device for MFA protection.
|
||||
if "root" in mfa.get("User", {}).get("Arn", ""):
|
||||
virtual_mfa = True
|
||||
report.status = "FAIL"
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has credentials with virtual MFA "
|
||||
"instead of hardware MFA despite organizational root management being enabled."
|
||||
)
|
||||
else:
|
||||
report.status_extended = "Root account has a virtual MFA instead of a hardware MFA device enabled."
|
||||
break
|
||||
|
||||
if not virtual_mfa:
|
||||
report.status = "PASS"
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has credentials with hardware MFA enabled. "
|
||||
"Consider removing individual root credentials since organizational "
|
||||
"root management is active."
|
||||
)
|
||||
else:
|
||||
report.status_extended = "Root account has a hardware MFA device enabled."
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has credentials without MFA "
|
||||
"despite organizational root management being enabled."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
"MFA is not enabled for root account."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Using IAM console navigate to Dashboard and expand Activate MFA on your root account.",
|
||||
"Text": "Using IAM console navigate to Dashboard and expand Activate MFA on your root account. If using AWS Organizations, consider enabling Centralized Root Management and removing individual root credentials.",
|
||||
"Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html#id_root-user_manage_mfa"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,28 +5,53 @@ from prowler.providers.aws.services.iam.iam_client import iam_client
|
||||
class iam_root_mfa_enabled(Check):
|
||||
def execute(self) -> Check_Report_AWS:
|
||||
findings = []
|
||||
# Check if the root credentials are managed by AWS Organizations
|
||||
if (
|
||||
iam_client.organization_features is not None
|
||||
and "RootCredentialsManagement" not in iam_client.organization_features
|
||||
):
|
||||
if iam_client.credential_report:
|
||||
for user in iam_client.credential_report:
|
||||
if user["user"] == "<root_account>":
|
||||
|
||||
if iam_client.credential_report:
|
||||
for user in iam_client.credential_report:
|
||||
if user["user"] == "<root_account>":
|
||||
password_enabled = user["password_enabled"] == "true"
|
||||
access_key_1_active = user["access_key_1_active"] == "true"
|
||||
access_key_2_active = user["access_key_2_active"] == "true"
|
||||
|
||||
# Only report if root actually has credentials
|
||||
if password_enabled or access_key_1_active or access_key_2_active:
|
||||
report = Check_Report_AWS(
|
||||
metadata=self.metadata(), resource=user
|
||||
)
|
||||
report.region = iam_client.region
|
||||
report.resource_id = user["user"]
|
||||
report.resource_arn = user["arn"]
|
||||
|
||||
# Check if organization manages root credentials
|
||||
org_managed = (
|
||||
iam_client.organization_features is not None
|
||||
and "RootCredentialsManagement"
|
||||
in iam_client.organization_features
|
||||
)
|
||||
|
||||
if user["mfa_active"] == "false":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"MFA is not enabled for root account."
|
||||
)
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has credentials without MFA "
|
||||
"despite organizational root management being enabled."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
"MFA is not enabled for root account."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = "MFA is enabled for root account."
|
||||
if org_managed:
|
||||
report.status_extended = (
|
||||
"Root account has credentials with MFA enabled. "
|
||||
"Consider removing individual root credentials since organizational "
|
||||
"root management is active."
|
||||
)
|
||||
else:
|
||||
report.status_extended = (
|
||||
"MFA is enabled for root account."
|
||||
)
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
@@ -54,7 +54,7 @@ class IAM(AWSService):
|
||||
self.role_arn_template = f"arn:{self.audited_partition}:iam:{self.region}:{self.audited_account}:role"
|
||||
self.password_policy_arn_template = f"arn:{self.audited_partition}:iam:{self.region}:{self.audited_account}:password-policy"
|
||||
self.mfa_arn_template = (
|
||||
f"arn:{self.audited_partition}:iam:{self.region}:{self.audited_account}:mfa"
|
||||
f"arn:{self.audited_partition}:iam::{self.audited_account}:mfa"
|
||||
)
|
||||
self.users = self._get_users()
|
||||
self.roles = self._get_roles()
|
||||
|
||||
+7
-9
@@ -23,16 +23,14 @@ class apiserver_strong_ciphers_only(Check):
|
||||
# Check if strong ciphers are set in "--tls-cipher-suites"
|
||||
for command in container.command:
|
||||
if command.startswith("--tls-cipher-suites"):
|
||||
if (
|
||||
command.split("=")[1]
|
||||
.split(",")
|
||||
.issubset(
|
||||
apiserver_client.audit_config.get(
|
||||
"apiserver_strong_ciphers",
|
||||
default_apiserver_strong_ciphers,
|
||||
)
|
||||
configured_ciphers = set(command.split("=")[1].split(","))
|
||||
allowed_ciphers = set(
|
||||
apiserver_client.audit_config.get(
|
||||
"apiserver_strong_ciphers",
|
||||
default_apiserver_strong_ciphers,
|
||||
)
|
||||
):
|
||||
)
|
||||
if configured_ciphers.issubset(allowed_ciphers):
|
||||
strong_ciphers_set = True
|
||||
if not strong_ciphers_set:
|
||||
break
|
||||
|
||||
@@ -1712,13 +1712,13 @@ aws:
|
||||
assert not recovered_regions
|
||||
|
||||
def test_get_regions_all_count(self):
|
||||
assert len(AwsProvider.get_regions(partition=None)) == 36
|
||||
assert len(AwsProvider.get_regions(partition=None)) == 37
|
||||
|
||||
def test_get_regions_cn_count(self):
|
||||
assert len(AwsProvider.get_regions("aws-cn")) == 2
|
||||
|
||||
def test_get_regions_aws_count(self):
|
||||
assert len(AwsProvider.get_regions(partition="aws")) == 32
|
||||
assert len(AwsProvider.get_regions(partition="aws")) == 33
|
||||
|
||||
def test_get_all_regions(self):
|
||||
with patch(
|
||||
|
||||
@@ -304,7 +304,7 @@ class TestAWSMutelist:
|
||||
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) > 0
|
||||
assert mutelist.mutelist == mutelist_fixture
|
||||
|
||||
def test_validate_mutelist_not_valid_key(self):
|
||||
@@ -317,7 +317,7 @@ class TestAWSMutelist:
|
||||
|
||||
mutelist = AWSMutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert not mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0
|
||||
assert mutelist.mutelist == {}
|
||||
assert mutelist.mutelist_file_path is None
|
||||
|
||||
|
||||
+184
@@ -33,11 +33,13 @@ class Test_iam_no_root_access_key_test:
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
@@ -75,11 +77,13 @@ class Test_iam_no_root_access_key_test:
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "true"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
@@ -117,11 +121,13 @@ class Test_iam_no_root_access_key_test:
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "false"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "true"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
@@ -159,11 +165,13 @@ class Test_iam_no_root_access_key_test:
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "false"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "true"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "true"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
@@ -174,3 +182,179 @@ class Test_iam_no_root_access_key_test:
|
||||
result[0].resource_arn
|
||||
== "arn:aws:iam::123456789012:user/<root_account>"
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_root_no_credentials(self):
|
||||
iam_client = client("iam")
|
||||
user = "test"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client:
|
||||
from prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import (
|
||||
iam_no_root_access_key,
|
||||
)
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "false"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
# Should return no findings since root has no credentials
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_root_no_access_keys_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client:
|
||||
from prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import (
|
||||
iam_no_root_access_key,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Root account has password configured but no access keys. Consider removing individual root credentials since organizational root management is active."
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:iam::123456789012:user/<root_account>"
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_root_access_keys_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client:
|
||||
from prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import (
|
||||
iam_no_root_access_key,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "true"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Root account has one active access key despite organizational root management being enabled."
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:iam::123456789012:user/<root_account>"
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_root_both_access_keys_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
):
|
||||
with mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client:
|
||||
from prowler.providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import (
|
||||
iam_no_root_access_key,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:user/<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "false"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "true"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "true"
|
||||
check = iam_no_root_access_key()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Root account has two active access keys despite organizational root management being enabled."
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:iam::123456789012:user/<root_account>"
|
||||
)
|
||||
|
||||
+267
-36
@@ -1,5 +1,9 @@
|
||||
from re import search
|
||||
from unittest import mock
|
||||
|
||||
from boto3 import client
|
||||
from moto import mock_aws
|
||||
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
@@ -15,23 +19,14 @@ class Test_iam_root_hardware_mfa_enabled_test:
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_root_virtual_mfa_enabled(self):
|
||||
iam_client = mock.MagicMock
|
||||
iam_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 1},
|
||||
}
|
||||
iam_client.virtual_mfa_devices = [
|
||||
{
|
||||
"SerialNumber": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa/mfa",
|
||||
"User": {"Arn": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"},
|
||||
}
|
||||
]
|
||||
iam_client.audited_partition = "aws"
|
||||
iam_client.region = AWS_REGION_US_EAST_1
|
||||
iam_client.mfa_arn_template = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
iam_client.organization_features = []
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -40,13 +35,32 @@ class Test_iam_root_hardware_mfa_enabled_test:
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client",
|
||||
new=iam_client,
|
||||
),
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import (
|
||||
iam_root_hardware_mfa_enabled,
|
||||
)
|
||||
|
||||
# Set up virtual MFA device for root
|
||||
service_client.virtual_mfa_devices = [
|
||||
{
|
||||
"SerialNumber": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa/mfa",
|
||||
"User": {"Arn": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"},
|
||||
}
|
||||
]
|
||||
service_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 1},
|
||||
}
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
check = iam_root_hardware_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
@@ -58,18 +72,14 @@ class Test_iam_root_hardware_mfa_enabled_test:
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
|
||||
@mock_aws
|
||||
def test_root_hardware_mfa_enabled(self):
|
||||
iam_client = mock.MagicMock
|
||||
iam_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 1},
|
||||
}
|
||||
iam_client.virtual_mfa_devices = []
|
||||
iam_client.audited_partition = "aws"
|
||||
iam_client.region = AWS_REGION_US_EAST_1
|
||||
iam_client.mfa_arn_template = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
iam_client.organization_features = []
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -78,13 +88,27 @@ class Test_iam_root_hardware_mfa_enabled_test:
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client",
|
||||
new=iam_client,
|
||||
),
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import (
|
||||
iam_root_hardware_mfa_enabled,
|
||||
)
|
||||
|
||||
# No virtual MFA devices (indicating hardware MFA)
|
||||
service_client.virtual_mfa_devices = []
|
||||
service_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 1},
|
||||
}
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
check = iam_root_hardware_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
@@ -96,16 +120,14 @@ class Test_iam_root_hardware_mfa_enabled_test:
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
|
||||
@mock_aws
|
||||
def test_root_hardware_mfa_enabled_none_summary(self):
|
||||
iam_client = mock.MagicMock
|
||||
iam_client.account_summary = None
|
||||
iam_client.virtual_mfa_devices = []
|
||||
iam_client.audited_partition = "aws"
|
||||
iam_client.region = AWS_REGION_US_EAST_1
|
||||
iam_client.mfa_arn_template = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
iam_client.organization_features = []
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
@@ -114,13 +136,222 @@ class Test_iam_root_hardware_mfa_enabled_test:
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client",
|
||||
new=iam_client,
|
||||
),
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import (
|
||||
iam_root_hardware_mfa_enabled,
|
||||
)
|
||||
|
||||
# No account summary
|
||||
service_client.account_summary = None
|
||||
service_client.virtual_mfa_devices = []
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
check = iam_root_hardware_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_root_no_credentials(self):
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import (
|
||||
iam_root_hardware_mfa_enabled,
|
||||
)
|
||||
|
||||
service_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 1},
|
||||
}
|
||||
service_client.virtual_mfa_devices = []
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "false"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
check = iam_root_hardware_mfa_enabled()
|
||||
result = check.execute()
|
||||
# Should return no findings since root has no credentials
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_root_hardware_mfa_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import (
|
||||
iam_root_hardware_mfa_enabled,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
service_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 1},
|
||||
}
|
||||
service_client.virtual_mfa_devices = []
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "true"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
check = iam_root_hardware_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert search(
|
||||
"Root account has credentials with hardware MFA enabled. "
|
||||
"Consider removing individual root credentials since organizational "
|
||||
"root management is active.",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
|
||||
@mock_aws
|
||||
def test_root_virtual_mfa_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import (
|
||||
iam_root_hardware_mfa_enabled,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
service_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 1},
|
||||
}
|
||||
service_client.virtual_mfa_devices = [
|
||||
{
|
||||
"SerialNumber": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa/mfa",
|
||||
"User": {"Arn": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"},
|
||||
}
|
||||
]
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "true"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
check = iam_root_hardware_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert search(
|
||||
"Root account has credentials with virtual MFA instead of hardware MFA "
|
||||
"despite organizational root management being enabled.",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
|
||||
@mock_aws
|
||||
def test_root_no_mfa_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import (
|
||||
iam_root_hardware_mfa_enabled,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
service_client.account_summary = {
|
||||
"SummaryMap": {"AccountMFAEnabled": 0},
|
||||
}
|
||||
service_client.virtual_mfa_devices = []
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
check = iam_root_hardware_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert search(
|
||||
"Root account has credentials without MFA "
|
||||
"despite organizational root management being enabled.",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:mfa"
|
||||
|
||||
@@ -42,12 +42,16 @@ class Test_iam_root_mfa_enabled_test:
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["mfa_active"] = "false"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:<root_account>:root"
|
||||
|
||||
check = iam_root_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert search(
|
||||
"MFA is not enabled for root account.", result[0].status_extended
|
||||
@@ -80,13 +84,149 @@ class Test_iam_root_mfa_enabled_test:
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["mfa_active"] = "true"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:<root_account>:root"
|
||||
|
||||
check = iam_root_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert search("MFA is enabled for root account.", result[0].status_extended)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == service_client.credential_report[0]["arn"]
|
||||
|
||||
@mock_aws
|
||||
def test_root_no_credentials(self):
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled import (
|
||||
iam_root_mfa_enabled,
|
||||
)
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["mfa_active"] = "false"
|
||||
service_client.credential_report[0]["password_enabled"] = "false"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:<root_account>:root"
|
||||
|
||||
check = iam_root_mfa_enabled()
|
||||
result = check.execute()
|
||||
# Should return no findings since root has no credentials
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_root_mfa_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled import (
|
||||
iam_root_mfa_enabled,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["mfa_active"] = "true"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "true"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "false"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:<root_account>:root"
|
||||
|
||||
check = iam_root_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert search(
|
||||
"Root account has credentials with MFA enabled. "
|
||||
"Consider removing individual root credentials since organizational "
|
||||
"root management is active.",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == service_client.credential_report[0]["arn"]
|
||||
|
||||
@mock_aws
|
||||
def test_root_mfa_disabled_with_organizational_management_enabled(self):
|
||||
iam_client = client("iam")
|
||||
user = "test-user"
|
||||
iam_client.create_user(UserName=user)["User"]["Arn"]
|
||||
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
from prowler.providers.aws.services.iam.iam_service import IAM
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled.iam_client",
|
||||
new=IAM(aws_provider),
|
||||
) as service_client,
|
||||
):
|
||||
from prowler.providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled import (
|
||||
iam_root_mfa_enabled,
|
||||
)
|
||||
|
||||
# Set up organizational root management
|
||||
service_client.organization_features = ["RootCredentialsManagement"]
|
||||
|
||||
service_client.credential_report[0]["user"] = "<root_account>"
|
||||
service_client.credential_report[0]["mfa_active"] = "false"
|
||||
service_client.credential_report[0]["password_enabled"] = "true"
|
||||
service_client.credential_report[0]["access_key_1_active"] = "false"
|
||||
service_client.credential_report[0]["access_key_2_active"] = "true"
|
||||
service_client.credential_report[0][
|
||||
"arn"
|
||||
] = "arn:aws:iam::123456789012:<root_account>:root"
|
||||
|
||||
check = iam_root_mfa_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert search(
|
||||
"Root account has credentials without MFA "
|
||||
"despite organizational root management being enabled.",
|
||||
result[0].status_extended,
|
||||
)
|
||||
assert result[0].resource_id == "<root_account>"
|
||||
assert result[0].resource_arn == service_client.credential_report[0]["arn"]
|
||||
|
||||
@@ -36,7 +36,7 @@ class TestAzureMutelist:
|
||||
|
||||
mutelist = AzureMutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert not mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0
|
||||
assert mutelist.mutelist == {}
|
||||
assert mutelist.mutelist_file_path is None
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestGCPMutelist:
|
||||
|
||||
mutelist = GCPMutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert not mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0
|
||||
assert mutelist.mutelist == {}
|
||||
assert mutelist.mutelist_file_path is None
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class TestGithubMutelist:
|
||||
|
||||
mutelist = GithubMutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert not mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0
|
||||
assert mutelist.mutelist == {}
|
||||
assert mutelist.mutelist_file_path is None
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class TestKubernetesMutelist:
|
||||
|
||||
mutelist = KubernetesMutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert not mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0
|
||||
assert mutelist.mutelist == {}
|
||||
assert mutelist.mutelist_file_path is None
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestM365Mutelist:
|
||||
|
||||
mutelist = M365Mutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert not mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0
|
||||
assert mutelist.mutelist == {}
|
||||
assert mutelist.mutelist_file_path is None
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestNHNMutelist:
|
||||
|
||||
mutelist = NHNMutelist(mutelist_content=mutelist_fixture)
|
||||
|
||||
assert not mutelist.validate_mutelist()
|
||||
assert len(mutelist.validate_mutelist(mutelist_fixture)) == 0
|
||||
assert mutelist.mutelist == {}
|
||||
assert mutelist.mutelist_file_path is None
|
||||
|
||||
|
||||
+86
-79
@@ -4,58 +4,63 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [v1.8.0] (Prowler v5.8.0) – Not released
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Fix sync between filter buttons and URL when filters change. [(#7928)](https://github.com/prowler-cloud/prowler/pull/7928)
|
||||
- Improve heatmap perfomance. [(#7934)](https://github.com/prowler-cloud/prowler/pull/7934)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- New profile page with details about the user and their roles. [(#7780)](https://github.com/prowler-cloud/prowler/pull/7780)
|
||||
- Improved `SnippetChip` component and show resource name in new findings table. [(#7813)](https://github.com/prowler-cloud/prowler/pull/7813)
|
||||
- Possibility to edit the organization name. [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829)
|
||||
- Add GCP credential method (Account Service Key). [(#7872)](https://github.com/prowler-cloud/prowler/pull/7872)
|
||||
- Add compliance detail view: ENS [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853)
|
||||
- Add compliance detail view: ISO [(#7897)](https://github.com/prowler-cloud/prowler/pull/7897)
|
||||
- Add compliance detail view: CIS [(#7913)](https://github.com/prowler-cloud/prowler/pull/7913)
|
||||
- Add compliance detail view: AWS Well-Architected Framework [(#7925)](https://github.com/prowler-cloud/prowler/pull/7925)
|
||||
- Add compliance detail view: KISA [(#7965)](https://github.com/prowler-cloud/prowler/pull/7965)
|
||||
- Add compliance detail view: ProwlerThreatScore [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966)
|
||||
- Improve `Scan ID` filter by adding more context and enhancing the UI/UX. [(#7979)](https://github.com/prowler-cloud/prowler/pull/7979)
|
||||
|
||||
- New profile page with details about the user and their roles [(#7780)](https://github.com/prowler-cloud/prowler/pull/7780)
|
||||
- Improved `SnippetChip` component and show resource name in new findings table [(#7813)](https://github.com/prowler-cloud/prowler/pull/7813)
|
||||
- Possibility to edit the organization name [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829)
|
||||
- GCP credential method (Account Service Key) [(#7872)](https://github.com/prowler-cloud/prowler/pull/7872)
|
||||
- Compliance detail view: ENS [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853)
|
||||
- Compliance detail view: ISO [(#7897)](https://github.com/prowler-cloud/prowler/pull/7897)
|
||||
- Compliance detail view: CIS [(#7913)](https://github.com/prowler-cloud/prowler/pull/7913)
|
||||
- Compliance detail view: AWS Well-Architected Framework [(#7925)](https://github.com/prowler-cloud/prowler/pull/7925)
|
||||
- Compliance detail view: KISA [(#7965)](https://github.com/prowler-cloud/prowler/pull/7965)
|
||||
- Compliance detail view: ProwlerThreatScore [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966)
|
||||
- Compliance detail view: Generic (rest of the compliances) [(#7990)](https://github.com/prowler-cloud/prowler/pull/7990)
|
||||
- Compliance detail view: MITRE ATTACK [(#8002)](https://github.com/prowler-cloud/prowler/pull/8002)
|
||||
- Improve `Scan ID` filter by adding more context and enhancing the UI/UX [(#7979)](https://github.com/prowler-cloud/prowler/pull/7979)
|
||||
- Lighthouse chat interface [(#7878)](https://github.com/prowler-cloud/prowler/pull/7878)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Add `Provider UID` filter to scans page. [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820)
|
||||
- Aligned Next.js version to `v14.2.29` across Prowler and Cloud environments for consistency and improved maintainability. [(#7962)](https://github.com/prowler-cloud/prowler/pull/7962)
|
||||
- `Provider UID` filter to scans page [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820)
|
||||
- Aligned Next.js version to `v14.2.29` across Prowler and Cloud environments for consistency and improved maintainability [(#7962)](https://github.com/prowler-cloud/prowler/pull/7962)
|
||||
- Refactor credentials forms with reusable components and error handling [(#7988)](https://github.com/prowler-cloud/prowler/pull/7988)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Sync between filter buttons and URL when filters change [(#7928)](https://github.com/prowler-cloud/prowler/pull/7928)
|
||||
- Improve heatmap perfomance [(#7934)](https://github.com/prowler-cloud/prowler/pull/7934)
|
||||
- SelectScanProvider warning fixed with empty alias [(#7998)](https://github.com/prowler-cloud/prowler/pull/7998)
|
||||
|
||||
---
|
||||
|
||||
## [v1.7.3] (Prowler v5.7.3)
|
||||
|
||||
### 🐞 Fixes
|
||||
### 🐞 Fixed
|
||||
|
||||
- Fix encrypted password typo in `formSchemas`. [(#7828)](https://github.com/prowler-cloud/prowler/pull/7828)
|
||||
- Encrypted password typo in `formSchemas` [(#7828)](https://github.com/prowler-cloud/prowler/pull/7828)
|
||||
|
||||
---
|
||||
|
||||
## [v1.7.2] (Prowler v5.7.2)
|
||||
|
||||
### 🐞 Fixes
|
||||
### 🐞 Fixed
|
||||
|
||||
- Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758)
|
||||
- Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)]
|
||||
- Retrieve more than 10 scans in /compliance page. [(#7865)](https://github.com/prowler-cloud/prowler/pull/7865)
|
||||
- Improve CustomDropdownFilter component. [(#7868)(https://github.com/prowler-cloud/prowler/pull/7868)]
|
||||
- Download report behaviour updated to show feedback based on API response [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758)
|
||||
- Missing KISA and ProwlerThreat icons added to the compliance page [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)]
|
||||
- Retrieve more than 10 scans in /compliance page [(#7865)](https://github.com/prowler-cloud/prowler/pull/7865)
|
||||
- Improve CustomDropdownFilter component [(#7868)(https://github.com/prowler-cloud/prowler/pull/7868)]
|
||||
|
||||
---
|
||||
|
||||
## [v1.7.1] (Prowler v5.7.1)
|
||||
|
||||
### 🐞 Fixes
|
||||
### 🐞 Fixed
|
||||
|
||||
- Added validation to AWS IAM role. [(#7787)](https://github.com/prowler-cloud/prowler/pull/7787)
|
||||
- Tweak some wording for consistency throughout the app. [(#7794)](https://github.com/prowler-cloud/prowler/pull/7794)
|
||||
- Retrieve more than 10 providers in /scans, /manage-groups and /findings pages. [(#7793)](https://github.com/prowler-cloud/prowler/pull/7793)
|
||||
- Validation to AWS IAM role [(#7787)](https://github.com/prowler-cloud/prowler/pull/7787)
|
||||
- Tweak some wording for consistency throughout the app [(#7794)](https://github.com/prowler-cloud/prowler/pull/7794)
|
||||
- Retrieve more than 10 providers in /scans, /manage-groups and /findings pages [(#7793)](https://github.com/prowler-cloud/prowler/pull/7793)
|
||||
|
||||
---
|
||||
|
||||
@@ -63,18 +68,20 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Add a new chart to show the split between passed and failed findings. [(#7680)](https://github.com/prowler-cloud/prowler/pull/7680)
|
||||
- Added `Accordion` component. [(#7700)](https://github.com/prowler-cloud/prowler/pull/7700)
|
||||
- Improve `Provider UID` filter by adding more context and enhancing the UI/UX. [(#7741)](https://github.com/prowler-cloud/prowler/pull/7741)
|
||||
- Added an AWS CloudFormation Quick Link to the IAM Role credentials step [(#7735)](https://github.com/prowler-cloud/prowler/pull/7735)
|
||||
– Use `getLatestFindings` on findings page when no scan or date filters are applied. [(#7756)](https://github.com/prowler-cloud/prowler/pull/7756)
|
||||
- Chart to show the split between passed and failed findings [(#7680)](https://github.com/prowler-cloud/prowler/pull/7680)
|
||||
- `Accordion` component [(#7700)](https://github.com/prowler-cloud/prowler/pull/7700)
|
||||
- Improve `Provider UID` filter by adding more context and enhancing the UI/UX [(#7741)](https://github.com/prowler-cloud/prowler/pull/7741)
|
||||
- AWS CloudFormation Quick Link to the IAM Role credentials step [(#7735)](https://github.com/prowler-cloud/prowler/pull/7735)
|
||||
– Use `getLatestFindings` on findings page when no scan or date filters are applied [(#7756)](https://github.com/prowler-cloud/prowler/pull/7756)
|
||||
|
||||
### 🐞 Fixes
|
||||
### 🐞 Fixed
|
||||
|
||||
- Fix form validation in launch scan workflow. [(#7693)](https://github.com/prowler-cloud/prowler/pull/7693)
|
||||
- Moved ProviderType to a shared types file and replaced all occurrences across the codebase. [(#7710)](https://github.com/prowler-cloud/prowler/pull/7710)
|
||||
- Added filter to retrieve only connected providers on the scan page. [(#7723)](https://github.com/prowler-cloud/prowler/pull/7723)
|
||||
- Removed the alias if not added from findings detail page. [(#7751)](https://github.com/prowler-cloud/prowler/pull/7751)
|
||||
- Form validation in launch scan workflow [(#7693)](https://github.com/prowler-cloud/prowler/pull/7693)
|
||||
- Moved ProviderType to a shared types file and replaced all occurrences across the codebase [(#7710)](https://github.com/prowler-cloud/prowler/pull/7710)
|
||||
- Added filter to retrieve only connected providers on the scan page [(#7723)](https://github.com/prowler-cloud/prowler/pull/7723)
|
||||
|
||||
### Removed
|
||||
- Alias if not added from findings detail page [(#7751)](https://github.com/prowler-cloud/prowler/pull/7751)
|
||||
|
||||
---
|
||||
|
||||
@@ -82,22 +89,22 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Support for the `M365` Cloud Provider. [(#7590)](https://github.com/prowler-cloud/prowler/pull/7590)
|
||||
- Added option to customize the number of items displayed per table page. [(#7634)](https://github.com/prowler-cloud/prowler/pull/7634)
|
||||
- Add delta attribute in findings detail view. [(#7654)](https://github.com/prowler-cloud/prowler/pull/7654)
|
||||
- Add delta indicator in new findings table. [(#7676)](https://github.com/prowler-cloud/prowler/pull/7676)
|
||||
- Add a button to download the CSV report in compliance card. [(#7665)](https://github.com/prowler-cloud/prowler/pull/7665)
|
||||
- Show loading state while checking provider connection. [(#7669)](https://github.com/prowler-cloud/prowler/pull/7669)
|
||||
- Support for the `M365` Cloud Provider [(#7590)](https://github.com/prowler-cloud/prowler/pull/7590)
|
||||
- Option to customize the number of items displayed per table page [(#7634)](https://github.com/prowler-cloud/prowler/pull/7634)
|
||||
- Delta attribute in findings detail view [(#7654)](https://github.com/prowler-cloud/prowler/pull/7654)
|
||||
- Delta indicator in new findings table [(#7676)](https://github.com/prowler-cloud/prowler/pull/7676)
|
||||
- Button to download the CSV report in compliance card [(#7665)](https://github.com/prowler-cloud/prowler/pull/7665)
|
||||
- Show loading state while checking provider connection [(#7669)](https://github.com/prowler-cloud/prowler/pull/7669)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Finding URLs now include the ID, allowing them to be shared within the organization. [(#7654)](https://github.com/prowler-cloud/prowler/pull/7654)
|
||||
- Show Add/Update credentials depending on whether a secret is already set or not. [(#7669)](https://github.com/prowler-cloud/prowler/pull/7669)
|
||||
- Finding URLs now include the ID, allowing them to be shared within the organization [(#7654)](https://github.com/prowler-cloud/prowler/pull/7654)
|
||||
- Show Add/Update credentials depending on whether a secret is already set or not [(#7669)](https://github.com/prowler-cloud/prowler/pull/7669)
|
||||
|
||||
### 🐞 Fixes
|
||||
### 🐞 Fixed
|
||||
|
||||
- Set a default session duration when configuring an AWS Cloud Provider using a role. [(#7639)](https://github.com/prowler-cloud/prowler/pull/7639)
|
||||
- Error about page number persistence when filters change. [(#7655)](https://github.com/prowler-cloud/prowler/pull/7655)
|
||||
- Set a default session duration when configuring an AWS Cloud Provider using a role [(#7639)](https://github.com/prowler-cloud/prowler/pull/7639)
|
||||
- Error about page number persistence when filters change [(#7655)](https://github.com/prowler-cloud/prowler/pull/7655)
|
||||
|
||||
---
|
||||
|
||||
@@ -106,18 +113,18 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🚀 Added
|
||||
|
||||
- Social login integration with Google and GitHub [(#7218)](https://github.com/prowler-cloud/prowler/pull/7218)
|
||||
- Added `one-time scan` feature: Adds support for single scan execution. [(#7188)](https://github.com/prowler-cloud/prowler/pull/7188)
|
||||
- Accepted invitations can no longer be edited. [(#7198)](https://github.com/prowler-cloud/prowler/pull/7198)
|
||||
- Added download column in scans table to download reports for completed scans. [(#7353)](https://github.com/prowler-cloud/prowler/pull/7353)
|
||||
- Show muted icon when a finding is muted. [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378)
|
||||
- Added static status icon with link to service status page. [(#7468)](https://github.com/prowler-cloud/prowler/pull/7468)
|
||||
- `one-time scan` feature: Adds support for single scan execution [(#7188)](https://github.com/prowler-cloud/prowler/pull/7188)
|
||||
- Accepted invitations can no longer be edited [(#7198)](https://github.com/prowler-cloud/prowler/pull/7198)
|
||||
- Download column in scans table to download reports for completed scans [(#7353)](https://github.com/prowler-cloud/prowler/pull/7353)
|
||||
- Show muted icon when a finding is muted [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378)
|
||||
- Static status icon with link to service status page [(#7468)](https://github.com/prowler-cloud/prowler/pull/7468)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Tweak styles for compliance cards. [(#7148)](https://github.com/prowler-cloud/prowler/pull/7148).
|
||||
- Upgrade Next.js to v14.2.25 to fix a middleware authorization vulnerability. [(#7339)](https://github.com/prowler-cloud/prowler/pull/7339)
|
||||
- Apply default filter to show only failed items when coming from scan table. [(#7356)](https://github.com/prowler-cloud/prowler/pull/7356)
|
||||
- Fix link behavior in scan cards: only disable "View Findings" when scan is not completed or executing. [(#7368)](https://github.com/prowler-cloud/prowler/pull/7368)
|
||||
- Tweak styles for compliance cards [(#7148)](https://github.com/prowler-cloud/prowler/pull/7148)
|
||||
- Upgrade Next.js to v14.2.25 to fix a middleware authorization vulnerability [(#7339)](https://github.com/prowler-cloud/prowler/pull/7339)
|
||||
- Apply default filter to show only failed items when coming from scan table [(#7356)](https://github.com/prowler-cloud/prowler/pull/7356)
|
||||
- Fix link behavior in scan cards: only disable "View Findings" when scan is not completed or executing [(#7368)](https://github.com/prowler-cloud/prowler/pull/7368)
|
||||
|
||||
---
|
||||
|
||||
@@ -125,23 +132,23 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Added `exports` feature: Users can now download artifacts via a new button. [(#7006)](https://github.com/prowler-cloud/prowler/pull/7006)
|
||||
- New sidebar with nested menus and integrated mobile navigation. [(#7018)](https://github.com/prowler-cloud/prowler/pull/7018)
|
||||
- Added animation for scan execution progress—it now updates automatically.[(#6972)](https://github.com/prowler-cloud/prowler/pull/6972)
|
||||
- Add `status_extended` attribute to finding details. [(#6997)](https://github.com/prowler-cloud/prowler/pull/6997)
|
||||
- Add `Prowler version` to the sidebar. [(#7086)](https://github.com/prowler-cloud/prowler/pull/7086)
|
||||
- `exports` feature: Users can now download artifacts via a new button [(#7006)](https://github.com/prowler-cloud/prowler/pull/7006)
|
||||
- New sidebar with nested menus and integrated mobile navigation [(#7018)](https://github.com/prowler-cloud/prowler/pull/7018)
|
||||
- Animation for scan execution progress—it now updates automatically.[(#6972)](https://github.com/prowler-cloud/prowler/pull/6972)
|
||||
- `status_extended` attribute to finding details [(#6997)](https://github.com/prowler-cloud/prowler/pull/6997)
|
||||
- `Prowler version` to the sidebar [(#7086)](https://github.com/prowler-cloud/prowler/pull/7086)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- New compliance dropdown. [(#7118)](https://github.com/prowler-cloud/prowler/pull/7118).
|
||||
- New compliance dropdown [(#7118)](https://github.com/prowler-cloud/prowler/pull/7118)
|
||||
|
||||
### 🐞 Fixes
|
||||
### 🐞 Fixed
|
||||
|
||||
- Revalidate the page when a role is deleted. [(#6976)](https://github.com/prowler-cloud/prowler/pull/6976)
|
||||
- Allows removing group visibility when creating a role. [(#7088)](https://github.com/prowler-cloud/prowler/pull/7088)
|
||||
- Displays correct error messages when deleting a user. [(#7089)](https://github.com/prowler-cloud/prowler/pull/7089)
|
||||
- Updated label: _"Select a scan job"_ → _"Select a cloud provider"_. [(#7107)](https://github.com/prowler-cloud/prowler/pull/7107)
|
||||
- Display uid if alias is missing when creating a group. [(#7137)](https://github.com/prowler-cloud/prowler/pull/7137)
|
||||
- Revalidate the page when a role is deleted [(#6976)](https://github.com/prowler-cloud/prowler/pull/6976)
|
||||
- Allows removing group visibility when creating a role [(#7088)](https://github.com/prowler-cloud/prowler/pull/7088)
|
||||
- Displays correct error messages when deleting a user [(#7089)](https://github.com/prowler-cloud/prowler/pull/7089)
|
||||
- Updated label: _"Select a scan job"_ → _"Select a cloud provider"_ [(#7107)](https://github.com/prowler-cloud/prowler/pull/7107)
|
||||
- Display uid if alias is missing when creating a group [(#7137)](https://github.com/prowler-cloud/prowler/pull/7137)
|
||||
|
||||
---
|
||||
|
||||
@@ -149,12 +156,12 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Findings endpoints now require at least one date filter [(#6864)](https://github.com/prowler-cloud/prowler/pull/6864).
|
||||
- Findings endpoints now require at least one date filter [(#6864)](https://github.com/prowler-cloud/prowler/pull/6864)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Scans now appear immediately after launch. [(#6791)](https://github.com/prowler-cloud/prowler/pull/6791).
|
||||
- Improved sign-in and sign-up forms. [(#6813)](https://github.com/prowler-cloud/prowler/pull/6813).
|
||||
- Scans now appear immediately after launch [(#6791)](https://github.com/prowler-cloud/prowler/pull/6791)
|
||||
- Improved sign-in and sign-up forms [(#6813)](https://github.com/prowler-cloud/prowler/pull/6813)
|
||||
|
||||
---
|
||||
|
||||
@@ -162,12 +169,12 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `First seen` field included in finding details. [(#6575)](https://github.com/prowler-cloud/prowler/pull/6575)
|
||||
- `First seen` field included in finding details [(#6575)](https://github.com/prowler-cloud/prowler/pull/6575)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Completely redesigned finding details layout. [(#6575)](https://github.com/prowler-cloud/prowler/pull/6575)
|
||||
- Completely redesigned scan details layout.[(#6665)](https://github.com/prowler-cloud/prowler/pull/6665)
|
||||
- Simplified provider setup: reduced from 4 to 3 steps. Successful connection now triggers an animation before redirecting to `/scans`. [(#6665)](https://github.com/prowler-cloud/prowler/pull/6665)
|
||||
- Completely redesigned finding details layout [(#6575)](https://github.com/prowler-cloud/prowler/pull/6575)
|
||||
- Completely redesigned scan details layout [(#6665)](https://github.com/prowler-cloud/prowler/pull/6665)
|
||||
- Simplified provider setup: reduced from 4 to 3 steps Successful connection now triggers an animation before redirecting to `/scans` [(#6665)](https://github.com/prowler-cloud/prowler/pull/6665)
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export const getLighthouseProviderChecks = async ({
|
||||
providerType,
|
||||
service,
|
||||
severity,
|
||||
compliances,
|
||||
}: {
|
||||
providerType: string;
|
||||
service: string[];
|
||||
severity: string[];
|
||||
compliances: string[];
|
||||
}) => {
|
||||
const url = new URL(
|
||||
`https://hub.prowler.com/api/check?fields=id&providers=${providerType}`,
|
||||
);
|
||||
if (service) {
|
||||
url.searchParams.append("services", service.join(","));
|
||||
}
|
||||
if (severity) {
|
||||
url.searchParams.append("severities", severity.join(","));
|
||||
}
|
||||
if (compliances) {
|
||||
url.searchParams.append("compliances", compliances.join(","));
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const ids = data.map((item: { id: string }) => item.id);
|
||||
return ids;
|
||||
};
|
||||
|
||||
export const getLighthouseCheckDetails = async ({
|
||||
checkId,
|
||||
}: {
|
||||
checkId: string;
|
||||
}) => {
|
||||
const url = new URL(`https://hub.prowler.com/api/check/${checkId}`);
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export const getLighthouseComplianceFrameworks = async (
|
||||
provider_type: string,
|
||||
) => {
|
||||
const url = new URL(
|
||||
`https://hub.prowler.com/api/compliance?fields=id&provider=${provider_type}`,
|
||||
);
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const frameworks = data.map((item: { id: string }) => item.id);
|
||||
return frameworks;
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper";
|
||||
|
||||
export const getLighthouseCompliancesOverview = async ({
|
||||
scanId, // required
|
||||
fields,
|
||||
filters,
|
||||
page,
|
||||
pageSize,
|
||||
sort,
|
||||
}: {
|
||||
scanId: string;
|
||||
fields?: string[];
|
||||
filters?: Record<string, string | number | boolean | undefined>;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sort?: string;
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const url = new URL(`${apiBaseUrl}/compliance-overviews`);
|
||||
|
||||
// Required filter
|
||||
url.searchParams.append("filter[scan_id]", scanId);
|
||||
|
||||
// Handle optional fields
|
||||
if (fields && fields.length > 0) {
|
||||
url.searchParams.append("fields[compliance-overviews]", fields.join(","));
|
||||
}
|
||||
|
||||
// Handle filters
|
||||
if (filters) {
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== "" && value !== null) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle pagination
|
||||
if (page) {
|
||||
url.searchParams.append("page[number]", page.toString());
|
||||
}
|
||||
if (pageSize) {
|
||||
url.searchParams.append("page[size]", pageSize.toString());
|
||||
}
|
||||
|
||||
// Handle sorting
|
||||
if (sort) {
|
||||
url.searchParams.append("sort", sort);
|
||||
}
|
||||
|
||||
try {
|
||||
const compliances = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
const data = await compliances.json();
|
||||
const parsedData = parseStringify(data);
|
||||
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching providers:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLighthouseComplianceOverview = async ({
|
||||
complianceId,
|
||||
fields,
|
||||
}: {
|
||||
complianceId: string;
|
||||
fields?: string[];
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const url = new URL(`${apiBaseUrl}/compliance-overviews/${complianceId}`);
|
||||
|
||||
if (fields) {
|
||||
url.searchParams.append("fields[compliance-overviews]", fields.join(","));
|
||||
}
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const parsedData = parseStringify(data);
|
||||
|
||||
return parsedData;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./checks";
|
||||
export * from "./complianceframeworks";
|
||||
export * from "./compliances";
|
||||
export * from "./lighthouse";
|
||||
export * from "./resources";
|
||||
@@ -0,0 +1,146 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib/helper";
|
||||
|
||||
const getLighthouseConfigId = async (): Promise<string> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const url = new URL(
|
||||
`${apiBaseUrl}/lighthouse-configurations?filter[name]=OpenAI`,
|
||||
);
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check if data array exists and has at least one item
|
||||
if (data?.data && data.data.length > 0) {
|
||||
return data.data[0].id;
|
||||
}
|
||||
|
||||
// Return empty string if no configuration found
|
||||
return "";
|
||||
} catch (error) {
|
||||
console.error("[Server] Error in getOpenAIConfigurationId:", error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export const getAIKey = async (): Promise<string> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const configId = await getLighthouseConfigId();
|
||||
|
||||
if (!configId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`${apiBaseUrl}/lighthouse-configurations/${configId}?fields[lighthouse-config]=api_key`,
|
||||
);
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data.data.attributes.api_key;
|
||||
};
|
||||
|
||||
export const createLighthouseConfig = async (config: {
|
||||
model: string;
|
||||
apiKey: string;
|
||||
businessContext: string;
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
const url = new URL(`${apiBaseUrl}/lighthouse-configurations`);
|
||||
try {
|
||||
const payload = {
|
||||
data: {
|
||||
type: "lighthouse-configurations",
|
||||
attributes: {
|
||||
name: "OpenAI",
|
||||
model: config.model,
|
||||
api_key: config.apiKey,
|
||||
business_context: config.businessContext,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("[Server] Error in createAIConfiguration:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLighthouseConfig = async () => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const configId = await getLighthouseConfigId();
|
||||
|
||||
if (!configId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/lighthouse-configurations/${configId}`);
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("[Server] Error in getLighthouseConfig:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateLighthouseConfig = async (config: {
|
||||
model: string;
|
||||
apiKey: string;
|
||||
businessContext: string;
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
const configId = await getLighthouseConfigId();
|
||||
|
||||
if (!configId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(`${apiBaseUrl}/lighthouse-configurations/${configId}`);
|
||||
|
||||
// Prepare the request payload following the JSONAPI format
|
||||
const payload = {
|
||||
data: {
|
||||
type: "lighthouse-configurations",
|
||||
id: configId,
|
||||
attributes: {
|
||||
model: config.model,
|
||||
api_key: config.apiKey,
|
||||
business_context: config.businessContext,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("[Server] Error in updateAIConfiguration:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper";
|
||||
|
||||
export async function getLighthouseResources(
|
||||
page: number = 1,
|
||||
query: string = "",
|
||||
sort: string = "",
|
||||
filters: any = {},
|
||||
fields: string[] = [],
|
||||
) {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/resources`);
|
||||
|
||||
if (page) {
|
||||
url.searchParams.append("page[number]", page.toString());
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
url.searchParams.append("sort", sort);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
url.searchParams.append("filter[search]", query);
|
||||
}
|
||||
|
||||
if (fields.length > 0) {
|
||||
url.searchParams.append("fields[resources]", fields.join(","));
|
||||
}
|
||||
|
||||
if (filters) {
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
url.searchParams.append(`filter[${key}]`, value as string);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
const data = await response.json();
|
||||
const parsedData = parseStringify(data);
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching resources:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLighthouseResourceById(
|
||||
id: string,
|
||||
fields: string[] = [],
|
||||
include: string[] = [],
|
||||
) {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const url = new URL(`${apiBaseUrl}/resources/${id}`);
|
||||
|
||||
if (fields.length > 0) {
|
||||
url.searchParams.append("fields", fields.join(","));
|
||||
}
|
||||
|
||||
if (include.length > 0) {
|
||||
url.searchParams.append("include", include.join(","));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
const data = await response.json();
|
||||
const parsedData = parseStringify(data);
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching resource:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,17 @@ import {
|
||||
apiBaseUrl,
|
||||
getAuthHeaders,
|
||||
getErrorMessage,
|
||||
getFormValue,
|
||||
parseStringify,
|
||||
wait,
|
||||
} from "@/lib";
|
||||
import {
|
||||
buildSecretConfig,
|
||||
buildUpdateSecretConfig,
|
||||
handleApiError,
|
||||
handleApiResponse,
|
||||
} from "@/lib/provider-credentials/build-crendentials";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { ProvidersApiResponse, ProviderType } from "@/types/providers";
|
||||
|
||||
export const getProviders = async ({
|
||||
@@ -74,10 +82,8 @@ export const getProvider = async (formData: FormData) => {
|
||||
|
||||
export const updateProvider = async (formData: FormData) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
|
||||
const providerId = formData.get("providerId");
|
||||
const providerAlias = formData.get("alias");
|
||||
|
||||
const providerId = formData.get(ProviderCredentialFields.PROVIDER_ID);
|
||||
const providerAlias = formData.get(ProviderCredentialFields.PROVIDER_ALIAS);
|
||||
const url = new URL(`${apiBaseUrl}/providers/${providerId}`);
|
||||
|
||||
try {
|
||||
@@ -88,22 +94,14 @@ export const updateProvider = async (formData: FormData) => {
|
||||
data: {
|
||||
type: "providers",
|
||||
id: providerId,
|
||||
attributes: {
|
||||
alias: providerAlias,
|
||||
},
|
||||
attributes: { alias: providerAlias },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
revalidatePath("/providers");
|
||||
return parseStringify(data);
|
||||
return handleApiResponse(response, "/providers");
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
return {
|
||||
error: getErrorMessage(error),
|
||||
};
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,120 +148,37 @@ export const addCredentialsProvider = async (formData: FormData) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
const url = new URL(`${apiBaseUrl}/providers/secrets`);
|
||||
|
||||
const secretName = formData.get("secretName");
|
||||
const providerId = formData.get("providerId");
|
||||
const providerType = formData.get("providerType") as ProviderType;
|
||||
|
||||
const isRole = formData.get("role_arn") !== null;
|
||||
const isServiceAccount = formData.get("service_account_key") !== null;
|
||||
|
||||
let secret = {};
|
||||
let secretType = "static"; // Default to static credentials
|
||||
|
||||
if (providerType === "aws") {
|
||||
if (isRole) {
|
||||
// Role-based configuration for AWS
|
||||
secretType = "role";
|
||||
secret = {
|
||||
role_arn: formData.get("role_arn"),
|
||||
external_id: formData.get("external_id"),
|
||||
aws_access_key_id: formData.get("aws_access_key_id") || undefined,
|
||||
aws_secret_access_key:
|
||||
formData.get("aws_secret_access_key") || undefined,
|
||||
aws_session_token: formData.get("aws_session_token") || undefined,
|
||||
session_duration:
|
||||
parseInt(formData.get("session_duration") as string, 10) || 3600,
|
||||
role_session_name: formData.get("role_session_name") || undefined,
|
||||
};
|
||||
} else {
|
||||
// Static credentials configuration for AWS
|
||||
secret = {
|
||||
aws_access_key_id: formData.get("aws_access_key_id"),
|
||||
aws_secret_access_key: formData.get("aws_secret_access_key"),
|
||||
aws_session_token: formData.get("aws_session_token") || undefined,
|
||||
};
|
||||
}
|
||||
} else if (providerType === "azure") {
|
||||
// Static credentials configuration for Azure
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
tenant_id: formData.get("tenant_id"),
|
||||
};
|
||||
} else if (providerType === "m365") {
|
||||
// Static credentials configuration for M365
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
tenant_id: formData.get("tenant_id"),
|
||||
user: formData.get("user"),
|
||||
password: formData.get("password"),
|
||||
};
|
||||
} else if (providerType === "gcp") {
|
||||
if (isServiceAccount) {
|
||||
// Service account configuration for GCP
|
||||
secretType = "service_account";
|
||||
const serviceAccountKeyRaw = formData.get(
|
||||
"service_account_key",
|
||||
) as string;
|
||||
|
||||
try {
|
||||
const serviceAccountKey = JSON.parse(serviceAccountKeyRaw);
|
||||
secret = {
|
||||
service_account_key: serviceAccountKey,
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("error", error);
|
||||
}
|
||||
} else {
|
||||
// Static credentials configuration for GCP
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
refresh_token: formData.get("refresh_token"),
|
||||
};
|
||||
}
|
||||
} else if (providerType === "kubernetes") {
|
||||
// Static credentials configuration for Kubernetes
|
||||
secret = {
|
||||
kubeconfig_content: formData.get("kubeconfig_content"),
|
||||
};
|
||||
}
|
||||
const bodyData = {
|
||||
data: {
|
||||
type: "provider-secrets",
|
||||
attributes: {
|
||||
secret_type: secretType,
|
||||
secret,
|
||||
name: secretName,
|
||||
},
|
||||
relationships: {
|
||||
provider: {
|
||||
data: {
|
||||
id: providerId,
|
||||
type: "providers",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const providerId = getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.PROVIDER_ID,
|
||||
);
|
||||
const providerType = getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.PROVIDER_TYPE,
|
||||
) as ProviderType;
|
||||
|
||||
try {
|
||||
const { secretType, secret } = buildSecretConfig(formData, providerType);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(bodyData),
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "provider-secrets",
|
||||
attributes: { secret_type: secretType, secret },
|
||||
relationships: {
|
||||
provider: {
|
||||
data: { id: providerId, type: "providers" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
revalidatePath("/providers");
|
||||
return parseStringify(data);
|
||||
|
||||
return handleApiResponse(response, "/providers");
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
return {
|
||||
error: getErrorMessage(error),
|
||||
};
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -273,139 +188,48 @@ export const updateCredentialsProvider = async (
|
||||
) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
const url = new URL(`${apiBaseUrl}/providers/secrets/${credentialsId}`);
|
||||
|
||||
const secretName = formData.get("secretName");
|
||||
const providerType = formData.get("providerType") as ProviderType;
|
||||
|
||||
const isRole = formData.get("role_arn") !== null;
|
||||
const isServiceAccount = formData.get("service_account_key") !== null;
|
||||
|
||||
let secret = {};
|
||||
|
||||
if (providerType === "aws") {
|
||||
if (isRole) {
|
||||
// Role-based configuration for AWS
|
||||
secret = {
|
||||
role_arn: formData.get("role_arn"),
|
||||
aws_access_key_id: formData.get("aws_access_key_id") || undefined,
|
||||
aws_secret_access_key:
|
||||
formData.get("aws_secret_access_key") || undefined,
|
||||
aws_session_token: formData.get("aws_session_token") || undefined,
|
||||
session_duration:
|
||||
parseInt(formData.get("session_duration") as string, 10) || 3600,
|
||||
external_id: formData.get("external_id") || undefined,
|
||||
role_session_name: formData.get("role_session_name") || undefined,
|
||||
};
|
||||
} else {
|
||||
// Static credentials configuration for AWS
|
||||
secret = {
|
||||
aws_access_key_id: formData.get("aws_access_key_id"),
|
||||
aws_secret_access_key: formData.get("aws_secret_access_key"),
|
||||
aws_session_token: formData.get("aws_session_token") || undefined,
|
||||
};
|
||||
}
|
||||
} else if (providerType === "azure") {
|
||||
// Static credentials configuration for Azure
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
tenant_id: formData.get("tenant_id"),
|
||||
};
|
||||
} else if (providerType === "m365") {
|
||||
// Static credentials configuration for M365
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
tenant_id: formData.get("tenant_id"),
|
||||
user: formData.get("user"),
|
||||
password: formData.get("password"),
|
||||
};
|
||||
} else if (providerType === "gcp") {
|
||||
if (isServiceAccount) {
|
||||
// Service account configuration for GCP
|
||||
const serviceAccountKeyRaw = formData.get(
|
||||
"service_account_key",
|
||||
) as string;
|
||||
|
||||
try {
|
||||
// Parse the service account key as JSON
|
||||
const serviceAccountKey = JSON.parse(serviceAccountKeyRaw);
|
||||
secret = {
|
||||
service_account_key: serviceAccountKey,
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("error", error);
|
||||
}
|
||||
} else {
|
||||
// Static credentials configuration for GCP
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
refresh_token: formData.get("refresh_token"),
|
||||
};
|
||||
}
|
||||
} else if (providerType === "kubernetes") {
|
||||
// Static credentials configuration for Kubernetes
|
||||
secret = {
|
||||
kubeconfig_content: formData.get("kubeconfig_content"),
|
||||
};
|
||||
}
|
||||
|
||||
const bodyData = {
|
||||
data: {
|
||||
type: "provider-secrets",
|
||||
id: credentialsId,
|
||||
attributes: {
|
||||
name: secretName,
|
||||
secret,
|
||||
},
|
||||
},
|
||||
};
|
||||
const providerType = getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.PROVIDER_TYPE,
|
||||
) as ProviderType;
|
||||
|
||||
try {
|
||||
const secret = buildUpdateSecretConfig(formData, providerType);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify(bodyData),
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
type: "provider-secrets",
|
||||
id: credentialsId,
|
||||
attributes: { secret },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update credentials: ${response.statusText}`);
|
||||
const data = await response.json();
|
||||
return parseStringify(data); // Return API errors for UI handling
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
revalidatePath("/providers");
|
||||
return parseStringify(data);
|
||||
return handleApiResponse(response, "/providers");
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
return {
|
||||
error: getErrorMessage(error),
|
||||
};
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkConnectionProvider = async (formData: FormData) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const providerId = formData.get("providerId");
|
||||
|
||||
const providerId = formData.get(ProviderCredentialFields.PROVIDER_ID);
|
||||
const url = new URL(`${apiBaseUrl}/providers/${providerId}/connection`);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers,
|
||||
});
|
||||
const data = await response.json();
|
||||
const response = await fetch(url.toString(), { method: "POST", headers });
|
||||
await wait(2000);
|
||||
revalidatePath("/providers");
|
||||
return parseStringify(data);
|
||||
return handleApiResponse(response, "/providers");
|
||||
} catch (error) {
|
||||
return {
|
||||
error: getErrorMessage(error),
|
||||
};
|
||||
return handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -451,7 +275,7 @@ export const deleteCredentials = async (secretId: string) => {
|
||||
|
||||
export const deleteProvider = async (formData: FormData) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const providerId = formData.get("id");
|
||||
const providerId = formData.get(ProviderCredentialFields.PROVIDER_ID);
|
||||
|
||||
if (!providerId) {
|
||||
return { error: "Provider ID is required" };
|
||||
|
||||
@@ -22,10 +22,7 @@ import {
|
||||
} from "@/components/compliance";
|
||||
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import {
|
||||
calculateCategoryHeatmapData,
|
||||
getComplianceMapper,
|
||||
} from "@/lib/compliance/commons";
|
||||
import { getComplianceMapper } from "@/lib/compliance/commons";
|
||||
import { ScanProps } from "@/types";
|
||||
import { Framework, RequirementsTotals } from "@/types/compliance";
|
||||
|
||||
@@ -239,7 +236,7 @@ const SSRComplianceContent = async ({
|
||||
);
|
||||
|
||||
// Calculate category heatmap data
|
||||
const categoryHeatmapData = calculateCategoryHeatmapData(data);
|
||||
const categoryHeatmapData = mapper.calculateCategoryHeatmapData(data);
|
||||
|
||||
const totalRequirements: RequirementsTotals = data.reduce(
|
||||
(acc: RequirementsTotals, framework: Framework) => ({
|
||||
@@ -271,6 +268,7 @@ const SSRComplianceContent = async ({
|
||||
|
||||
<Spacer className="h-1 w-full rounded-full bg-gray-200 dark:bg-gray-800" />
|
||||
<ClientAccordionWrapper
|
||||
hideExpandButton={complianceId.includes("mitre_attack")}
|
||||
items={accordionItems}
|
||||
defaultExpandedKeys={defaultKeys}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getLighthouseConfig } from "@/actions/lighthouse";
|
||||
import { ChatbotConfig } from "@/components/lighthouse";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ChatbotConfigPage() {
|
||||
const response = await getLighthouseConfig();
|
||||
|
||||
const initialValues = response?.data?.attributes
|
||||
? {
|
||||
model: response.data.attributes.model,
|
||||
apiKey: response.data.attributes.api_key || "",
|
||||
businessContext: response.data.attributes.business_context || "",
|
||||
}
|
||||
: {
|
||||
model: "gpt-4o",
|
||||
apiKey: "",
|
||||
businessContext: "",
|
||||
};
|
||||
|
||||
const configExists = !!response;
|
||||
|
||||
return (
|
||||
<ContentLayout title="Configure Lighthouse" icon="lucide:settings">
|
||||
<ChatbotConfig
|
||||
initialValues={initialValues}
|
||||
configExists={configExists}
|
||||
/>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getAIKey } from "@/actions/lighthouse/lighthouse";
|
||||
import { Chat } from "@/components/lighthouse";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
|
||||
export default async function AIChatbot() {
|
||||
const apiKey = await getAIKey();
|
||||
|
||||
return (
|
||||
<ContentLayout title="Cloud Security Analyst" icon="lucide:bot">
|
||||
<Chat hasApiKey={!!apiKey} />
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
ViaCredentialsForm,
|
||||
ViaRoleForm,
|
||||
AddViaCredentialsForm,
|
||||
AddViaRoleForm,
|
||||
} from "@/components/providers/workflow/forms";
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import {
|
||||
AddViaServiceAccountForm,
|
||||
SelectViaGCP,
|
||||
ViaServiceAccountForm,
|
||||
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
@@ -29,16 +29,16 @@ export default function AddCredentialsPage({ searchParams }: Props) {
|
||||
{((searchParams.type === "aws" && searchParams.via === "credentials") ||
|
||||
(searchParams.type === "gcp" && searchParams.via === "credentials") ||
|
||||
(searchParams.type !== "aws" && searchParams.type !== "gcp")) && (
|
||||
<ViaCredentialsForm searchParams={searchParams} />
|
||||
<AddViaCredentialsForm searchParams={searchParams} />
|
||||
)}
|
||||
|
||||
{searchParams.type === "aws" && searchParams.via === "role" && (
|
||||
<ViaRoleForm searchParams={searchParams} />
|
||||
<AddViaRoleForm searchParams={searchParams} />
|
||||
)}
|
||||
|
||||
{searchParams.type === "gcp" &&
|
||||
searchParams.via === "service-account" && (
|
||||
<ViaServiceAccountForm searchParams={searchParams} />
|
||||
<AddViaServiceAccountForm searchParams={searchParams} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { LangChainAdapter, Message } from "ai";
|
||||
|
||||
import { getLighthouseConfig } from "@/actions/lighthouse/lighthouse";
|
||||
import { getCurrentDataSection } from "@/lib/lighthouse/data";
|
||||
import {
|
||||
convertLangChainMessageToVercelMessage,
|
||||
convertVercelMessageToLangChainMessage,
|
||||
} from "@/lib/lighthouse/utils";
|
||||
import { initLighthouseWorkflow } from "@/lib/lighthouse/workflow";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const {
|
||||
messages,
|
||||
}: {
|
||||
messages: Message[];
|
||||
} = await req.json();
|
||||
|
||||
if (!messages) {
|
||||
return Response.json({ error: "No messages provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Create a new array for processed messages
|
||||
const processedMessages = [...messages];
|
||||
|
||||
// Get AI configuration to access business context
|
||||
const aiConfig = await getLighthouseConfig();
|
||||
const businessContext = aiConfig?.data?.attributes?.business_context;
|
||||
|
||||
// Get current user data
|
||||
const currentData = await getCurrentDataSection();
|
||||
|
||||
// Add context messages at the beginning
|
||||
const contextMessages: Message[] = [];
|
||||
|
||||
// Add business context if available
|
||||
if (businessContext) {
|
||||
contextMessages.push({
|
||||
id: "business-context",
|
||||
role: "assistant",
|
||||
content: `Business Context Information:\n${businessContext}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Add current data if available
|
||||
if (currentData) {
|
||||
contextMessages.push({
|
||||
id: "current-data",
|
||||
role: "assistant",
|
||||
content: currentData,
|
||||
});
|
||||
}
|
||||
|
||||
// Insert all context messages at the beginning
|
||||
processedMessages.unshift(...contextMessages);
|
||||
|
||||
const app = await initLighthouseWorkflow();
|
||||
|
||||
const agentStream = app.streamEvents(
|
||||
{
|
||||
messages: processedMessages
|
||||
.filter(
|
||||
(message: Message) =>
|
||||
message.role === "user" || message.role === "assistant",
|
||||
)
|
||||
.map(convertVercelMessageToLangChainMessage),
|
||||
},
|
||||
{
|
||||
streamMode: ["values", "messages", "custom"],
|
||||
version: "v2",
|
||||
},
|
||||
);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const { event, data, tags } of agentStream) {
|
||||
if (event === "on_chat_model_stream") {
|
||||
if (data.chunk.content && !!tags && tags.includes("supervisor")) {
|
||||
const chunk = data.chunk;
|
||||
const aiMessage = convertLangChainMessageToVercelMessage(chunk);
|
||||
controller.enqueue(aiMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
return LangChainAdapter.toDataStreamResponse(stream);
|
||||
} catch (error) {
|
||||
console.error("Error in POST request:", error);
|
||||
return Response.json({ error: "An error occurred" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ export const ClientAccordionContent = ({
|
||||
|
||||
return (
|
||||
<div className="mb-1 mt-3 text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
⚠️ There are no findings for this regions
|
||||
⚠️ There are no findings for these regions
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,9 +8,11 @@ import { CustomButton } from "@/components/ui/custom";
|
||||
export const ClientAccordionWrapper = ({
|
||||
items,
|
||||
defaultExpandedKeys,
|
||||
hideExpandButton = false,
|
||||
}: {
|
||||
items: AccordionItemProps[];
|
||||
defaultExpandedKeys: string[];
|
||||
hideExpandButton?: boolean;
|
||||
}) => {
|
||||
const [selectedKeys, setSelectedKeys] =
|
||||
useState<string[]>(defaultExpandedKeys);
|
||||
@@ -56,16 +58,18 @@ export const ClientAccordionWrapper = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<CustomButton
|
||||
variant="flat"
|
||||
size="sm"
|
||||
onPress={handleToggleExpand}
|
||||
ariaLabel={isExpanded ? "Collapse all" : "Expand all"}
|
||||
>
|
||||
{isExpanded ? "Collapse all" : "Expand all"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
{!hideExpandButton && (
|
||||
<div className="flex justify-end">
|
||||
<CustomButton
|
||||
variant="flat"
|
||||
size="sm"
|
||||
onPress={handleToggleExpand}
|
||||
ariaLabel={isExpanded ? "Collapse all" : "Expand all"}
|
||||
>
|
||||
{isExpanded ? "Collapse all" : "Expand all"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
)}
|
||||
<Accordion
|
||||
items={items}
|
||||
variant="light"
|
||||
|
||||
@@ -71,20 +71,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
return "success";
|
||||
};
|
||||
|
||||
const isPressable =
|
||||
id.includes("ens") ||
|
||||
id.includes("iso") ||
|
||||
id.includes("cis_") ||
|
||||
id.includes("kisa") ||
|
||||
id.includes("threat") ||
|
||||
id.includes("pillar");
|
||||
|
||||
const navigateToDetail = () => {
|
||||
// We will unlock this while developing the rest of complainces.
|
||||
if (!isPressable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedTitleForUrl = encodeURIComponent(title);
|
||||
const path = `/compliance/${formattedTitleForUrl}`;
|
||||
const params = new URLSearchParams();
|
||||
@@ -109,7 +96,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
fullWidth
|
||||
isHoverable
|
||||
shadow="sm"
|
||||
isPressable={isPressable}
|
||||
isPressable
|
||||
onPress={navigateToDetail}
|
||||
>
|
||||
<CardBody className="flex flex-row items-center justify-between space-x-4 dark:bg-prowler-blue-800">
|
||||
|
||||
@@ -101,9 +101,7 @@ export const BarChart = ({ sections }: FailedSectionsListProps) => {
|
||||
<div className="flex w-[400px] flex-col items-center justify-between">
|
||||
{title}
|
||||
<div className="flex h-[320px] w-full items-center justify-center">
|
||||
<p className="text-sm text-gray-800 dark:text-gray-200">
|
||||
There are no failed sections
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">There are no failed sections</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ export const AWSWellArchitectedCustomDetails = ({
|
||||
<ComplianceLink
|
||||
href={requirement.implementation_guidance_url as string}
|
||||
>
|
||||
{requirement.implementation_guidance_url}
|
||||
{requirement.implementation_guidance_url as string}
|
||||
</ComplianceLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface CISDetailsProps {
|
||||
|
||||
export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
const processReferences = (
|
||||
references: string | number | string[] | undefined,
|
||||
references: string | number | string[] | object[] | undefined,
|
||||
): string[] => {
|
||||
if (typeof references !== "string") return [];
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
export const GenericCustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
}) => {
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{requirement.description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.item_id && (
|
||||
<ComplianceBadge
|
||||
label="Item ID"
|
||||
value={requirement.item_id as string}
|
||||
color="indigo"
|
||||
/>
|
||||
)}
|
||||
|
||||
{requirement.service && (
|
||||
<ComplianceBadge
|
||||
label="Service"
|
||||
value={requirement.service as string}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
|
||||
{requirement.type && (
|
||||
<ComplianceBadge
|
||||
label="Type"
|
||||
value={requirement.type as string}
|
||||
color="orange"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
{requirement.subsection && (
|
||||
<ComplianceDetailSection title="SubSection">
|
||||
<ComplianceDetailText>
|
||||
{requirement.subsection as string}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{requirement.subgroup && (
|
||||
<ComplianceDetailSection title="SubGroup">
|
||||
<ComplianceDetailText>
|
||||
{requirement.subgroup as string}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceChipContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
ComplianceLink,
|
||||
} from "./shared-components";
|
||||
|
||||
export const MITRECustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
}) => {
|
||||
const cloudServices = requirement.cloud_services as
|
||||
| Array<{
|
||||
service: string;
|
||||
category: string;
|
||||
value: string;
|
||||
comment: string;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{requirement.description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.technique_id && (
|
||||
<ComplianceBadge
|
||||
label="Technique ID"
|
||||
value={requirement.technique_id as string}
|
||||
color="indigo"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
<ComplianceChipContainer
|
||||
title="Tactics"
|
||||
items={(requirement.tactics as string[]) || []}
|
||||
/>
|
||||
|
||||
<ComplianceChipContainer
|
||||
title="Platforms"
|
||||
items={(requirement.platforms as string[]) || []}
|
||||
/>
|
||||
|
||||
{requirement.subtechniques &&
|
||||
Array.isArray(requirement.subtechniques) &&
|
||||
requirement.subtechniques.length > 0 && (
|
||||
<ComplianceChipContainer
|
||||
title="Subtechniques"
|
||||
items={requirement.subtechniques as string[]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{requirement.technique_url && (
|
||||
<ComplianceDetailSection title="MITRE ATT&CK Reference">
|
||||
<ComplianceLink href={requirement.technique_url as string}>
|
||||
{requirement.technique_url as string}
|
||||
</ComplianceLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{cloudServices && cloudServices.length > 0 && (
|
||||
<ComplianceDetailSection title="Cloud Security Mappings">
|
||||
<div className="space-y-4">
|
||||
{cloudServices.map((service, index) => (
|
||||
<div key={index} className="space-y-3 rounded-lg border p-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<ComplianceBadge
|
||||
label="Service"
|
||||
value={service.service}
|
||||
color="blue"
|
||||
/>
|
||||
<ComplianceBadge
|
||||
label="Category"
|
||||
value={service.category}
|
||||
color="indigo"
|
||||
/>
|
||||
<ComplianceBadge
|
||||
label="Coverage"
|
||||
value={service.value}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
{service.comment && (
|
||||
<div>
|
||||
<h5 className="text-muted-foreground mb-1 text-xs font-medium">
|
||||
Details
|
||||
</h5>
|
||||
<ComplianceDetailText className="text-xs">
|
||||
{service.comment}
|
||||
</ComplianceDetailText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -24,7 +24,7 @@ export const ThreatCustomDetails = ({
|
||||
{requirement.attributeDescription && (
|
||||
<ComplianceDetailSection title="Attribute Description">
|
||||
<ComplianceDetailText>
|
||||
{requirement.attributeDescription}
|
||||
{requirement.attributeDescription as string}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
@@ -59,7 +59,7 @@ export const ThreatCustomDetails = ({
|
||||
{requirement.additionalInformation && (
|
||||
<ComplianceDetailSection title="Additional Information">
|
||||
<ComplianceDetailText>
|
||||
{requirement.additionalInformation}
|
||||
{requirement.additionalInformation as string}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
@@ -31,9 +31,12 @@ export const ScanSelector = ({
|
||||
size="lg"
|
||||
labelPlacement="outside"
|
||||
selectedKeys={new Set([selectedScanId])}
|
||||
onSelectionChange={(keys) =>
|
||||
onSelectionChange(Array.from(keys)[0] as string)
|
||||
}
|
||||
onSelectionChange={(keys) => {
|
||||
const newSelectedId = Array.from(keys)[0] as string;
|
||||
if (newSelectedId && newSelectedId !== selectedScanId) {
|
||||
onSelectionChange(newSelectedId);
|
||||
}
|
||||
}}
|
||||
renderValue={() => {
|
||||
const selectedItem = scans.find((item) => item.id === selectedScanId);
|
||||
return selectedItem ? (
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown";
|
||||
import { CustomButton, CustomTextarea } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
interface SuggestedAction {
|
||||
title: string;
|
||||
label: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface ChatProps {
|
||||
hasApiKey: boolean;
|
||||
}
|
||||
|
||||
interface ChatFormData {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const Chat = ({ hasApiKey }: ChatProps) => {
|
||||
const { messages, handleSubmit, handleInputChange, append, status } = useChat(
|
||||
{
|
||||
api: "/api/lighthouse/analyst",
|
||||
credentials: "same-origin",
|
||||
experimental_throttle: 100,
|
||||
sendExtraMessageFields: true,
|
||||
onFinish: () => {
|
||||
// Handle chat completion
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Chat error:", error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<ChatFormData>({
|
||||
defaultValues: {
|
||||
message: "",
|
||||
},
|
||||
});
|
||||
|
||||
const messageValue = form.watch("message");
|
||||
const messagesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const latestUserMsgRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Sync form value with chat input
|
||||
useEffect(() => {
|
||||
const syntheticEvent = {
|
||||
target: { value: messageValue },
|
||||
} as React.ChangeEvent<HTMLInputElement>;
|
||||
handleInputChange(syntheticEvent);
|
||||
}, [messageValue, handleInputChange]);
|
||||
|
||||
// Reset form when message is sent
|
||||
useEffect(() => {
|
||||
if (status === "submitted") {
|
||||
form.reset({ message: "" });
|
||||
}
|
||||
}, [status, form]);
|
||||
|
||||
const onFormSubmit = form.handleSubmit((data) => {
|
||||
if (data.message.trim()) {
|
||||
handleSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
// Global keyboard shortcut handler
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (messageValue?.trim()) {
|
||||
onFormSubmit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [messageValue, onFormSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && latestUserMsgRef.current) {
|
||||
const container = messagesContainerRef.current;
|
||||
const userMsg = latestUserMsgRef.current;
|
||||
const containerPadding = 16; // p-4 in Tailwind = 16px
|
||||
container.scrollTop =
|
||||
userMsg.offsetTop - container.offsetTop - containerPadding;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const suggestedActions: SuggestedAction[] = [
|
||||
{
|
||||
title: "Are there any exposed S3",
|
||||
label: "buckets in my AWS accounts?",
|
||||
action: "List exposed S3 buckets in my AWS accounts",
|
||||
},
|
||||
{
|
||||
title: "What is the risk of having",
|
||||
label: "RDS databases unencrypted?",
|
||||
action: "What is the risk of having RDS databases unencrypted?",
|
||||
},
|
||||
{
|
||||
title: "What is the CIS 1.10 compliance status",
|
||||
label: "of my Kubernetes cluster?",
|
||||
action:
|
||||
"What is the CIS 1.10 compliance status of my Kubernetes cluster?",
|
||||
},
|
||||
{
|
||||
title: "List my highest privileged",
|
||||
label: "AWS IAM users with full admin access?",
|
||||
action: "List my highest privileged AWS IAM users with full admin access",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative flex h-[calc(100vh-theme(spacing.16))] min-w-0 flex-col bg-background">
|
||||
{!hasApiKey && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||
<div className="bg-card max-w-md rounded-lg p-6 text-center shadow-lg">
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
OpenAI API Key Required
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Please configure your OpenAI API key to use the Lighthouse Cloud
|
||||
Security Analyst.
|
||||
</p>
|
||||
<Link
|
||||
href="/lighthouse/config"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Configure API Key
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<div className="w-full max-w-2xl">
|
||||
<h2 className="mb-4 text-center font-sans text-xl">Suggestions</h2>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{suggestedActions.map((action, index) => (
|
||||
<CustomButton
|
||||
key={`suggested-action-${index}`}
|
||||
ariaLabel={`Send message: ${action.action}`}
|
||||
onPress={() => {
|
||||
append({
|
||||
role: "user",
|
||||
content: action.action,
|
||||
});
|
||||
}}
|
||||
className="hover:bg-muted flex h-auto w-full flex-col items-start justify-start rounded-xl border bg-gray-50 px-4 py-3.5 text-left font-sans text-sm dark:bg-gray-900"
|
||||
>
|
||||
<span>{action.title}</span>
|
||||
<span className="text-muted-foreground">{action.label}</span>
|
||||
</CustomButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex-1 space-y-4 overflow-y-auto p-4"
|
||||
ref={messagesContainerRef}
|
||||
>
|
||||
{messages.map((message, idx) => {
|
||||
const lastUserIdx = messages
|
||||
.map((m, i) => (m.role === "user" ? i : -1))
|
||||
.filter((i) => i !== -1)
|
||||
.pop();
|
||||
const isLatestUserMsg =
|
||||
message.role === "user" && lastUserIdx === idx;
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
ref={isLatestUserMsg ? latestUserMsgRef : undefined}
|
||||
className={`flex ${
|
||||
message.role === "user" ? "justify-end" : "justify-start"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg px-4 py-2 ${
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground dark:!text-black"
|
||||
: "bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`prose dark:prose-invert ${message.role === "user" ? "dark:!text-black" : ""}`}
|
||||
>
|
||||
<MemoizedMarkdown
|
||||
id={message.id}
|
||||
content={message.content}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{status === "submitted" && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-muted max-w-[80%] rounded-lg px-4 py-2">
|
||||
<div className="animate-pulse">Thinking...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={onFormSubmit}
|
||||
className="mx-auto flex w-full gap-2 px-4 pb-4 md:max-w-3xl md:pb-6"
|
||||
>
|
||||
<div className="flex w-full items-end gap-2">
|
||||
<div className="w-full flex-1">
|
||||
<CustomTextarea
|
||||
control={form.control}
|
||||
name="message"
|
||||
label=""
|
||||
placeholder="Type your message..."
|
||||
variant="bordered"
|
||||
minRows={1}
|
||||
maxRows={6}
|
||||
fullWidth={true}
|
||||
disableAutosize={false}
|
||||
/>
|
||||
</div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={
|
||||
status === "submitted" ? "Stop generation" : "Send message"
|
||||
}
|
||||
isDisabled={status === "submitted" || !messageValue?.trim()}
|
||||
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-primary p-2 text-primary-foreground hover:bg-primary/90 disabled:opacity-50 dark:bg-primary/90"
|
||||
>
|
||||
{status === "submitted" ? <span>■</span> : <span>➤</span>}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Select, SelectItem, Spacer } from "@nextui-org/react";
|
||||
import { SaveIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import {
|
||||
createLighthouseConfig,
|
||||
updateLighthouseConfig,
|
||||
} from "@/actions/lighthouse";
|
||||
import { useToast } from "@/components/ui";
|
||||
import {
|
||||
CustomButton,
|
||||
CustomInput,
|
||||
CustomTextarea,
|
||||
} from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
const chatbotConfigSchema = z.object({
|
||||
model: z.string().nonempty("Model selection is required"),
|
||||
apiKey: z.string().nonempty("API Key is required").optional(),
|
||||
businessContext: z
|
||||
.string()
|
||||
.max(1000, "Business context cannot exceed 1000 characters")
|
||||
.optional(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof chatbotConfigSchema>;
|
||||
|
||||
interface ChatbotConfigClientProps {
|
||||
initialValues: FormValues;
|
||||
configExists: boolean;
|
||||
}
|
||||
|
||||
export const ChatbotConfig = ({
|
||||
initialValues,
|
||||
configExists: initialConfigExists,
|
||||
}: ChatbotConfigClientProps) => {
|
||||
const { toast } = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [configExists, setConfigExists] = useState(initialConfigExists);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(chatbotConfigSchema),
|
||||
defaultValues: initialValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const configData: any = {
|
||||
model: data.model,
|
||||
businessContext: data.businessContext || "",
|
||||
};
|
||||
if (data.apiKey && !data.apiKey.includes("*")) {
|
||||
configData.apiKey = data.apiKey;
|
||||
}
|
||||
|
||||
const result = configExists
|
||||
? await updateLighthouseConfig(configData)
|
||||
: await createLighthouseConfig(configData);
|
||||
|
||||
if (result) {
|
||||
setConfigExists(true);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Lighthouse configuration ${
|
||||
configExists ? "updated" : "created"
|
||||
} successfully`,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Failed to save configuration");
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
"Failed to save lighthouse configuration: " + String(error),
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-gray-900">
|
||||
<h2 className="mb-4 text-xl font-semibold">Chatbot Settings</h2>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-300">
|
||||
Configure your chatbot model and API settings.
|
||||
</p>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col space-y-6"
|
||||
>
|
||||
<Controller
|
||||
name="model"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
label="Model"
|
||||
placeholder="Select a model"
|
||||
labelPlacement="inside"
|
||||
value={field.value}
|
||||
defaultSelectedKeys={[field.value]}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
variant="bordered"
|
||||
size="md"
|
||||
isRequired
|
||||
>
|
||||
<SelectItem key="gpt-4o-2024-08-06" value="gpt-4o-2024-08-06">
|
||||
GPT-4o (Recommended)
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
key="gpt-4o-mini-2024-07-18"
|
||||
value="gpt-4o-mini-2024-07-18"
|
||||
>
|
||||
GPT-4o Mini
|
||||
</SelectItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Spacer y={2} />
|
||||
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
type="password"
|
||||
label="API Key"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter your API key"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!form.formState.errors.apiKey}
|
||||
/>
|
||||
|
||||
<Spacer y={2} />
|
||||
|
||||
<CustomTextarea
|
||||
control={form.control}
|
||||
name="businessContext"
|
||||
label="Business Context"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter business context and relevant information for the chatbot (max 1000 characters)"
|
||||
variant="bordered"
|
||||
minRows={4}
|
||||
maxRows={8}
|
||||
description={`${form.watch("businessContext")?.length || 0}/1000 characters`}
|
||||
isInvalid={!!form.formState.errors.businessContext}
|
||||
/>
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Save Configuration"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="md"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <SaveIcon size={20} />}
|
||||
>
|
||||
{isLoading ? "Saving..." : "Save"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./chat";
|
||||
export * from "./chatbot-config";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { marked } from "marked";
|
||||
import { memo, useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
function parseMarkdownIntoBlocks(markdown: string): string[] {
|
||||
const tokens = marked.lexer(markdown);
|
||||
return tokens.map((token) => token.raw);
|
||||
}
|
||||
|
||||
const MemoizedMarkdownBlock = memo(
|
||||
({ content }: { content: string }) => {
|
||||
return <ReactMarkdown>{content}</ReactMarkdown>;
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.content !== nextProps.content) return false;
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
|
||||
|
||||
export const MemoizedMarkdown = memo(
|
||||
({ content, id }: { content: string; id: string }) => {
|
||||
const blocks = useMemo(() => parseMarkdownIntoBlocks(content), [content]);
|
||||
|
||||
return blocks.map((block, index) => (
|
||||
<MemoizedMarkdownBlock content={block} key={`${id}-block_${index}`} />
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
MemoizedMarkdown.displayName = "MemoizedMarkdown";
|
||||
@@ -10,9 +10,10 @@ import { DeleteIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
|
||||
const formSchema = z.object({
|
||||
providerId: z.string(),
|
||||
[ProviderCredentialFields.PROVIDER_ID]: z.string(),
|
||||
});
|
||||
|
||||
export const DeleteForm = ({
|
||||
@@ -53,7 +54,11 @@ export const DeleteForm = ({
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form action={onSubmitClient}>
|
||||
<input type="hidden" name="id" value={providerId} />
|
||||
<input
|
||||
type="hidden"
|
||||
name={ProviderCredentialFields.PROVIDER_ID}
|
||||
value={providerId}
|
||||
/>
|
||||
<div className="flex w-full justify-center sm:space-x-6">
|
||||
<CustomButton
|
||||
type="button"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SaveIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { editProviderFormSchema } from "@/types";
|
||||
|
||||
export const EditForm = ({
|
||||
@@ -26,8 +27,8 @@ export const EditForm = ({
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
providerId: providerId,
|
||||
alias: providerAlias,
|
||||
[ProviderCredentialFields.PROVIDER_ID]: providerId,
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: providerAlias,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -74,14 +75,16 @@ export const EditForm = ({
|
||||
<div>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="alias"
|
||||
name={ProviderCredentialFields.PROVIDER_ALIAS}
|
||||
type="text"
|
||||
label="Alias"
|
||||
labelPlacement="outside"
|
||||
placeholder={providerAlias}
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!form.formState.errors.alias}
|
||||
isInvalid={
|
||||
!!form.formState.errors[ProviderCredentialFields.PROVIDER_ALIAS]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { addCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { BaseCredentialsForm } from "./base-credentials-form";
|
||||
|
||||
export const AddViaCredentialsForm = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { type: string; id: string };
|
||||
}) => {
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
|
||||
const handleAddCredentials = async (formData: FormData) => {
|
||||
return await addCredentialsProvider(formData);
|
||||
};
|
||||
|
||||
const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}`;
|
||||
|
||||
return (
|
||||
<BaseCredentialsForm
|
||||
providerType={providerType}
|
||||
providerId={providerId}
|
||||
onSubmit={handleAddCredentials}
|
||||
successNavigationUrl={successNavigationUrl}
|
||||
submitButtonText="Next"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { addCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { BaseCredentialsForm } from "./base-credentials-form";
|
||||
|
||||
export const AddViaRoleForm = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { type: string; id: string };
|
||||
}) => {
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
|
||||
const handleAddCredentials = async (formData: FormData) => {
|
||||
return await addCredentialsProvider(formData);
|
||||
};
|
||||
|
||||
const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}`;
|
||||
|
||||
return (
|
||||
<BaseCredentialsForm
|
||||
providerType={providerType}
|
||||
providerId={providerId}
|
||||
onSubmit={handleAddCredentials}
|
||||
successNavigationUrl={successNavigationUrl}
|
||||
submitButtonText="Next"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { Divider } from "@nextui-org/react";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { useCredentialsForm } from "@/hooks/use-credentials-form";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import {
|
||||
AWSCredentials,
|
||||
AWSCredentialsRole,
|
||||
AzureCredentials,
|
||||
GCPDefaultCredentials,
|
||||
GCPServiceAccountKey,
|
||||
KubernetesCredentials,
|
||||
M365Credentials,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
|
||||
import { ProviderTitleDocs } from "../provider-title-docs";
|
||||
import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type";
|
||||
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type/aws-role-credentials-form";
|
||||
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
|
||||
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
|
||||
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
|
||||
|
||||
type BaseCredentialsFormProps = {
|
||||
providerType: ProviderType;
|
||||
providerId: string;
|
||||
onSubmit: (formData: FormData) => Promise<any>;
|
||||
successNavigationUrl: string;
|
||||
submitButtonText?: string;
|
||||
showBackButton?: boolean;
|
||||
};
|
||||
|
||||
export const BaseCredentialsForm = ({
|
||||
providerType,
|
||||
providerId,
|
||||
onSubmit,
|
||||
successNavigationUrl,
|
||||
submitButtonText = "Next",
|
||||
showBackButton = true,
|
||||
}: BaseCredentialsFormProps) => {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
handleSubmit,
|
||||
handleBackStep,
|
||||
searchParamsObj,
|
||||
externalId,
|
||||
} = useCredentialsForm({
|
||||
providerType,
|
||||
providerId,
|
||||
onSubmit,
|
||||
successNavigationUrl,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name={ProviderCredentialFields.PROVIDER_ID}
|
||||
value={providerId}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name={ProviderCredentialFields.PROVIDER_TYPE}
|
||||
value={providerType}
|
||||
/>
|
||||
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{providerType === "aws" && searchParamsObj.get("via") === "role" && (
|
||||
<AWSRoleCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentialsRole>}
|
||||
setValue={form.setValue as any}
|
||||
externalId={externalId}
|
||||
/>
|
||||
)}
|
||||
{providerType === "aws" && searchParamsObj.get("via") !== "role" && (
|
||||
<AWSStaticCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "azure" && (
|
||||
<AzureCredentialsForm
|
||||
control={form.control as unknown as Control<AzureCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" && (
|
||||
<M365CredentialsForm
|
||||
control={form.control as unknown as Control<M365Credentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" &&
|
||||
searchParamsObj.get("via") === "service-account" && (
|
||||
<GCPServiceAccountKeyForm
|
||||
control={form.control as unknown as Control<GCPServiceAccountKey>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" &&
|
||||
searchParamsObj.get("via") !== "service-account" && (
|
||||
<GCPDefaultCredentialsForm
|
||||
control={
|
||||
form.control as unknown as Control<GCPDefaultCredentials>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{providerType === "kubernetes" && (
|
||||
<KubernetesCredentialsForm
|
||||
control={form.control as unknown as Control<KubernetesCredentials>}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full justify-end sm:space-x-6">
|
||||
{showBackButton &&
|
||||
(searchParamsObj.get("via") === "credentials" ||
|
||||
searchParamsObj.get("via") === "role" ||
|
||||
searchParamsObj.get("via") === "service-account") && (
|
||||
<CustomButton
|
||||
type="button"
|
||||
ariaLabel="Back"
|
||||
className="w-1/2 bg-transparent"
|
||||
variant="faded"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
onPress={handleBackStep}
|
||||
startContent={!isLoading && <ChevronLeftIcon size={24} />}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
<span>Back</span>
|
||||
</CustomButton>
|
||||
)}
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Save"
|
||||
className="w-1/2"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
endContent={!isLoading && <ChevronRightIcon size={24} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>{submitButtonText}</span>}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "./add-via-credentials-form";
|
||||
export * from "./add-via-role-form";
|
||||
export * from "./connect-account-form";
|
||||
export * from "./test-connection-form";
|
||||
export * from "./update-via-credentials-form";
|
||||
export * from "./update-via-role-form";
|
||||
export * from "./via-credentials-form";
|
||||
export * from "./via-role-form";
|
||||
|
||||
+42
-17
@@ -3,6 +3,7 @@ import { Control, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
|
||||
import { CredentialsRoleHelper } from "@/components/providers/workflow";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { AWSCredentialsRole } from "@/types";
|
||||
|
||||
export const AWSRoleCredentialsForm = ({
|
||||
@@ -16,7 +17,7 @@ export const AWSRoleCredentialsForm = ({
|
||||
}) => {
|
||||
const credentialsType = useWatch({
|
||||
control,
|
||||
name: "credentials_type" as const,
|
||||
name: ProviderCredentialFields.CREDENTIALS_TYPE,
|
||||
defaultValue: "aws-sdk-default",
|
||||
});
|
||||
|
||||
@@ -34,7 +35,7 @@ export const AWSRoleCredentialsForm = ({
|
||||
<span className="text-xs font-bold text-default-500">Authentication</span>
|
||||
|
||||
<Select
|
||||
name="credentials_type"
|
||||
name={ProviderCredentialFields.CREDENTIALS_TYPE}
|
||||
label="Authentication Method"
|
||||
placeholder="Select credentials type"
|
||||
defaultSelectedKeys={["aws-sdk-default"]}
|
||||
@@ -42,7 +43,7 @@ export const AWSRoleCredentialsForm = ({
|
||||
variant="bordered"
|
||||
onSelectionChange={(keys) =>
|
||||
setValue(
|
||||
"credentials_type",
|
||||
ProviderCredentialFields.CREDENTIALS_TYPE,
|
||||
Array.from(keys)[0] as "aws-sdk-default" | "access-secret-key",
|
||||
)
|
||||
}
|
||||
@@ -55,36 +56,48 @@ export const AWSRoleCredentialsForm = ({
|
||||
<>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="aws_access_key_id"
|
||||
name={ProviderCredentialFields.AWS_ACCESS_KEY_ID}
|
||||
type="password"
|
||||
label="AWS Access Key ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the AWS Access Key ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.aws_access_key_id}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.AWS_ACCESS_KEY_ID
|
||||
]
|
||||
}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="aws_secret_access_key"
|
||||
name={ProviderCredentialFields.AWS_SECRET_ACCESS_KEY}
|
||||
type="password"
|
||||
label="AWS Secret Access Key"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the AWS Secret Access Key"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.aws_secret_access_key}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.AWS_SECRET_ACCESS_KEY
|
||||
]
|
||||
}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="aws_session_token"
|
||||
name={ProviderCredentialFields.AWS_SESSION_TOKEN}
|
||||
type="password"
|
||||
label="AWS Session Token (optional)"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the AWS Session Token"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.aws_session_token}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.AWS_SESSION_TOKEN
|
||||
]
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -96,18 +109,20 @@ export const AWSRoleCredentialsForm = ({
|
||||
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="role_arn"
|
||||
name={ProviderCredentialFields.ROLE_ARN}
|
||||
type="text"
|
||||
label="Role ARN"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Role ARN"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.role_arn}
|
||||
isInvalid={
|
||||
!!control._formState.errors[ProviderCredentialFields.ROLE_ARN]
|
||||
}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="external_id"
|
||||
name={ProviderCredentialFields.EXTERNAL_ID}
|
||||
type="text"
|
||||
label="External ID"
|
||||
labelPlacement="inside"
|
||||
@@ -116,32 +131,42 @@ export const AWSRoleCredentialsForm = ({
|
||||
defaultValue={externalId}
|
||||
isDisabled
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.external_id}
|
||||
isInvalid={
|
||||
!!control._formState.errors[ProviderCredentialFields.EXTERNAL_ID]
|
||||
}
|
||||
/>
|
||||
|
||||
<span className="text-xs text-default-500">Optional fields</span>
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="role_session_name"
|
||||
name={ProviderCredentialFields.ROLE_SESSION_NAME}
|
||||
type="text"
|
||||
label="Role Session Name"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Role Session Name"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.role_session_name}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.ROLE_SESSION_NAME
|
||||
]
|
||||
}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="session_duration"
|
||||
name={ProviderCredentialFields.SESSION_DURATION}
|
||||
type="number"
|
||||
label="Session Duration (seconds)"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the session duration (default: 3600)"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.session_duration}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.SESSION_DURATION
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+19
-6
@@ -1,6 +1,7 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { AWSCredentials } from "@/types";
|
||||
|
||||
export const AWSStaticCredentialsForm = ({
|
||||
@@ -20,36 +21,48 @@ export const AWSStaticCredentialsForm = ({
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="aws_access_key_id"
|
||||
name={ProviderCredentialFields.AWS_ACCESS_KEY_ID}
|
||||
type="password"
|
||||
label="AWS Access Key ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the AWS Access Key ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.aws_access_key_id}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.AWS_ACCESS_KEY_ID
|
||||
]
|
||||
}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="aws_secret_access_key"
|
||||
name={ProviderCredentialFields.AWS_SECRET_ACCESS_KEY}
|
||||
type="password"
|
||||
label="AWS Secret Access Key"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the AWS Secret Access Key"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.aws_secret_access_key}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.AWS_SECRET_ACCESS_KEY
|
||||
]
|
||||
}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="aws_session_token"
|
||||
name={ProviderCredentialFields.AWS_SESSION_TOKEN}
|
||||
type="password"
|
||||
label="AWS Session Token"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the AWS Session Token"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.aws_session_token}
|
||||
isInvalid={
|
||||
!!control._formState.errors[
|
||||
ProviderCredentialFields.AWS_SESSION_TOKEN
|
||||
]
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { addCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { BaseCredentialsForm } from "../../base-credentials-form";
|
||||
|
||||
export const AddViaServiceAccountForm = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { type: ProviderType; id: string };
|
||||
}) => {
|
||||
const providerType = searchParams.type;
|
||||
const providerId = searchParams.id;
|
||||
|
||||
const handleAddCredentials = async (formData: FormData) => {
|
||||
return await addCredentialsProvider(formData);
|
||||
};
|
||||
|
||||
const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}`;
|
||||
|
||||
return (
|
||||
<BaseCredentialsForm
|
||||
providerType={providerType}
|
||||
providerId={providerId}
|
||||
onSubmit={handleAddCredentials}
|
||||
successNavigationUrl={successNavigationUrl}
|
||||
submitButtonText="Next"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./add-via-service-account-form";
|
||||
export * from "./radio-group-gcp-via-credentials-type-form";
|
||||
export * from "./select-via-gcp";
|
||||
export * from "./via-service-account-form";
|
||||
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Divider } from "@nextui-org/react";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Control, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { addCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { ProviderTitleDocs } from "@/components/providers/workflow";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import {
|
||||
addCredentialsServiceAccountFormSchema,
|
||||
ApiError,
|
||||
GCPServiceAccountKey,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
|
||||
import { GCPServiceAccountKeyForm } from "./credentials-type/gcp-service-account-key-form";
|
||||
|
||||
export const ViaServiceAccountForm = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { type: ProviderType; id: string };
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const searchParamsObj = useSearchParams();
|
||||
|
||||
// Handler for back button
|
||||
const handleBackStep = () => {
|
||||
const currentParams = new URLSearchParams(window.location.search);
|
||||
currentParams.delete("via");
|
||||
router.push(`?${currentParams.toString()}`);
|
||||
};
|
||||
|
||||
const providerType = searchParams.type;
|
||||
const providerId = searchParams.id;
|
||||
|
||||
const formSchema = addCredentialsServiceAccountFormSchema(providerType);
|
||||
type FormSchemaType = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormSchemaType>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
providerId,
|
||||
providerType,
|
||||
...(providerType === "gcp"
|
||||
? {
|
||||
service_account_key: "",
|
||||
secretName: "",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmitClient = async (values: FormSchemaType) => {
|
||||
const formData = new FormData();
|
||||
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== "") {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await addCredentialsProvider(formData);
|
||||
|
||||
if (data?.errors && data.errors.length > 0) {
|
||||
data.errors.forEach((error: ApiError) => {
|
||||
const errorMessage = error.detail;
|
||||
|
||||
switch (error.source.pointer) {
|
||||
case "/data/attributes/secret/service_account_key":
|
||||
form.setError("service_account_key" as keyof FormSchemaType, {
|
||||
type: "server",
|
||||
message: errorMessage,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Oops! Something went wrong",
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.push(
|
||||
`/providers/test-connection?type=${providerType}&id=${providerId}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error during submission:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Submission failed",
|
||||
description: "An error occurred while processing your request.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
<input type="hidden" name="providerType" value={providerType} />
|
||||
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{providerType === "gcp" && (
|
||||
<GCPServiceAccountKeyForm
|
||||
control={form.control as unknown as Control<GCPServiceAccountKey>}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full justify-end sm:space-x-6">
|
||||
{searchParamsObj.get("via") === "service-account" && (
|
||||
<CustomButton
|
||||
type="button"
|
||||
ariaLabel="Back"
|
||||
className="w-1/2 bg-transparent"
|
||||
variant="faded"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
onPress={handleBackStep}
|
||||
startContent={!isLoading && <ChevronLeftIcon size={24} />}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
<span>Back</span>
|
||||
</CustomButton>
|
||||
)}
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={"Save"}
|
||||
className="w-1/2"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
endContent={!isLoading && <ChevronRightIcon size={24} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Next</span>}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user