diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 6afe7abb1b..09d2fef7e4 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the **Prowler API** are documented in this file. +## [1.11.0] (Prowler UNRELEASED) + +### Added +- Github provider support [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) + +--- + ## [1.10.2] (Prowler v5.9.2) ### Changed diff --git a/api/src/backend/api/migrations/0043_github_provider.py b/api/src/backend/api/migrations/0043_github_provider.py new file mode 100644 index 0000000000..3607ce4af3 --- /dev/null +++ b/api/src/backend/api/migrations/0043_github_provider.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.7 on 2025-07-09 14:44 + +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0042_scan_scans_prov_ins_desc_idx"), + ] + + 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"), + ], + default="aws", + ), + ), + migrations.RunSQL( + "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'github';", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 632e3b5dd3..8e4614675d 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -205,6 +205,7 @@ class Provider(RowLevelSecurityProtectedModel): GCP = "gcp", _("GCP") KUBERNETES = "kubernetes", _("Kubernetes") M365 = "m365", _("M365") + GITHUB = "github", _("GitHub") @staticmethod def validate_aws_uid(value): @@ -265,6 +266,16 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_github_uid(value): + if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9-]{0,38}$", value): + raise ModelValidationError( + detail="GitHub provider ID must be a valid GitHub username or organization name (1-39 characters, " + "starting with alphanumeric, containing only alphanumeric characters and hyphens).", + code="github-uid", + pointer="/data/attributes/uid", + ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 3ea3610e29..f13cd95325 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.10.2 + version: 1.11.0 description: |- Prowler API specification. @@ -544,6 +544,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -552,6 +553,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -562,6 +564,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -572,6 +575,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -1061,6 +1065,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1069,6 +1074,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -1079,6 +1085,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1089,6 +1096,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -1486,6 +1494,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1494,6 +1503,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -1504,6 +1514,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1514,6 +1525,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -1909,6 +1921,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1917,6 +1930,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -1927,6 +1941,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -1937,6 +1952,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -2320,6 +2336,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -2328,6 +2345,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -2338,6 +2356,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -2348,6 +2367,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -3121,6 +3141,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3129,6 +3150,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -3139,6 +3161,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3149,6 +3172,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -3282,6 +3306,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3290,6 +3315,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -3300,6 +3326,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3310,6 +3337,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -3459,6 +3487,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3467,6 +3496,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -3477,6 +3507,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -3487,6 +3518,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -4165,6 +4197,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -4173,6 +4206,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider__in] schema: @@ -4746,6 +4780,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -4754,6 +4789,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -4764,6 +4800,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -4774,6 +4811,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -6457,6 +6495,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -6465,6 +6504,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub - in: query name: filter[provider_type__in] schema: @@ -6475,6 +6515,7 @@ paths: - aws - azure - gcp + - github - kubernetes - m365 description: |- @@ -6485,6 +6526,7 @@ paths: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub explode: false style: form - in: query @@ -11130,6 +11172,34 @@ components: encoded as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret @@ -12035,6 +12105,7 @@ components: - gcp - kubernetes - m365 + - github type: string description: |- * `aws` - AWS @@ -12042,6 +12113,7 @@ components: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub uid: type: string title: Unique identifier for the provider, set by the provider @@ -12149,6 +12221,7 @@ components: - gcp - kubernetes - m365 + - github type: string description: |- * `aws` - AWS @@ -12156,6 +12229,7 @@ components: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub uid: type: string title: Unique identifier for the provider, set by the provider @@ -12194,6 +12268,7 @@ components: - gcp - kubernetes - m365 + - github type: string description: |- * `aws` - AWS @@ -12201,6 +12276,7 @@ components: * `gcp` - GCP * `kubernetes` - Kubernetes * `m365` - M365 + * `github` - GitHub uid: type: string minLength: 3 @@ -12852,6 +12928,34 @@ components: as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret_type @@ -13071,6 +13175,34 @@ components: encoded as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret_type @@ -13305,6 +13437,34 @@ components: as a string. required: - kubeconfig_content + - type: object + title: GitHub Personal Access Token + properties: + personal_access_token: + type: string + description: GitHub personal access token for authentication. + required: + - personal_access_token + - type: object + title: GitHub OAuth App Token + properties: + oauth_app_token: + type: string + description: GitHub OAuth App token for authentication. + required: + - oauth_app_token + - type: object + title: GitHub App Credentials + properties: + github_app_id: + type: integer + description: GitHub App ID for authentication. + github_app_key: + type: string + description: Path to the GitHub App private key file. + required: + - github_app_id + - github_app_key writeOnly: true required: - secret diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 3071127228..c651e8fb81 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -966,6 +966,31 @@ class TestProviderViewSet: "uid": "subdomain1.subdomain2.subdomain3.subdomain4.domain.net", "alias": "test", }, + { + "provider": "github", + "uid": "test-user", + "alias": "test", + }, + { + "provider": "github", + "uid": "test-organization", + "alias": "GitHub Org", + }, + { + "provider": "github", + "uid": "prowler-cloud", + "alias": "Prowler", + }, + { + "provider": "github", + "uid": "microsoft", + "alias": "Microsoft", + }, + { + "provider": "github", + "uid": "a12345678901234567890123456789012345678", + "alias": "Long Username", + }, ] ), ) @@ -1079,6 +1104,42 @@ class TestProviderViewSet: "m365-uid", "uid", ), + ( + { + "provider": "github", + "uid": "-invalid-start", + "alias": "test", + }, + "github-uid", + "uid", + ), + ( + { + "provider": "github", + "uid": "invalid@username", + "alias": "test", + }, + "github-uid", + "uid", + ), + ( + { + "provider": "github", + "uid": "invalid_username", + "alias": "test", + }, + "github-uid", + "uid", + ), + ( + { + "provider": "github", + "uid": "a" * 40, + "alias": "test", + }, + "github-uid", + "uid", + ), ] ), ) diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 5a601ef7c9..0fe81a7ace 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -13,6 +13,7 @@ from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.common.models import Connection from prowler.providers.gcp.gcp_provider import GcpProvider +from prowler.providers.github.github_provider import GithubProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.m365.m365_provider import M365Provider @@ -55,14 +56,21 @@ def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict: def return_prowler_provider( provider: Provider, -) -> [AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider]: +) -> [ + AwsProvider + | AzureProvider + | GcpProvider + | GithubProvider + | KubernetesProvider + | M365Provider +]: """Return the Prowler provider class based on the given provider type. Args: provider (Provider): The provider object containing the provider type and associated secrets. Returns: - AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: The corresponding provider class. + AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: The corresponding provider class. Raises: ValueError: If the provider type specified in `provider.provider` is not supported. @@ -78,6 +86,8 @@ def return_prowler_provider( prowler_provider = KubernetesProvider case Provider.ProviderChoices.M365.value: prowler_provider = M365Provider + case Provider.ProviderChoices.GITHUB.value: + prowler_provider = GithubProvider case _: raise ValueError(f"Provider type {provider.provider} not supported") return prowler_provider @@ -120,7 +130,14 @@ def get_prowler_provider_kwargs( def initialize_prowler_provider( provider: Provider, mutelist_processor: Processor | None = None, -) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: +) -> ( + AwsProvider + | AzureProvider + | GcpProvider + | GithubProvider + | KubernetesProvider + | M365Provider +): """Initialize a Prowler provider instance based on the given provider type. Args: @@ -128,8 +145,8 @@ def initialize_prowler_provider( mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration. Returns: - AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class - (`AwsProvider`, `AzureProvider`, `GcpProvider`, `KubernetesProvider` or `M365Provider`) initialized with the + AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class + (`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `KubernetesProvider` or `M365Provider`) initialized with the provider's secrets. """ prowler_provider = return_prowler_provider(provider) diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 0a1e999fe2..76fa0b4911 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -176,6 +176,43 @@ from rest_framework_json_api import serializers }, "required": ["kubeconfig_content"], }, + { + "type": "object", + "title": "GitHub Personal Access Token", + "properties": { + "personal_access_token": { + "type": "string", + "description": "GitHub personal access token for authentication.", + } + }, + "required": ["personal_access_token"], + }, + { + "type": "object", + "title": "GitHub OAuth App Token", + "properties": { + "oauth_app_token": { + "type": "string", + "description": "GitHub OAuth App token for authentication.", + } + }, + "required": ["oauth_app_token"], + }, + { + "type": "object", + "title": "GitHub App Credentials", + "properties": { + "github_app_id": { + "type": "integer", + "description": "GitHub App ID for authentication.", + }, + "github_app_key": { + "type": "string", + "description": "Path to the GitHub App private key file.", + }, + }, + "required": ["github_app_id", "github_app_key"], + }, ] } ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index d19a68f61d..6061cf99fe 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1217,6 +1217,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): serializer = AzureProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.GCP.value: serializer = GCPProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.GITHUB.value: + serializer = GithubProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.KUBERNETES.value: serializer = KubernetesProviderSecret(data=secret) elif provider_type == Provider.ProviderChoices.M365.value: @@ -1296,6 +1298,16 @@ class KubernetesProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class GithubProviderSecret(serializers.Serializer): + personal_access_token = serializers.CharField(required=False) + oauth_app_token = serializers.CharField(required=False) + github_app_id = serializers.IntegerField(required=False) + github_app_key_content = serializers.CharField(required=False) + + class Meta: + resource_name = "provider-secrets" + + class AWSRoleAssumptionProviderSecret(serializers.Serializer): role_arn = serializers.CharField() external_id = serializers.CharField() diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index f4b389d8f0..78026a5875 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -20,6 +20,7 @@ from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected im from prowler.lib.outputs.compliance.cis.cis_aws import AWSCIS from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS +from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS @@ -93,6 +94,9 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "prowler_threatscore_m365", ProwlerThreatScoreM365), (lambda name: name.startswith("iso27001_"), M365ISO27001), ], + "github": [ + (lambda name: name.startswith("cis_"), GithubCIS), + ], } diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 8ed58cec41..1883fe67f9 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Added - Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) +- Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) --- diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 9cabffec86..74baa752ec 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -353,6 +353,8 @@ class Finding(BaseModel): finding.region = resource.region # Azure, GCP specified field finding.location = resource.region + # GitHub specified field + finding.owner = resource.region # K8s specified field if provider.type == "kubernetes": finding.namespace = resource.region.removeprefix("namespace: ") diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py index df6c8ee046..30d3ed865a 100644 --- a/prowler/providers/github/github_provider.py +++ b/prowler/providers/github/github_provider.py @@ -99,6 +99,7 @@ class GithubProvider(Provider): personal_access_token: str = "", oauth_app_token: str = "", github_app_key: str = "", + github_app_key_content: str = "", github_app_id: int = 0, # Provider configuration config_path: str = None, @@ -114,6 +115,7 @@ class GithubProvider(Provider): personal_access_token (str): GitHub personal access token. oauth_app_token (str): GitHub OAuth App token. github_app_key (str): GitHub App key. + github_app_key_content (str): GitHub App key content. github_app_id (int): GitHub App ID. config_path (str): Path to the audit configuration file. config_content (dict): Audit configuration content. @@ -128,6 +130,7 @@ class GithubProvider(Provider): oauth_app_token, github_app_id, github_app_key, + github_app_key_content, ) # Set the authentication method