feat: add Lovable AI app builder provider

- Add lovable SDK provider with apps and published services
- Add 12 security checks mapped to Lovable security best practices
- Add Prowler ThreatScore for Lovable compliance framework
- Add API support: provider type, secret schema, UID validator, migration
- Add UI support: provider choice, credentials form, badges, wiring
- Add SDK tests for provider, services, and critical checks
This commit is contained in:
Toni de la Fuente
2026-05-08 18:14:18 +02:00
parent 34727a7237
commit 077a4e7dab
100 changed files with 3106 additions and 4 deletions
+1
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler API** are documented in this file.
### 🚀 Added
- Support for the new `lovable` provider type: provider choice, UID workspace ID validator, secret schema (`api_token` plus optional `supabase_access_token`), connection-test wiring, and the `0091_lovable_provider` migration
- New `scan-reset-ephemeral-resources` post-scan task zeroes `failed_findings_count` for resources missing from the latest full-scope scan, keeping ephemeral resources from polluting the Resources page sort [(#10929)](https://github.com/prowler-cloud/prowler/pull/10929)
- ASD Essential Eight (AWS) compliance framework support [(#10982)](https://github.com/prowler-cloud/prowler/pull/10982)
- `scan-reset-ephemeral-resources` post-scan task zeroes `failed_findings_count` for resources missing from the latest full-scope scan, keeping ephemeral resources from polluting the Resources page sort [(#10929)](https://github.com/prowler-cloud/prowler/pull/10929)
@@ -0,0 +1,41 @@
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0090_attack_paths_cleanup_priority"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
("mongodbatlas", "MongoDB Atlas"),
("iac", "IaC"),
("oraclecloud", "Oracle Cloud Infrastructure"),
("alibabacloud", "Alibaba Cloud"),
("cloudflare", "Cloudflare"),
("openstack", "OpenStack"),
("image", "Image"),
("googleworkspace", "Google Workspace"),
("vercel", "Vercel"),
("lovable", "Lovable"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'lovable';",
reverse_sql=migrations.RunSQL.noop,
),
]
+12
View File
@@ -296,6 +296,7 @@ class Provider(RowLevelSecurityProtectedModel):
IMAGE = "image", _("Image")
GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace")
VERCEL = "vercel", _("Vercel")
LOVABLE = "lovable", _("Lovable")
@staticmethod
def validate_aws_uid(value):
@@ -448,6 +449,17 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_lovable_uid(value):
# Lovable workspace IDs are alphanumeric (with optional dashes/underscores),
# commonly between 8 and 64 characters. Accept both raw IDs and slugs.
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]{2,63}$", value):
raise ModelValidationError(
detail="Lovable provider ID must be a valid workspace ID or slug (3-64 alphanumeric characters, dashes, or underscores).",
code="lovable-uid",
pointer="/data/attributes/uid",
)
@staticmethod
def validate_image_uid(value):
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._/:@-]{2,249}$", value):
+20
View File
@@ -40,6 +40,7 @@ if TYPE_CHECKING:
from prowler.providers.openstack.openstack_provider import OpenstackProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
from prowler.providers.vercel.vercel_provider import VercelProvider
from prowler.providers.lovable.lovable_provider import LovableProvider
class CustomOAuth2Client(OAuth2Client):
@@ -96,6 +97,7 @@ def return_prowler_provider(
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
| LovableProvider
):
"""Return the Prowler provider class based on the given provider type.
@@ -181,6 +183,10 @@ def return_prowler_provider(
from prowler.providers.vercel.vercel_provider import VercelProvider
prowler_provider = VercelProvider
case Provider.ProviderChoices.LOVABLE.value:
from prowler.providers.lovable.lovable_provider import LovableProvider
prowler_provider = LovableProvider
case _:
raise ValueError(f"Provider type {provider.provider} not supported")
return prowler_provider
@@ -246,6 +252,11 @@ def get_prowler_provider_kwargs(
**prowler_provider_kwargs,
"team_id": provider.uid,
}
elif provider.provider == Provider.ProviderChoices.LOVABLE.value:
prowler_provider_kwargs = {
**prowler_provider_kwargs,
"workspace_id": provider.uid,
}
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
# Detect whether uid is a registry URL (e.g. "docker.io/andoniaf") or
# a concrete image reference (e.g. "docker.io/andoniaf/myimage:latest").
@@ -293,6 +304,7 @@ def initialize_prowler_provider(
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
| LovableProvider
):
"""Initialize a Prowler provider instance based on the given provider type.
@@ -351,6 +363,14 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
"raise_on_exception": False,
}
return prowler_provider.test_connection(**vercel_kwargs)
elif provider.provider == Provider.ProviderChoices.LOVABLE.value:
lovable_kwargs = {
**prowler_provider_kwargs,
"workspace_id": provider.uid,
"raise_on_exception": False,
"provider_id": provider.uid,
}
return prowler_provider.test_connection(**lovable_kwargs)
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
image_kwargs = {
"image": provider.uid,
@@ -415,6 +415,21 @@ from rest_framework_json_api import serializers
},
"required": ["api_token"],
},
{
"type": "object",
"title": "Lovable API Token",
"properties": {
"api_token": {
"type": "string",
"description": "Lovable Cloud API token used to authenticate against the Lovable Cloud API.",
},
"supabase_access_token": {
"type": "string",
"description": "Optional Supabase Management API token for deeper RLS / auth posture checks on Supabase-backed Lovable apps.",
},
},
"required": ["api_token"],
},
]
}
)
+19
View File
@@ -1575,6 +1575,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = ImageProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.VERCEL.value:
serializer = VercelProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.LOVABLE.value:
serializer = LovableProviderSecret(data=secret)
else:
raise serializers.ValidationError(
{"provider": f"Provider type not supported {provider_type}"}
@@ -1788,6 +1790,23 @@ class VercelProviderSecret(serializers.Serializer):
resource_name = "provider-secrets"
class LovableProviderSecret(serializers.Serializer):
api_token = serializers.CharField(
help_text="Lovable Cloud API token used to authenticate against the Lovable Cloud API.",
)
supabase_access_token = serializers.CharField(
required=False,
allow_blank=True,
help_text=(
"Optional Supabase Management API token used for deeper RLS / "
"auth posture checks on Supabase-backed Lovable apps."
),
)
class Meta:
resource_name = "provider-secrets"
class AlibabaCloudProviderSecret(serializers.Serializer):
access_key_id = serializers.CharField()
access_key_secret = serializers.CharField()