Compare commits

...

5 Commits

Author SHA1 Message Date
Pepe Fagoaga
e07e45c8e5 chore(api): update lock for SDK 2025-12-23 16:28:14 +01:00
Pepe Fagoaga
a37aea84e7 chore: changelog for v5.16.1 (#9661) 2025-12-23 12:51:47 +01:00
Pedro Martín
8d1d041092 chore(aws): support new eusc partition (#9649)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-12-23 12:28:10 +01:00
Rubén De la Torre Vico
6f018183cd ci(mcp): add GitHub Actions workflow for PyPI release (#9660) 2025-12-23 12:27:08 +01:00
Pedro Martín
8ce56b5ed6 feat(ui): add search bar when adding a provider (#9634)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-12-23 12:09:55 +01:00
22 changed files with 4138 additions and 2130 deletions

81
.github/workflows/mcp-pypi-release.yml vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"]
}
}

View File

@@ -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"

View File

@@ -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__"]

View File

@@ -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

View File

@@ -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]

View File

@@ -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)
---

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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"

View File

@@ -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(

View File

@@ -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")

View File

@@ -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"

View File

@@ -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

View File

@@ -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 &quot;{searchTerm}&quot;
</p>
)}
</div>
</RadioGroup>
</div>
{errorMessage && (
<FormMessage className="text-text-error">
{errorMessage}
</FormMessage>
)}
</>
</div>
)}
/>
);

View File

@@ -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";

View 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 };

View 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 };

View File

@@ -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",
],