Merge branch 'master' into PRWLR-7874-support-labels-on-jira-tickets

This commit is contained in:
Pedro Martín
2025-09-03 10:49:00 +02:00
committed by GitHub
11 changed files with 496 additions and 89 deletions
@@ -157,21 +157,21 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Push README to Docker Hub (toniblyx)
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}
readme-filepath: ./README.md
- name: Push README to Docker Hub (prowlercloud)
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}
readme-filepath: ./README.md
# - name: Push README to Docker Hub (toniblyx)
# uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
# repository: ${{ env.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}
# readme-filepath: ./README.md
#
# - name: Push README to Docker Hub (prowlercloud)
# uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
# repository: ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}
# readme-filepath: ./README.md
dispatch-action:
needs: container-build-push
+1 -1
View File
@@ -273,7 +273,7 @@ python prowler-cli.py -v
- **Prowler API**: A backend service, developed with Django REST Framework, responsible for running Prowler scans and storing the generated results.
- **Prowler SDK**: A Python SDK designed to extend the functionality of the Prowler CLI for advanced capabilities.
![Prowler App Architecture](docs/img/prowler-app-architecture.png)
![Prowler App Architecture](docs/products/img/prowler-app-architecture.png)
## Prowler CLI
+2 -33
View File
@@ -3,43 +3,11 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [v5.12.0] (Prowler UNRELEASED)
### Added
### Changed
### Fixed
---
## [v5.11.1] (Prowler UNRELEASED)
### Fixed
---
## [v5.12.0] (Prowler UNRELEASED)
### Added
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
### Changed
### Fixed
---
## [v5.11.1] (Prowler UNRELEASED)
### Fixed
---
## [v5.12.0] (Prowler UNRELEASED)
### Added
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
- Support labels on Jira tickets [(#8603)](https://github.com/prowler-cloud/prowler/pull/8603)
- Add finding url and tenant info inside Jira tickets [(#8607)](https://github.com/prowler-cloud/prowler/pull/8607)
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
### Changed
@@ -50,6 +18,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
## [v5.11.1] (Prowler UNRELEASED)
### Fixed
- `AdditionalUrls` field in CheckMetadata [(#8590)](https://github.com/prowler-cloud/prowler/pull/8590)
---
+19 -3
View File
@@ -7,7 +7,7 @@ from dataclasses import asdict, dataclass, is_dataclass
from enum import Enum
from typing import Any, Dict, Optional, Set
from pydantic.v1 import BaseModel, ValidationError, validator
from pydantic.v1 import BaseModel, Field, ValidationError, validator
from prowler.config.config import Provider
from prowler.lib.check.compliance_models import Compliance
@@ -85,6 +85,7 @@ class CheckMetadata(BaseModel):
Risk (str): The risk associated with the check.
RelatedUrl (str): The URL related to the check.
Remediation (Remediation): The remediation steps for the check.
AdditionalUrls (list[str]): Additional URLs related to the check. Defaults to an empty list.
Categories (list[str]): The categories of the check.
DependsOn (list[str]): The dependencies of the check.
RelatedTo (list[str]): The related checks.
@@ -97,13 +98,14 @@ class CheckMetadata(BaseModel):
valid_severity(severity): Validator function to validate the severity of the check.
valid_cli_command(remediation): Validator function to validate the CLI command is not an URL.
valid_resource_type(resource_type): Validator function to validate the resource type is not empty.
validate_additional_urls(additional_urls): Validator function to ensure AdditionalUrls contains no duplicates.
"""
Provider: str
CheckID: str
CheckTitle: str
CheckType: list[str]
CheckAliases: list[str] = []
CheckAliases: list[str] = Field(default_factory=list)
ServiceName: str
SubServiceName: str
ResourceIdTemplate: str
@@ -113,13 +115,14 @@ class CheckMetadata(BaseModel):
Risk: str
RelatedUrl: str
Remediation: Remediation
AdditionalUrls: list[str] = Field(default_factory=list)
Categories: list[str]
DependsOn: list[str]
RelatedTo: list[str]
Notes: str
# We set the compliance to None to
# store the compliance later if supplied
Compliance: Optional[list[Any]] = []
Compliance: Optional[list[Any]] = Field(default_factory=list)
@validator("Categories", each_item=True, pre=True, always=True)
def valid_category(value):
@@ -178,6 +181,19 @@ class CheckMetadata(BaseModel):
return check_id
@validator("AdditionalUrls", pre=True, always=True)
def validate_additional_urls(cls, additional_urls):
if not isinstance(additional_urls, list):
raise ValueError("AdditionalUrls must be a list")
if any(not url or not url.strip() for url in additional_urls):
raise ValueError("AdditionalUrls cannot contain empty items")
if len(additional_urls) != len(set(additional_urls)):
raise ValueError("AdditionalUrls cannot contain duplicate items")
return additional_urls
@staticmethod
def get_bulk(provider: str) -> dict[str, "CheckMetadata"]:
"""
+391
View File
@@ -1,5 +1,8 @@
from unittest import mock
import pytest
from pydantic.v1 import ValidationError
from prowler.lib.check.models import CheckMetadata
from tests.lib.check.compliance_check_test import custom_compliance_metadata
@@ -325,3 +328,391 @@ class TestCheckMetada:
result = CheckMetadata.list(bulk_checks_metadata=bulk_metadata)
assert result == set()
def test_additional_urls_valid_empty_list(self):
"""Test AdditionalUrls with valid empty list (default)"""
metadata = CheckMetadata(
Provider="aws",
CheckID="test_check",
CheckTitle="Test Check",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="url1",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=[],
Compliance=[],
)
assert metadata.AdditionalUrls == []
def test_additional_urls_valid_with_urls(self):
"""Test AdditionalUrls with valid URLs"""
valid_urls = [
"https://example.com/doc1",
"https://example.com/doc2",
"https://aws.amazon.com/docs",
]
metadata = CheckMetadata(
Provider="aws",
CheckID="test_check",
CheckTitle="Test Check",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="url1",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=valid_urls,
Compliance=[],
)
assert metadata.AdditionalUrls == valid_urls
def test_additional_urls_invalid_not_list(self):
"""Test AdditionalUrls with non-list value"""
with pytest.raises(ValidationError) as exc_info:
CheckMetadata(
Provider="aws",
CheckID="test_check",
CheckTitle="Test Check",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="url1",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls="not_a_list",
Compliance=[],
)
assert "AdditionalUrls must be a list" in str(exc_info.value)
def test_additional_urls_invalid_empty_items(self):
"""Test AdditionalUrls with empty string items"""
with pytest.raises(ValidationError) as exc_info:
CheckMetadata(
Provider="aws",
CheckID="test_check",
CheckTitle="Test Check",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="url1",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=["https://example.com", "", "https://example2.com"],
Compliance=[],
)
assert "AdditionalUrls cannot contain empty items" in str(exc_info.value)
def test_additional_urls_invalid_whitespace_items(self):
"""Test AdditionalUrls with whitespace-only items"""
with pytest.raises(ValidationError) as exc_info:
CheckMetadata(
Provider="aws",
CheckID="test_check",
CheckTitle="Test Check",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="url1",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=["https://example.com", " ", "https://example2.com"],
Compliance=[],
)
assert "AdditionalUrls cannot contain empty items" in str(exc_info.value)
def test_additional_urls_invalid_duplicates(self):
"""Test AdditionalUrls with duplicate items"""
with pytest.raises(ValidationError) as exc_info:
CheckMetadata(
Provider="aws",
CheckID="test_check",
CheckTitle="Test Check",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="url1",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=[
"https://example.com",
"https://example2.com",
"https://example.com",
],
Compliance=[],
)
assert "AdditionalUrls cannot contain duplicate items" in str(exc_info.value)
def test_fields_with_explicit_empty_values(self):
"""Test that RelatedUrl and AdditionalUrls can be set to explicit empty values"""
metadata = CheckMetadata(
Provider="aws",
CheckID="test_check_empty_fields",
CheckTitle="Test Check with Empty Fields",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="", # Explicit empty string
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=[], # Explicit empty list
Compliance=[],
)
# Assert that the fields are set to empty values
assert metadata.RelatedUrl == ""
assert metadata.AdditionalUrls == []
def test_fields_default_values(self):
"""Test that RelatedUrl and AdditionalUrls use proper defaults when not provided"""
metadata = CheckMetadata(
Provider="aws",
CheckID="test_check_defaults",
CheckTitle="Test Check with Default Fields",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
# AdditionalUrls not provided - should default to empty list via default_factory
Compliance=[],
)
# Assert that the fields use their default values
assert metadata.RelatedUrl == "" # Should default to empty string
assert metadata.AdditionalUrls == [] # Should default to empty list
def test_related_url_none_fails(self):
"""Test that setting RelatedUrl to None raises a ValidationError"""
with pytest.raises(ValidationError) as exc_info:
CheckMetadata(
Provider="aws",
CheckID="test_check_none_related_url",
CheckTitle="Test Check with None RelatedUrl",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl=None, # This should fail
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=[],
Compliance=[],
)
# Should contain a validation error for RelatedUrl
assert "RelatedUrl" in str(exc_info.value)
def test_additional_urls_none_fails(self):
"""Test that setting AdditionalUrls to None raises a ValidationError"""
with pytest.raises(ValidationError) as exc_info:
CheckMetadata(
Provider="aws",
CheckID="test_check_none_additional_urls",
CheckTitle="Test Check with None AdditionalUrls",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="https://example.com",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls=None, # This should fail
Compliance=[],
)
# Should contain the validation error we set in the validator
assert "AdditionalUrls must be a list" in str(exc_info.value)
def test_additional_urls_invalid_type_fails(self):
"""Test that setting AdditionalUrls to non-list value raises a ValidationError"""
with pytest.raises(ValidationError) as exc_info:
CheckMetadata(
Provider="aws",
CheckID="test_check_invalid_additional_urls",
CheckTitle="Test Check with Invalid AdditionalUrls",
CheckType=["type1"],
ServiceName="test",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
ResourceType="resource1",
Description="Description 1",
Risk="risk1",
RelatedUrl="https://example.com",
Remediation={
"Code": {
"CLI": "cli1",
"NativeIaC": "native1",
"Other": "other1",
"Terraform": "terraform1",
},
"Recommendation": {"Text": "text1", "Url": "url1"},
},
Categories=["categoryone"],
DependsOn=["dependency1"],
RelatedTo=["related1"],
Notes="notes1",
AdditionalUrls="not_a_list", # This should fail
Compliance=[],
)
# Should contain the validation error we set in the validator
assert "AdditionalUrls must be a list" in str(exc_info.value)
+3
View File
@@ -12,6 +12,9 @@ All notable changes to the **Prowler UI** are documented in this file.
- Markdown rendering in finding details page [(#8604)](https://github.com/prowler-cloud/prowler/pull/8604)
### 🐞 Fixed
- Scan page shows NoProvidersAdded when no providers [(#8626)](https://github.com/prowler-cloud/prowler/pull/8626)
## [1.11.0] (Prowler v5.11.0)
### 🚀 Added
+2 -2
View File
@@ -48,8 +48,8 @@ export const getComplianceOverviewMetadataInfo = async ({
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
// Define filters to exclude
if (key !== "filter[search]") {
// Define filters to exclude and check for valid values
if (key !== "filter[search]" && value && String(value).trim() !== "") {
url.searchParams.append(key, String(value));
}
});
+17 -11
View File
@@ -91,12 +91,14 @@ export default async function Compliance({
}
: undefined;
const metadataInfoData = await getComplianceOverviewMetadataInfo({
query,
filters: {
"filter[scan_id]": selectedScanId,
},
});
const metadataInfoData = selectedScanId
? await getComplianceOverviewMetadataInfo({
query,
filters: {
"filter[scan_id]": selectedScanId,
},
})
: { data: { attributes: { regions: [] } } };
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
@@ -140,11 +142,15 @@ const SSRComplianceGrid = async ({
// Extract query from filters
const query = (filters["filter[search]"] as string) || "";
const compliancesData = await getCompliancesOverview({
scanId,
region: regionFilter,
query,
});
// Only fetch compliance data if we have a valid scanId
const compliancesData =
scanId && scanId.trim() !== ""
? await getCompliancesOverview({
scanId,
region: regionFilter,
query,
})
: { data: [], errors: [] };
const type = compliancesData?.data?.type;
+7
View File
@@ -1,3 +1,5 @@
import { redirect } from "next/navigation";
import { getLighthouseConfig } from "@/actions/lighthouse/lighthouse";
import { LighthouseIcon } from "@/components/icons/Icons";
import { Chat } from "@/components/lighthouse";
@@ -7,6 +9,11 @@ export default async function AIChatbot() {
const lighthouseConfig = await getLighthouseConfig();
const hasConfig = !!lighthouseConfig;
if (!hasConfig) {
return redirect("/lighthouse/config");
}
const isActive = lighthouseConfig.is_active ?? false;
return (
+2 -1
View File
@@ -48,7 +48,8 @@ export default async function Scans({
connected: provider.attributes.connection.connected,
})) || [];
const thereIsNoProviders = !providersData?.data;
const thereIsNoProviders =
!providersData?.data || providersData.data.length === 0;
const thereIsNoProvidersConnected = providersData?.data?.every(
(provider: ProviderProps) => !provider.attributes.connection.connected,
+37 -23
View File
@@ -55,14 +55,16 @@ const validateAwsCredentialsCreate = (
if (!data.aws_access_key_id) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Access Key ID is required when using access and secret key",
message:
"AWS Access Key ID is required when using access and secret key",
path: ["aws_access_key_id"],
});
}
if (!data.aws_secret_access_key) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Secret Access Key is required when using access and secret key",
message:
"AWS Secret Access Key is required when using access and secret key",
path: ["aws_secret_access_key"],
});
}
@@ -78,7 +80,8 @@ const validateAwsCredentialsEdit = (data: any, ctx: z.RefinementCtx) => {
if (hasAccessKey && !hasSecretKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Secret Access Key is required when providing Access Key ID",
message:
"AWS Secret Access Key is required when providing Access Key ID",
path: ["aws_secret_access_key"],
});
}
@@ -86,7 +89,8 @@ const validateAwsCredentialsEdit = (data: any, ctx: z.RefinementCtx) => {
if (hasSecretKey && !hasAccessKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Access Key ID is required when providing Secret Access Key",
message:
"AWS Access Key ID is required when providing Secret Access Key",
path: ["aws_access_key_id"],
});
}
@@ -99,8 +103,10 @@ const validateIamRole = (
ctx: z.RefinementCtx,
checkShowSection: boolean = true,
) => {
const shouldValidate = checkShowSection ? data.show_role_section === true : true;
const shouldValidate = checkShowSection
? data.show_role_section === true
: true;
if (shouldValidate && data.role_arn) {
if (data.role_arn.trim() === "") {
ctx.addIssue({
@@ -160,9 +166,14 @@ export const s3IntegrationFormSchema = baseS3IntegrationSchema
export const editS3IntegrationFormSchema = baseS3IntegrationSchema
.extend({
bucket_name: z.string().min(1, "Bucket name is required").optional(),
output_directory: z.string().min(1, "Output directory is required").optional(),
output_directory: z
.string()
.min(1, "Output directory is required")
.optional(),
providers: z.array(z.string()).optional(),
credentials_type: z.enum(["aws-sdk-default", "access-secret-key"]).optional(),
credentials_type: z
.enum(["aws-sdk-default", "access-secret-key"])
.optional(),
})
.superRefine((data, ctx) => {
validateAwsCredentialsEdit(data, ctx);
@@ -201,18 +212,21 @@ export const securityHubIntegrationFormSchema = baseSecurityHubIntegrationSchema
}
});
export const editSecurityHubIntegrationFormSchema = baseSecurityHubIntegrationSchema
.extend({
provider_id: z.string().optional(),
send_only_fails: z.boolean().optional(),
archive_previous_findings: z.boolean().optional(),
use_custom_credentials: z.boolean().optional(),
credentials_type: z.enum(["aws-sdk-default", "access-secret-key"]).optional(),
})
.superRefine((data, ctx) => {
if (data.use_custom_credentials !== false) {
validateAwsCredentialsEdit(data, ctx);
}
// Always validate role if role_arn is provided
validateIamRole(data, ctx, false);
});
export const editSecurityHubIntegrationFormSchema =
baseSecurityHubIntegrationSchema
.extend({
provider_id: z.string().optional(),
send_only_fails: z.boolean().optional(),
archive_previous_findings: z.boolean().optional(),
use_custom_credentials: z.boolean().optional(),
credentials_type: z
.enum(["aws-sdk-default", "access-secret-key"])
.optional(),
})
.superRefine((data, ctx) => {
if (data.use_custom_credentials !== false) {
validateAwsCredentialsEdit(data, ctx);
}
// Always validate role if role_arn is provided
validateIamRole(data, ctx, false);
});