mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
Compare commits
5 Commits
review_met
...
update-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e07e45c8e5 | ||
|
|
a37aea84e7 | ||
|
|
8d1d041092 | ||
|
|
6f018183cd | ||
|
|
8ce56b5ed6 |
81
.github/workflows/mcp-pypi-release.yml
vendored
Normal file
81
.github/workflows/mcp-pypi-release.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: "MCP: PyPI Release"
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- "published"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.release.tag_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
PYTHON_VERSION: "3.12"
|
||||
WORKING_DIRECTORY: ./mcp_server
|
||||
|
||||
jobs:
|
||||
validate-release:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
prowler_version: ${{ steps.parse-version.outputs.version }}
|
||||
major_version: ${{ steps.parse-version.outputs.major }}
|
||||
|
||||
steps:
|
||||
- name: Parse and validate version
|
||||
id: parse-version
|
||||
run: |
|
||||
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
|
||||
echo "version=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# Extract major version
|
||||
MAJOR_VERSION="${PROWLER_VERSION%%.*}"
|
||||
echo "major=${MAJOR_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# Validate major version (only Prowler 3, 4, 5 supported)
|
||||
case ${MAJOR_VERSION} in
|
||||
3|4|5)
|
||||
echo "✓ Releasing Prowler MCP for tag ${PROWLER_VERSION}"
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unsupported Prowler major version: ${MAJOR_VERSION}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
publish-prowler-mcp:
|
||||
needs: validate-release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
environment:
|
||||
name: pypi-prowler-mcp
|
||||
url: https://pypi.org/project/prowler-mcp/
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Build prowler-mcp package
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
run: uv build
|
||||
|
||||
- name: Publish prowler-mcp package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
packages-dir: ${{ env.WORKING_DIRECTORY }}/dist/
|
||||
print-hash: true
|
||||
4869
api/poetry.lock
generated
4869
api/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -220,6 +220,7 @@ The function returns a JSON file containing the list of regions for the provider
|
||||
"sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2"
|
||||
],
|
||||
"aws-cn": ["cn-north-1", "cn-northwest-1"],
|
||||
"aws-eusc": ["eusc-de-east-1"],
|
||||
"aws-us-gov": ["us-gov-east-1", "us-gov-west-1"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ By default Prowler is able to scan the following AWS partitions:
|
||||
|
||||
- Commercial: `aws`
|
||||
- China: `aws-cn`
|
||||
- European Sovereign Cloud: `aws-eusc`
|
||||
- GovCloud (US): `aws-us-gov`
|
||||
|
||||
<Note>
|
||||
To check the available regions for each partition and service, refer to: [aws\_regions\_by\_service.json](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/aws_regions_by_service.json)
|
||||
|
||||
</Note>
|
||||
## Scanning AWS China and GovCloud Partitions in Prowler
|
||||
## Scanning AWS China, European Sovereign Cloud and GovCloud Partitions in Prowler
|
||||
|
||||
When scanning the China (`aws-cn`) or GovCloud (`aws-us-gov`), ensure one of the following:
|
||||
When scanning the China (`aws-cn`), European Sovereign Cloud (`aws-eusc`) or GovCloud (`aws-us-gov`) partitions, ensure one of the following:
|
||||
|
||||
- Your AWS credentials include a valid region within the desired partition.
|
||||
|
||||
@@ -83,6 +84,29 @@ To scan an account in the AWS GovCloud (US) partition (`aws-us-gov`):
|
||||
<Note>
|
||||
With this configuration, all partition regions will be scanned without needing the `-f/--region` flag
|
||||
|
||||
</Note>
|
||||
### AWS European Sovereign Cloud
|
||||
|
||||
To scan an account in the AWS European Sovereign Cloud partition (`aws-eusc`):
|
||||
|
||||
- By using the `-f/--region` flag:
|
||||
|
||||
```
|
||||
prowler aws --region eusc-de-east-1
|
||||
```
|
||||
|
||||
- By using the region configured in your AWS profile at `~/.aws/credentials` or `~/.aws/config`:
|
||||
|
||||
```
|
||||
[default]
|
||||
aws_access_key_id = XXXXXXXXXXXXXXXXXXX
|
||||
aws_secret_access_key = XXXXXXXXXXXXXXXXXXX
|
||||
region = eusc-de-east-1
|
||||
```
|
||||
|
||||
<Note>
|
||||
With this configuration, all partition regions will be scanned without needing the `-f/--region` flag
|
||||
|
||||
</Note>
|
||||
### AWS ISO (US \& Europe)
|
||||
|
||||
@@ -99,6 +123,9 @@ The AWS ISO partitions—commonly referred to as "secret partitions"—are air-g
|
||||
"cn-north-1",
|
||||
"cn-northwest-1"
|
||||
],
|
||||
"aws-eusc": [
|
||||
"eusc-de-east-1"
|
||||
],
|
||||
"aws-us-gov": [
|
||||
"us-gov-east-1",
|
||||
"us-gov-west-1"
|
||||
|
||||
@@ -5,8 +5,8 @@ This package provides MCP tools for accessing:
|
||||
- Prowler Hub: All security artifacts (detections, remediations and frameworks) supported by Prowler
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.3.0"
|
||||
__author__ = "Prowler Team"
|
||||
__email__ = "engineering@prowler.com"
|
||||
|
||||
__all__ = ["__version__", "prowler_mcp_server"]
|
||||
__all__ = ["__version__", "__author__", "__email__"]
|
||||
|
||||
@@ -6,14 +6,13 @@ across all providers.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.resources import (
|
||||
DetailedResource,
|
||||
ResourcesListResponse,
|
||||
ResourcesMetadataResponse,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class ResourcesTools(BaseTool):
|
||||
@@ -188,7 +187,7 @@ class ResourcesTools(BaseTool):
|
||||
|
||||
1. Configuration Details:
|
||||
- metadata: Provider-specific configuration (tags, policies, encryption settings, network rules)
|
||||
- partition: Provider-specific partition/region grouping (e.g., aws, aws-cn, aws-us-gov for AWS)
|
||||
- partition: Provider-specific partition/region grouping (e.g., aws, aws-cn, aws-eusc, aws-us-gov for AWS)
|
||||
|
||||
2. Temporal Tracking:
|
||||
- inserted_at: When Prowler first discovered this resource
|
||||
|
||||
@@ -14,7 +14,6 @@ requires-python = ">=3.12"
|
||||
version = "0.3.0"
|
||||
|
||||
[project.scripts]
|
||||
generate-prowler-app-mcp-server = "prowler_mcp_server.prowler_app.utils.server_generator:generate_server_file"
|
||||
prowler-mcp = "prowler_mcp_server.main:main"
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
### Added
|
||||
- Add Prowler ThreatScore for the Alibaba Cloud provider [(#9511)](https://github.com/prowler-cloud/prowler/pull/9511)
|
||||
- `compute_instance_group_multiple_zones` check for GCP provider [(#9566)](https://github.com/prowler-cloud/prowler/pull/9566)
|
||||
- Support AWS European Sovereign Cloud [(#9649)](https://github.com/prowler-cloud/prowler/pull/9649)
|
||||
- `compute_instance_disk_auto_delete_disabled` check for GCP provider [(#9604)](https://github.com/prowler-cloud/prowler/pull/9604)
|
||||
- Bedrock service pagination [(#9606)](https://github.com/prowler-cloud/prowler/pull/9606)
|
||||
|
||||
@@ -20,10 +21,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [5.16.1] (Prowler UNRELEASED)
|
||||
## [5.16.1] (Prowler v5.16.1)
|
||||
|
||||
### Fixed
|
||||
- Fix ZeroDivision error from Prowler ThreatScore [(#9653)](https://github.com/prowler-cloud/prowler/pull/9653)
|
||||
- ZeroDivision error from Prowler ThreatScore [(#9653)](https://github.com/prowler-cloud/prowler/pull/9653)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -984,6 +984,8 @@ class AwsProvider(Provider):
|
||||
global_region = "us-east-1"
|
||||
if self._identity.partition == "aws-cn":
|
||||
global_region = "cn-north-1"
|
||||
elif self._identity.partition == "aws-eusc":
|
||||
global_region = "eusc-de-east-1"
|
||||
elif self._identity.partition == "aws-us-gov":
|
||||
global_region = "us-gov-east-1"
|
||||
elif "aws-iso" in self._identity.partition:
|
||||
@@ -1473,11 +1475,12 @@ class AwsProvider(Provider):
|
||||
sts_client = create_sts_session(session, 'us-west-2')
|
||||
"""
|
||||
try:
|
||||
sts_endpoint_url = (
|
||||
f"https://sts.{aws_region}.amazonaws.com"
|
||||
if not aws_region.startswith("cn-")
|
||||
else f"https://sts.{aws_region}.amazonaws.com.cn"
|
||||
)
|
||||
if aws_region.startswith("cn-"):
|
||||
sts_endpoint_url = f"https://sts.{aws_region}.amazonaws.com.cn"
|
||||
elif aws_region.startswith("eusc-"):
|
||||
sts_endpoint_url = f"https://sts.{aws_region}.amazonaws.eu"
|
||||
else:
|
||||
sts_endpoint_url = f"https://sts.{aws_region}.amazonaws.com"
|
||||
return session.client("sts", aws_region, endpoint_url=sts_endpoint_url)
|
||||
except Exception as error:
|
||||
logger.critical(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,5 +59,5 @@ def parse_iam_credentials_arn(arn: str) -> ARN:
|
||||
|
||||
def is_valid_arn(arn: str) -> bool:
|
||||
"""is_valid_arn returns True or False whether the given AWS ARN (Amazon Resource Name) is valid or not."""
|
||||
regex = r"^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$"
|
||||
regex = r"^arn:aws(-cn|-eusc|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$"
|
||||
return re.match(regex, arn) is not None
|
||||
|
||||
@@ -55,7 +55,7 @@ class SecurityHubConnection(Connection):
|
||||
Attributes:
|
||||
enabled_regions (set): Set of regions where Security Hub is enabled.
|
||||
disabled_regions (set): Set of regions where Security Hub is disabled.
|
||||
partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed.
|
||||
partition (str): AWS partition (e.g., aws, aws-cn, aws-eusc, aws-us-gov) where SecurityHub is deployed.
|
||||
"""
|
||||
|
||||
enabled_regions: set = None
|
||||
@@ -70,7 +70,7 @@ class SecurityHub:
|
||||
Attributes:
|
||||
_session (Session): AWS session object for authentication and communication with AWS services.
|
||||
_aws_account_id (str): AWS account ID associated with the SecurityHub instance.
|
||||
_aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed.
|
||||
_aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-eusc, aws-us-gov) where SecurityHub is deployed.
|
||||
_findings_per_region (dict): Dictionary containing findings per region.
|
||||
_enabled_regions (dict): Dictionary containing enabled regions with SecurityHub clients.
|
||||
|
||||
@@ -115,7 +115,7 @@ class SecurityHub:
|
||||
Args:
|
||||
- aws_session (Session): AWS session object for authentication and communication with AWS services.
|
||||
- aws_account_id (str): AWS account ID associated with the SecurityHub instance.
|
||||
- aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed.
|
||||
- aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-eusc, aws-us-gov) where SecurityHub is deployed.
|
||||
- findings (list[AWSSecurityFindingFormat]): List of findings to filter and send to Security Hub.
|
||||
- aws_security_hub_available_regions (list[str]): List of regions where Security Hub is available.
|
||||
- send_only_fails (bool): Flag indicating whether to send only findings with status 'FAIL'.
|
||||
@@ -477,7 +477,7 @@ class SecurityHub:
|
||||
|
||||
Args:
|
||||
aws_account_id (str): AWS account ID to check for Prowler integration.
|
||||
aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov).
|
||||
aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-eusc, aws-us-gov).
|
||||
regions (set): Set of regions to check for Security Hub integration.
|
||||
raise_on_exception (bool): Whether to raise an exception if an error occurs.
|
||||
profile (str): AWS profile name to use for authentication.
|
||||
|
||||
@@ -90,6 +90,7 @@ class Partition(str, Enum):
|
||||
Attributes:
|
||||
aws (str): Represents the standard AWS commercial regions.
|
||||
aws_cn (str): Represents the AWS China regions.
|
||||
aws_eusc (str): Represents the AWS European Sovereign Cloud regions.
|
||||
aws_us_gov (str): Represents the AWS GovCloud (US) Regions.
|
||||
aws_iso (str): Represents the AWS ISO (US) Regions.
|
||||
aws_iso_b (str): Represents the AWS ISOB (US) Regions.
|
||||
@@ -99,6 +100,7 @@ class Partition(str, Enum):
|
||||
|
||||
aws = "aws"
|
||||
aws_cn = "aws-cn"
|
||||
aws_eusc = "aws-eusc"
|
||||
aws_us_gov = "aws-us-gov"
|
||||
aws_iso = "aws-iso"
|
||||
aws_iso_b = "aws-iso-b"
|
||||
|
||||
@@ -45,6 +45,7 @@ from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_CHINA_PARTITION,
|
||||
AWS_COMMERCIAL_PARTITION,
|
||||
AWS_EUSC_PARTITION,
|
||||
AWS_GOV_CLOUD_ACCOUNT_ARN,
|
||||
AWS_GOV_CLOUD_PARTITION,
|
||||
AWS_ISO_PARTITION,
|
||||
@@ -52,6 +53,7 @@ from tests.providers.aws.utils import (
|
||||
AWS_REGION_CN_NORTHWEST_1,
|
||||
AWS_REGION_EU_CENTRAL_1,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
AWS_REGION_EUSC_DE_EAST_1,
|
||||
AWS_REGION_GOV_CLOUD_US_EAST_1,
|
||||
AWS_REGION_ISO_GLOBAL,
|
||||
AWS_REGION_US_EAST_1,
|
||||
@@ -956,6 +958,13 @@ aws:
|
||||
|
||||
assert aws_provider.get_global_region() == AWS_REGION_ISO_GLOBAL
|
||||
|
||||
@mock_aws
|
||||
def test_aws_eusc_get_global_region(self):
|
||||
aws_provider = AwsProvider()
|
||||
aws_provider._identity.partition = AWS_EUSC_PARTITION
|
||||
|
||||
assert aws_provider.get_global_region() == AWS_REGION_EUSC_DE_EAST_1
|
||||
|
||||
@mock_aws
|
||||
def test_get_available_aws_service_regions_with_us_east_1_audited(self):
|
||||
region = [AWS_REGION_US_EAST_1]
|
||||
@@ -1506,6 +1515,17 @@ aws:
|
||||
sts_session._endpoint.host == f"https://sts.{aws_region}.amazonaws.com.cn"
|
||||
)
|
||||
|
||||
@mock_aws
|
||||
def test_create_sts_session_eusc(self):
|
||||
current_session = session.Session()
|
||||
aws_region = AWS_REGION_EUSC_DE_EAST_1
|
||||
sts_session = AwsProvider.create_sts_session(current_session, aws_region)
|
||||
|
||||
assert sts_session._service_model.service_name == "sts"
|
||||
assert sts_session._client_config.region_name == aws_region
|
||||
assert sts_session._endpoint._endpoint_prefix == "sts"
|
||||
assert sts_session._endpoint.host == f"https://sts.{aws_region}.amazonaws.eu"
|
||||
|
||||
@mock_aws
|
||||
@patch(
|
||||
"prowler.lib.check.utils.recover_checks_from_provider",
|
||||
@@ -1760,7 +1780,7 @@ aws:
|
||||
assert len(AwsProvider.get_regions("aws-cn")) == 2
|
||||
|
||||
def test_get_regions_aws_count(self):
|
||||
assert len(AwsProvider.get_regions(partition="aws")) == 35
|
||||
assert len(AwsProvider.get_regions(partition="aws")) == 34
|
||||
|
||||
def test_get_all_regions(self):
|
||||
with patch(
|
||||
|
||||
@@ -19,6 +19,7 @@ IAM_ROLE = "test-role"
|
||||
IAM_SERVICE = "iam"
|
||||
COMMERCIAL_PARTITION = "aws"
|
||||
CHINA_PARTITION = "aws-cn"
|
||||
EUSC_PARTITION = "aws-eusc"
|
||||
GOVCLOUD_PARTITION = "aws-us-gov"
|
||||
|
||||
|
||||
@@ -245,6 +246,28 @@ class Test_ARN_Parsing:
|
||||
"resource": IAM_ROLE,
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{EUSC_PARTITION}:{IAM_SERVICE}::{ACCOUNT_ID}:{RESOURCE_TYPE_ROLE}/{IAM_ROLE}",
|
||||
"expected": {
|
||||
"partition": EUSC_PARTITION,
|
||||
"service": IAM_SERVICE,
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": RESOURCE_TYPE_ROLE,
|
||||
"resource": IAM_ROLE,
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{EUSC_PARTITION}:{IAM_SERVICE}::{ACCOUNT_ID}:{RESOUCE_TYPE_USER}/{IAM_ROLE}",
|
||||
"expected": {
|
||||
"partition": EUSC_PARTITION,
|
||||
"service": IAM_SERVICE,
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": RESOUCE_TYPE_USER,
|
||||
"resource": IAM_ROLE,
|
||||
},
|
||||
},
|
||||
# Root user
|
||||
{
|
||||
"input_arn": f"arn:aws:{IAM_SERVICE}::{ACCOUNT_ID}:root",
|
||||
@@ -279,6 +302,17 @@ class Test_ARN_Parsing:
|
||||
"resource": "root",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{EUSC_PARTITION}:{IAM_SERVICE}::{ACCOUNT_ID}:root",
|
||||
"expected": {
|
||||
"partition": EUSC_PARTITION,
|
||||
"service": IAM_SERVICE,
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "root",
|
||||
"resource": "root",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:aws:sts::{ACCOUNT_ID}:federated-user/Bob",
|
||||
"expected": {
|
||||
@@ -312,6 +346,17 @@ class Test_ARN_Parsing:
|
||||
"resource": "Bob",
|
||||
},
|
||||
},
|
||||
{
|
||||
"input_arn": f"arn:{EUSC_PARTITION}:sts::{ACCOUNT_ID}:federated-user/Bob",
|
||||
"expected": {
|
||||
"partition": EUSC_PARTITION,
|
||||
"service": "sts",
|
||||
"region": None,
|
||||
"account_id": ACCOUNT_ID,
|
||||
"resource_type": "federated-user",
|
||||
"resource": "Bob",
|
||||
},
|
||||
},
|
||||
]
|
||||
for test in test_cases:
|
||||
input_arn = test["input_arn"]
|
||||
@@ -379,6 +424,7 @@ class Test_ARN_Parsing:
|
||||
def test_is_valid_arn(self):
|
||||
assert is_valid_arn("arn:aws:iam::012345678910:user/test")
|
||||
assert is_valid_arn("arn:aws-cn:ec2:us-east-1:123456789012:vpc/vpc-12345678")
|
||||
assert is_valid_arn("arn:aws-eusc:ec2:us-east-1:123456789012:vpc/vpc-12345678")
|
||||
assert is_valid_arn("arn:aws-us-gov:s3:::bucket")
|
||||
assert is_valid_arn("arn:aws-iso:iam::012345678910:user/test")
|
||||
assert is_valid_arn("arn:aws-iso-b:ec2:us-east-1:123456789012:vpc/vpc-12345678")
|
||||
|
||||
@@ -17,6 +17,7 @@ from prowler.providers.common.models import Audit_Metadata
|
||||
AWS_COMMERCIAL_PARTITION = "aws"
|
||||
AWS_GOV_CLOUD_PARTITION = "aws-us-gov"
|
||||
AWS_CHINA_PARTITION = "aws-cn"
|
||||
AWS_EUSC_PARTITION = "aws-eusc"
|
||||
AWS_ISO_PARTITION = "aws-iso"
|
||||
|
||||
# Root AWS Account
|
||||
@@ -52,6 +53,9 @@ AWS_REGION_GOV_CLOUD_US_EAST_1 = "us-gov-east-1"
|
||||
# Iso Regions
|
||||
AWS_REGION_ISO_GLOBAL = "aws-iso-global"
|
||||
|
||||
# European Sovereign Cloud Regions
|
||||
AWS_REGION_EUSC_DE_EAST_1 = "eusc-de-east-1"
|
||||
|
||||
# EC2
|
||||
EXAMPLE_AMI_ID = "ami-12c6146b"
|
||||
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.16.1] (Prowler v5.17.1)
|
||||
## [1.17.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Add search bar when adding a provider [(#9634)](https://github.com/prowler-cloud/prowler/pull/9634)
|
||||
|
||||
---
|
||||
|
||||
## [1.16.1] (Prowler v5.16.1)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { RadioGroup } from "@heroui/radio";
|
||||
import { FC } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SearchInput } from "@/components/shadcn";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { addProviderFormSchema } from "@/types";
|
||||
|
||||
import {
|
||||
@@ -19,9 +20,61 @@ import {
|
||||
MongoDBAtlasProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
} from "../icons/providers-badge";
|
||||
import { CustomRadio } from "../ui/custom";
|
||||
import { FormMessage } from "../ui/form";
|
||||
|
||||
const PROVIDERS = [
|
||||
{
|
||||
value: "aws",
|
||||
label: "Amazon Web Services",
|
||||
badge: AWSProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "gcp",
|
||||
label: "Google Cloud Platform",
|
||||
badge: GCPProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "azure",
|
||||
label: "Microsoft Azure",
|
||||
badge: AzureProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "m365",
|
||||
label: "Microsoft 365",
|
||||
badge: M365ProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "mongodbatlas",
|
||||
label: "MongoDB Atlas",
|
||||
badge: MongoDBAtlasProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "kubernetes",
|
||||
label: "Kubernetes",
|
||||
badge: KS8ProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "github",
|
||||
label: "GitHub",
|
||||
badge: GitHubProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "iac",
|
||||
label: "Infrastructure as Code",
|
||||
badge: IacProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "oraclecloud",
|
||||
label: "Oracle Cloud Infrastructure",
|
||||
badge: OracleCloudProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "alibabacloud",
|
||||
label: "Alibaba Cloud",
|
||||
badge: AlibabaCloudProviderBadge,
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface RadioGroupProviderProps {
|
||||
control: Control<z.infer<typeof addProviderFormSchema>>;
|
||||
isInvalid: boolean;
|
||||
@@ -33,90 +86,90 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
|
||||
isInvalid,
|
||||
errorMessage,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const lowerSearch = searchTerm.trim().toLowerCase();
|
||||
const filteredProviders = lowerSearch
|
||||
? PROVIDERS.filter(
|
||||
(provider) =>
|
||||
provider.label.toLowerCase().includes(lowerSearch) ||
|
||||
provider.value.toLowerCase().includes(lowerSearch),
|
||||
)
|
||||
: PROVIDERS;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="providerType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<RadioGroup
|
||||
className="flex flex-wrap"
|
||||
isInvalid={isInvalid}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<CustomRadio description="Amazon Web Services" value="aws">
|
||||
<div className="flex items-center">
|
||||
<AWSProviderBadge size={26} />
|
||||
<span className="ml-2">Amazon Web Services</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Google Cloud Platform" value="gcp">
|
||||
<div className="flex items-center">
|
||||
<GCPProviderBadge size={26} />
|
||||
<span className="ml-2">Google Cloud Platform</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Microsoft Azure" value="azure">
|
||||
<div className="flex items-center">
|
||||
<AzureProviderBadge size={26} />
|
||||
<span className="ml-2">Microsoft Azure</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Microsoft 365" value="m365">
|
||||
<div className="flex items-center">
|
||||
<M365ProviderBadge size={26} />
|
||||
<span className="ml-2">Microsoft 365</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="MongoDB Atlas" value="mongodbatlas">
|
||||
<div className="flex items-center">
|
||||
<MongoDBAtlasProviderBadge size={26} />
|
||||
<span className="ml-2">MongoDB Atlas</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Kubernetes" value="kubernetes">
|
||||
<div className="flex items-center">
|
||||
<KS8ProviderBadge size={26} />
|
||||
<span className="ml-2">Kubernetes</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="GitHub" value="github">
|
||||
<div className="flex items-center">
|
||||
<GitHubProviderBadge size={26} />
|
||||
<span className="ml-2">GitHub</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Infrastructure as Code" value="iac">
|
||||
<div className="flex items-center">
|
||||
<IacProviderBadge size={26} />
|
||||
<span className="ml-2">Infrastructure as Code</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio
|
||||
description="Oracle Cloud Infrastructure"
|
||||
value="oraclecloud"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<OracleCloudProviderBadge size={26} />
|
||||
<span className="ml-2">Oracle Cloud Infrastructure</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio description="Alibaba Cloud" value="alibabacloud">
|
||||
<div className="flex items-center">
|
||||
<AlibabaCloudProviderBadge size={26} />
|
||||
<span className="ml-2">Alibaba Cloud</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<div className="flex h-[calc(100vh-200px)] flex-col px-4">
|
||||
<div className="relative z-10 shrink-0 pb-4">
|
||||
<SearchInput
|
||||
aria-label="Search providers"
|
||||
placeholder="Search providers..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onClear={() => setSearchTerm("")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="minimal-scrollbar relative flex-1 overflow-y-auto pr-3">
|
||||
<div
|
||||
role="listbox"
|
||||
aria-label="Select a provider"
|
||||
className="flex flex-col gap-3"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(to bottom, transparent, black 24px)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to bottom, transparent, black 24px)",
|
||||
paddingTop: "24px",
|
||||
marginTop: "-24px",
|
||||
}}
|
||||
>
|
||||
{filteredProviders.length > 0 ? (
|
||||
filteredProviders.map((provider) => {
|
||||
const BadgeComponent = provider.badge;
|
||||
const isSelected = field.value === provider.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={provider.value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => field.onChange(provider.value)}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center gap-3 rounded-lg border p-4 text-left transition-all",
|
||||
"hover:border-button-primary",
|
||||
"focus-visible:border-button-primary focus-visible:ring-button-primary focus:outline-none focus-visible:ring-1",
|
||||
isSelected
|
||||
? "border-button-primary bg-bg-neutral-tertiary"
|
||||
: "border-border-neutral-secondary bg-bg-neutral-secondary",
|
||||
isInvalid && "border-bg-fail",
|
||||
)}
|
||||
>
|
||||
<BadgeComponent size={26} />
|
||||
<span className="text-text-neutral-primary text-sm font-medium">
|
||||
{provider.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-text-neutral-tertiary py-4 text-sm">
|
||||
No providers found matching "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<FormMessage className="text-text-error">
|
||||
{errorMessage}
|
||||
</FormMessage>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ export * from "./card/resource-stats-card/resource-stats-card-header";
|
||||
export * from "./checkbox/checkbox";
|
||||
export * from "./combobox";
|
||||
export * from "./dropdown/dropdown";
|
||||
export * from "./input/input";
|
||||
export * from "./search-input/search-input";
|
||||
export * from "./select/multiselect";
|
||||
export * from "./select/select";
|
||||
export * from "./separator/separator";
|
||||
|
||||
51
ui/components/shadcn/input/input.tsx
Normal file
51
ui/components/shadcn/input/input.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { ComponentProps, forwardRef } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const inputVariants = cva(
|
||||
"flex w-full rounded-lg border text-sm transition-all outline-none file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-border-input-primary-press focus:ring-offset-1 placeholder:text-text-neutral-tertiary",
|
||||
ghost:
|
||||
"border-transparent bg-transparent hover:bg-bg-neutral-tertiary focus:bg-bg-neutral-tertiary placeholder:text-text-neutral-tertiary",
|
||||
},
|
||||
inputSize: {
|
||||
default: "h-10 px-4 py-3",
|
||||
sm: "h-8 px-3 py-2 text-xs",
|
||||
lg: "h-12 px-5 py-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
inputSize: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface InputProps
|
||||
extends Omit<ComponentProps<"input">, "size">,
|
||||
VariantProps<typeof inputVariants> {}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, variant, inputSize, type = "text", ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(inputVariants({ variant, inputSize, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input, inputVariants };
|
||||
125
ui/components/shadcn/search-input/search-input.tsx
Normal file
125
ui/components/shadcn/search-input/search-input.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { SearchIcon, XCircle } from "lucide-react";
|
||||
import { ComponentProps, forwardRef } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const searchInputWrapperVariants = cva("relative flex items-center w-full", {
|
||||
variants: {
|
||||
size: {
|
||||
default: "",
|
||||
sm: "",
|
||||
lg: "",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
const searchInputVariants = cva(
|
||||
"flex w-full rounded-lg border text-sm transition-all outline-none placeholder:text-text-neutral-tertiary disabled:cursor-not-allowed disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-border-input-primary-press focus:ring-offset-1",
|
||||
ghost:
|
||||
"border-transparent bg-transparent hover:bg-bg-neutral-tertiary focus:bg-bg-neutral-tertiary",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 pl-10 pr-10 py-3",
|
||||
sm: "h-8 pl-8 pr-8 py-2 text-xs",
|
||||
lg: "h-12 pl-12 pr-12 py-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const iconSizeMap = {
|
||||
default: 16,
|
||||
sm: 14,
|
||||
lg: 20,
|
||||
} as const;
|
||||
|
||||
const iconPositionMap = {
|
||||
default: "left-3",
|
||||
sm: "left-2.5",
|
||||
lg: "left-4",
|
||||
} as const;
|
||||
|
||||
const clearButtonPositionMap = {
|
||||
default: "right-3",
|
||||
sm: "right-2.5",
|
||||
lg: "right-4",
|
||||
} as const;
|
||||
|
||||
export interface SearchInputProps
|
||||
extends Omit<ComponentProps<"input">, "size">,
|
||||
VariantProps<typeof searchInputVariants> {
|
||||
onClear?: () => void;
|
||||
}
|
||||
|
||||
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size = "default",
|
||||
value,
|
||||
onClear,
|
||||
placeholder = "Search...",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const iconSize = iconSizeMap[size || "default"];
|
||||
const iconPosition = iconPositionMap[size || "default"];
|
||||
const clearButtonPosition = clearButtonPositionMap[size || "default"];
|
||||
const hasValue = value && String(value).length > 0;
|
||||
|
||||
return (
|
||||
<div className={cn(searchInputWrapperVariants({ size }))}>
|
||||
<SearchIcon
|
||||
size={iconSize}
|
||||
className={cn(
|
||||
"text-text-neutral-tertiary pointer-events-none absolute",
|
||||
iconPosition,
|
||||
)}
|
||||
/>
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
data-slot="search-input"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
className={cn(searchInputVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
{hasValue && onClear && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Clear search"
|
||||
onClick={onClear}
|
||||
className={cn(
|
||||
"text-text-neutral-tertiary hover:text-text-neutral-primary absolute transition-colors focus:outline-none",
|
||||
clearButtonPosition,
|
||||
)}
|
||||
>
|
||||
<XCircle size={iconSize} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SearchInput.displayName = "SearchInput";
|
||||
|
||||
export { SearchInput, searchInputVariants };
|
||||
@@ -25,7 +25,7 @@ for page in get_parameters_by_path_paginator.paginate(
|
||||
for service in page["Parameters"]:
|
||||
regions_by_service["services"][service["Value"]] = {}
|
||||
# Get all AWS Regions for the specific service
|
||||
regions = {"aws": [], "aws-cn": [], "aws-us-gov": []}
|
||||
regions = {"aws": [], "aws-cn": [], "aws-eusc": [], "aws-us-gov": []}
|
||||
for page in get_parameters_by_path_paginator.paginate(
|
||||
Path="/aws/service/global-infrastructure/services/"
|
||||
+ service["Value"]
|
||||
@@ -34,6 +34,8 @@ for page in get_parameters_by_path_paginator.paginate(
|
||||
for region in page["Parameters"]:
|
||||
if "cn" in region["Value"]:
|
||||
regions["aws-cn"].append(region["Value"])
|
||||
elif "eusc" in region["Value"]:
|
||||
regions["aws-eusc"].append(region["Value"])
|
||||
elif "gov" in region["Value"]:
|
||||
regions["aws-us-gov"].append(region["Value"])
|
||||
else:
|
||||
@@ -41,6 +43,7 @@ for page in get_parameters_by_path_paginator.paginate(
|
||||
# Sort regions per partition
|
||||
regions["aws"] = sorted(regions["aws"])
|
||||
regions["aws-cn"] = sorted(regions["aws-cn"])
|
||||
regions["aws-eusc"] = sorted(regions["aws-eusc"])
|
||||
regions["aws-us-gov"] = sorted(regions["aws-us-gov"])
|
||||
regions_by_service["services"][service["Value"]]["regions"] = regions
|
||||
|
||||
@@ -69,6 +72,7 @@ regions_by_service["services"]["bedrock-agent"] = {
|
||||
"us-west-2",
|
||||
],
|
||||
"aws-cn": [],
|
||||
"aws-eusc": [],
|
||||
"aws-us-gov": [
|
||||
"us-gov-west-1",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user