feat(api): add okta provider support (#11184)

This commit is contained in:
Daniel Barranquero
2026-05-20 10:46:29 +02:00
committed by GitHub
parent 9d8b69abda
commit 6eebfcfe77
15 changed files with 536 additions and 5 deletions
+8
View File
@@ -2,6 +2,14 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.29.0] (Prowler UNRELEASED)
### 🚀 Added
- `okta` provider support [(#11184)](https://github.com/prowler-cloud/prowler/pull/11184)
---
## [1.28.0] (Prowler v5.27.0)
### 🚀 Added
@@ -0,0 +1,41 @@
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0092_findings_arrays_gin_index_parent"),
]
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"),
("okta", "Okta"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'okta';",
reverse_sql=migrations.RunSQL.noop,
),
]
+27
View File
@@ -296,6 +296,7 @@ class Provider(RowLevelSecurityProtectedModel):
IMAGE = "image", _("Image")
GOOGLEWORKSPACE = "googleworkspace", _("Google Workspace")
VERCEL = "vercel", _("Vercel")
OKTA = "okta", _("Okta")
@staticmethod
def validate_aws_uid(value):
@@ -354,6 +355,26 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_okta_uid(value):
if not re.match(
r"^[a-z0-9][a-z0-9-]*\.("
r"okta\.com|oktapreview\.com|okta-emea\.com|"
r"okta-gov\.com|okta\.mil|okta-miltest\.com|trex-govcloud\.com"
r")$",
value,
):
raise ModelValidationError(
detail=(
"Okta provider ID must be a valid Okta-managed org domain "
"(e.g., acme.okta.com, also .oktapreview.com / .okta-emea.com "
"/ .okta-gov.com / .okta.mil / .okta-miltest.com / "
".trex-govcloud.com), without scheme or path."
),
code="okta-uid",
pointer="/data/attributes/uid",
)
@staticmethod
def validate_kubernetes_uid(value):
if not re.match(
@@ -480,6 +501,12 @@ class Provider(RowLevelSecurityProtectedModel):
def clean(self):
super().clean()
if self.provider == self.ProviderChoices.OKTA and self.uid:
# Mirror the SDK, which lowercases the org domain before connecting.
# Without this the API would reject Acme.okta.com even though the
# SDK would accept it, and stored uids could disagree with the
# authenticated org domain.
self.uid = self.uid.strip().lower()
getattr(self, f"validate_{self.provider}_uid")(self.uid)
def save(self, *args, **kwargs):
+174
View File
@@ -373,6 +373,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -389,6 +390,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -412,6 +414,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -430,6 +433,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -1453,6 +1457,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -1469,6 +1474,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -1491,6 +1497,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -1509,6 +1516,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -1997,6 +2005,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -2013,6 +2022,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -2035,6 +2045,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -2053,6 +2064,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -2584,6 +2596,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -2600,6 +2613,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -2622,6 +2636,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -2640,6 +2655,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -3134,6 +3150,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -3150,6 +3167,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -3173,6 +3191,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -3191,6 +3210,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -3740,6 +3760,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -3756,6 +3777,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -3779,6 +3801,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -3797,6 +3820,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -4254,6 +4278,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -4270,6 +4295,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -4293,6 +4319,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -4311,6 +4338,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -4766,6 +4794,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -4782,6 +4811,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -4805,6 +4835,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -4823,6 +4854,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -5266,6 +5298,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -5282,6 +5315,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -5305,6 +5339,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -5323,6 +5358,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -7156,6 +7192,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7172,6 +7209,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -7195,6 +7233,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -7213,6 +7252,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- name: filter[search]
@@ -7335,6 +7375,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7351,6 +7392,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -7374,6 +7416,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -7392,6 +7435,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- name: filter[search]
@@ -7503,6 +7547,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7519,6 +7564,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -7541,6 +7587,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -7559,6 +7606,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- name: filter[search]
@@ -7702,6 +7750,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7718,6 +7767,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -7741,6 +7791,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -7759,6 +7810,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -7915,6 +7967,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -7931,6 +7984,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -7954,6 +8008,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -7972,6 +8027,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -8122,6 +8178,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -8138,6 +8195,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -8160,6 +8218,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -8178,6 +8237,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- name: filter[search]
@@ -8370,6 +8430,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -8386,6 +8447,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -8409,6 +8471,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -8427,6 +8490,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -8548,6 +8612,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -8564,6 +8629,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -8587,6 +8653,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -8605,6 +8672,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -8750,6 +8818,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -8766,6 +8835,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -8789,6 +8859,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -8807,6 +8878,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -9593,6 +9665,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -9609,6 +9682,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider__in]
schema:
@@ -9632,6 +9706,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -9650,6 +9725,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -9673,6 +9749,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -9689,6 +9766,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -9712,6 +9790,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -9730,6 +9809,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- name: filter[search]
@@ -10400,6 +10480,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -10416,6 +10497,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -10439,6 +10521,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -10457,6 +10540,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -10951,6 +11035,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -10967,6 +11052,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -10990,6 +11076,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -11008,6 +11095,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -11315,6 +11403,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -11331,6 +11420,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -11354,6 +11444,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -11372,6 +11463,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -11685,6 +11777,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -11701,6 +11794,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -11724,6 +11818,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -11742,6 +11837,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -12580,6 +12676,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
* `aws` - AWS
* `azure` - Azure
@@ -12596,6 +12693,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
- in: query
name: filter[provider_type__in]
schema:
@@ -12619,6 +12717,7 @@ paths:
- openstack
- oraclecloud
- vercel
- okta
description: |-
Multiple values may be separated by commas.
@@ -12637,6 +12736,7 @@ paths:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
explode: false
style: form
- in: query
@@ -20115,6 +20215,23 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Okta OAuth Credentials
properties:
okta_client_id:
type: string
description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication.
okta_private_key:
type: string
description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app.
okta_scopes:
type: array
items:
type: string
description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks.
required:
- okta_client_id
- okta_private_key
- type: object
title: Vercel API Token
properties:
@@ -21127,6 +21244,7 @@ components:
- image
- googleworkspace
- vercel
- okta
type: string
description: |-
* `aws` - AWS
@@ -21144,6 +21262,7 @@ components:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
x-spec-enum-id: 91f917e0c3ab97e8
uid:
type: string
@@ -21265,6 +21384,7 @@ components:
- image
- googleworkspace
- vercel
- okta
type: string
x-spec-enum-id: 91f917e0c3ab97e8
description: |-
@@ -21285,6 +21405,7 @@ components:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
uid:
type: string
title: Unique identifier for the provider, set by the provider
@@ -21337,6 +21458,7 @@ components:
- image
- googleworkspace
- vercel
- okta
type: string
x-spec-enum-id: 91f917e0c3ab97e8
description: |-
@@ -21357,6 +21479,7 @@ components:
* `image` - Image
* `googleworkspace` - Google Workspace
* `vercel` - Vercel
* `okta` - Okta
uid:
type: string
minLength: 3
@@ -22206,6 +22329,23 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Okta OAuth Credentials
properties:
okta_client_id:
type: string
description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication.
okta_private_key:
type: string
description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app.
okta_scopes:
type: array
items:
type: string
description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks.
required:
- okta_client_id
- okta_private_key
- type: object
title: Vercel API Token
properties:
@@ -22631,6 +22771,23 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Okta OAuth Credentials
properties:
okta_client_id:
type: string
description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication.
okta_private_key:
type: string
description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app.
okta_scopes:
type: array
items:
type: string
description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks.
required:
- okta_client_id
- okta_private_key
- type: object
title: Vercel API Token
properties:
@@ -23066,6 +23223,23 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: Okta OAuth Credentials
properties:
okta_client_id:
type: string
description: Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication.
okta_private_key:
type: string
description: PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app.
okta_scopes:
type: array
items:
type: string
description: OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks.
required:
- okta_client_id
- okta_private_key
- type: object
title: Vercel API Token
properties:
+31
View File
@@ -31,6 +31,7 @@ from prowler.providers.image.image_provider import ImageProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.m365.m365_provider import M365Provider
from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider
from prowler.providers.okta.okta_provider import OktaProvider
from prowler.providers.openstack.openstack_provider import OpenstackProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
from prowler.providers.vercel.vercel_provider import VercelProvider
@@ -130,6 +131,7 @@ class TestReturnProwlerProvider:
(Provider.ProviderChoices.OPENSTACK.value, OpenstackProvider),
(Provider.ProviderChoices.IMAGE.value, ImageProvider),
(Provider.ProviderChoices.VERCEL.value, VercelProvider),
(Provider.ProviderChoices.OKTA.value, OktaProvider),
],
)
def test_return_prowler_provider(self, provider_type, expected_provider):
@@ -238,6 +240,31 @@ class TestProwlerProviderConnectionTest:
raise_on_exception=False,
)
@patch("api.utils.return_prowler_provider")
def test_prowler_provider_connection_test_okta_provider(
self, mock_return_prowler_provider
):
"""Test connection test for Okta provider passes org domain and provider_id."""
provider = MagicMock()
provider.uid = "acme.okta.com"
provider.provider = Provider.ProviderChoices.OKTA.value
provider.secret.secret = {
"okta_client_id": "0oa123456789abcdef",
"okta_private_key": "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----",
"okta_scopes": ["okta.policies.read"],
}
mock_return_prowler_provider.return_value = MagicMock()
prowler_provider_connection_test(provider)
mock_return_prowler_provider.return_value.test_connection.assert_called_once_with(
okta_client_id="0oa123456789abcdef",
okta_private_key="-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----",
okta_scopes=["okta.policies.read"],
okta_org_domain="acme.okta.com",
provider_id="acme.okta.com",
raise_on_exception=False,
)
@patch("api.utils.return_prowler_provider")
def test_prowler_provider_connection_test_image_provider_no_creds(
self, mock_return_prowler_provider
@@ -308,6 +335,10 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.VERCEL.value,
{"team_id": "provider_uid"},
),
(
Provider.ProviderChoices.OKTA.value,
{"okta_org_domain": "provider_uid"},
),
],
)
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
+108 -3
View File
@@ -1625,6 +1625,21 @@ class TestProviderViewSet:
"uid": "C12",
"alias": "Google Workspace Minimum Length",
},
{
"provider": "okta",
"uid": "acme.okta.com",
"alias": "Okta Org",
},
{
"provider": "okta",
"uid": "agency.okta-gov.com",
"alias": "Okta Gov Org",
},
{
"provider": "okta",
"uid": "agency.okta.mil",
"alias": "Okta Mil Org",
},
]
),
)
@@ -2143,6 +2158,24 @@ class TestProviderViewSet:
"googleworkspace-uid",
"uid",
),
(
{
"provider": "okta",
"uid": "https://acme.okta.com",
"alias": "test",
},
"okta-uid",
"uid",
),
(
{
"provider": "okta",
"uid": "acme.example.com",
"alias": "test",
},
"okta-uid",
"uid",
),
]
),
)
@@ -2163,6 +2196,25 @@ class TestProviderViewSet:
== f"/data/attributes/{error_pointer}"
)
@pytest.mark.parametrize(
"input_uid,stored_uid",
[
("Acme.okta.com", "acme.okta.com"),
(" ACME.OKTA.COM ", "acme.okta.com"),
("Agency.Okta-Gov.com", "agency.okta-gov.com"),
],
)
def test_providers_create_okta_uid_normalized(
self, authenticated_client, input_uid, stored_uid
):
response = authenticated_client.post(
reverse("provider-list"),
data={"provider": "okta", "uid": input_uid, "alias": "Okta"},
format="json",
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.get().uid == stored_uid
def test_providers_partial_update(self, authenticated_client, providers_fixture):
provider1, *_ = providers_fixture
new_alias = "This is the new name"
@@ -2320,17 +2372,17 @@ class TestProviderViewSet:
),
("alias", "aws_testing_1", 1),
("alias.icontains", "aws", 2),
("inserted_at", TODAY, 13),
("inserted_at", TODAY, 14),
(
"inserted_at.gte",
"2024-01-01",
13,
14,
),
("inserted_at.lte", "2024-01-01", 0),
(
"updated_at.gte",
"2024-01-01",
13,
14,
),
("updated_at.lte", "2024-01-01", 0),
]
@@ -2963,6 +3015,19 @@ class TestProviderSecretViewSet:
"api_token": "fake-vercel-api-token-for-testing",
},
),
# Okta with inline private key credentials
(
Provider.ProviderChoices.OKTA.value,
ProviderSecret.TypeChoices.STATIC,
{
"okta_client_id": "0oa123456789abcdef",
"okta_private_key": "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----",
"okta_scopes": [
"okta.policies.read",
"okta.groups.read",
],
},
),
],
)
def test_provider_secrets_create_valid(
@@ -3075,6 +3140,46 @@ class TestProviderSecretViewSet:
== f"/data/attributes/{error_pointer}"
)
def test_provider_secrets_invalid_create_okta_missing_private_key(
self,
providers_fixture,
authenticated_client,
):
okta_provider = next(
provider
for provider in providers_fixture
if provider.provider == Provider.ProviderChoices.OKTA.value
)
data = {
"data": {
"type": "provider-secrets",
"attributes": {
"name": "Okta Secret",
"secret_type": ProviderSecret.TypeChoices.STATIC,
"secret": {
"okta_client_id": "0oa123456789abcdef",
},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": str(okta_provider.id)}
}
},
}
}
response = authenticated_client.post(
reverse("providersecret-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == "required"
assert response.json()["errors"][0]["source"]["pointer"] == (
"/data/attributes/secret/okta_private_key"
)
def test_provider_secrets_partial_update(
self, authenticated_client, provider_secret_fixture
):
+20
View File
@@ -37,6 +37,7 @@ if TYPE_CHECKING:
from prowler.providers.mongodbatlas.mongodbatlas_provider import (
MongodbatlasProvider,
)
from prowler.providers.okta.okta_provider import OktaProvider
from prowler.providers.openstack.openstack_provider import OpenstackProvider
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
from prowler.providers.vercel.vercel_provider import VercelProvider
@@ -93,6 +94,7 @@ def return_prowler_provider(
| KubernetesProvider
| M365Provider
| MongodbatlasProvider
| OktaProvider
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
@@ -181,6 +183,10 @@ def return_prowler_provider(
from prowler.providers.vercel.vercel_provider import VercelProvider
prowler_provider = VercelProvider
case Provider.ProviderChoices.OKTA.value:
from prowler.providers.okta.okta_provider import OktaProvider
prowler_provider = OktaProvider
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.OKTA.value:
prowler_provider_kwargs = {
**prowler_provider_kwargs,
"okta_org_domain": 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").
@@ -290,6 +301,7 @@ def initialize_prowler_provider(
| KubernetesProvider
| M365Provider
| MongodbatlasProvider
| OktaProvider
| OpenstackProvider
| OraclecloudProvider
| VercelProvider
@@ -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.OKTA.value:
okta_kwargs = {
**prowler_provider_kwargs,
"okta_org_domain": provider.uid,
"provider_id": provider.uid,
"raise_on_exception": False,
}
return prowler_provider.test_connection(**okta_kwargs)
elif provider.provider == Provider.ProviderChoices.IMAGE.value:
image_kwargs = {
"image": provider.uid,
@@ -404,6 +404,26 @@ from rest_framework_json_api import serializers
},
"required": ["clouds_yaml_content", "clouds_yaml_cloud"],
},
{
"type": "object",
"title": "Okta OAuth Credentials",
"properties": {
"okta_client_id": {
"type": "string",
"description": "Client ID of the Okta API Services app used for OAuth 2.0 private-key JWT authentication.",
},
"okta_private_key": {
"type": "string",
"description": "PEM-encoded private key whose matching public key (JWK) is registered on the Okta service app.",
},
"okta_scopes": {
"type": "array",
"items": {"type": "string"},
"description": "OAuth scopes to request. Optional; defaults to the minimum set required to run the currently enabled Okta checks.",
},
},
"required": ["okta_client_id", "okta_private_key"],
},
{
"type": "object",
"title": "Vercel API Token",
+11
View File
@@ -1543,6 +1543,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
serializer = GCPProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.GOOGLEWORKSPACE.value:
serializer = GoogleWorkspaceProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.OKTA.value:
serializer = OktaProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.GITHUB.value:
serializer = GithubProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.IAC.value:
@@ -1688,6 +1690,15 @@ class GoogleWorkspaceProviderSecret(serializers.Serializer):
resource_name = "provider-secrets"
class OktaProviderSecret(serializers.Serializer):
okta_client_id = serializers.CharField()
okta_private_key = serializers.CharField()
okta_scopes = serializers.ListField(child=serializers.CharField(), required=False)
class Meta:
resource_name = "provider-secrets"
class MongoDBAtlasProviderSecret(serializers.Serializer):
atlas_public_key = serializers.CharField()
atlas_private_key = serializers.CharField()
+7
View File
@@ -571,6 +571,12 @@ def providers_fixture(tenants_fixture):
alias="vercel_testing",
tenant_id=tenant.id,
)
provider14 = Provider.objects.create(
provider="okta",
uid="acme.okta.com",
alias="okta_testing",
tenant_id=tenant.id,
)
return (
provider1,
@@ -586,6 +592,7 @@ def providers_fixture(tenants_fixture):
provider11,
provider12,
provider13,
provider14,
)
+4
View File
@@ -9,6 +9,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `entra_app_registration_client_secret_unused` check for M365 provider [(#11232)](https://github.com/prowler-cloud/prowler/pull/11232)
- `cloudsql_instance_cmek_encryption_enabled` check for GCP provider [(#11023)](https://github.com/prowler-cloud/prowler/pull/11023)
### 🔄 Changed
- `OktaProvider.test_connection` accepts an optional `provider_id` (org domain) and raises `OktaInvalidProviderIdError` (14007) when it doesn't match the authenticated org — guards against stored UID drifting from the credentials' org [(#11184)](https://github.com/prowler-cloud/prowler/pull/11184)
---
## [5.27.1] (Prowler UNRELEASED)
@@ -34,6 +34,10 @@ class OktaBaseException(ProwlerException):
"message": "Okta service app is missing required scopes",
"remediation": "Have a Super Admin grant the required *.read scopes to the service app and assign the Read-Only Administrator role.",
},
(14007, "OktaInvalidProviderIdError"): {
"message": "The provided provider_id does not match the credentials org domain",
"remediation": "Check the provider_id (Okta org domain) and ensure it matches the org the service app credentials were issued for.",
},
}
def __init__(self, code, file=None, original_exception=None, message=None):
@@ -110,3 +114,10 @@ class OktaInsufficientPermissionsError(OktaCredentialsError):
super().__init__(
14006, file=file, original_exception=original_exception, message=message
)
class OktaInvalidProviderIdError(OktaCredentialsError):
def __init__(self, file=None, original_exception=None, message=None):
super().__init__(
14007, file=file, original_exception=original_exception, message=message
)
+22 -2
View File
@@ -22,6 +22,7 @@ from prowler.providers.okta.exceptions.exceptions import (
OktaInsufficientPermissionsError,
OktaInvalidCredentialsError,
OktaInvalidOrgDomainError,
OktaInvalidProviderIdError,
OktaPrivateKeyFileError,
OktaSetUpIdentityError,
OktaSetUpSessionError,
@@ -348,8 +349,17 @@ class OktaProvider(Provider):
okta_private_key_file: str = "",
okta_scopes: Optional[Union[str, list[str]]] = None,
raise_on_exception: bool = True,
provider_id: str = None,
) -> Connection:
"""Test the connection to Okta with the provided OAuth credentials."""
"""Test the connection to Okta with the provided OAuth credentials.
Args:
provider_id: The provider ID (Okta org domain). When supplied, the
authenticated org domain must match it — guards against the
stored provider UID drifting from the org the credentials were
actually issued for. Compared case-insensitively, matching the
normalization applied during session setup.
"""
try:
OktaProvider.validate_arguments(
okta_org_domain=okta_org_domain,
@@ -364,7 +374,17 @@ class OktaProvider(Provider):
private_key_file=okta_private_key_file,
scopes=okta_scopes,
)
OktaProvider.setup_identity(session)
identity = OktaProvider.setup_identity(session)
if provider_id and provider_id.strip().lower() != identity.org_domain:
raise OktaInvalidProviderIdError(
file=os.path.basename(__file__),
message=(
f"The provider ID '{provider_id}' does not match the "
f"authenticated Okta org domain '{identity.org_domain}'."
),
)
return Connection(is_connected=True)
except Exception as error:
logger.critical(
@@ -7,6 +7,7 @@ from prowler.providers.okta.exceptions.exceptions import (
OktaInsufficientPermissionsError,
OktaInvalidCredentialsError,
OktaInvalidOrgDomainError,
OktaInvalidProviderIdError,
OktaPrivateKeyFileError,
OktaSetUpIdentityError,
OktaSetUpSessionError,
@@ -20,6 +21,7 @@ EXPECTED_CODES = {
OktaInvalidOrgDomainError: 14004,
OktaPrivateKeyFileError: 14005,
OktaInsufficientPermissionsError: 14006,
OktaInvalidProviderIdError: 14007,
}
@@ -7,6 +7,7 @@ from prowler.providers.okta.exceptions.exceptions import (
OktaInsufficientPermissionsError,
OktaInvalidCredentialsError,
OktaInvalidOrgDomainError,
OktaInvalidProviderIdError,
OktaPrivateKeyFileError,
OktaSetUpIdentityError,
)
@@ -396,6 +397,55 @@ class Test_OktaProvider_test_connection:
with pytest.raises(OktaEnvironmentVariableError):
OktaProvider.test_connection()
def test_provider_id_match_succeeds(self, _clear_okta_env, tmp_path):
validate_p, session_p, identity_p = _mock_setup_paths()
with validate_p, session_p, identity_p:
connection = OktaProvider.test_connection(
okta_org_domain=OKTA_ORG_DOMAIN,
okta_client_id=OKTA_CLIENT_ID,
okta_private_key_file="/tmp/key.pem",
provider_id=OKTA_ORG_DOMAIN,
)
assert connection.is_connected is True
assert connection.error is None
def test_provider_id_match_is_case_insensitive(self, _clear_okta_env, tmp_path):
validate_p, session_p, identity_p = _mock_setup_paths()
with validate_p, session_p, identity_p:
connection = OktaProvider.test_connection(
okta_org_domain=OKTA_ORG_DOMAIN,
okta_client_id=OKTA_CLIENT_ID,
okta_private_key_file="/tmp/key.pem",
provider_id=OKTA_ORG_DOMAIN.upper(),
)
assert connection.is_connected is True
def test_provider_id_mismatch_raises(self, _clear_okta_env, tmp_path):
validate_p, session_p, identity_p = _mock_setup_paths()
with validate_p, session_p, identity_p:
with pytest.raises(OktaInvalidProviderIdError):
OktaProvider.test_connection(
okta_org_domain=OKTA_ORG_DOMAIN,
okta_client_id=OKTA_CLIENT_ID,
okta_private_key_file="/tmp/key.pem",
provider_id="other.okta.com",
)
def test_provider_id_mismatch_returns_error_when_raise_disabled(
self, _clear_okta_env, tmp_path
):
validate_p, session_p, identity_p = _mock_setup_paths()
with validate_p, session_p, identity_p:
connection = OktaProvider.test_connection(
okta_org_domain=OKTA_ORG_DOMAIN,
okta_client_id=OKTA_CLIENT_ID,
okta_private_key_file="/tmp/key.pem",
provider_id="other.okta.com",
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, OktaInvalidProviderIdError)
class Test_OktaProvider_print_credentials:
def test_invokes_print_boxes_with_org_and_client(self, _clear_okta_env, tmp_path):