Merge branch 'master' of https://github.com/prowler-cloud/prowler into review-metadata-aws-neptune
No files matched your search
@@ -127,6 +127,7 @@ jQIDAQAB
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
DJANGO_SENTRY_DSN=
|
||||
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
|
||||
|
||||
# Sentry settings
|
||||
SENTRY_ENVIRONMENT=local
|
||||
|
||||
@@ -37,6 +37,11 @@ provider/iac:
|
||||
- any-glob-to-any-file: "prowler/providers/iac/**"
|
||||
- any-glob-to-any-file: "tests/providers/iac/**"
|
||||
|
||||
provider/mongodbatlas:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/mongodbatlas/**"
|
||||
- any-glob-to-any-file: "tests/providers/mongodbatlas/**"
|
||||
|
||||
github_actions:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ".github/workflows/*"
|
||||
@@ -52,11 +57,13 @@ mutelist:
|
||||
- any-glob-to-any-file: "prowler/providers/azure/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/gcp/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/kubernetes/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/mongodbatlas/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/aws/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/azure/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/gcp/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/kubernetes/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/mongodbatlas/lib/mutelist/**"
|
||||
|
||||
integration/s3:
|
||||
- changed-files:
|
||||
@@ -112,3 +119,7 @@ compliance:
|
||||
review-django-migrations:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "api/src/backend/api/migrations/**"
|
||||
|
||||
metadata-review:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "**/*.metadata.json"
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
env:
|
||||
MONITORED_FOLDERS: "api ui prowler"
|
||||
MONITORED_FOLDERS: "api ui prowler dashboard"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
@@ -234,6 +234,21 @@ jobs:
|
||||
run: |
|
||||
poetry run pytest -n auto --cov=./prowler/providers/iac --cov-report=xml:iac_coverage.xml tests/providers/iac
|
||||
|
||||
# Test MongoDB Atlas
|
||||
- name: MongoDB Atlas - Check if any file has changed
|
||||
id: mongodb-atlas-changed-files
|
||||
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
with:
|
||||
files: |
|
||||
./prowler/providers/mongodbatlas/**
|
||||
./tests/providers/mongodbatlas/**
|
||||
.poetry.lock
|
||||
|
||||
- name: MongoDB Atlas - Test
|
||||
if: steps.mongodb-atlas-changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pytest -n auto --cov=./prowler/providers/mongodbatlas --cov-report=xml:mongodb_atlas_coverage.xml tests/providers/mongodbatlas
|
||||
|
||||
# Common Tests
|
||||
- name: Lib - Test
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
|
||||
@@ -80,15 +80,17 @@ prowler dashboard
|
||||
> For the most accurate and up-to-date information about checks, services, frameworks, and categories, visit [**Prowler Hub**](https://hub.prowler.com).
|
||||
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 576 | 82 | 36 | 10 |
|
||||
| GCP | 79 | 13 | 10 | 3 |
|
||||
| Azure | 162 | 19 | 11 | 4 |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 |
|
||||
| GitHub | 17 | 2 | 1 | 0 |
|
||||
| M365 | 70 | 7 | 3 | 2 |
|
||||
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Stage | Interface |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| AWS | 576 | 82 | 36 | 10 | Official | Stable | UI, API, CLI |
|
||||
| GCP | 79 | 13 | 10 | 3 | Official | Stable | UI, API, CLI |
|
||||
| Azure | 162 | 19 | 11 | 4 | Official | Stable | UI, API, CLI |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 | Official | Stable | UI, API, CLI |
|
||||
| GitHub | 17 | 2 | 1 | 0 | Official | Stable | UI, API, CLI |
|
||||
| M365 | 70 | 7 | 3 | 2 | Official | Stable | UI, API, CLI |
|
||||
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | Beta | CLI |
|
||||
| MongoDB Atlas | 10 | 3 | 0 | 0 | Official | Beta | CLI |
|
||||
| NHN | 6 | 2 | 1 | 0 | Unofficial | Beta | CLI |
|
||||
|
||||
> [!Note]
|
||||
> The numbers in the table are updated periodically.
|
||||
|
||||
@@ -19,6 +19,8 @@ DJANGO_REFRESH_TOKEN_LIFETIME=1440
|
||||
DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY=""
|
||||
# Throttle, two options: Empty means no throttle; or if desired use one in DRF format: https://www.django-rest-framework.org/api-guide/throttling/#setting-the-throttling-policy
|
||||
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
|
||||
# Decide whether to allow Django manage database table partitions
|
||||
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
|
||||
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.13.0] (Prowler UNRELEASED)
|
||||
## [1.13.0] (Prowler 5.12.0)
|
||||
|
||||
### Added
|
||||
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622)
|
||||
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622), [(#8637)](https://github.com/prowler-cloud/prowler/pull/8637)
|
||||
- `GET /overviews/findings_severity` now supports `filter[status]` and `filter[status__in]` to aggregate by specific statuses (`FAIL`, `PASS`)[(#8186)](https://github.com/prowler-cloud/prowler/pull/8186)
|
||||
- Throttling options for `/api/v1/tokens` using the `DJANGO_THROTTLE_TOKEN_OBTAIN` environment variable [(#8647)](https://github.com/prowler-cloud/prowler/pull/8647)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from dateutil.parser import parse
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.db.models import F, Q
|
||||
from django_filters.rest_framework import (
|
||||
BaseInFilter,
|
||||
BooleanFilter,
|
||||
@@ -28,6 +28,7 @@ from api.models import (
|
||||
Integration,
|
||||
Invitation,
|
||||
Membership,
|
||||
OverviewStatusChoices,
|
||||
PermissionChoices,
|
||||
Processor,
|
||||
Provider,
|
||||
@@ -750,6 +751,72 @@ class ScanSummaryFilter(FilterSet):
|
||||
}
|
||||
|
||||
|
||||
class ScanSummarySeverityFilter(ScanSummaryFilter):
|
||||
"""Filter for findings_severity ScanSummary endpoint - includes status filters"""
|
||||
|
||||
# Custom status filters - only for severity grouping endpoint
|
||||
status = ChoiceFilter(method="filter_status", choices=OverviewStatusChoices.choices)
|
||||
status__in = CharInFilter(method="filter_status_in", lookup_expr="in")
|
||||
|
||||
def filter_status(self, queryset, name, value):
|
||||
# Validate the status value
|
||||
if value not in [choice[0] for choice in OverviewStatusChoices.choices]:
|
||||
raise ValidationError(f"Invalid status value: {value}")
|
||||
|
||||
# Apply the filter by annotating the queryset with the status field
|
||||
if value == OverviewStatusChoices.FAIL:
|
||||
return queryset.annotate(status_count=F("fail"))
|
||||
elif value == OverviewStatusChoices.PASS:
|
||||
return queryset.annotate(status_count=F("_pass"))
|
||||
else:
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
def filter_status_in(self, queryset, name, value):
|
||||
# Validate the status values
|
||||
valid_statuses = [choice[0] for choice in OverviewStatusChoices.choices]
|
||||
for status_val in value:
|
||||
if status_val not in valid_statuses:
|
||||
raise ValidationError(f"Invalid status value: {status_val}")
|
||||
|
||||
# If all statuses or no valid statuses, use total
|
||||
if (
|
||||
set(value)
|
||||
>= {
|
||||
OverviewStatusChoices.FAIL,
|
||||
OverviewStatusChoices.PASS,
|
||||
}
|
||||
or not value
|
||||
):
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
# Build the sum expression based on status values
|
||||
sum_expression = None
|
||||
for status in value:
|
||||
if status == OverviewStatusChoices.FAIL:
|
||||
field_expr = F("fail")
|
||||
elif status == OverviewStatusChoices.PASS:
|
||||
field_expr = F("_pass")
|
||||
else:
|
||||
continue
|
||||
|
||||
if sum_expression is None:
|
||||
sum_expression = field_expr
|
||||
else:
|
||||
sum_expression = sum_expression + field_expr
|
||||
|
||||
if sum_expression is None:
|
||||
return queryset.annotate(status_count=F("total"))
|
||||
|
||||
return queryset.annotate(status_count=sum_expression)
|
||||
|
||||
class Meta:
|
||||
model = ScanSummary
|
||||
fields = {
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
}
|
||||
|
||||
|
||||
class ServiceOverviewFilter(ScanSummaryFilter):
|
||||
def is_valid(self):
|
||||
# Check if at least one of the inserted_at filters is present
|
||||
@@ -793,3 +860,23 @@ class ProcessorFilter(FilterSet):
|
||||
field_name="processor_type",
|
||||
lookup_expr="in",
|
||||
)
|
||||
|
||||
|
||||
class IntegrationJiraFindingsFilter(FilterSet):
|
||||
# To be expanded as needed
|
||||
finding_id = UUIDFilter(field_name="id", lookup_expr="exact")
|
||||
finding_id__in = UUIDInFilter(field_name="id", lookup_expr="in")
|
||||
|
||||
class Meta:
|
||||
model = Finding
|
||||
fields = {}
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
# Validate that there is at least one filter provided
|
||||
if not self.data:
|
||||
raise ValidationError(
|
||||
{
|
||||
"findings": "No finding filters provided. At least one filter is required."
|
||||
}
|
||||
)
|
||||
return super().filter_queryset(queryset)
|
||||
@@ -1 +1 @@
|
||||
[{"model": "api.complianceoverview","pk": "07d0c342-abcb-4d91-b865-88f9c96adbfc","fields": {"tenant": "12646005-9067-4d2a-a098-8bb378604362","inserted_at": "2024-11-15T13:14:10.043Z","compliance_id": "cisa_aws","framework": "CISA","version": "","description": "Cybersecurity & Infrastructure Security Agency's (CISA) Cyber Essentials is a guide for leaders of small businesses as well as leaders of small and local government agencies to develop an actionable understanding of where to start implementing organizational cybersecurity practices.","region": "eu-west-1","requirements": {"your-data-1": {"name": "Your Data-1","checks": {"ec2_ebs_volume_encryption": "PASS","ec2_ebs_default_encryption": "PASS","s3_bucket_default_encryption": "PASS","efs_encryption_at_rest_enabled": "FAIL","rds_instance_storage_encrypted": "FAIL","redshift_cluster_audit_logging": null,"cloudtrail_kms_encryption_enabled": "FAIL","sns_topics_kms_encryption_at_rest_enabled": "FAIL","dynamodb_tables_kms_cmk_encryption_enabled": null,"sagemaker_notebook_instance_encryption_enabled": null,"opensearch_service_domains_encryption_at_rest_enabled": null,"sagemaker_training_jobs_volume_and_output_encryption_enabled": null},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-1","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Learn how your data is protected.","checks_status": {"fail": 4,"pass": 3,"total": 13,"manual": 0}},"your-data-2": {"name": "Your Data-2","checks": {"elb_ssl_listeners": "FAIL","elb_logging_enabled": "FAIL","elbv2_ssl_listeners": "FAIL","vpc_flow_logs_enabled": "FAIL","ec2_instance_public_ip": "FAIL","elbv2_waf_acl_attached": "FAIL","efs_have_backup_enabled": "FAIL","s3_bucket_public_access": null,"s3_bucket_acl_prohibited": "FAIL","ec2_ebs_volume_encryption": "PASS","rds_snapshots_public_access": "PASS","s3_bucket_default_encryption": "PASS","rds_instance_no_public_access": "PASS","efs_encryption_at_rest_enabled": "FAIL","rds_instance_storage_encrypted": "FAIL","redshift_cluster_audit_logging": null,"redshift_cluster_public_access": null,"cloudtrail_multi_region_enabled": "PASS","acm_certificates_expiration_check": "PASS","s3_bucket_secure_transport_policy": "FAIL","apigateway_restapi_logging_enabled": "PASS","s3_bucket_policy_public_write_access": "PASS","cloudtrail_s3_dataevents_read_enabled": null,"ec2_networkacl_allow_ingress_any_port": "FAIL","emr_cluster_master_nodes_no_public_ip": null,"s3_account_level_public_access_blocks": null,"cloudtrail_s3_dataevents_write_enabled": null,"s3_bucket_server_access_logging_enabled": "FAIL","rds_instance_integration_cloudwatch_logs": "FAIL","sns_topics_kms_encryption_at_rest_enabled": "FAIL","awslambda_function_not_publicly_accessible": "PASS","dynamodb_tables_kms_cmk_encryption_enabled": null,"ec2_securitygroup_default_restrict_traffic": "FAIL","cloudwatch_log_group_kms_encryption_enabled": "FAIL","apigateway_restapi_client_certificate_enabled": "FAIL","sagemaker_notebook_instance_encryption_enabled": null,"opensearch_service_domains_cloudwatch_logging_enabled": null,"opensearch_service_domains_encryption_at_rest_enabled": null,"ec2_securitygroup_allow_ingress_from_internet_to_all_ports": "PASS","opensearch_service_domains_node_to_node_encryption_enabled": null,"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22": "PASS","sagemaker_training_jobs_volume_and_output_encryption_enabled": null,"sagemaker_notebook_instance_without_direct_internet_access_configured": null,"cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled": null},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-2","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Learn what is happening on your network, manage network and perimeter components, host and device components, data-at-rest and in-transit, and user behavior activities.","checks_status": {"fail": 18,"pass": 11,"total": 49,"manual": 0}},"your-data-3": {"name": "Your Data-3","checks": {"elbv2_waf_acl_attached": "FAIL"},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-3","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Domain name system protection.","checks_status": {"fail": 1,"pass": 0,"total": 1,"manual": 0}},"your-data-4": {"name": "Your Data-4","checks": {"efs_have_backup_enabled": "FAIL","elbv2_deletion_protection": "FAIL","rds_instance_backup_enabled": "PASS","s3_bucket_object_versioning": "FAIL","dynamodb_tables_pitr_enabled": null,"rds_instance_deletion_protection": "FAIL","redshift_cluster_automated_snapshot": null},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-4","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Establish regular automated backups and redundancies of key systems.","checks_status": {"fail": 4,"pass": 1,"total": 8,"manual": 0}},"your-data-5": {"name": "YLine truncated
|
||||
[{"model": "api.complianceoverview","pk": "07d0c342-abcb-4d91-b865-88f9c96adbfc","fields": {"tenant": "12646005-9067-4d2a-a098-8bb378604362","inserted_at": "2024-11-15T13:14:10.043Z","compliance_id": "cisa_aws","framework": "CISA","version": "","description": "Cybersecurity & Infrastructure Security Agency's (CISA) Cyber Essentials is a guide for leaders of small businesses as well as leaders of small and local government agencies to develop an actionable understanding of where to start implementing organizational cybersecurity practices.","region": "eu-west-1","requirements": {"your-data-1": {"name": "Your Data-1","checks": {"ec2_ebs_volume_encryption": "PASS","ec2_ebs_default_encryption": "PASS","s3_bucket_default_encryption": "PASS","efs_encryption_at_rest_enabled": "FAIL","rds_instance_storage_encrypted": "FAIL","redshift_cluster_audit_logging": null,"cloudtrail_kms_encryption_enabled": "FAIL","sns_topics_kms_encryption_at_rest_enabled": "FAIL","dynamodb_tables_kms_cmk_encryption_enabled": null,"sagemaker_notebook_instance_encryption_enabled": null,"opensearch_service_domains_encryption_at_rest_enabled": null,"sagemaker_training_jobs_volume_and_output_encryption_enabled": null},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-1","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Learn how your data is protected.","checks_status": {"fail": 4,"pass": 3,"total": 13,"manual": 0}},"your-data-2": {"name": "Your Data-2","checks": {"elb_ssl_listeners": "FAIL","elb_logging_enabled": "FAIL","elbv2_ssl_listeners": "FAIL","vpc_flow_logs_enabled": "FAIL","ec2_instance_public_ip": "FAIL","elbv2_waf_acl_attached": "FAIL","efs_have_backup_enabled": "FAIL","s3_bucket_public_access": null,"s3_bucket_acl_prohibited": "FAIL","ec2_ebs_volume_encryption": "PASS","rds_snapshots_public_access": "PASS","s3_bucket_default_encryption": "PASS","rds_instance_no_public_access": "PASS","efs_encryption_at_rest_enabled": "FAIL","rds_instance_storage_encrypted": "FAIL","redshift_cluster_audit_logging": null,"redshift_cluster_public_access": null,"cloudtrail_multi_region_enabled": "PASS","acm_certificates_expiration_check": "PASS","s3_bucket_secure_transport_policy": "FAIL","apigateway_restapi_logging_enabled": "PASS","s3_bucket_policy_public_write_access": "PASS","cloudtrail_s3_dataevents_read_enabled": null,"ec2_networkacl_allow_ingress_any_port": "FAIL","emr_cluster_master_nodes_no_public_ip": null,"s3_account_level_public_access_blocks": null,"cloudtrail_s3_dataevents_write_enabled": null,"s3_bucket_server_access_logging_enabled": "FAIL","rds_instance_integration_cloudwatch_logs": "FAIL","sns_topics_kms_encryption_at_rest_enabled": "FAIL","awslambda_function_not_publicly_accessible": "PASS","dynamodb_tables_kms_cmk_encryption_enabled": null,"ec2_securitygroup_default_restrict_traffic": "FAIL","cloudwatch_log_group_kms_encryption_enabled": "FAIL","apigateway_restapi_client_certificate_enabled": "FAIL","sagemaker_notebook_instance_encryption_enabled": null,"opensearch_service_domains_cloudwatch_logging_enabled": null,"opensearch_service_domains_encryption_at_rest_enabled": null,"ec2_securitygroup_allow_ingress_from_internet_to_all_ports": "PASS","opensearch_service_domains_node_to_node_encryption_enabled": null,"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22": "PASS","sagemaker_training_jobs_volume_and_output_encryption_enabled": null,"sagemaker_notebook_instance_without_direct_internet_access_configured": null,"cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled": null},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-2","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Learn what is happening on your network, manage network and perimeter components, host and device components, data-at-rest and in-transit, and user behavior activities.","checks_status": {"fail": 18,"pass": 11,"total": 49,"manual": 0}},"your-data-3": {"name": "Your Data-3","checks": {"elbv2_waf_acl_attached": "FAIL"},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-3","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Domain name system protection.","checks_status": {"fail": 1,"pass": 0,"total": 1,"manual": 0}},"your-data-4": {"name": "Your Data-4","checks": {"efs_have_backup_enabled": "FAIL","elbv2_deletion_protection": "FAIL","rds_instance_backup_enabled": "PASS","s3_bucket_object_versioning": "FAIL","dynamodb_tables_pitr_enabled": null,"rds_instance_deletion_protection": "FAIL","redshift_cluster_automated_snapshot": null},"status": "FAIL","attributes": [{"Type": null,"ItemId": "your-data-4","Section": "your data","Service": "aws","SubGroup": null,"SubSection": null}],"description": "Establish regular automated backups and redundancies of key systems.","checks_status": {"fail": 4,"pass": 1,"total": 8,"manual": 0}},"your-data-5": {"name": "YLine truncated
|
||||
@@ -74,6 +74,15 @@ class StatusChoices(models.TextChoices):
|
||||
MANUAL = "MANUAL", _("Manual")
|
||||
|
||||
|
||||
class OverviewStatusChoices(models.TextChoices):
|
||||
"""
|
||||
Status filters allowed in overview/severity endpoints.
|
||||
"""
|
||||
|
||||
FAIL = "FAIL", _("Fail")
|
||||
PASS = "PASS", _("Pass")
|
||||
|
||||
|
||||
class StateChoices(models.TextChoices):
|
||||
AVAILABLE = "available", _("Available")
|
||||
SCHEDULED = "scheduled", _("Scheduled")
|
||||
|
||||
@@ -2846,6 +2846,71 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntegrationCreateResponse'
|
||||
description: ''
|
||||
/api/v1/integrations/{integration_pk}/jira/dispatches:
|
||||
post:
|
||||
operationId: integrations_jira_dispatches_create
|
||||
description: Send a set of filtered findings to the given integration. At least
|
||||
one finding filter must be provided.
|
||||
summary: Send findings to a Jira integration
|
||||
parameters:
|
||||
- in: query
|
||||
name: filter[finding_id]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[finding_id__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- in: path
|
||||
name: integration_pk
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
tags:
|
||||
- Integration
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntegrationJiraDispatchRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntegrationJiraDispatchRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IntegrationJiraDispatchRequest'
|
||||
required: true
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
'202':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TaskResponse'
|
||||
examples:
|
||||
Task queued:
|
||||
summary: Task queued
|
||||
value:
|
||||
data:
|
||||
type: tasks
|
||||
id: 497f6eca-6276-4993-bfeb-53cbbbba6f08
|
||||
attributes:
|
||||
inserted_at: '2019-08-24T14:15:22Z'
|
||||
completed_at: '2019-08-24T14:15:22Z'
|
||||
name: string
|
||||
state: available
|
||||
result: null
|
||||
task_args: null
|
||||
metadata: null
|
||||
description: ''
|
||||
/api/v1/integrations/{id}:
|
||||
get:
|
||||
operationId: integrations_retrieve
|
||||
@@ -3027,8 +3092,8 @@ paths:
|
||||
/api/v1/lighthouse-configurations:
|
||||
get:
|
||||
operationId: lighthouse_configurations_list
|
||||
description: Retrieve a list of all Lighthouse configurations.
|
||||
summary: List all Lighthouse configurations
|
||||
description: Retrieve a list of all Lighthouse AI configurations.
|
||||
summary: List all Lighthouse AI configurations
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[lighthouse-configurations]
|
||||
@@ -3087,7 +3152,7 @@ paths:
|
||||
- -is_active
|
||||
explode: false
|
||||
tags:
|
||||
- Lighthouse
|
||||
- Lighthouse AI
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
@@ -3099,10 +3164,10 @@ paths:
|
||||
description: ''
|
||||
post:
|
||||
operationId: lighthouse_configurations_create
|
||||
description: Create a new Lighthouse configuration with the specified details.
|
||||
summary: Create a new Lighthouse configuration
|
||||
description: Create a new Lighthouse AI configuration with the specified details.
|
||||
summary: Create a new Lighthouse AI configuration
|
||||
tags:
|
||||
- Lighthouse
|
||||
- Lighthouse AI
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
@@ -3127,8 +3192,8 @@ paths:
|
||||
/api/v1/lighthouse-configurations/{id}:
|
||||
patch:
|
||||
operationId: lighthouse_configurations_partial_update
|
||||
description: Update certain fields of an existing Lighthouse configuration.
|
||||
summary: Partially update a Lighthouse configuration
|
||||
description: Update certain fields of an existing Lighthouse AI configuration.
|
||||
summary: Partially update a Lighthouse AI configuration
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -3136,7 +3201,7 @@ paths:
|
||||
type: string
|
||||
required: true
|
||||
tags:
|
||||
- Lighthouse
|
||||
- Lighthouse AI
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
@@ -3160,8 +3225,8 @@ paths:
|
||||
description: ''
|
||||
delete:
|
||||
operationId: lighthouse_configurations_destroy
|
||||
description: Remove a Lighthouse configuration by its ID.
|
||||
summary: Delete a Lighthouse configuration
|
||||
description: Remove a Lighthouse AI configuration by its ID.
|
||||
summary: Delete a Lighthouse AI configuration
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -3169,7 +3234,7 @@ paths:
|
||||
type: string
|
||||
required: true
|
||||
tags:
|
||||
- Lighthouse
|
||||
- Lighthouse AI
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
@@ -3179,7 +3244,7 @@ paths:
|
||||
post:
|
||||
operationId: lighthouse_configurations_connection_create
|
||||
description: Verify the connection to the OpenAI API for a specific Lighthouse
|
||||
configuration.
|
||||
AI configuration.
|
||||
summary: Check the connection to the OpenAI API
|
||||
parameters:
|
||||
- in: path
|
||||
@@ -3188,7 +3253,7 @@ paths:
|
||||
type: string
|
||||
required: true
|
||||
tags:
|
||||
- Lighthouse
|
||||
- Lighthouse AI
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
@@ -3506,6 +3571,25 @@ paths:
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[status]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- FAIL
|
||||
- PASS
|
||||
description: |-
|
||||
* `FAIL` - Fail
|
||||
* `PASS` - Pass
|
||||
- in: query
|
||||
name: filter[status__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
@@ -9134,23 +9218,10 @@ components:
|
||||
the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration in
|
||||
the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9197,17 +9268,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
required:
|
||||
- integration_type
|
||||
@@ -9331,23 +9402,10 @@ components:
|
||||
in the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration
|
||||
in the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9395,17 +9453,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
required:
|
||||
- integration_type
|
||||
@@ -9451,6 +9509,39 @@ components:
|
||||
$ref: '#/components/schemas/IntegrationCreate'
|
||||
required:
|
||||
- data
|
||||
IntegrationJiraDispatchRequest:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- integrations-jira-dispatches
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
minLength: 1
|
||||
issue_type:
|
||||
enum:
|
||||
- Task
|
||||
type: string
|
||||
description: '* `Task` - Task'
|
||||
x-spec-enum-id: b527b0cec62087c1
|
||||
required:
|
||||
- project_key
|
||||
- issue_type
|
||||
required:
|
||||
- data
|
||||
IntegrationResponse:
|
||||
type: object
|
||||
properties:
|
||||
@@ -9544,23 +9635,10 @@ components:
|
||||
the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration in
|
||||
the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9607,17 +9685,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
relationships:
|
||||
type: object
|
||||
@@ -10986,23 +11064,10 @@ components:
|
||||
in the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration
|
||||
in the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -11050,17 +11115,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
relationships:
|
||||
type: object
|
||||
@@ -15755,8 +15820,8 @@ tags:
|
||||
description: Endpoints for managing third-party integrations, including registration,
|
||||
configuration, retrieval, and deletion of integrations such as S3, JIRA, or other
|
||||
services.
|
||||
- name: Lighthouse
|
||||
description: Endpoints for managing Lighthouse configurations, including creation,
|
||||
- name: Lighthouse AI
|
||||
description: Endpoints for managing Lighthouse AI configurations, including creation,
|
||||
retrieval, updating, and deletion of configurations such as OpenAI keys, models,
|
||||
and business context.
|
||||
- name: SAML
|
||||
|
||||
@@ -503,28 +503,38 @@ class TestProwlerIntegrationConnectionTest:
|
||||
assert integration.configuration["regions"]["ap-south-1"] is False
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_success_basic_auth(self, mock_jira_class):
|
||||
def test_jira_connection_success_basic_auth(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test_api_token",
|
||||
}
|
||||
integration.configuration = {
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
integration.configuration = {}
|
||||
|
||||
# Mock successful JIRA connection with projects
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
mock_connection.projects = {"PROJ1": "Project 1", "PROJ2": "Project 2"}
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
|
||||
# Verify JIRA connection was called with correct parameters including domain from credentials
|
||||
mock_jira_class.test_connection.assert_called_once_with(
|
||||
user_mail="test@example.com",
|
||||
api_token="test_api_token",
|
||||
@@ -532,32 +542,112 @@ class TestProwlerIntegrationConnectionTest:
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
# Verify rls_transaction was called with correct tenant_id
|
||||
mock_rls_transaction.assert_called_once_with("test-tenant-id")
|
||||
|
||||
# Verify projects were saved to integration configuration
|
||||
assert integration.configuration["projects"] == {
|
||||
"PROJ1": "Project 1",
|
||||
"PROJ2": "Project 2",
|
||||
}
|
||||
|
||||
# Verify integration.save() was called
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_failure_invalid_credentials(self, mock_jira_class):
|
||||
def test_jira_connection_failure_invalid_credentials(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "invalid@example.com",
|
||||
"api_token": "invalid_token",
|
||||
}
|
||||
integration.configuration = {
|
||||
"domain": "invalid.atlassian.net",
|
||||
}
|
||||
integration.configuration = {}
|
||||
|
||||
# Mock failed JIRA connection
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = False
|
||||
mock_connection.error = Exception("Authentication failed: Invalid credentials")
|
||||
mock_connection.projects = {} # Empty projects when connection fails
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert "Authentication failed: Invalid credentials" in str(result.error)
|
||||
|
||||
# Verify JIRA connection was called with correct parameters
|
||||
mock_jira_class.test_connection.assert_called_once_with(
|
||||
user_mail="invalid@example.com",
|
||||
api_token="invalid_token",
|
||||
domain="invalid.atlassian.net",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
# Verify rls_transaction was called even on failure
|
||||
mock_rls_transaction.assert_called_once_with("test-tenant-id")
|
||||
|
||||
# Verify empty projects dict was saved to integration configuration
|
||||
assert integration.configuration["projects"] == {}
|
||||
|
||||
# Verify integration.save() was called even on connection failure
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_projects_update_with_existing_configuration(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
"""Test that projects are properly updated when integration already has configuration data"""
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test_api_token",
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
integration.configuration = {
|
||||
"issue_types": ["Task"], # Existing configuration
|
||||
"projects": {"OLD_PROJ": "Old Project"}, # Will be overwritten
|
||||
}
|
||||
|
||||
# Mock successful JIRA connection with new projects
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
mock_connection.projects = {
|
||||
"NEW_PROJ1": "New Project 1",
|
||||
"NEW_PROJ2": "New Project 2",
|
||||
}
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
|
||||
# Verify projects were updated (old projects replaced with new ones)
|
||||
assert integration.configuration["projects"] == {
|
||||
"NEW_PROJ1": "New Project 1",
|
||||
"NEW_PROJ2": "New Project 2",
|
||||
}
|
||||
|
||||
# Verify other configuration fields were preserved
|
||||
assert integration.configuration["issue_types"] == ["Task"]
|
||||
|
||||
# Verify integration.save() was called
|
||||
integration.save.assert_called_once()
|
||||
@@ -5660,18 +5660,6 @@ class TestIntegrationViewSet:
|
||||
},
|
||||
{},
|
||||
),
|
||||
# JIRA
|
||||
(
|
||||
Integration.IntegrationChoices.JIRA,
|
||||
{
|
||||
"project_key": "JIRA",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
{
|
||||
"api_token": "this-is-an-api-token-for-jira-that-works-for-sure",
|
||||
"user_mail": "testing@prowler.com",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_integrations_create_valid(
|
||||
@@ -5720,6 +5708,47 @@ class TestIntegrationViewSet:
|
||||
== data["data"]["relationships"]["providers"]["data"][0]["id"]
|
||||
)
|
||||
|
||||
def test_integrations_create_valid_jira(
|
||||
self,
|
||||
authenticated_client,
|
||||
):
|
||||
"""Jira integrations are special"""
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"domain": "prowlerdomain",
|
||||
"api_token": "this-is-an-api-token-for-jira-that-works-for-sure",
|
||||
"user_mail": "testing@prowler.com",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert Integration.objects.count() == 1
|
||||
integration = Integration.objects.first()
|
||||
integration_configuration = response.json()["data"]["attributes"][
|
||||
"configuration"
|
||||
]
|
||||
assert "projects" in integration_configuration
|
||||
assert "issue_types" in integration_configuration
|
||||
assert "domain" in integration_configuration
|
||||
assert integration.enabled == data["data"]["attributes"]["enabled"]
|
||||
assert (
|
||||
integration.integration_type
|
||||
== data["data"]["attributes"]["integration_type"]
|
||||
)
|
||||
assert "credentials" not in response.json()["data"]["attributes"]
|
||||
|
||||
def test_integrations_create_valid_relationships(
|
||||
self,
|
||||
authenticated_client,
|
||||
@@ -5822,31 +5851,37 @@ class TestIntegrationViewSet:
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"projects": ["JIRA"],
|
||||
},
|
||||
"credentials": {},
|
||||
"credentials": {"domain": "prowlerdomain"},
|
||||
},
|
||||
"required",
|
||||
"domain",
|
||||
"invalid",
|
||||
"configuration",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"credentialss": {
|
||||
"domain": "prowlerdomain",
|
||||
"api_token": "api-token",
|
||||
"user_mail": "test@prowler.com",
|
||||
},
|
||||
},
|
||||
"required",
|
||||
"configuration",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {},
|
||||
},
|
||||
"required",
|
||||
"credentials",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
"configuration": {},
|
||||
"credentials": {"api_token": "api-token"},
|
||||
},
|
||||
"invalid",
|
||||
@@ -6093,32 +6128,21 @@ class TestIntegrationViewSet:
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
def test_integrations_create_duplicate_jira(
|
||||
self, authenticated_client, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
|
||||
def test_integrations_create_duplicate_jira(self, authenticated_client):
|
||||
# Create first JIRA integration
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {
|
||||
"project_key": "TEST",
|
||||
"domain": "test.atlassian.net",
|
||||
},
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
"relationships": {
|
||||
"providers": {
|
||||
"data": [{"type": "providers", "id": str(provider.id)}]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6145,6 +6169,124 @@ class TestIntegrationViewSet:
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
def test_integrations_update_jira_configuration_readonly(
|
||||
self, authenticated_client
|
||||
):
|
||||
# Create JIRA integration first
|
||||
create_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "initial-domain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Create the integration
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(create_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
integration_id = response.json()["data"]["id"]
|
||||
|
||||
# Attempt to update configuration - should be ignored/not allowed
|
||||
update_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"id": integration_id,
|
||||
"attributes": {
|
||||
"configuration": {
|
||||
"projects": {"NEW_PROJECT": "New Project"},
|
||||
"issue_types": ["Epic", "Story"],
|
||||
"domain": "malicious-domain",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("integration-detail", kwargs={"pk": integration_id}),
|
||||
data=json.dumps(update_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_integrations_update_jira_credentials_domain_reflects_in_configuration(
|
||||
self, authenticated_client
|
||||
):
|
||||
# Create JIRA integration first
|
||||
create_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "original-domain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Create the integration
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(create_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
integration_id = response.json()["data"]["id"]
|
||||
|
||||
# Verify initial domain in configuration
|
||||
initial_integration = response.json()["data"]
|
||||
assert (
|
||||
initial_integration["attributes"]["configuration"]["domain"]
|
||||
== "original-domain"
|
||||
)
|
||||
|
||||
# Update credentials with new domain
|
||||
update_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"id": integration_id,
|
||||
"attributes": {
|
||||
"credentials": {
|
||||
"user_mail": "updated@example.com",
|
||||
"api_token": "updated-api-token",
|
||||
"domain": "updated-domain",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("integration-detail", kwargs={"pk": integration_id}),
|
||||
data=json.dumps(update_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Verify the new domain is reflected in configuration
|
||||
updated_integration = response.json()["data"]
|
||||
configuration = updated_integration["attributes"]["configuration"]
|
||||
assert configuration["domain"] == "updated-domain"
|
||||
|
||||
# Verify other configuration fields are preserved
|
||||
assert "projects" in configuration
|
||||
assert "issue_types" in configuration
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSAMLTokenValidation:
|
||||
|
||||
@@ -6,10 +6,11 @@ from django.db.models import Subquery
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import rls_transaction
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Integration, Invitation, Processor, Provider, Resource
|
||||
from api.v1.serializers import FindingMetadataSerializer
|
||||
from prowler.lib.outputs.jira.jira import Jira
|
||||
from prowler.lib.outputs.jira.jira import Jira, JiraBasicAuthError
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.lib.s3.s3 import S3
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
|
||||
@@ -238,11 +239,15 @@ def prowler_integration_connection_test(integration: Integration) -> Connection:
|
||||
|
||||
return connection
|
||||
elif integration.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
return Jira.test_connection(
|
||||
jira_connection = Jira.test_connection(
|
||||
**integration.credentials,
|
||||
domain=integration.configuration["domain"],
|
||||
raise_on_exception=False,
|
||||
)
|
||||
project_keys = jira_connection.projects if jira_connection.is_connected else {}
|
||||
with rls_transaction(str(integration.tenant_id)):
|
||||
integration.configuration["projects"] = project_keys
|
||||
integration.save()
|
||||
return jira_connection
|
||||
elif integration.integration_type == Integration.IntegrationChoices.SLACK:
|
||||
pass
|
||||
else:
|
||||
@@ -342,3 +347,17 @@ def get_findings_metadata_no_aggregations(tenant_id: str, filtered_queryset):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return serializer.data
|
||||
|
||||
|
||||
def initialize_prowler_integration(integration: Integration) -> Jira:
|
||||
# TODO Refactor other integrations to use this function
|
||||
if integration.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
try:
|
||||
return Jira(**integration.credentials)
|
||||
except JiraBasicAuthError as jira_auth_error:
|
||||
with rls_transaction(str(integration.tenant_id)):
|
||||
integration.configuration["projects"] = {}
|
||||
integration.connected = False
|
||||
integration.connection_last_checked_at = datetime.now(tz=timezone.utc)
|
||||
integration.save()
|
||||
raise jira_auth_error
|
||||
@@ -68,10 +68,11 @@ class SecurityHubConfigSerializer(BaseValidateSerializer):
|
||||
|
||||
|
||||
class JiraConfigSerializer(BaseValidateSerializer):
|
||||
project_key = serializers.CharField(required=True)
|
||||
domain = serializers.CharField(required=True)
|
||||
issue_types = serializers.ListField(required=False, child=serializers.CharField())
|
||||
issue_labels = serializers.ListField(required=False, child=serializers.CharField())
|
||||
domain = serializers.CharField(read_only=True)
|
||||
issue_types = serializers.ListField(
|
||||
read_only=True, child=serializers.CharField(), default=["Task"]
|
||||
)
|
||||
projects = serializers.DictField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
@@ -95,6 +96,7 @@ class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
class JiraCredentialSerializer(BaseValidateSerializer):
|
||||
user_mail = serializers.EmailField(required=True)
|
||||
api_token = serializers.CharField(required=True)
|
||||
domain = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
@@ -165,8 +167,12 @@ class JiraCredentialSerializer(BaseValidateSerializer):
|
||||
"description": "The API token for authentication with JIRA. This can be generated from your "
|
||||
"Atlassian account settings.",
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').",
|
||||
},
|
||||
},
|
||||
"required": ["user_mail", "api_token"],
|
||||
"required": ["user_mail", "api_token", "domain"],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -218,27 +224,10 @@ class IntegrationCredentialField(serializers.JSONField):
|
||||
{
|
||||
"type": "object",
|
||||
"title": "JIRA",
|
||||
"properties": {
|
||||
"project_key": {
|
||||
"type": "string",
|
||||
"description": "The JIRA project key where issues will be created (e.g., 'PROJ', 'SEC').",
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').",
|
||||
},
|
||||
"issue_types": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of JIRA issue types to create for findings.",
|
||||
},
|
||||
"issue_labels": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of labels to apply to created JIRA issues..",
|
||||
},
|
||||
},
|
||||
"required": ["project_key", "domain"],
|
||||
"description": "JIRA integration does not accept any configuration in the payload. Leave it as an "
|
||||
"empty JSON object (`{}`).",
|
||||
"properties": {},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1972,8 +1972,7 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
integration_type == Integration.IntegrationChoices.JIRA
|
||||
and Integration.objects.filter(
|
||||
configuration__contains={
|
||||
"domain": attrs.get("configuration").get("domain"),
|
||||
"project_key": attrs.get("configuration").get("project_key"),
|
||||
"domain": attrs.get("configuration").get("domain")
|
||||
}
|
||||
).exists()
|
||||
):
|
||||
@@ -2038,7 +2037,28 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
config_serializer = SecurityHubConfigSerializer
|
||||
credentials_serializers = [AWSCredentialSerializer]
|
||||
elif integration_type == Integration.IntegrationChoices.JIRA:
|
||||
if providers:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"providers": "Relationship field is not accepted. This integration applies to all providers."
|
||||
}
|
||||
)
|
||||
if configuration:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"configuration": "This integration does not support custom configuration."
|
||||
}
|
||||
)
|
||||
config_serializer = JiraConfigSerializer
|
||||
# Create non-editable configuration for JIRA integration
|
||||
default_jira_issue_types = ["Task"]
|
||||
configuration.update(
|
||||
{
|
||||
"projects": {},
|
||||
"issue_types": default_jira_issue_types,
|
||||
"domain": credentials.get("domain"),
|
||||
}
|
||||
)
|
||||
credentials_serializers = [JiraCredentialSerializer]
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
@@ -2103,6 +2123,10 @@ class IntegrationSerializer(RLSSerializer):
|
||||
for provider in representation["providers"]
|
||||
if provider["id"] in allowed_provider_ids
|
||||
]
|
||||
if instance.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
representation["configuration"].update(
|
||||
{"domain": instance.credentials.get("domain")}
|
||||
)
|
||||
return representation
|
||||
|
||||
|
||||
@@ -2203,7 +2227,10 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
|
||||
def validate(self, attrs):
|
||||
integration_type = self.instance.integration_type
|
||||
providers = attrs.get("providers")
|
||||
configuration = attrs.get("configuration") or self.instance.configuration
|
||||
if integration_type != Integration.IntegrationChoices.JIRA:
|
||||
configuration = attrs.get("configuration") or self.instance.configuration
|
||||
else:
|
||||
configuration = attrs.get("configuration", {})
|
||||
credentials = attrs.get("credentials") or self.instance.credentials
|
||||
|
||||
self.validate_integration_data(
|
||||
@@ -2233,6 +2260,53 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
# Ensure JIRA integrations show updated domain in configuration from credentials
|
||||
if instance.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
representation["configuration"].update(
|
||||
{"domain": instance.credentials.get("domain")}
|
||||
)
|
||||
return representation
|
||||
|
||||
|
||||
class IntegrationJiraDispatchSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for dispatching findings to JIRA integration.
|
||||
"""
|
||||
|
||||
project_key = serializers.CharField(required=True)
|
||||
issue_type = serializers.ChoiceField(required=True, choices=["Task"])
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "integrations-jira-dispatches"
|
||||
|
||||
def validate(self, attrs):
|
||||
validated_attrs = super().validate(attrs)
|
||||
integration_instance = Integration.objects.get(
|
||||
id=self.context.get("integration_id")
|
||||
)
|
||||
if integration_instance.integration_type != Integration.IntegrationChoices.JIRA:
|
||||
raise ValidationError(
|
||||
{"integration_type": "The given integration is not a JIRA integration"}
|
||||
)
|
||||
|
||||
if not integration_instance.enabled:
|
||||
raise ValidationError(
|
||||
{"integration": "The given integration is not enabled"}
|
||||
)
|
||||
|
||||
project_key = attrs.get("project_key")
|
||||
if project_key not in integration_instance.configuration.get("projects", {}):
|
||||
raise ValidationError(
|
||||
{
|
||||
"project_key": "The given project key is not available for this JIRA integration. Refresh the "
|
||||
"connection if this is an error."
|
||||
}
|
||||
)
|
||||
|
||||
return validated_attrs
|
||||
|
||||
|
||||
# Processors
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from api.v1.views import (
|
||||
FindingViewSet,
|
||||
GithubSocialLoginView,
|
||||
GoogleSocialLoginView,
|
||||
IntegrationJiraViewSet,
|
||||
IntegrationViewSet,
|
||||
InvitationAcceptViewSet,
|
||||
InvitationViewSet,
|
||||
@@ -73,6 +74,13 @@ tenants_router.register(
|
||||
users_router = routers.NestedSimpleRouter(router, r"users", lookup="user")
|
||||
users_router.register(r"memberships", MembershipViewSet, basename="user-membership")
|
||||
|
||||
integrations_router = routers.NestedSimpleRouter(
|
||||
router, r"integrations", lookup="integration"
|
||||
)
|
||||
integrations_router.register(
|
||||
r"jira", IntegrationJiraViewSet, basename="integration-jira"
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("tokens", CustomTokenObtainView.as_view(), name="token-obtain"),
|
||||
path("tokens/refresh", CustomTokenRefreshView.as_view(), name="token-refresh"),
|
||||
@@ -162,6 +170,7 @@ urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
path("", include(tenants_router.urls)),
|
||||
path("", include(users_router.urls)),
|
||||
path("", include(integrations_router.urls)),
|
||||
path("schema", SchemaView.as_view(), name="schema"),
|
||||
path("docs", SpectacularRedocView.as_view(url_name="schema"), name="docs"),
|
||||
]
|
||||
@@ -62,6 +62,7 @@ from tasks.tasks import (
|
||||
check_provider_connection_task,
|
||||
delete_provider_task,
|
||||
delete_tenant_task,
|
||||
jira_integration_task,
|
||||
perform_scan_task,
|
||||
)
|
||||
|
||||
@@ -75,8 +76,10 @@ from api.db_utils import rls_transaction
|
||||
from api.exceptions import TaskFailedException
|
||||
from api.filters import (
|
||||
ComplianceOverviewFilter,
|
||||
CustomDjangoFilterBackend,
|
||||
FindingFilter,
|
||||
IntegrationFilter,
|
||||
IntegrationJiraFindingsFilter,
|
||||
InvitationFilter,
|
||||
LatestFindingFilter,
|
||||
LatestResourceFilter,
|
||||
@@ -89,6 +92,7 @@ from api.filters import (
|
||||
RoleFilter,
|
||||
ScanFilter,
|
||||
ScanSummaryFilter,
|
||||
ScanSummarySeverityFilter,
|
||||
ServiceOverviewFilter,
|
||||
TaskFilter,
|
||||
TenantFilter,
|
||||
@@ -142,6 +146,7 @@ from api.v1.serializers import (
|
||||
FindingMetadataSerializer,
|
||||
FindingSerializer,
|
||||
IntegrationCreateSerializer,
|
||||
IntegrationJiraDispatchSerializer,
|
||||
IntegrationSerializer,
|
||||
IntegrationUpdateSerializer,
|
||||
InvitationAcceptSerializer,
|
||||
@@ -214,6 +219,8 @@ class RelationshipViewSchema(JsonApiAutoSchema):
|
||||
description="Obtain a token by providing valid credentials and an optional tenant ID.",
|
||||
)
|
||||
class CustomTokenObtainView(GenericAPIView):
|
||||
throttle_scope = "token-obtain"
|
||||
|
||||
resource_name = "tokens"
|
||||
serializer_class = TokenSerializer
|
||||
http_method_names = ["post"]
|
||||
@@ -370,8 +377,8 @@ class SchemaView(SpectacularAPIView):
|
||||
" retrieval, and deletion of integrations such as S3, JIRA, or other services.",
|
||||
},
|
||||
{
|
||||
"name": "Lighthouse",
|
||||
"description": "Endpoints for managing Lighthouse configurations, including creation, retrieval, "
|
||||
"name": "Lighthouse AI",
|
||||
"description": "Endpoints for managing Lighthouse AI configurations, including creation, retrieval, "
|
||||
"updating, and deletion of configurations such as OpenAI keys, models, and business "
|
||||
"context.",
|
||||
},
|
||||
@@ -3546,8 +3553,10 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
def get_filterset_class(self):
|
||||
if self.action == "providers":
|
||||
return None
|
||||
elif self.action in ["findings", "findings_severity"]:
|
||||
elif self.action == "findings":
|
||||
return ScanSummaryFilter
|
||||
elif self.action == "findings_severity":
|
||||
return ScanSummarySeverityFilter
|
||||
elif self.action == "services":
|
||||
return ServiceOverviewFilter
|
||||
return None
|
||||
@@ -3669,7 +3678,12 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
@action(detail=False, methods=["get"], url_name="findings_severity")
|
||||
def findings_severity(self, request):
|
||||
tenant_id = self.request.tenant_id
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# Load only required fields
|
||||
queryset = self.get_queryset().only(
|
||||
"tenant_id", "scan_id", "severity", "fail", "_pass", "total"
|
||||
)
|
||||
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
provider_filter = (
|
||||
{"provider__in": self.allowed_providers}
|
||||
@@ -3689,16 +3703,22 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
tenant_id=tenant_id, scan_id__in=latest_scan_ids
|
||||
)
|
||||
|
||||
# The filter will have added a status_count annotation if any status filter was used
|
||||
if "status_count" in filtered_queryset.query.annotations:
|
||||
sum_expression = Sum("status_count")
|
||||
else:
|
||||
sum_expression = Sum("total")
|
||||
|
||||
severity_counts = (
|
||||
filtered_queryset.values("severity")
|
||||
.annotate(count=Sum("total"))
|
||||
.annotate(count=sum_expression)
|
||||
.order_by("severity")
|
||||
)
|
||||
|
||||
severity_data = {sev[0]: 0 for sev in SeverityChoices}
|
||||
|
||||
for item in severity_counts:
|
||||
severity_data[item["severity"]] = item["count"]
|
||||
severity_data.update(
|
||||
{item["severity"]: item["count"] for item in severity_counts}
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(severity_data)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -3886,31 +3906,111 @@ class IntegrationViewSet(BaseRLSViewSet):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
dispatches=extend_schema(
|
||||
tags=["Integration"],
|
||||
summary="Send findings to a Jira integration",
|
||||
description="Send a set of filtered findings to the given integration. At least one finding filter must be "
|
||||
"provided.",
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
filters=True,
|
||||
)
|
||||
)
|
||||
class IntegrationJiraViewSet(BaseRLSViewSet):
|
||||
queryset = Finding.all_objects.all()
|
||||
serializer_class = IntegrationJiraDispatchSerializer
|
||||
http_method_names = ["post"]
|
||||
filter_backends = [CustomDjangoFilterBackend]
|
||||
filterset_class = IntegrationJiraFindingsFilter
|
||||
# RBAC required permissions
|
||||
required_permissions = [Permissions.MANAGE_INTEGRATIONS]
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def create(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(method="POST")
|
||||
|
||||
def get_queryset(self):
|
||||
tenant_id = self.request.tenant_id
|
||||
user_roles = get_role(self.request.user)
|
||||
if user_roles.unlimited_visibility:
|
||||
# User has unlimited visibility, return all findings
|
||||
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
|
||||
else:
|
||||
# User lacks permission, filter findings based on provider groups associated with the role
|
||||
queryset = Finding.all_objects.filter(
|
||||
scan__provider__in=get_providers(user_roles)
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
@action(detail=False, methods=["post"], url_name="dispatches")
|
||||
def dispatches(self, request, integration_pk=None):
|
||||
get_object_or_404(Integration, pk=integration_pk)
|
||||
serializer = self.get_serializer(
|
||||
data=request.data, context={"integration_id": integration_pk}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
if self.filter_queryset(self.get_queryset()).count() == 0:
|
||||
raise ValidationError(
|
||||
{"findings": "No findings match the provided filters"}
|
||||
)
|
||||
|
||||
finding_ids = [
|
||||
str(finding_id)
|
||||
for finding_id in self.filter_queryset(self.get_queryset()).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
]
|
||||
project_key = serializer.validated_data["project_key"]
|
||||
issue_type = serializer.validated_data["issue_type"]
|
||||
|
||||
with transaction.atomic():
|
||||
task = jira_integration_task.delay(
|
||||
tenant_id=self.request.tenant_id,
|
||||
integration_id=integration_pk,
|
||||
project_key=project_key,
|
||||
issue_type=issue_type,
|
||||
finding_ids=finding_ids,
|
||||
)
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={
|
||||
"Content-Location": reverse(
|
||||
"task-detail", kwargs={"pk": prowler_task.id}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="List all Lighthouse configurations",
|
||||
description="Retrieve a list of all Lighthouse configurations.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="List all Lighthouse AI configurations",
|
||||
description="Retrieve a list of all Lighthouse AI configurations.",
|
||||
),
|
||||
create=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Create a new Lighthouse configuration",
|
||||
description="Create a new Lighthouse configuration with the specified details.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Create a new Lighthouse AI configuration",
|
||||
description="Create a new Lighthouse AI configuration with the specified details.",
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Partially update a Lighthouse configuration",
|
||||
description="Update certain fields of an existing Lighthouse configuration.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Partially update a Lighthouse AI configuration",
|
||||
description="Update certain fields of an existing Lighthouse AI configuration.",
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Delete a Lighthouse configuration",
|
||||
description="Remove a Lighthouse configuration by its ID.",
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Delete a Lighthouse AI configuration",
|
||||
description="Remove a Lighthouse AI configuration by its ID.",
|
||||
),
|
||||
connection=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
tags=["Lighthouse AI"],
|
||||
summary="Check the connection to the OpenAI API",
|
||||
description="Verify the connection to the OpenAI API for a specific Lighthouse configuration.",
|
||||
description="Verify the connection to the OpenAI API for a specific Lighthouse AI configuration.",
|
||||
request=None,
|
||||
responses={202: OpenApiResponse(response=TaskSerializer)},
|
||||
),
|
||||
|
||||
@@ -108,6 +108,13 @@ REST_FRAMEWORK = {
|
||||
),
|
||||
"TEST_REQUEST_DEFAULT_FORMAT": "vnd.api+json",
|
||||
"JSON_API_UNIFORM_EXCEPTIONS": True,
|
||||
"DEFAULT_THROTTLE_CLASSES": [
|
||||
"rest_framework.throttling.ScopedRateThrottle",
|
||||
],
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"token-obtain": env("DJANGO_THROTTLE_TOKEN_OBTAIN", default=None),
|
||||
"dj_rest_auth": None,
|
||||
},
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
|
||||
@@ -7,7 +7,7 @@ from tasks.utils import batched
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Finding, Integration, Provider
|
||||
from api.utils import initialize_prowler_provider
|
||||
from api.utils import initialize_prowler_integration, initialize_prowler_provider
|
||||
from prowler.lib.outputs.asff.asff import ASFF
|
||||
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
|
||||
from prowler.lib.outputs.csv.csv import CSV
|
||||
@@ -429,3 +429,78 @@ def upload_security_hub_integration(
|
||||
f"Security Hub integrations failed for provider {provider_id}: {str(e)}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def send_findings_to_jira(
|
||||
tenant_id: str,
|
||||
integration_id: str,
|
||||
project_key: str,
|
||||
issue_type: str,
|
||||
finding_ids: list[str],
|
||||
):
|
||||
with rls_transaction(tenant_id):
|
||||
integration = Integration.objects.get(id=integration_id)
|
||||
jira_integration = initialize_prowler_integration(integration)
|
||||
|
||||
num_tickets_created = 0
|
||||
for finding_id in finding_ids:
|
||||
with rls_transaction(tenant_id):
|
||||
finding_instance = (
|
||||
Finding.all_objects.select_related("scan__provider")
|
||||
.prefetch_related("resources")
|
||||
.get(id=finding_id)
|
||||
)
|
||||
|
||||
# Extract resource information
|
||||
resource = (
|
||||
finding_instance.resources.first()
|
||||
if finding_instance.resources.exists()
|
||||
else None
|
||||
)
|
||||
resource_uid = resource.uid if resource else ""
|
||||
resource_name = resource.name if resource else ""
|
||||
resource_tags = {}
|
||||
if resource and hasattr(resource, "tags"):
|
||||
resource_tags = resource.get_tags(tenant_id)
|
||||
|
||||
# Get region
|
||||
region = resource.region if resource and resource.region else ""
|
||||
|
||||
# Extract remediation information from check_metadata
|
||||
check_metadata = finding_instance.check_metadata
|
||||
remediation = check_metadata.get("remediation", {})
|
||||
recommendation = remediation.get("recommendation", {})
|
||||
remediation_code = remediation.get("code", {})
|
||||
|
||||
# Send the individual finding to Jira
|
||||
result = jira_integration.send_finding(
|
||||
check_id=finding_instance.check_id,
|
||||
check_title=check_metadata.get("checktitle", ""),
|
||||
severity=finding_instance.severity,
|
||||
status=finding_instance.status,
|
||||
status_extended=finding_instance.status_extended or "",
|
||||
provider=finding_instance.scan.provider.provider,
|
||||
region=region,
|
||||
resource_uid=resource_uid,
|
||||
resource_name=resource_name,
|
||||
risk=check_metadata.get("risk", ""),
|
||||
recommendation_text=recommendation.get("text", ""),
|
||||
recommendation_url=recommendation.get("url", ""),
|
||||
remediation_code_native_iac=remediation_code.get("nativeiac", ""),
|
||||
remediation_code_terraform=remediation_code.get("terraform", ""),
|
||||
remediation_code_cli=remediation_code.get("cli", ""),
|
||||
remediation_code_other=remediation_code.get("other", ""),
|
||||
resource_tags=resource_tags,
|
||||
compliance=finding_instance.compliance or {},
|
||||
project_key=project_key,
|
||||
issue_type=issue_type,
|
||||
)
|
||||
if result:
|
||||
num_tickets_created += 1
|
||||
else:
|
||||
logger.error(f"Failed to send finding {finding_id} to Jira")
|
||||
|
||||
return {
|
||||
"created_count": num_tickets_created,
|
||||
"failed_count": len(finding_ids) - num_tickets_created,
|
||||
}
|
||||
@@ -22,6 +22,7 @@ from tasks.jobs.export import (
|
||||
_upload_to_s3,
|
||||
)
|
||||
from tasks.jobs.integrations import (
|
||||
send_findings_to_jira,
|
||||
upload_s3_integration,
|
||||
upload_security_hub_integration,
|
||||
)
|
||||
@@ -595,3 +596,20 @@ def security_hub_integration_task(
|
||||
scan_id (str): The scan identifier
|
||||
"""
|
||||
return upload_security_hub_integration(tenant_id, provider_id, scan_id)
|
||||
|
||||
|
||||
@shared_task(
|
||||
base=RLSTask,
|
||||
name="integration-jira",
|
||||
queue="integrations",
|
||||
)
|
||||
def jira_integration_task(
|
||||
tenant_id: str,
|
||||
integration_id: str,
|
||||
project_key: str,
|
||||
issue_type: str,
|
||||
finding_ids: list[str],
|
||||
):
|
||||
return send_findings_to_jira(
|
||||
tenant_id, integration_id, project_key, issue_type, finding_ids
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
from tasks.jobs.integrations import (
|
||||
get_s3_client_from_integration,
|
||||
get_security_hub_client_from_integration,
|
||||
send_findings_to_jira,
|
||||
upload_s3_integration,
|
||||
upload_security_hub_integration,
|
||||
)
|
||||
@@ -1557,3 +1558,354 @@ class TestSecurityHubIntegrationUploads:
|
||||
|
||||
mock_security_hub.batch_send_to_security_hub.assert_called_once()
|
||||
mock_security_hub.archive_previous_findings.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestJiraIntegration:
|
||||
@patch("tasks.jobs.integrations.rls_transaction")
|
||||
@patch("tasks.jobs.integrations.Finding")
|
||||
@patch("tasks.jobs.integrations.Integration")
|
||||
@patch("tasks.jobs.integrations.initialize_prowler_integration")
|
||||
def test_send_findings_to_jira_success(
|
||||
self,
|
||||
mock_initialize_integration,
|
||||
mock_integration_model,
|
||||
mock_finding_model,
|
||||
mock_rls_transaction,
|
||||
):
|
||||
"""Test successful sending of findings to Jira using send_finding method"""
|
||||
tenant_id = "tenant-123"
|
||||
integration_id = "integration-456"
|
||||
project_key = "PROJ"
|
||||
issue_type = "Task"
|
||||
finding_ids = ["finding-1", "finding-2"]
|
||||
|
||||
# Mock RLS transaction
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
# Mock integration
|
||||
integration = MagicMock()
|
||||
mock_integration_model.objects.get.return_value = integration
|
||||
|
||||
# Mock Jira integration
|
||||
mock_jira_integration = MagicMock()
|
||||
mock_jira_integration.send_finding.side_effect = [True, True] # Both succeed
|
||||
mock_initialize_integration.return_value = mock_jira_integration
|
||||
|
||||
# Mock findings with resources
|
||||
resource1 = MagicMock()
|
||||
resource1.uid = "resource-uid-1"
|
||||
resource1.name = "resource-name-1"
|
||||
resource1.region = "us-east-1"
|
||||
resource1.get_tags.return_value = {"env": "prod", "team": "security"}
|
||||
|
||||
resource2 = MagicMock()
|
||||
resource2.uid = "resource-uid-2"
|
||||
resource2.name = "resource-name-2"
|
||||
resource2.region = "eu-west-1"
|
||||
resource2.get_tags.return_value = {"env": "dev"}
|
||||
|
||||
finding1 = MagicMock()
|
||||
finding1.id = "finding-1"
|
||||
finding1.check_id = "check_001"
|
||||
finding1.severity = "high"
|
||||
finding1.status = "FAIL"
|
||||
finding1.status_extended = "Resource is not compliant"
|
||||
finding1.resource_regions = ["us-east-1"]
|
||||
finding1.compliance = {"cis": ["1.1", "1.2"]}
|
||||
finding1.resources.exists.return_value = True
|
||||
finding1.resources.first.return_value = resource1
|
||||
finding1.scan.provider.provider = "aws"
|
||||
finding1.check_metadata = {
|
||||
"checktitle": "Check Title 1",
|
||||
"risk": "High risk finding",
|
||||
"remediation": {
|
||||
"recommendation": {
|
||||
"text": "Fix this issue",
|
||||
"url": "https://docs.example.com/fix",
|
||||
},
|
||||
"code": {
|
||||
"nativeiac": "native code",
|
||||
"terraform": "terraform code",
|
||||
"cli": "aws cli command",
|
||||
"other": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
finding2 = MagicMock()
|
||||
finding2.id = "finding-2"
|
||||
finding2.check_id = "check_002"
|
||||
finding2.severity = "medium"
|
||||
finding2.status = "PASS"
|
||||
finding2.status_extended = None
|
||||
finding2.resource_regions = []
|
||||
finding2.compliance = {}
|
||||
finding2.resources.exists.return_value = True
|
||||
finding2.resources.first.return_value = resource2
|
||||
finding2.scan.provider.provider = "azure"
|
||||
finding2.check_metadata = {
|
||||
"checktitle": "Check Title 2",
|
||||
"risk": "Medium risk",
|
||||
"remediation": {
|
||||
"recommendation": {"text": "Consider fixing", "url": ""},
|
||||
"code": {},
|
||||
},
|
||||
}
|
||||
|
||||
mock_finding_model.all_objects.select_related.return_value.prefetch_related.return_value.get.side_effect = [
|
||||
finding1,
|
||||
finding2,
|
||||
]
|
||||
|
||||
# Call the function
|
||||
result = send_findings_to_jira(
|
||||
tenant_id, integration_id, project_key, issue_type, finding_ids
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result == {"created_count": 2, "failed_count": 0}
|
||||
|
||||
# Verify Jira integration was initialized
|
||||
mock_initialize_integration.assert_called_once_with(integration)
|
||||
|
||||
# Verify send_finding was called twice with correct parameters
|
||||
assert mock_jira_integration.send_finding.call_count == 2
|
||||
|
||||
# Verify first call
|
||||
first_call = mock_jira_integration.send_finding.call_args_list[0]
|
||||
assert first_call.kwargs["check_id"] == "check_001"
|
||||
assert first_call.kwargs["check_title"] == "Check Title 1"
|
||||
assert first_call.kwargs["severity"] == "high"
|
||||
assert first_call.kwargs["status"] == "FAIL"
|
||||
assert first_call.kwargs["resource_uid"] == "resource-uid-1"
|
||||
assert first_call.kwargs["resource_name"] == "resource-name-1"
|
||||
assert first_call.kwargs["region"] == "us-east-1"
|
||||
assert first_call.kwargs["provider"] == "aws"
|
||||
assert first_call.kwargs["project_key"] == project_key
|
||||
assert first_call.kwargs["issue_type"] == issue_type
|
||||
|
||||
# Verify second call
|
||||
second_call = mock_jira_integration.send_finding.call_args_list[1]
|
||||
assert second_call.kwargs["check_id"] == "check_002"
|
||||
assert second_call.kwargs["severity"] == "medium"
|
||||
assert second_call.kwargs["status"] == "PASS"
|
||||
|
||||
@patch("tasks.jobs.integrations.rls_transaction")
|
||||
@patch("tasks.jobs.integrations.Finding")
|
||||
@patch("tasks.jobs.integrations.Integration")
|
||||
@patch("tasks.jobs.integrations.initialize_prowler_integration")
|
||||
@patch("tasks.jobs.integrations.logger")
|
||||
def test_send_findings_to_jira_partial_failure(
|
||||
self,
|
||||
mock_logger,
|
||||
mock_initialize_integration,
|
||||
mock_integration_model,
|
||||
mock_finding_model,
|
||||
mock_rls_transaction,
|
||||
):
|
||||
"""Test partial failure when sending findings to Jira"""
|
||||
tenant_id = "tenant-123"
|
||||
integration_id = "integration-456"
|
||||
project_key = "PROJ"
|
||||
issue_type = "Task"
|
||||
finding_ids = ["finding-1", "finding-2", "finding-3"]
|
||||
|
||||
# Mock RLS transaction
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
# Mock integration
|
||||
integration = MagicMock()
|
||||
mock_integration_model.objects.get.return_value = integration
|
||||
|
||||
# Mock Jira integration with mixed results
|
||||
mock_jira_integration = MagicMock()
|
||||
mock_jira_integration.send_finding.side_effect = [
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
] # Second fails
|
||||
mock_initialize_integration.return_value = mock_jira_integration
|
||||
|
||||
# Mock findings (simplified for this test)
|
||||
findings = []
|
||||
for i in range(3):
|
||||
finding = MagicMock()
|
||||
finding.id = f"finding-{i + 1}"
|
||||
finding.check_id = f"check_{i + 1:03d}"
|
||||
finding.severity = "low"
|
||||
finding.status = "FAIL"
|
||||
finding.status_extended = ""
|
||||
finding.resource_regions = []
|
||||
finding.compliance = {}
|
||||
|
||||
finding.resources.exists.return_value = False
|
||||
finding.resources.first.return_value = None
|
||||
finding.scan.provider.provider = "aws"
|
||||
finding.check_metadata = {
|
||||
"checktitle": f"Check {i + 1}",
|
||||
"risk": "Low risk",
|
||||
"remediation": {"recommendation": {}, "code": {}},
|
||||
}
|
||||
findings.append(finding)
|
||||
|
||||
mock_finding_model.all_objects.select_related.return_value.prefetch_related.return_value.get.side_effect = findings
|
||||
|
||||
# Call the function
|
||||
result = send_findings_to_jira(
|
||||
tenant_id, integration_id, project_key, issue_type, finding_ids
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result == {"created_count": 2, "failed_count": 1}
|
||||
|
||||
# Verify error was logged for the failed finding
|
||||
mock_logger.error.assert_called_with("Failed to send finding finding-2 to Jira")
|
||||
|
||||
@patch("tasks.jobs.integrations.rls_transaction")
|
||||
@patch("tasks.jobs.integrations.Finding")
|
||||
@patch("tasks.jobs.integrations.Integration")
|
||||
@patch("tasks.jobs.integrations.initialize_prowler_integration")
|
||||
def test_send_findings_to_jira_no_resources(
|
||||
self,
|
||||
mock_initialize_integration,
|
||||
mock_integration_model,
|
||||
mock_finding_model,
|
||||
mock_rls_transaction,
|
||||
):
|
||||
"""Test sending findings to Jira when finding has no resources"""
|
||||
tenant_id = "tenant-123"
|
||||
integration_id = "integration-456"
|
||||
project_key = "PROJ"
|
||||
issue_type = "Task"
|
||||
finding_ids = ["finding-1"]
|
||||
|
||||
# Mock RLS transaction
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
# Mock integration
|
||||
integration = MagicMock()
|
||||
mock_integration_model.objects.get.return_value = integration
|
||||
|
||||
# Mock Jira integration
|
||||
mock_jira_integration = MagicMock()
|
||||
mock_jira_integration.send_finding.return_value = True
|
||||
mock_initialize_integration.return_value = mock_jira_integration
|
||||
|
||||
# Mock finding without resources
|
||||
finding = MagicMock()
|
||||
finding.id = "finding-1"
|
||||
finding.check_id = "check_001"
|
||||
finding.severity = "critical"
|
||||
finding.status = "FAIL"
|
||||
finding.status_extended = "Critical issue found"
|
||||
finding.resource_regions = None
|
||||
finding.compliance = {"pci": ["3.1"]}
|
||||
|
||||
finding.resources.exists.return_value = False
|
||||
finding.resources.first.return_value = None
|
||||
finding.scan.provider.provider = "gcp"
|
||||
finding.check_metadata = {
|
||||
"checktitle": "Critical Check",
|
||||
"risk": "Very high risk",
|
||||
"remediation": {
|
||||
"recommendation": {
|
||||
"text": "Immediate action required",
|
||||
"url": "https://example.com/critical",
|
||||
},
|
||||
"code": {
|
||||
"nativeiac": "",
|
||||
"terraform": "terraform fix",
|
||||
"cli": "",
|
||||
"other": "manual fix",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mock_finding_model.all_objects.select_related.return_value.prefetch_related.return_value.get.return_value = finding
|
||||
|
||||
# Call the function
|
||||
result = send_findings_to_jira(
|
||||
tenant_id, integration_id, project_key, issue_type, finding_ids
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result == {"created_count": 1, "failed_count": 0}
|
||||
|
||||
# Verify send_finding was called with empty resource fields
|
||||
call_kwargs = mock_jira_integration.send_finding.call_args.kwargs
|
||||
assert call_kwargs["resource_uid"] == ""
|
||||
assert call_kwargs["resource_name"] == ""
|
||||
assert call_kwargs["resource_tags"] == {}
|
||||
assert call_kwargs["region"] == ""
|
||||
|
||||
@patch("tasks.jobs.integrations.rls_transaction")
|
||||
@patch("tasks.jobs.integrations.Finding")
|
||||
@patch("tasks.jobs.integrations.Integration")
|
||||
@patch("tasks.jobs.integrations.initialize_prowler_integration")
|
||||
def test_send_findings_to_jira_with_empty_check_metadata(
|
||||
self,
|
||||
mock_initialize_integration,
|
||||
mock_integration_model,
|
||||
mock_finding_model,
|
||||
mock_rls_transaction,
|
||||
):
|
||||
"""Test sending findings to Jira when check_metadata is empty or missing fields"""
|
||||
tenant_id = "tenant-123"
|
||||
integration_id = "integration-456"
|
||||
project_key = "PROJ"
|
||||
issue_type = "Task"
|
||||
finding_ids = ["finding-1"]
|
||||
|
||||
# Mock RLS transaction
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
# Mock integration
|
||||
integration = MagicMock()
|
||||
mock_integration_model.objects.get.return_value = integration
|
||||
|
||||
# Mock Jira integration
|
||||
mock_jira_integration = MagicMock()
|
||||
mock_jira_integration.send_finding.return_value = True
|
||||
mock_initialize_integration.return_value = mock_jira_integration
|
||||
|
||||
# Mock finding with minimal/empty check_metadata
|
||||
finding = MagicMock()
|
||||
finding.id = "finding-1"
|
||||
finding.check_id = "check_001"
|
||||
finding.severity = "low"
|
||||
finding.status = "PASS"
|
||||
finding.status_extended = None
|
||||
finding.resource_regions = []
|
||||
finding.compliance = None
|
||||
|
||||
finding.resources.exists.return_value = False
|
||||
finding.resources.first.return_value = None
|
||||
finding.scan.provider.provider = "kubernetes"
|
||||
finding.check_metadata = {} # Empty metadata
|
||||
|
||||
mock_finding_model.all_objects.select_related.return_value.prefetch_related.return_value.get.return_value = finding
|
||||
|
||||
# Call the function
|
||||
result = send_findings_to_jira(
|
||||
tenant_id, integration_id, project_key, issue_type, finding_ids
|
||||
)
|
||||
|
||||
# Assertions
|
||||
assert result == {"created_count": 1, "failed_count": 0}
|
||||
|
||||
# Verify send_finding was called with default/empty values
|
||||
call_kwargs = mock_jira_integration.send_finding.call_args.kwargs
|
||||
assert call_kwargs["check_title"] == ""
|
||||
assert call_kwargs["risk"] == ""
|
||||
assert call_kwargs["recommendation_text"] == ""
|
||||
assert call_kwargs["recommendation_url"] == ""
|
||||
assert call_kwargs["remediation_code_native_iac"] == ""
|
||||
assert call_kwargs["remediation_code_terraform"] == ""
|
||||
assert call_kwargs["remediation_code_cli"] == ""
|
||||
assert call_kwargs["remediation_code_other"] == ""
|
||||
assert call_kwargs["compliance"] == {}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* Override Tailwind CSS reset for markdown content */
|
||||
.markdown-content ul {
|
||||
list-style: disc !important;
|
||||
margin-left: 20px !important;
|
||||
padding-left: 10px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.markdown-content ol {
|
||||
list-style: decimal !important;
|
||||
margin-left: 20px !important;
|
||||
padding-left: 10px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 4px !important;
|
||||
display: list-item !important;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
/* Ensure nested lists work properly */
|
||||
.markdown-content ul ul {
|
||||
margin-top: 4px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.markdown-content ol ol {
|
||||
margin-top: 4px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
@@ -1654,6 +1654,39 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
[
|
||||
html.Div(
|
||||
[
|
||||
# Description as first details item
|
||||
html.Div(
|
||||
[
|
||||
html.P(
|
||||
html.Strong(
|
||||
"Description: ",
|
||||
style={
|
||||
"margin-bottom": "8px"
|
||||
},
|
||||
)
|
||||
),
|
||||
html.Div(
|
||||
dcc.Markdown(
|
||||
str(
|
||||
data.get(
|
||||
"DESCRIPTION",
|
||||
"",
|
||||
)
|
||||
),
|
||||
dangerously_allow_html=True,
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
className="markdown-content",
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
html.P(
|
||||
@@ -1793,19 +1826,27 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
html.P(
|
||||
html.Strong(
|
||||
"Risk: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
},
|
||||
style={},
|
||||
)
|
||||
),
|
||||
html.P(
|
||||
str(data.get("RISK", "")),
|
||||
html.Div(
|
||||
dcc.Markdown(
|
||||
str(
|
||||
data.get("RISK", "")
|
||||
),
|
||||
dangerously_allow_html=True,
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
className="markdown-content",
|
||||
style={
|
||||
"margin-left": "5px"
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
],
|
||||
style={"display": "flex"},
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
@@ -1847,23 +1888,32 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
|
||||
html.Strong(
|
||||
"Recommendation: ",
|
||||
style={
|
||||
"margin-right": "5px"
|
||||
"margin-bottom": "8px"
|
||||
},
|
||||
)
|
||||
),
|
||||
html.P(
|
||||
str(
|
||||
data.get(
|
||||
"REMEDIATION_RECOMMENDATION_TEXT",
|
||||
"",
|
||||
)
|
||||
html.Div(
|
||||
dcc.Markdown(
|
||||
str(
|
||||
data.get(
|
||||
"REMEDIATION_RECOMMENDATION_TEXT",
|
||||
"",
|
||||
)
|
||||
),
|
||||
dangerously_allow_html=True,
|
||||
style={
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
className="markdown-content",
|
||||
style={
|
||||
"margin-left": "5px"
|
||||
"margin-left": "0px",
|
||||
"padding-left": "10px",
|
||||
},
|
||||
),
|
||||
],
|
||||
style={"display": "flex"},
|
||||
style={"margin-bottom": "15px"},
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Running Prowler
|
||||
|
||||
Running Prowler requires specifying the provider (e.g `aws`, `gcp`, `azure`, `m365`, `github` or `kubernetes`):
|
||||
Running Prowler requires specifying the provider (e.g `aws`, `gcp`, `azure`, `kubernetes`, `m365`, `github`, `iac` or `mongodbatlas`):
|
||||
|
||||
???+ note
|
||||
If no provider is specified, AWS is used by default for backward compatibility with Prowler v2.
|
||||
@@ -255,3 +255,28 @@ prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/tes
|
||||
- For more details on supported scanners, see the [Trivy documentation](https://trivy.dev/latest/docs/scanner/vulnerability/)
|
||||
|
||||
See more details about IaC scanning in the [IaC Tutorial](../tutorials/iac/getting-started-iac.md) section.
|
||||
|
||||
## MongoDB Atlas
|
||||
|
||||
Prowler allows you to scan your MongoDB Atlas cloud database deployments for security and compliance issues.
|
||||
|
||||
Authentication is done using MongoDB Atlas API key pairs:
|
||||
|
||||
```console
|
||||
# Using command-line arguments
|
||||
prowler mongodbatlas --atlas-public-key <public_key> --atlas-private-key <private_key>
|
||||
|
||||
# Using environment variables
|
||||
export ATLAS_PUBLIC_KEY=<public_key>
|
||||
export ATLAS_PRIVATE_KEY=<private_key>
|
||||
prowler mongodbatlas
|
||||
```
|
||||
|
||||
You can filter scans to specific organizations or projects:
|
||||
|
||||
```console
|
||||
# Scan specific project
|
||||
prowler mongodbatlas --atlas-project-id <project_id>
|
||||
```
|
||||
|
||||
See more details about MongoDB Atlas Authentication in [MongoDB Atlas Authentication](../tutorials/mongodbatlas/authentication.md)
|
||||
@@ -0,0 +1,213 @@
|
||||
# Check Metadata Guidelines
|
||||
|
||||
## Introduction
|
||||
|
||||
This guide provides comprehensive guidelines for creating check metadata in Prowler. For basic information on check metadata structure, refer to the [check metadata](./checks.md#metadata-structure-for-prowler-checks) section.
|
||||
|
||||
## Check Title Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Determine Resource Finding Scope (Singular vs. Plural)**:
|
||||
When determining whether to use singular or plural in the check title, examine the code for certain patterns. If the code contains a loop that generates an individual report for each resource, use the singular form. If the code produces a single report that covers all resources collectively, use the plural form. For organization- or account-wide checks, select the scope that best matches the breadth of the evaluation. Additionally, review the `status_extended` field messages in the code, as they often provide clues about whether the check is scoped to individual resources or to groups of resources.
|
||||
Analyze the detection code to determine if the check reports on individual resources or aggregated resources:
|
||||
- **Singular**: Use when the check creates one report per resource (e.g., "EC2 instance has IMDSv2 enforced", "S3 bucket does not allow public write access").
|
||||
- **Plural**: Use when the check creates one report for all resources together (e.g., "All EC2 instances have IMDSv2 enforced", "S3 buckets do not allow public write access").
|
||||
2. **Describe the Compliant (*PASS*) State**:
|
||||
Always write the title to describe the **desired, compliant state** of the resources. The title should reflect what it looks like when the audited resource is following the check's requirements.
|
||||
3. **Be Specific and Factual**:
|
||||
Include the exact secure configuration being verified. Avoid vague or generic terms like "properly configured".
|
||||
4. **Avoid Redundant or Action Words**:
|
||||
Do not include verbs like "Check", "Verify", "Ensure", or "Monitor". The title is a declarative statement of the secure condition.
|
||||
5. **Length Limit**:
|
||||
Keep the title under 150 characters.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- Starting with verbs like "Check", "Verify", "Ensure", "Make sure". Always start with the affected resource instead.
|
||||
- Being too vague or generic (e.g., "Ensure security groups are properly configured", what does it mean? "properly configured" is not a clear description of the compliant state).
|
||||
- Focusing on the non-compliant state instead of the compliant state.
|
||||
- Using unclear scope and resource identification.
|
||||
|
||||
## Check Type Guidelines (AWS Only)
|
||||
|
||||
### AWS Security Hub Type Format
|
||||
|
||||
AWS Security Hub uses a three-part type taxonomy:
|
||||
|
||||
- **Namespace**: The top-level security domain.
|
||||
- **Category**: The security control family or area.
|
||||
- **Classifier**: The specific security concern (optional).
|
||||
|
||||
A partial path may be defined (e.g., `TTPs` or `TTPs/Defense Evasion` are valid).
|
||||
|
||||
### Selection Guidelines
|
||||
|
||||
1. **Be Specific**: Use the most specific classifier that accurately describes the check.
|
||||
2. **Standard Compliance**: Consider if the check relates to specific compliance standards.
|
||||
3. **Multiple Types**: You can specify multiple types if the check addresses multiple concerns.
|
||||
|
||||
## Description Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Focus on the Finding**: All fields should address how the finding affects the security posture, rather than the control itself.
|
||||
2. **Use Natural Language**: Write in simple, clear paragraphs with complete, grammatically correct sentences.
|
||||
3. **Use Markdown Formatting**: Enhance readability with:
|
||||
- Use **bold** for emphasis on key security concepts.
|
||||
- Use *italic* for a secondary emphasis. Use it for clarifications, conditions, or optional notes. But don't abuse it.
|
||||
- Use `code` formatting for specific configuration values, or technical details. Don't use it for service names or common technical terms.
|
||||
- Use one or two line breaks (`\n` or `\n\n`) to separate distinct ideas.
|
||||
- Use bullet points (`-`) for listing multiple concepts or actions.
|
||||
- Use numbers for listing steps or sequential actions.
|
||||
4. **Be Concise**: Maximum 400 characters (spaces count). Every word should add value.
|
||||
5. **Explain What the Finding Means**: Focus on what the security control evaluates and what it means when it passes or fails, but without explicitly stating the pass or fail state.
|
||||
6. **Be Technical but Clear**: Use appropriate technical terminology while remaining understandable.
|
||||
7. **Avoid Risk Descriptions**: Do not describe potential risks, threats, or consequences.
|
||||
8. **CheckTitle and Description can be the same**: If the check is very simple and the title is already clear, you can use the same text for the description.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- **Technical Implementation Details**: "The control loops through all instances and calls the describe_instances API...".
|
||||
- **Vague Descriptions**: "This control verifies proper configuration of resources". What does it mean? "proper configuration" is not a clear description of the compliant state.
|
||||
- **Risk Descriptions**: "This could lead to data breaches" or "This poses a security threat".
|
||||
- **Starting with Verbs**: "Check if...", "Verify...", "Ensure...". Always start with the affected resource instead.
|
||||
- **References to Pass/Fail States**: Avoid using words like "pass" or "fail".
|
||||
|
||||
## Risk Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Explain the Cybersecurity Impact**: Focus on how the finding affects confidentiality, integrity, or availability (CIA triad). If the CIA triad does not apply, explain the risk in terms of the organization's business objectives.
|
||||
2. **Be Specific About Threats**: Clearly state what could happen if this security control is not in place. What attacks or incidents become possible?
|
||||
3. **Focus on Risk Context**: Explain the specific security implications of the finding, not just generic security risks.
|
||||
4. **Use Markdown Formatting**: Enhance readability with markdown formatting:
|
||||
- Use **bold** for emphasis on key security concepts.
|
||||
- Use *italic* for a secondary emphasis. Use it for clarifications, conditions, or optional notes. But don't abuse it.
|
||||
- Use `code` formatting for specific configuration values, or technical details. Don't use it for service names or common technical terms.
|
||||
- Use one or two line breaks (`\n` or `\n\n`) to separate distinct ideas.
|
||||
- Use bullet points (`-`) for listing multiple concepts or actions.
|
||||
- Use numbers for listing steps or sequential actions.
|
||||
5. **Be Concise**: Maximum 400 characters. Make every word count.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- **Generic Risks**: "This could lead to security issues" or "Regulatory compliance violations".
|
||||
- **Technical Implementation Focus**: "The API call might fail and return incorrect results...".
|
||||
- **Overly Broad Statements**: "This is a serious security risk that could impact everything".
|
||||
- **Vague Threats**: "This could be exploited by threat actors" without explaining how.
|
||||
|
||||
## Recommendation Guidelines
|
||||
|
||||
### Writing Guidelines
|
||||
|
||||
1. **Provide Actionable Best Practice Guidance**: Explain what should be done to maintain security posture. Focus on preventive measures and proactive security practices.
|
||||
2. **Be Principle-Based**: Reference established security principles (least privilege, defense in depth, zero trust, separation of duties) where applicable.
|
||||
3. **Focus on Prevention**: Explain best practices that prevent the security issue from occurring, not just detection or remediation.
|
||||
4. **Use Markdown Formatting**: Enhance readability with markdown formatting:
|
||||
- Use **bold** for emphasis on key security concepts.
|
||||
- Use *italic* for a secondary emphasis. Use it for clarifications, conditions, or optional notes. But don't abuse it.
|
||||
- Use `code` formatting for specific configuration values, or technical details. Don't use it for service names or common technical terms.
|
||||
- Use one or two line breaks (`\n` or `\n\n`) to separate distinct ideas.
|
||||
- Use bullet points (`-`) for listing multiple concepts or actions.
|
||||
- Use numbers for listing steps or sequential actions.
|
||||
5. **Be Concise**: Maximum 400 characters.
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- **Specific Remediation Steps**: "1. Go to the console\n2. Click on settings..." - Focus on principles, not click-by-click instructions.
|
||||
- **Implementation Details**: "Configure the JSON policy with the following IAM actions..." - Explain what to achieve, not how.
|
||||
- **Vague Guidance**: "Follow security best practices..." without explaining what those practices are.
|
||||
- **Resource-Specific Recommendations**: "Enable MFA on user john.doe@example.com" - Keep it general.
|
||||
- **Missing Context**: Not explaining why the best practice is important for security.
|
||||
|
||||
### Good Examples
|
||||
|
||||
- *"Avoid exposing sensitive resources directly to the Internet; configure access controls to limit exposure."*
|
||||
- *"Apply the principle of least privilege when assigning permissions to users and services."*
|
||||
- *"Regularly review and update your security configurations to align with current best practices."*
|
||||
|
||||
## Remediation Code Guidelines
|
||||
|
||||
### Critical Requirement
|
||||
|
||||
The **fundamental principle** is to focus on the **specific change** that converts the finding from non-compliant to compliant.
|
||||
|
||||
Also is important to keep all code examples as short as possible, including the essential code to fix the issue. Remove any extra configuration, optional parameters, or nice-to-have settings and add comments to explain the code when possible.
|
||||
|
||||
### Common Guidelines for All Code Fields
|
||||
|
||||
1. **Be Minimal**: Keep code blocks as short as possible - only include what is absolutely necessary.
|
||||
2. **Focus on the Fix**: Remove any extra configuration, optional parameters, or nice-to-have settings.
|
||||
3. **Be Accurate**: Ensure all commands and code are syntactically correct.
|
||||
4. **Use Markdown Formatting**: Format code properly using code blocks and appropriate syntax highlighting.
|
||||
5. **Follow Best Practices**: Use the most secure and recommended approaches for each platform.
|
||||
|
||||
### CLI Guidelines
|
||||
|
||||
- Only provide a single command that directly changes the finding from fail to pass.
|
||||
- The command must be executable as-is and resolve the security issue completely.
|
||||
- Use proper command syntax for the provider (AWS CLI, Azure CLI, gcloud, kubectl, etc.).
|
||||
- Do not use markdown formatting or code blocks - just the raw command.
|
||||
- Do not include multiple commands, comments, or explanations.
|
||||
- If the issue cannot be resolved with a single command, leave this field empty.
|
||||
|
||||
### Native IaC Guidelines
|
||||
|
||||
- **Keep It Minimal**: Only include the specific resource/configuration that fixes the security issue.
|
||||
- Format as markdown code blocks with proper syntax highlighting.
|
||||
- Include only the required properties to fix the issue.
|
||||
- Add comments indicating the critical line(s) that remediate the check.
|
||||
- Use `example_resource` as the generic name for all resources and IDs.
|
||||
|
||||
### Terraform Guidelines
|
||||
|
||||
- **Keep It Minimal**: Only include the specific resource/configuration that fixes the security issue.
|
||||
- Provide valid HCL (HashiCorp Configuration Language) code with an example of a compliant configuration.
|
||||
- Use the latest Terraform syntax and provider versions.
|
||||
- Include only the required arguments to fix the issue - skip optional parameters.
|
||||
- Format as markdown code blocks with `hcl` syntax highlighting.
|
||||
- Add comments indicating the critical line(s) that remediate the check.
|
||||
- Use `example_resource` as the generic name for all resources and IDs.
|
||||
- Skip provider requirements unless critical for the fix.
|
||||
|
||||
### Other (Manual Steps) Guidelines
|
||||
|
||||
- **Keep It Minimal**: Only include the exact steps needed to fix the security issue.
|
||||
- Provide step-by-step instructions for manual remediation through web interfaces.
|
||||
- Use numbered lists for sequential steps.
|
||||
- Be specific about menu locations, button names, and settings.
|
||||
- Skip optional configurations or nice-to-have settings.
|
||||
- Format using markdown for better readability.
|
||||
|
||||
## Categories Guidelines
|
||||
|
||||
### Selection Guidelines
|
||||
|
||||
1. **Be Specific**: Only select categories that directly relate to what the automated control evaluates.
|
||||
2. **Primary Focus**: Consider the primary security concern the automated control addresses.
|
||||
3. **Avoid Over-Categorization**: Do not select categories just because they are tangentially related.
|
||||
|
||||
### Available Categories
|
||||
|
||||
| Category | Definition |
|
||||
|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| encryption | Ensures data is encrypted in transit and/or at rest, including key management practices |
|
||||
| internet-exposed | Checks that limit or flag public access to services, APIs, or assets from the Internet |
|
||||
| logging | Ensures appropriate logging of events, activities, and system interactions for traceability |
|
||||
| secrets | Manages and protects credentials, API keys, tokens, and other sensitive information |
|
||||
| resilience | Ensures systems can maintain availability and recover from disruptions, failures, or degradation. Includes redundancy, fault-tolerance, auto-scaling, backup, disaster recovery, and failover strategies |
|
||||
| threat-detection | Identifies suspicious activity or behaviors using IDS, malware scanning, or anomaly detection |
|
||||
| trust-boundaries | Enforces isolation or segmentation between different trust levels (e.g., VPCs, tenants, network zones) |
|
||||
| vulnerabilities | Detects or remediates known software, infrastructure, or config vulnerabilities (e.g., CVEs) |
|
||||
| cluster-security | Secures Kubernetes cluster components such as API server, etcd, and role-based access |
|
||||
| container-security | Ensures container images and runtimes follow security best practices |
|
||||
| node-security | Secures nodes running containers or services |
|
||||
| gen-ai | Checks related to safe and secure use of generative AI services or models |
|
||||
| ci-cd | Ensures secure configurations in CI/CD pipelines |
|
||||
| identity-access | Governs user and service identities, including least privilege, MFA, and permission boundaries |
|
||||
| email-security | Ensures detection and protection against phishing, spam, spoofing, etc. |
|
||||
| forensics-ready | Ensures systems are instrumented to support post-incident investigations. Any digital trace or evidence (logs, volume snapshots, memory dumps, network captures, etc.) preserved immutably and accompanied by integrity guarantees, which can be used in a forensic analysis |
|
||||
| software-supply-chain | Detects or prevents tampering, unauthorized packages, or third-party risks in software supply chain |
|
||||
| e3 | M365-specific controls enabled by or dependent on an E3 license (e.g., baseline security policies, conditional access) |
|
||||
| e5 | M365-specific controls enabled by or dependent on an E5 license (e.g., advanced threat protection, audit, DLP, and eDiscovery) |
|
||||
@@ -40,7 +40,7 @@ Each check in Prowler follows a straightforward structure. Within the newly crea
|
||||
|
||||
- `__init__.py` (empty file) – Ensures Python treats the check folder as a package.
|
||||
- `<check_name>.py` (code file) – Contains the check logic, following the prescribed format. Please refer to the [prowler's check code structure](./checks.md#prowlers-check-code-structure) for more information.
|
||||
- `<check_name>.metadata.json` (metadata file) – Defines the check's metadata for contextual information. Please refer to the [check metadata](./checks.md#) for more information.
|
||||
- `<check_name>.metadata.json` (metadata file) – Defines the check's metadata for contextual information. Please refer to the [check metadata](./checks.md#metadata-structure-for-prowler-checks) for more information.
|
||||
|
||||
## Prowler's Check Code Structure
|
||||
|
||||
@@ -226,68 +226,148 @@ Below is a generic example of a check metadata file. **Do not include comments i
|
||||
```json
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "example_check_id",
|
||||
"CheckTitle": "Example Check Title",
|
||||
"CheckType": ["Infrastructure Security"],
|
||||
"ServiceName": "ec2",
|
||||
"SubServiceName": "ami",
|
||||
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
|
||||
"Severity": "critical",
|
||||
"CheckID": "service_resource_security_setting",
|
||||
"CheckTitle": "Service resource has security setting enabled",
|
||||
"CheckType": [],
|
||||
"ServiceName": "service",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "Other",
|
||||
"Description": "Example description of the check.",
|
||||
"Risk": "Example risk if the check fails.",
|
||||
"RelatedUrl": "https://example.com",
|
||||
"Description": "This check verifies that the service resource has the required **security setting** enabled to protect against potential vulnerabilities.\n\nIt ensures that the resource follows security best practices and maintains proper access controls. The check evaluates whether the security configuration is properly implemented and active.",
|
||||
"Risk": "Without proper security settings, the resource may be vulnerable to:\n\n- **Unauthorized access** - Malicious actors could gain entry\n- **Data breaches** - Sensitive information could be compromised\n- **Security threats** - Various attack vectors could be exploited\n\nThis could result in compliance violations and potential financial or reputational damage.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": ["https://example.com/security-documentation", "https://example.com/best-practices"],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "example CLI command",
|
||||
"NativeIaC": "",
|
||||
"Other": "",
|
||||
"Terraform": ""
|
||||
"CLI": "provider-cli service enable-security-setting --resource-id resource-123",
|
||||
"NativeIaC": "```yaml\nType: Provider::Service::Resource\nProperties:\n SecuritySetting: enabled\n ResourceId: resource-123\n```",
|
||||
"Other": "1. Open the provider management console\n2. Navigate to the service section\n3. Select the resource\n4. Enable the security setting\n5. Save the configuration",
|
||||
"Terraform": "```hcl\nresource \"provider_service_resource\" \"example\" {\n resource_id = \"resource-123\"\n security_setting = true\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Example recommendation text.",
|
||||
"Url": "https://example.com/remediation"
|
||||
"Text": "Enable security settings on all service resources to ensure proper protection. Regularly review and update security configurations to align with current best practices.",
|
||||
"Url": "https://hub.prowler.com/check/service_resource_security_setting"
|
||||
}
|
||||
},
|
||||
"Categories": ["example-category"],
|
||||
"Categories": ["internet-exposed", "secrets"],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": ""
|
||||
"RelatedTo": ["service_resource_security_setting", "service_resource_security_setting_2"],
|
||||
"Notes": "This is a generic example check that should be customized for specific provider and service requirements."
|
||||
}
|
||||
```
|
||||
|
||||
### Metadata Fields and Their Purpose
|
||||
|
||||
- **Provider** — The Prowler provider related to the check. The name **must** be lowercase and match the provider folder name. For supported providers refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
- **CheckID** — The unique identifier for the check inside the provider, this field **must** match the check's folder and python file and json metadata file name. For more information about the naming refer to the [Naming Format for Checks](#naming-format-for-checks) section.
|
||||
- **CheckTitle** — A concise, descriptive title for the check.
|
||||
- **CheckType** — *For now this field is only standardized for the AWS provider*.
|
||||
- For AWS this field must follow the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format. So the common pattern to follow is `namespace/category/classifier`, refer to the attached documentation for the valid values for this fields.
|
||||
- **ServiceName** — The name of the provider service being audited. This field **must** be in lowercase and match with the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
- **SubServiceName** — The subservice or resource within the service, if applicable. For more information refer to the [Naming Format for Checks](#naming-format-for-checks) section.
|
||||
- **ResourceIdTemplate** — A template for the unique resource identifier. For more information refer to the [Resource Identification in Prowler](#resource-identification-in-prowler) section.
|
||||
- **Severity** — The severity of the finding if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, this field **must** be in lowercase. To get more information about the severity levels refer to the [Prowler's Check Severity Levels](#prowlers-check-severity-levels) section.
|
||||
- **ResourceType** — The type of resource being audited. *For now this field is only standardized for the AWS provider*.
|
||||
- For AWS use the [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or, if not available, the PascalCase version of the [CloudFormation type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) (e.g., `AwsEc2Instance`). Use "Other" if no match exists.
|
||||
- **Description** — A short description of what the check does.
|
||||
- **Risk** — The risk or impact if the check fails, explaining why the finding matters.
|
||||
- **RelatedUrl** — A URL to official documentation or further reading about the check's purpose. If no official documentation is available, use the risk and recommendation text from trusted third-party sources.
|
||||
- **Remediation** — Guidance for fixing a failed check, including:
|
||||
- **Code** — Remediation commands or code snippets for CLI, Terraform, native IaC, or other tools like the Web Console.
|
||||
- **Recommendation** — A textual human readable recommendation. Here it is not necessary to include actual steps, but rather a general recommendation about what to do to fix the check.
|
||||
- **Categories** — One or more categories for grouping checks in execution (e.g., `internet-exposed`). For the current list of categories, refer to the [Prowler Hub](https://hub.prowler.com/check).
|
||||
- **DependsOn** — Currently not used.
|
||||
- **RelatedTo** — Currently not used.
|
||||
- **Notes** — Any additional information not covered by other fields.
|
||||
#### Provider
|
||||
|
||||
### Remediation Code Guidelines
|
||||
The Prowler provider related to the check. The name **must** be lowercase and match the provider folder name. For supported providers refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
|
||||
When providing remediation steps, reference the following sources:
|
||||
#### CheckID
|
||||
|
||||
- Official provider documentation.
|
||||
- [Prowler Checks Remediation Index](https://docs.prowler.com/checks/checks-index)
|
||||
- [TrendMicro Cloud One Conformity](https://www.trendmicro.com/cloudoneconformity)
|
||||
- [CloudMatos Remediation Repository](https://github.com/cloudmatos/matos/tree/master/remediations)
|
||||
The unique identifier for the check inside the provider. This field **must** match the check's folder, Python file, and JSON metadata file name. For more information about naming, refer to the [Naming Format for Checks](#naming-format-for-checks) section.
|
||||
|
||||
#### CheckTitle
|
||||
|
||||
The `CheckTitle` field must be plain text, clearly and succinctly define **the best practice being evaluated and which resource(s) each finding applies to**. The title should be specific, concise (no more than 150 characters), and reference the relevant resource(s) involved.
|
||||
|
||||
**Always write the `CheckTitle` to describe the *PASS* case**, the desired secure or compliant state of the resource(s). This helps ensure that findings are easy to interpret and that the title always reflects the best practice being met.
|
||||
|
||||
For detailed guidelines on writing effective check titles, including how to determine singular vs. plural scope and common mistakes to avoid, see [CheckTitle Guidelines](./check-metadata-guidelines.md#checktitle-guidelines).
|
||||
|
||||
#### CheckType
|
||||
|
||||
???+ warning
|
||||
This field is only applicable to the AWS provider.
|
||||
|
||||
It follows the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format using the pattern `namespace/category/classifier`.
|
||||
|
||||
For the complete AWS Security Hub selection guidelines, see [CheckType Guidelines](./check-metadata-guidelines.md#checktype-guidelines-aws-only).
|
||||
|
||||
#### ServiceName
|
||||
|
||||
The name of the provider service being audited. Must be lowercase and match the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or the [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
|
||||
|
||||
#### SubServiceName
|
||||
|
||||
This field is in the process of being deprecated and should be **left empty**.
|
||||
|
||||
#### ResourceIdTemplate
|
||||
|
||||
This field is in the process of being deprecated and should be **left empty**.
|
||||
|
||||
#### Severity
|
||||
|
||||
Severity level if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, and written in lowercase. See [Prowler's Check Severity Levels](#prowlers-check-severity-levels) for details.
|
||||
|
||||
#### ResourceType
|
||||
|
||||
The type of resource being audited. This field helps categorize and organize findings by resource type for better analysis and reporting. For each provider:
|
||||
|
||||
- **AWS**: Use [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or PascalCase CloudFormation types removing the `::` separator used in CloudFormation templates (e.g., in CloudFormation template the type of an EC2 instance is `AWS::EC2::Instance` but in the check it should be `AwsEc2Instance`). Use `Other` if none apply.
|
||||
- **Azure**: Use types from [Azure Resource Graph](https://learn.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources), for example: `Microsoft.Storage/storageAccounts`.
|
||||
- **Google Cloud**: Use [Cloud Asset Inventory asset types](https://cloud.google.com/asset-inventory/docs/asset-types), for example: `compute.googleapis.com/Instance`.
|
||||
- **Kubernetes**: Use types shown under `KIND` from `kubectl api-resources`.
|
||||
- **M365 / GitHub**: Leave empty due to lack of standardized types.
|
||||
|
||||
#### Description
|
||||
|
||||
A concise, natural language explanation that **clearly describes what the finding means**, focusing on clarity and context rather than technical implementation details. Use simple paragraphs with line breaks if needed, but avoid sections, code blocks, or complex formatting. This field is limited to maximum 400 characters.
|
||||
|
||||
For detailed writing guidelines and common mistakes to avoid, see [Description Guidelines](./check-metadata-guidelines.md#description-guidelines).
|
||||
|
||||
#### Risk
|
||||
|
||||
A clear, natural language explanation of **why this finding poses a cybersecurity risk**. Focus on how it may impact confidentiality, integrity, or availability. If those do not apply, describe any relevant operational or financial risks. Use simple paragraphs with line breaks if needed, but avoid sections, code blocks, or complex formatting. Limit your explanation to 400 characters.
|
||||
|
||||
For detailed writing guidelines and common mistakes to avoid, see [Risk Guidelines](./check-metadata-guidelines.md#risk-guidelines).
|
||||
|
||||
#### RelatedUrl
|
||||
|
||||
*Deprecated*. Use `AdditionalURLs` for adding your URLs references.
|
||||
|
||||
#### AdditionalURLs
|
||||
|
||||
???+ warning
|
||||
URLs must be valid and not repeated.
|
||||
|
||||
A list of official documentation URLs for further reading. These should be authoritative sources that provide additional context, best practices, or detailed information about the security control being checked. Prefer official provider documentation, security standards, or well-established security resources. Avoid third-party blogs or unofficial sources unless they are highly reputable and directly relevant.
|
||||
|
||||
#### Remediation
|
||||
|
||||
Provides both code examples and best practice recommendations for addressing the security issue.
|
||||
|
||||
- **Code**: Contains remediation examples in different formats:
|
||||
- **CLI**: Command-line interface commands to make the finding compliant in runtime.
|
||||
- **NativeIaC**: Native Infrastructure as Code templates with an example of a compliant configuration. For now it applies to:
|
||||
- **AWS**: CloudFormation YAML formatted code (do not use JSON format).
|
||||
- **Azure**: Bicep formatted code (do not use ARM templates).
|
||||
- **Terraform**: HashiCorp Configuration Language (HCL) code with an example of a compliant configuration.
|
||||
- **Other**: Manual steps through web interfaces or other tools to make the finding compliant.
|
||||
|
||||
For detailed guidelines on writing remediation code, see [Remediation Code Guidelines](./check-metadata-guidelines.md#remediation-code-guidelines).
|
||||
|
||||
- **Recommendation**
|
||||
- **Text**: Generic best practice guidance in natural language using Markdown format (maximum 400 characters). For writing guidelines, see [Recommendation Guidelines](./check-metadata-guidelines.md#recommendation-guidelines).
|
||||
- **Url**: [Prowler Hub URL](https://hub.prowler.com/) of the check. This URL is always composed by `https://hub.prowler.com/check/<check_id>`.
|
||||
|
||||
#### Categories
|
||||
|
||||
One or more functional groupings used for execution filtering (e.g., `internet-exposed`). You can define new categories just by adding to this field.
|
||||
|
||||
For the complete list of available categories, see [Categories Guidelines](./check-metadata-guidelines.md#categories-guidelines).
|
||||
|
||||
#### DependsOn
|
||||
|
||||
List of check IDs of checks that if are compliant, this check will be a compliant too or it is not going to give any finding.
|
||||
|
||||
#### RelatedTo
|
||||
|
||||
List of check IDs of checks that are conceptually related, even if they do not share a technical dependency.
|
||||
|
||||
#### Notes
|
||||
|
||||
Any additional information not covered in the above fields.
|
||||
|
||||
### Python Model Reference
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ Prowler supports multiple output formats, allowing users to tailor findings pres
|
||||
finding_dict["DESCRIPTION"] = finding.metadata.Description
|
||||
finding_dict["RISK"] = finding.metadata.Risk
|
||||
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
|
||||
finding_dict["ADDITIONAL_URLS"] = unroll_list(finding.metadata.AdditionalURLs)
|
||||
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
|
||||
finding.metadata.Remediation.Recommendation.Text
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ A provider is any platform or service that offers resources, data, or functional
|
||||
- Software as a Service (SaaS) Platforms (like Microsoft 365)
|
||||
- Development Platforms (like GitHub)
|
||||
- Container Orchestration Platforms (like Kubernetes)
|
||||
- Database-as-a-Service Platforms (like MongoDB Atlas)
|
||||
|
||||
For providers supported by Prowler, refer to [Prowler Hub](https://hub.prowler.com/).
|
||||
|
||||
@@ -63,6 +64,7 @@ Given the complexity and variability of providers, use existing provider impleme
|
||||
- [Kubernetes](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/kubernetes_provider.py)
|
||||
- [Microsoft365](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/microsoft365/microsoft365_provider.py)
|
||||
- [GitHub](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/github/github_provider.py)
|
||||
- [MongoDB Atlas](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/mongodbatlas/mongodbatlas_provider.py)
|
||||
|
||||
### Basic Provider Implementation: Pseudocode Example
|
||||
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
|
||||
The official supported providers right now are:
|
||||
|
||||
- **AWS**
|
||||
- **Azure**
|
||||
- **Google Cloud**
|
||||
- **Kubernetes**
|
||||
- **M365**
|
||||
- **Github**
|
||||
- **IaC**
|
||||
|
||||
Unofficially, Prowler supports: NHN.
|
||||
| Provider | Support | Stage | Interface |
|
||||
|----------|--------|-------|----------|
|
||||
| **AWS** | Official | Stable | UI, API, CLI |
|
||||
| **Azure** | Official | Stable | UI, API, CLI |
|
||||
| **Google Cloud** | Official | Stable | UI, API, CLI |
|
||||
| **Kubernetes** | Official | Stable | UI, API, CLI |
|
||||
| **M365** | Official | Stable | UI, API, CLI |
|
||||
| **Github** | Official | Stable | UI, API, CLI |
|
||||
| **IaC** | Official | Beta | CLI |
|
||||
| **MongoDB Atlas** | Official | Beta | CLI |
|
||||
| **NHN** | Unofficial | Beta | CLI |
|
||||
|
||||
Prowler supports **auditing, incident response, continuous monitoring, hardening, forensic readiness, and remediation**.
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
Prowler requires AWS credentials to function properly. Authentication is available through the following methods:
|
||||
|
||||
- Static Credentials
|
||||
- Assumed Role
|
||||
|
||||
## Required Permissions
|
||||
|
||||
To ensure full functionality, attach the following AWS managed policies to the designated user or role:
|
||||
|
||||
- `arn:aws:iam::aws:policy/SecurityAudit`
|
||||
@@ -13,37 +17,114 @@ To ensure full functionality, attach the following AWS managed policies to the d
|
||||
For certain checks, additional read-only permissions are required. Attach the following custom policy to your role: [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json)
|
||||
|
||||
|
||||
## Configure AWS Credentials
|
||||
## Assume Role (Recommended)
|
||||
|
||||
Use one of the following methods to authenticate:
|
||||
This method grants permanent access and is the recommended setup for production environments.
|
||||
|
||||
```console
|
||||
aws configure
|
||||
```
|
||||
=== "CloudFormation"
|
||||
|
||||
or
|
||||
1. Download the [Prowler Scan Role Template](https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/permissions/templates/cloudformation/prowler-scan-role.yml)
|
||||
|
||||
```console
|
||||
export AWS_ACCESS_KEY_ID="ASXXXXXXX"
|
||||
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
|
||||
export AWS_SESSION_TOKEN="XXXXXXXXX"
|
||||
```
|
||||

|
||||
|
||||
These credentials must be associated with a user or role with the necessary permissions to perform security checks.
|
||||

|
||||
|
||||
2. Open the [AWS Console](https://console.aws.amazon.com), search for **CloudFormation**
|
||||
|
||||

|
||||
|
||||
## AWS Profiles
|
||||
3. Go to **Stacks** and click "Create stack" > "With new resources (standard)"
|
||||
|
||||
Specify a custom AWS profile using the following command:
|
||||

|
||||
|
||||
```console
|
||||
prowler aws -p/--profile <profile_name>
|
||||
```
|
||||
4. In **Specify Template**, choose "Upload a template file" and select the downloaded file
|
||||
|
||||
## Multi-Factor Authentication (MFA)
|
||||

|
||||

|
||||
|
||||
For IAM entities requiring Multi-Factor Authentication (MFA), use the `--mfa` flag. Prowler prompts for the following values to initiate a new session:
|
||||
5. Click "Next", provide a stack name and the **External ID** shown in the Prowler Cloud setup screen
|
||||
|
||||
- **ARN of your MFA device**
|
||||
- **TOTP (Time-Based One-Time Password)**
|
||||

|
||||

|
||||
|
||||
!!! info
|
||||
An **External ID** is required when assuming the *ProwlerScan* role to comply with AWS [confused deputy prevention](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
|
||||
|
||||
6. Acknowledge the IAM resource creation warning and proceed
|
||||
|
||||

|
||||
|
||||
7. Click "Submit" to deploy the stack
|
||||
|
||||

|
||||
|
||||
=== "Terraform"
|
||||
|
||||
To provision the scan role using Terraform:
|
||||
|
||||
1. Run the following commands:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
2. During `plan` and `apply`, provide the **External ID** when prompted, which is available in the Prowler Cloud or Prowler App UI:
|
||||
|
||||

|
||||
|
||||
> 💡 Note: Terraform will use the AWS credentials of the default profile.
|
||||
|
||||
---
|
||||
|
||||
## Credentials
|
||||
=== "Long term credentials"
|
||||
|
||||
1. Go to the [AWS Console](https://console.aws.amazon.com), open **CloudShell**
|
||||
|
||||

|
||||
|
||||
2. Run:
|
||||
|
||||
```bash
|
||||
aws iam create-access-key
|
||||
```
|
||||
|
||||
3. Copy the output containing:
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
|
||||

|
||||
|
||||
=== "Short term credentials (Recommended)"
|
||||
|
||||
Use the [AWS Access Portal](https://docs.aws.amazon.com/singlesignon/latest/userguide/howtogetcredentials.html) or the CLI:
|
||||
|
||||
1. Retrieve short-term credentials for the IAM identity using this command:
|
||||
|
||||
```bash
|
||||
aws sts get-session-token --duration-seconds 900
|
||||
```
|
||||
|
||||
???+ note
|
||||
Check the aws documentation [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/sts_example_sts_GetSessionToken_section.html)
|
||||
|
||||
2. Copy the output containing:
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
- `SessionToken`
|
||||
|
||||
> Sample output:
|
||||
```json
|
||||
{
|
||||
"Credentials": {
|
||||
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
|
||||
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
|
||||
"SessionToken": "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE",
|
||||
"Expiration": "2020-05-19T18:06:10+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,39 +1,31 @@
|
||||
# Getting Started with AWS on Prowler Cloud/App
|
||||
# Getting Started With AWS on Prowler
|
||||
|
||||
## Prowler App
|
||||
|
||||
<iframe width="560" height="380" src="https://www.youtube-nocookie.com/embed/RPgIWOCERzY" title="Prowler Cloud Onboarding AWS" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
|
||||
|
||||
Set up your AWS account to enable security scanning using Prowler Cloud/App.
|
||||
> Walkthrough video onboarding an AWS Account using Assumed Role.
|
||||
|
||||
## Requirements
|
||||
|
||||
To configure your AWS account, you’ll need:
|
||||
|
||||
1. Access to Prowler Cloud/App
|
||||
2. Properly configured AWS credentials (either static or via an assumed IAM role)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Get Your AWS Account ID
|
||||
### Step 1: Get Your AWS Account ID
|
||||
|
||||
1. Log in to the [AWS Console](https://console.aws.amazon.com)
|
||||
2. Locate your AWS account ID in the top-right dropdown menu
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Step 2: Access Prowler Cloud/App
|
||||
### Step 2: Access Prowler Cloud or Prowler App
|
||||
|
||||
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](../prowler-app.md)
|
||||
2. Go to `Configuration` > `Cloud Providers`
|
||||
2. Go to "Configuration" > "Cloud Providers"
|
||||
|
||||

|
||||
|
||||
3. Click `Add Cloud Provider`
|
||||
3. Click "Add Cloud Provider"
|
||||
|
||||

|
||||
|
||||
4. Select `Amazon Web Services`
|
||||
4. Select "Amazon Web Services"
|
||||
|
||||

|
||||
|
||||
@@ -41,96 +33,39 @@ To configure your AWS account, you’ll need:
|
||||
|
||||

|
||||
|
||||
6. Choose your preferred authentication method (next step)
|
||||
6. Choose the preferred authentication method (next step)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Step 3: Set Up AWS Authentication
|
||||
### Step 3: Set Up AWS Authentication
|
||||
|
||||
Before proceeding, choose your preferred authentication mode:
|
||||
Before proceeding, choose the preferred authentication mode:
|
||||
|
||||
Credentials
|
||||
**Credentials**
|
||||
|
||||
* Quick scan as current user ✅
|
||||
* No extra setup ✅
|
||||
* Credentials time out ❌
|
||||
* Quick scan as current user
|
||||
* No extra setup
|
||||
* Credentials time out
|
||||
|
||||
Assumed Role
|
||||
**Assumed Role**
|
||||
|
||||
* Preferred Setup
|
||||
* Permanent Credentials
|
||||
* Requires access to create role
|
||||
|
||||
* Preferred Setup ✅
|
||||
* Permanent Credentials ✅
|
||||
* Requires access to create role ❌
|
||||
|
||||
---
|
||||
|
||||
### 🔐 Assume Role (Recommended)
|
||||
|
||||

|
||||
#### Assume Role (Recommended)
|
||||
|
||||
This method grants permanent access and is the recommended setup for production environments.
|
||||
|
||||
=== "CloudFormation"
|
||||

|
||||
|
||||
1. Download the [Prowler Scan Role Template](https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/permissions/templates/cloudformation/prowler-scan-role.yml)
|
||||
For detailed instructions on how to create the role, see [Authentication > Assume Role](./authentication.md#assume-role-recommended).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2. Open the [AWS Console](https://console.aws.amazon.com), search for **CloudFormation**
|
||||
|
||||

|
||||
|
||||
3. Go to **Stacks** and click `Create stack` > `With new resources (standard)`
|
||||
|
||||

|
||||
|
||||
4. In **Specify Template**, choose `Upload a template file` and select the downloaded file
|
||||
|
||||

|
||||

|
||||
|
||||
5. Click `Next`, provide a stack name and the **External ID** shown in the Prowler Cloud setup screen
|
||||
|
||||

|
||||

|
||||
|
||||
!!! info
|
||||
An **External ID** is required when assuming the *ProwlerScan* role to comply with AWS [confused deputy prevention](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
|
||||
|
||||
6. Acknowledge the IAM resource creation warning and proceed
|
||||
|
||||

|
||||
|
||||
7. Click `Submit` to deploy the stack
|
||||
|
||||

|
||||
|
||||
=== "Terraform"
|
||||
|
||||
To provision the scan role using Terraform:
|
||||
|
||||
1. Run the following commands:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
2. During `plan` and `apply`, you will be prompted for the **External ID**, which is available in the Prowler Cloud/App UI:
|
||||
|
||||

|
||||
|
||||
> 💡 Note: Terraform will use the AWS credentials of your default profile.
|
||||
|
||||
---
|
||||
|
||||
### Finish Setup with Assume Role
|
||||
|
||||
8. Once the role is created, go to the **IAM Console**, click on the `ProwlerScan` role to open its details:
|
||||
8. Once the role is created, go to the **IAM Console**, click on the "ProwlerScan" role to open its details:
|
||||
|
||||

|
||||
|
||||
@@ -138,80 +73,69 @@ This method grants permanent access and is the recommended setup for production
|
||||
|
||||

|
||||
|
||||
10. Paste the ARN into the corresponding field in Prowler Cloud/App
|
||||
10. Paste the ARN into the corresponding field in Prowler Cloud or Prowler App
|
||||
|
||||

|
||||
|
||||
11. Click `Next`, then `Launch Scan`
|
||||
11. Click "Next", then "Launch Scan"
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
### 🔑 Credentials (Static Access Keys)
|
||||
#### Credentials (Static Access Keys)
|
||||
|
||||
You can also configure your AWS account using static credentials (not recommended for long-term use):
|
||||
AWS accounts can also be configured using static credentials (not recommended for long-term use):
|
||||
|
||||

|
||||
|
||||
=== "Long term credentials"
|
||||
For detailed instructions on how to create the credentials, see [Authentication > Credentials](./authentication.md#credentials).
|
||||
|
||||
1. Go to the [AWS Console](https://console.aws.amazon.com), open **CloudShell**
|
||||
1. Complete the form in Prowler Cloud or Prowler App and click "Next"
|
||||
|
||||

|
||||

|
||||
|
||||
2. Run:
|
||||
2. Click "Launch Scan"
|
||||
|
||||
```bash
|
||||
aws iam create-access-key
|
||||
```
|
||||

|
||||
|
||||
3. Copy the output containing:
|
||||
---
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
## Prowler CLI
|
||||
|
||||

|
||||
### Configure AWS Credentials
|
||||
|
||||
> ⚠️ Save these credentials securely and paste them into the Prowler Cloud/App setup screen.
|
||||
To authenticate with AWS, use one of the following methods:
|
||||
|
||||
=== "Short term credentials (Recommended)"
|
||||
```console
|
||||
aws configure
|
||||
```
|
||||
|
||||
You can use your [AWS Access Portal](https://docs.aws.amazon.com/singlesignon/latest/userguide/howtogetcredentials.html) or the CLI:
|
||||
or
|
||||
|
||||
1. Retrieve short-term credentials for the IAM identity using this command:
|
||||
```console
|
||||
export AWS_ACCESS_KEY_ID="ASXXXXXXX"
|
||||
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
|
||||
export AWS_SESSION_TOKEN="XXXXXXXXX"
|
||||
```
|
||||
|
||||
```bash
|
||||
aws sts get-session-token --duration-seconds 900
|
||||
```
|
||||
These credentials must be associated with a user or role with the necessary permissions to perform security checks.
|
||||
|
||||
???+ note
|
||||
Check the aws documentation [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/sts_example_sts_GetSessionToken_section.html)
|
||||
More details on Assume Role settings from the CLI in [Assume Role](./role-assumption.md) page.
|
||||
|
||||
2. Copy the output containing:
|
||||
|
||||
- `AccessKeyId`
|
||||
- `SecretAccessKey`
|
||||
### AWS Profiles
|
||||
|
||||
> Sample output:
|
||||
```json
|
||||
{
|
||||
"Credentials": {
|
||||
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
|
||||
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
|
||||
"SessionToken": "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE",
|
||||
"Expiration": "2020-05-19T18:06:10+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
To use a custom AWS profile, specify it with the following command:
|
||||
|
||||
> ⚠️ Save these credentials securely and paste them into the Prowler Cloud/App setup screen.
|
||||
```console
|
||||
prowler aws -p/--profile <profile_name>
|
||||
```
|
||||
|
||||
Complete the form in Prowler Cloud/App and click `Next`
|
||||
### Multi-Factor Authentication (MFA)
|
||||
|
||||

|
||||
For IAM entities requiring Multi-Factor Authentication (MFA), use the `--mfa` flag. Prowler prompts for the following values to initiate a new session:
|
||||
|
||||
Click `Launch Scan`
|
||||
|
||||

|
||||
- **ARN of your MFA device**
|
||||
- **TOTP (time-based one-time password)**
|
||||
@@ -1,4 +1,4 @@
|
||||
# AWS Assume Role in Prowler
|
||||
# AWS Assume Role in Prowler (CLI)
|
||||
|
||||
## Authentication Overview
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
Set up your Azure subscription to enable security scanning using Prowler Cloud/App.
|
||||
|
||||
???+ note "Government Cloud Support"
|
||||
Government cloud subscriptions (Azure Government) are not currently supported, but we expect to add support for them in the near future.
|
||||
|
||||
## Requirements
|
||||
|
||||
To configure your Azure subscription, you’ll need:
|
||||
|
||||
@@ -23,97 +23,16 @@ Standard results will be shown and additionally the framework information as the
|
||||
|
||||
## List Available Compliance Frameworks
|
||||
|
||||
In order to see which compliance frameworks are covered by Prowler, you can use option `--list-compliance`:
|
||||
To see which compliance frameworks are covered by Prowler, use the `--list-compliance` option:
|
||||
|
||||
```sh
|
||||
prowler <provider> --list-compliance
|
||||
```
|
||||
|
||||
### AWS (36 frameworks)
|
||||
|
||||
- `aws_account_security_onboarding_aws`
|
||||
- `aws_audit_manager_control_tower_guardrails_aws`
|
||||
- `aws_foundational_security_best_practices_aws`
|
||||
- `aws_foundational_technical_review_aws`
|
||||
- `aws_well_architected_framework_reliability_pillar_aws`
|
||||
- `aws_well_architected_framework_security_pillar_aws`
|
||||
- `cis_1.4_aws`
|
||||
- `cis_1.5_aws`
|
||||
- `cis_2.0_aws`
|
||||
- `cis_3.0_aws`
|
||||
- `cis_4.0_aws`
|
||||
- `cis_5.0_aws`
|
||||
- `cisa_aws`
|
||||
- `ens_rd2022_aws`
|
||||
- `fedramp_low_revision_4_aws`
|
||||
- `fedramp_moderate_revision_4_aws`
|
||||
- `ffiec_aws`
|
||||
- `gdpr_aws`
|
||||
- `gxp_21_cfr_part_11_aws`
|
||||
- `gxp_eu_annex_11_aws`
|
||||
- `hipaa_aws`
|
||||
- `iso27001_2013_aws`
|
||||
- `iso27001_2022_aws`
|
||||
- `kisa_isms_p_2023_aws`
|
||||
- `kisa_isms_p_2023_korean_aws`
|
||||
- `mitre_attack_aws`
|
||||
- `nis2_aws`
|
||||
- `nist_800_171_revision_2_aws`
|
||||
- `nist_800_53_revision_4_aws`
|
||||
- `nist_800_53_revision_5_aws`
|
||||
- `nist_csf_1.1_aws`
|
||||
- `pci_3.2.1_aws`
|
||||
- `pci_4.0_aws`
|
||||
- `prowler_threatscore_aws`
|
||||
- `rbi_cyber_security_framework_aws`
|
||||
- `soc2_aws`
|
||||
|
||||
### Azure (10 frameworks)
|
||||
|
||||
- `cis_2.0_azure`
|
||||
- `cis_2.1_azure`
|
||||
- `cis_3.0_azure`
|
||||
- `ens_rd2022_azure`
|
||||
- `iso27001_2022_azure`
|
||||
- `mitre_attack_azure`
|
||||
- `nis2_azure`
|
||||
- `pci_4.0_azure`
|
||||
- `prowler_threatscore_azure`
|
||||
- `soc2_azure`
|
||||
|
||||
### GCP (10 frameworks)
|
||||
|
||||
- `cis_2.0_gcp`
|
||||
- `cis_3.0_gcp`
|
||||
- `cis_4.0_gcp`
|
||||
- `ens_rd2022_gcp`
|
||||
- `iso27001_2022_gcp`
|
||||
- `mitre_attack_gcp`
|
||||
- `nis2_gcp`
|
||||
- `pci_4.0_gcp`
|
||||
- `prowler_threatscore_gcp`
|
||||
- `soc2_gcp`
|
||||
|
||||
### Kubernetes (5 frameworks)
|
||||
|
||||
- `cis_1.10_kubernetes`
|
||||
- `cis_1.11_kubernetes`
|
||||
- `cis_1.8_kubernetes`
|
||||
- `iso27001_2022_kubernetes`
|
||||
- `pci_4.0_kubernetes`
|
||||
|
||||
### M365 (3 frameworks)
|
||||
|
||||
- `cis_4.0_m365`
|
||||
- `iso27001_2022_m365`
|
||||
- `prowler_threatscore_m365`
|
||||
|
||||
### GitHub (1 framework)
|
||||
|
||||
- `cis_1.0_github`
|
||||
Or you can visit [Prowler Hub](https://hub.prowler.com/compliance).
|
||||
|
||||
## List Requirements of Compliance Frameworks
|
||||
For each compliance framework, you can use the `--list-compliance-requirements` option to list its requirements:
|
||||
To list requirements for a compliance framework, use the `--list-compliance-requirements` option:
|
||||
|
||||
```sh
|
||||
prowler <provider> --list-compliance-requirements <compliance_framework(s)>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Prowler Fixer (remediation)
|
||||
# Prowler Fixers (Remediations)
|
||||
|
||||
Prowler allows you to fix some of the failed findings it identifies. You can use the `--fixer` flag to run the fixes that are available for the checks that failed.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 584 KiB |
@@ -2,6 +2,9 @@
|
||||
|
||||
Set up your M365 account to enable security scanning using Prowler Cloud/App.
|
||||
|
||||
???+ note "Government Cloud Support"
|
||||
Government cloud accounts or tenants (Microsoft 365 Government) are not currently supported, but we expect to add support for them in the near future.
|
||||
|
||||
## Requirements
|
||||
|
||||
To configure your M365 account, you'll need:
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# MongoDB Atlas Authentication
|
||||
|
||||
MongoDB Atlas provider uses [HTTP Digest Authentication with API key pairs consisting of a public key and private key](https://www.mongodb.com/docs/atlas/configure-api-access/#grant-programmatic-access-to-service).
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### Command-Line Arguments
|
||||
|
||||
```bash
|
||||
prowler mongodbatlas --atlas-public-key <public_key> --atlas-private-key <private_key>
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
export ATLAS_PUBLIC_KEY=<public_key>
|
||||
export ATLAS_PRIVATE_KEY=<private_key>
|
||||
prowler mongodbatlas
|
||||
```
|
||||
|
||||
## Creating API Keys
|
||||
|
||||
### Step-by-Step Guide
|
||||
|
||||
1. **Log into MongoDB Atlas**
|
||||
- Access the MongoDB Atlas console
|
||||
|
||||
2. **Navigate to Access Manager**
|
||||
- Go to the organization or project access management section
|
||||
|
||||
3. **Select API Keys Tab**
|
||||
- Click on the "API Keys" tab
|
||||
|
||||
4. **Create API Key**
|
||||
- Click "Create API Key"
|
||||
- Provide a description for the key
|
||||
|
||||
5. **Set Permissions**
|
||||
- Grant minimum required permissions
|
||||
|
||||
6. **Save Credentials**
|
||||
- Note the public key and private key
|
||||
- Store credentials securely
|
||||
|
||||
For more details about MongoDB Atlas, see the [MongoDB Atlas Tutorial](./getting-started-mongodbatlas.md).
|
||||
@@ -0,0 +1,87 @@
|
||||
# Getting Started with MongoDB Atlas
|
||||
|
||||
MongoDB Atlas provider enables security assessments of MongoDB Atlas cloud database deployments.
|
||||
|
||||
## Features
|
||||
|
||||
- **Authentication**: Supports MongoDB Atlas API key authentication
|
||||
- **Services**: Projects and clusters services
|
||||
- **Checks**: Network access security and encryption at rest validation
|
||||
|
||||
## Creating API Keys
|
||||
|
||||
To create MongoDB Atlas API keys:
|
||||
|
||||
1. **Log into MongoDB Atlas**: Access the MongoDB Atlas console
|
||||
2. **Navigate to Access Manager**: Go to the organization access management section:
|
||||
|
||||
- Click on Access Manager and Organization Access:
|
||||
|
||||

|
||||
|
||||
- After that click on the Applications tab inside the Access Manager:
|
||||
|
||||

|
||||
|
||||
3. **Select API Keys Tab**: Click on the "API Keys" tab that appears in the image above
|
||||
|
||||
4. **Create API Key**: Click "Create API Key" and provide a description
|
||||
|
||||

|
||||
|
||||
5. **Set Permissions**: Project permissions are recommended for security, you can modify them after creating the key
|
||||
|
||||

|
||||
|
||||
6. **Save Credentials**: Note the public key and private key and store them securely
|
||||
|
||||

|
||||
|
||||
7. **Add IP Access List**: Add the IP where you are running Prowler to the IP Access List of the API Key. If you want to skip this step and use your API key in all type of IP addresses you need to uncheck the `Require IP Access List for the Atlas Administration API` button on the [Organization Settings](#needed-permissions), but this is not recommended.
|
||||
|
||||

|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Scan All Projects and Clusters
|
||||
|
||||
After storing your API keys, you can run Prowler with the following command:
|
||||
|
||||
```bash
|
||||
prowler mongodbatlas --atlas-public-key <key> --atlas-private-key <secret>
|
||||
```
|
||||
|
||||
Also, you can set your API keys as environment variables:
|
||||
|
||||
```bash
|
||||
export ATLAS_PUBLIC_KEY=<key>
|
||||
export ATLAS_PRIVATE_KEY=<secret>
|
||||
```
|
||||
|
||||
And then just run Prowler with the following command:
|
||||
|
||||
```bash
|
||||
prowler mongodbatlas
|
||||
```
|
||||
|
||||
### Scanning a Specific Project
|
||||
|
||||
If you want to scan a specific project, you can use the following argument added to the command above:
|
||||
|
||||
```bash
|
||||
prowler mongodbatlas --atlas-project-id <project-id>
|
||||
```
|
||||
|
||||
### Needed Permissions
|
||||
|
||||
MongoDB Atlas API keys require appropriate permissions to perform security checks:
|
||||
|
||||
- **Organization Read Only**: Provides read-only access to everything in the organization, including all projects in the organization.
|
||||
- If you want to be able to [audit the Auditing configuration for the project](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/group/endpoint-auditing), **Organization Owner** is needed.
|
||||
|
||||
Also, it's important to note that the IP where you are running Prowler must be added to the IP Access List of the MongoDB Atlas organization API key. If you want to skip this step and use your API key in all type of IP addresses you need to uncheck the `Require IP Access List for the Atlas Administration API` button on the Organization Settings, that setting is [enabled by default](https://www.mongodb.com/docs/atlas/configure-api-access/#optional--require-an-ip-access-list-for-the-atlas-administration-api).
|
||||
|
||||
???+ warning
|
||||
If you want the check `organizations_api_access_list_required` to pass you will need to enable the API access list for the organization, so to make sure that your API Key is working you need to add your IP to the IP Access List of the organization. If you are running the check from Prowler Cloud, you will need to add our IP to the IP Access List.
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 100 KiB |
@@ -0,0 +1,189 @@
|
||||
# Jira Integration
|
||||
|
||||
Prowler App enables automatic export of security findings to Jira, providing seamless integration with Atlassian's work item tracking and project management platform. This comprehensive guide demonstrates how to configure and manage Jira integrations to streamline security incident management and enhance team collaboration across security workflows.
|
||||
|
||||
Integrating Prowler App with Jira provides:
|
||||
|
||||
* **Streamlined management:** Convert security findings directly into actionable Jira work items
|
||||
* **Enhanced team collaboration:** Leverage existing project management workflows for security remediation
|
||||
* **Automated ticket creation:** Reduce manual effort in tracking and assigning security work items
|
||||
|
||||
## How It Works
|
||||
|
||||
When enabled and configured:
|
||||
|
||||
1. Security findings can be manually sent to Jira from the Findings table.
|
||||
2. Each finding creates a Jira work item with all the check's metadata, including guidance on how to remediate it.
|
||||
|
||||
## Configuration
|
||||
|
||||
To configure Jira integration in Prowler App:
|
||||
|
||||
1. Navigate to **Integrations** in the Prowler App interface
|
||||
2. Locate the **Jira** card and click **Manage**, then select **Add integration**
|
||||
|
||||

|
||||
|
||||
3. Complete the integration settings:
|
||||
* **Jira domain:** Enter the Jira domain (e.g., from `https://your-domain.atlassian.net` -> `your-domain`)
|
||||
* **Email:** Your Jira account email
|
||||
* **API Token:** API token with the following scopes: `read:jira-user`, `read:jira-work`, `write:jira-work`
|
||||

|
||||
|
||||
!!! note "Generate Jira API Token"
|
||||
To generate a Jira API token, visit: https://id.atlassian.com/manage-profile/security/api-tokens
|
||||
|
||||
|
||||
Once configured successfully, the integration is ready to send findings to Jira.
|
||||
|
||||
## Sending Findings to Jira
|
||||
|
||||
### Manual Export
|
||||
|
||||
To manually send individual findings to Jira:
|
||||
|
||||
1. Navigate to the **Findings** section in Prowler App
|
||||
2. Select one finding you want to export
|
||||
3. Click the action button on the table row and select **Send to Jira**
|
||||
4. Select the Jira integration and project
|
||||
5. Click **Send to Jira**
|
||||
|
||||

|
||||
|
||||
## Integration Status
|
||||
|
||||
Monitor and manage your Jira integrations through the management interface:
|
||||
|
||||
1. Review configured integrations in the integrations dashboard
|
||||
2. Each integration displays:
|
||||
|
||||
- **Connection Status:** Connected or Disconnected indicator
|
||||
- **Instance Information:** Jira domain and last checked timestamp
|
||||
|
||||
### Actions
|
||||
|
||||
Each Jira integration provides management actions through dedicated buttons:
|
||||
|
||||
| Button | Purpose | Available Actions | Notes |
|
||||
|--------|---------|------------------|-------|
|
||||
| **Test** | Verify integration connectivity | • Test Jira API access<br/>• Validate credentials<br/>• Check project permissions<br/>• Verify work item creation capability | Results displayed in notification message |
|
||||
| **Credentials** | Update authentication settings | • Change API token<br/>• Update email<br/>• Update Jira domain | Click "Update Credentials" to save changes |
|
||||
| **Enable/Disable** | Toggle integration status | • Enable or disable integration<br/>| Status change takes effect immediately |
|
||||
| **Delete** | Remove integration permanently | • Permanently delete integration<br/>• Remove all configuration data | ⚠️ **Cannot be undone** - confirm before deleting |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection test fails
|
||||
|
||||
- Verify Jira instance domain is correct and accessible
|
||||
- Confirm API token or credentials are valid
|
||||
- Ensure API access is enabled in Jira settings and the needed scopes are granted
|
||||
|
||||
### Check task status (API)
|
||||
|
||||
If the Jira issue does not appear in your Jira project, follow these steps to verify the export task status via the API.
|
||||
|
||||
!!! note
|
||||
Replace `http://localhost:8080` with the base URL where your Prowler API is accessible (for example, `https://api.yourdomain.com`).
|
||||
|
||||
1) Get an access token (replace email and password):
|
||||
|
||||
```
|
||||
curl --location 'http://localhost:8080/api/v1/tokens' \
|
||||
--header 'Content-Type: application/vnd.api+json' \
|
||||
--header 'Accept: application/vnd.api+json' \
|
||||
--data-raw '{
|
||||
"data": {
|
||||
"type": "tokens",
|
||||
"attributes": {
|
||||
"email": "YOUR_USER_EMAIL",
|
||||
"password": "YOUR_USER_PASSWORD"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
2) List tasks filtered by the Jira task (`integration-jira`) using the access token:
|
||||
|
||||
```
|
||||
curl --location --globoff 'http://localhost:8080/api/v1/tasks?filter[name]=integration-jira' \
|
||||
--header 'Accept: application/vnd.api+json' \
|
||||
--header 'Authorization: Bearer ACCESS_TOKEN' | jq
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you don’t have `jq` installed, run the command without `| jq`.
|
||||
|
||||
3) Share the output so we can help. A typical result will look like:
|
||||
|
||||
```
|
||||
{
|
||||
"links": {
|
||||
"first": "https://api.dev.prowler.com/api/v1/tasks?page%5Bnumber%5D=1",
|
||||
"last": "https://api.dev.prowler.com/api/v1/tasks?page%5Bnumber%5D=122",
|
||||
"next": "https://api.dev.prowler.com/api/v1/tasks?page%5Bnumber%5D=2",
|
||||
"prev": null
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"type": "tasks",
|
||||
"id": "9a79ab21-39ae-4161-9f6e-2844eb0da0fb",
|
||||
"attributes": {
|
||||
"inserted_at": "2025-09-09T08:11:38.643620Z",
|
||||
"completed_at": "2025-09-09T08:11:41.264285Z",
|
||||
"name": "integration-jira",
|
||||
"state": "completed",
|
||||
"result": {
|
||||
"created_count": 0,
|
||||
"failed_count": 1
|
||||
},
|
||||
"task_args": {
|
||||
"integration_id": "a476c2c0-0a00-4720-bfb9-286e9eb5c7bd",
|
||||
"project_key": "PRWLR",
|
||||
"issue_type": "Task",
|
||||
"finding_ids": [
|
||||
"01992d53-3af7-7759-be48-68fc405391e6"
|
||||
]
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tasks",
|
||||
"id": "5f525135-9d37-4b01-9ac8-afeaf8793eac",
|
||||
"attributes": {
|
||||
"inserted_at": "2025-09-09T08:07:22.184164Z",
|
||||
"completed_at": "2025-09-09T08:07:24.909185Z",
|
||||
"name": "integration-jira",
|
||||
"state": "completed",
|
||||
"result": {
|
||||
"created_count": 1,
|
||||
"failed_count": 0
|
||||
},
|
||||
"task_args": {
|
||||
"integration_id": "a476c2c0-0a00-4720-bfb9-286e9eb5c7bd",
|
||||
"project_key": "JIRA",
|
||||
"issue_type": "Task",
|
||||
"finding_ids": [
|
||||
"0198f018-8b7b-7154-a509-1a2b1ffba02d"
|
||||
]
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pages": 122,
|
||||
"count": 1214
|
||||
},
|
||||
"version": "v1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
How to read it:
|
||||
|
||||
- "created_count": number of Jira issues successfully created.
|
||||
- "failed_count": number of Jira issues that could not be created. If `failed_count > 0` or the issue does not appear in Jira, please contact us so we can assist while detailed logs are not available through the UI.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Managing Users and Roles
|
||||
# Managing Users and Role-Based Access Control (RBAC)
|
||||
|
||||
**Prowler App** supports multiple users within a single tenant, enabling seamless collaboration by allowing team members to easily share insights and manage security findings.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
This page provides instructions for creating and configuring a Microsoft Entra ID (formerly Azure AD) application to use SAML SSO with Prowler App.
|
||||
|
||||
You can find a walkthrough video [here](https://youtu.be/UtcjDh5cAjI).
|
||||
|
||||
## Creating and Configuring the Enterprise Application
|
||||
|
||||
1. From the "Enterprise Applications" page in the Azure Portal, click "+ New application".
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Prowler App
|
||||
|
||||
**Prowler App** is a user-friendly interface for Prowler CLI, providing a visual dashboard to monitor your cloud security posture. This tutorial will guide you through setting up and using Prowler App.
|
||||
**Prowler App** is a web application that simplifies running Prowler. This tutorial will guide you through setting up and using it.
|
||||
|
||||
## Accessing Prowler App and API Documentation
|
||||
|
||||
@@ -39,7 +39,7 @@ Upon logging in, the Overview page will display. At this stage, no data is prese
|
||||
|
||||
## **Step 3: Add a Provider**
|
||||
|
||||
To perform security scans, link a cloud provider account. Prowler supports the following providers:
|
||||
To perform security scans, link a cloud provider account. Prowler supports the following providers and more:
|
||||
|
||||
- **AWS**
|
||||
|
||||
@@ -51,6 +51,8 @@ To perform security scans, link a cloud provider account. Prowler supports the f
|
||||
|
||||
- **M365**
|
||||
|
||||
- **GitHub**
|
||||
|
||||
Steps to add a provider:
|
||||
|
||||
1. Navigate to `Settings > Cloud Providers`.
|
||||
|
||||
@@ -59,77 +59,85 @@ nav:
|
||||
- Basic Usage:
|
||||
- Prowler App: basic-usage/prowler-app.md
|
||||
- Prowler CLI: basic-usage/prowler-cli.md
|
||||
- Tutorials:
|
||||
- User Guide:
|
||||
- Prowler App:
|
||||
- Getting Started: tutorials/prowler-app.md
|
||||
- Authentication:
|
||||
- Social Login: tutorials/prowler-app-social-login.md
|
||||
- SSO with SAML: tutorials/prowler-app-sso.md
|
||||
- Role-Based Access Control: tutorials/prowler-app-rbac.md
|
||||
- Social Login: tutorials/prowler-app-social-login.md
|
||||
- SSO with SAML: tutorials/prowler-app-sso.md
|
||||
- Mute findings: tutorials/prowler-app-mute-findings.md
|
||||
- Mutelist: tutorials/prowler-app-mute-findings.md
|
||||
- Integrations:
|
||||
- Amazon S3: tutorials/prowler-app-s3-integration.md
|
||||
- AWS Security Hub: tutorials/prowler-app-security-hub-integration.md
|
||||
- Lighthouse: tutorials/prowler-app-lighthouse.md
|
||||
- Bulk Provider Provisioning: tutorials/bulk-provider-provisioning.md
|
||||
- Jira: tutorials/prowler-app-jira-integration.md
|
||||
- Lighthouse AI: tutorials/prowler-app-lighthouse.md
|
||||
- Tutorials:
|
||||
- SSO with Entra: tutorials/prowler-app-sso-entra.md
|
||||
- Bulk Provider Provisioning: tutorials/bulk-provider-provisioning.md
|
||||
- CLI:
|
||||
- Miscellaneous: tutorials/misc.md
|
||||
- Reporting: tutorials/reporting.md
|
||||
- Compliance: tutorials/compliance.md
|
||||
- Dashboard: tutorials/dashboard.md
|
||||
- Fixer (remediations): tutorials/fixer.md
|
||||
- Quick Inventory: tutorials/quick-inventory.md
|
||||
- Slack Integration: tutorials/integrations.md
|
||||
- Configuration File: tutorials/configuration_file.md
|
||||
- Logging: tutorials/logging.md
|
||||
- Mutelist: tutorials/mutelist.md
|
||||
- Integrations:
|
||||
- AWS Security Hub: tutorials/aws/securityhub.md
|
||||
- Slack: tutorials/integrations.md
|
||||
- Send reports to AWS S3: tutorials/aws/s3.md
|
||||
- Fixers (Remediations): tutorials/fixer.md
|
||||
- Check Aliases: tutorials/check-aliases.md
|
||||
- Custom Metadata: tutorials/custom-checks-metadata.md
|
||||
- Scan Unused Services: tutorials/scan-unused-services.md
|
||||
- Pentesting: tutorials/pentesting.md
|
||||
- Parallel Execution: tutorials/parallel-execution.md
|
||||
- Developer Guide: developer-guide/introduction.md
|
||||
- Prowler Check Kreator: tutorials/prowler-check-kreator.md
|
||||
- AWS:
|
||||
- Getting Started: tutorials/aws/getting-started-aws.md
|
||||
- Authentication: tutorials/aws/authentication.md
|
||||
- Assume Role: tutorials/aws/role-assumption.md
|
||||
- AWS Security Hub: tutorials/aws/securityhub.md
|
||||
- AWS Organizations: tutorials/aws/organizations.md
|
||||
- AWS Regions and Partitions: tutorials/aws/regions-and-partitions.md
|
||||
- Scan Multiple AWS Accounts: tutorials/aws/multiaccount.md
|
||||
- Send reports to AWS S3: tutorials/aws/s3.md
|
||||
- AWS CloudShell: tutorials/aws/cloudshell.md
|
||||
- Checks v2 to v3 and v4 Mapping: tutorials/aws/v2_to_v3_checks_mapping.md
|
||||
- Tag-based Scan: tutorials/aws/tag-based-scan.md
|
||||
- Resource ARNs based Scan: tutorials/aws/resource-arn-based-scan.md
|
||||
- Boto3 Configuration: tutorials/aws/boto3-configuration.md
|
||||
- Threat Detection: tutorials/aws/threat-detection.md
|
||||
- Azure:
|
||||
- Getting Started: tutorials/azure/getting-started-azure.md
|
||||
- Authentication: tutorials/azure/authentication.md
|
||||
- Non default clouds: tutorials/azure/use-non-default-cloud.md
|
||||
- Subscriptions: tutorials/azure/subscriptions.md
|
||||
- Create Prowler Service Principal: tutorials/azure/create-prowler-service-principal.md
|
||||
- Google Cloud:
|
||||
- Getting Started: tutorials/gcp/getting-started-gcp.md
|
||||
- Authentication: tutorials/gcp/authentication.md
|
||||
- Projects: tutorials/gcp/projects.md
|
||||
- Organization: tutorials/gcp/organization.md
|
||||
- Retry Configuration: tutorials/gcp/retry-configuration.md
|
||||
- Kubernetes:
|
||||
- In-Cluster Execution: tutorials/kubernetes/in-cluster.md
|
||||
- Non In-Cluster Execution: tutorials/kubernetes/outside-cluster.md
|
||||
- Miscellaneous: tutorials/kubernetes/misc.md
|
||||
- Microsoft 365:
|
||||
- Getting Started: tutorials/microsoft365/getting-started-m365.md
|
||||
- Authentication: tutorials/microsoft365/authentication.md
|
||||
- Use of PowerShell: tutorials/microsoft365/use-of-powershell.md
|
||||
- GitHub:
|
||||
- Getting Started: tutorials/github/getting-started-github.md
|
||||
- Authentication: tutorials/github/authentication.md
|
||||
- IaC:
|
||||
- Getting Started: tutorials/iac/getting-started-iac.md
|
||||
- Authentication: tutorials/iac/authentication.md
|
||||
- Scan Unused Services: tutorials/scan-unused-services.md
|
||||
- Quick Inventory: tutorials/quick-inventory.md
|
||||
- Tutorials:
|
||||
- Parallel Execution: tutorials/parallel-execution.md
|
||||
|
||||
- Providers:
|
||||
- AWS:
|
||||
- Getting Started: tutorials/aws/getting-started-aws.md
|
||||
- Authentication: tutorials/aws/authentication.md
|
||||
- Assume Role (CLI): tutorials/aws/role-assumption.md
|
||||
- AWS Organizations: tutorials/aws/organizations.md
|
||||
- AWS Regions and Partitions: tutorials/aws/regions-and-partitions.md
|
||||
- Tag-based Scan: tutorials/aws/tag-based-scan.md
|
||||
- Resource ARNs based Scan: tutorials/aws/resource-arn-based-scan.md
|
||||
- Boto3 Configuration: tutorials/aws/boto3-configuration.md
|
||||
- Threat Detection: tutorials/aws/threat-detection.md
|
||||
- Tutorial > AWS CloudShell: tutorials/aws/cloudshell.md
|
||||
- Tutorial > Scan Multiple AWS Accounts: tutorials/aws/multiaccount.md
|
||||
- Azure:
|
||||
- Getting Started: tutorials/azure/getting-started-azure.md
|
||||
- Authentication: tutorials/azure/authentication.md
|
||||
- Non default clouds: tutorials/azure/use-non-default-cloud.md
|
||||
- Subscriptions: tutorials/azure/subscriptions.md
|
||||
- Create Prowler Service Principal: tutorials/azure/create-prowler-service-principal.md
|
||||
- Google Cloud:
|
||||
- Getting Started: tutorials/gcp/getting-started-gcp.md
|
||||
- Authentication: tutorials/gcp/authentication.md
|
||||
- Projects: tutorials/gcp/projects.md
|
||||
- Organization: tutorials/gcp/organization.md
|
||||
- Retry Configuration: tutorials/gcp/retry-configuration.md
|
||||
- Kubernetes:
|
||||
- In-Cluster Execution: tutorials/kubernetes/in-cluster.md
|
||||
- Non In-Cluster Execution: tutorials/kubernetes/outside-cluster.md
|
||||
- Miscellaneous: tutorials/kubernetes/misc.md
|
||||
- Microsoft 365:
|
||||
- Getting Started: tutorials/microsoft365/getting-started-m365.md
|
||||
- Authentication: tutorials/microsoft365/authentication.md
|
||||
- Use of PowerShell: tutorials/microsoft365/use-of-powershell.md
|
||||
- GitHub:
|
||||
- Getting Started: tutorials/github/getting-started-github.md
|
||||
- Authentication: tutorials/github/authentication.md
|
||||
- IaC:
|
||||
- Getting Started: tutorials/iac/getting-started-iac.md
|
||||
- Authentication: tutorials/iac/authentication.md
|
||||
- MongoDB Atlas:
|
||||
- Getting Started: tutorials/mongodbatlas/getting-started-mongodbatlas.md
|
||||
- Authentication: tutorials/mongodbatlas/authentication.md
|
||||
- Developer Guide:
|
||||
- Concepts:
|
||||
- Introduction: developer-guide/introduction.md
|
||||
@@ -154,6 +162,7 @@ nav:
|
||||
- Integration Tests: developer-guide/integration-testing.md
|
||||
- Debugging: developer-guide/debugging.md
|
||||
- Configurable Checks: developer-guide/configurable-checks.md
|
||||
- Check Metadata Writting Guidelines: developer-guide/check-metadata-guidelines.md
|
||||
- Security: security.md
|
||||
- Contact Us: contact.md
|
||||
- Troubleshooting: troubleshooting.md
|
||||
|
||||
@@ -1,25 +1,54 @@
|
||||
# Prowler SDK Changelog
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
## [v5.12.0] (Prowler UNRELEASED)
|
||||
|
||||
## [v5.13.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Get Jira Project's metadata [(#8630)](https://github.com/prowler-cloud/prowler/pull/8630)
|
||||
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
|
||||
- Get Jira projects from test_connection [(#8634)](https://github.com/prowler-cloud/prowler/pull/8634)
|
||||
- `AdditionalUrls` field in CheckMetadata [(#8590)](https://github.com/prowler-cloud/prowler/pull/8590)
|
||||
|
||||
- Support for markdown metadata fields in Dashboard [(#8667)](https://github.com/prowler-cloud/prowler/pull/8667)
|
||||
### Changed
|
||||
- Update AWS Neptune service metadata to new format [(#8494)](https://github.com/prowler-cloud/prowler/pull/8494)
|
||||
|
||||
### Fixed
|
||||
- Renamed `AdditionalUrls` to `AdditionalURLs` field in CheckMetadata [(#8639)](https://github.com/prowler-cloud/prowler/pull/8639)
|
||||
|
||||
---
|
||||
|
||||
## [v5.11.1] (Prowler UNRELEASED)
|
||||
## [v5.12.1] (Prowler v5.12.1)
|
||||
|
||||
### Fixed
|
||||
- Replaced old check id with new ones for compliance files [(#8682)](https://github.com/prowler-cloud/prowler/pull/8682)
|
||||
|
||||
## [v5.12.0] (Prowler v5.12.0)
|
||||
|
||||
### 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)
|
||||
- Get Jira Project's metadata [(#8630)](https://github.com/prowler-cloud/prowler/pull/8630)
|
||||
- Get Jira projects from test_connection [(#8634)](https://github.com/prowler-cloud/prowler/pull/8634)
|
||||
- `AdditionalUrls` field in CheckMetadata [(#8590)](https://github.com/prowler-cloud/prowler/pull/8590)
|
||||
- Support color for MANUAL finidngs in Jira tickets [(#8642)](https://github.com/prowler-cloud/prowler/pull/8642)
|
||||
- `--excluded-checks-file` flag [(#8301)](https://github.com/prowler-cloud/prowler/pull/8301)
|
||||
- Send finding in Jira integration with the needed values [(#8648)](https://github.com/prowler-cloud/prowler/pull/8648)
|
||||
- Add language enforcement for Jira requests [(#8674)](https://github.com/prowler-cloud/prowler/pull/8674)
|
||||
- MongoDB Atlas provider with 10 security checks [(#8312)](https://github.com/prowler-cloud/prowler/pull/8312)
|
||||
- `clusters_authentication_enabled` - Ensure clusters have authentication enabled
|
||||
- `clusters_backup_enabled` - Ensure clusters have backup enabled
|
||||
- `clusters_encryption_at_rest_enabled` - Ensure clusters have encryption at rest enabled
|
||||
- `clusters_tls_enabled` - Ensure clusters have TLS authentication required
|
||||
- `organizations_api_access_list_required` - Ensure organization requires API access list
|
||||
- `organizations_mfa_required` - Ensure organization requires MFA
|
||||
- `organizations_security_contact_defined` - Ensure organization has security contact defined
|
||||
- `organizations_service_account_secrets_expiration` - Ensure organization has maximum period expiration for service account secrets
|
||||
- `projects_auditing_enabled` - Ensure database auditing is enabled
|
||||
- `projects_network_access_list_exposed_to_internet` - Ensure project network access list is not exposed to internet
|
||||
|
||||
### Changed
|
||||
- Rename ftp and mongo checks to follow pattern `ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_*` [(#8293)](https://github.com/prowler-cloud/prowler/pull/8293)
|
||||
|
||||
### Fixed
|
||||
- Renamed `AdditionalUrls` to `AdditionalURLs` field in CheckMetadata [(#8639)](https://github.com/prowler-cloud/prowler/pull/8639)
|
||||
- TypeError from Python 3.9 in Security Hub module by updating type annotations [(#8619)](https://github.com/prowler-cloud/prowler/pull/8619)
|
||||
- KeyError when SecurityGroups field is missing in MemoryDB check [(#8666)](https://github.com/prowler-cloud/prowler/pull/8666)
|
||||
- NoneType error in Opensearch, Firehose and Cognito checks [(#8670)](https://github.com/prowler-cloud/prowler/pull/8670)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from prowler.lib.check.check import (
|
||||
list_checks_json,
|
||||
list_fixers,
|
||||
list_services,
|
||||
parse_checks_from_file,
|
||||
parse_checks_from_folder,
|
||||
print_categories,
|
||||
print_checks,
|
||||
@@ -102,6 +103,7 @@ from prowler.providers.github.models import GithubOutputOptions
|
||||
from prowler.providers.iac.models import IACOutputOptions
|
||||
from prowler.providers.kubernetes.models import KubernetesOutputOptions
|
||||
from prowler.providers.m365.models import M365OutputOptions
|
||||
from prowler.providers.mongodbatlas.models import MongoDBAtlasOutputOptions
|
||||
from prowler.providers.nhn.models import NHNOutputOptions
|
||||
|
||||
|
||||
@@ -121,6 +123,7 @@ def prowler():
|
||||
|
||||
checks = args.check
|
||||
excluded_checks = args.excluded_check
|
||||
excluded_checks_file = args.excluded_checks_file
|
||||
excluded_services = args.excluded_service
|
||||
services = args.service
|
||||
categories = args.category
|
||||
@@ -257,6 +260,15 @@ def prowler():
|
||||
checks_to_execute, excluded_checks
|
||||
)
|
||||
|
||||
# Exclude checks if --excluded-checks-file
|
||||
if excluded_checks_file:
|
||||
excluded_checks_from_file = parse_checks_from_file(
|
||||
excluded_checks_file, provider
|
||||
)
|
||||
checks_to_execute = exclude_checks_to_run(
|
||||
checks_to_execute, list(excluded_checks_from_file)
|
||||
)
|
||||
|
||||
# Exclude services if --excluded-services
|
||||
if excluded_services:
|
||||
checks_to_execute = exclude_services_to_run(
|
||||
@@ -300,6 +312,10 @@ def prowler():
|
||||
output_options = M365OutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "mongodbatlas":
|
||||
output_options = MongoDBAtlasOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
)
|
||||
elif provider == "nhn":
|
||||
output_options = NHNOutputOptions(
|
||||
args, bulk_checks_metadata, global_provider.identity
|
||||
|
||||
@@ -364,8 +364,8 @@
|
||||
"ec2_ami_public",
|
||||
"ec2_instance_public_ip",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
|
||||
@@ -721,8 +721,8 @@
|
||||
"ec2_networkacl_allow_ingress_tcp_port_22",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
|
||||
@@ -1510,8 +1510,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1604,8 +1604,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1698,8 +1698,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
|
||||
@@ -1558,8 +1558,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1682,7 +1682,7 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601",
|
||||
@@ -1814,7 +1814,7 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306",
|
||||
@@ -1917,7 +1917,7 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23",
|
||||
@@ -3024,8 +3024,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -4588,4 +4588,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1557,8 +1557,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1682,7 +1682,7 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601",
|
||||
@@ -1816,7 +1816,7 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306",
|
||||
@@ -1919,7 +1919,7 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23",
|
||||
@@ -3028,8 +3028,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -4603,4 +4603,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -107,8 +107,8 @@
|
||||
"ec2_networkacl_allow_ingress_tcp_port_22",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1024,8 +1024,8 @@
|
||||
"ec2_networkacl_allow_ingress_tcp_port_22",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1470,8 +1470,8 @@
|
||||
"ec2_networkacl_allow_ingress_tcp_port_22",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1650,8 +1650,8 @@
|
||||
"ec2_networkacl_allow_ingress_tcp_port_22",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
@@ -1902,8 +1902,8 @@
|
||||
"ec2_networkacl_allow_ingress_tcp_port_22",
|
||||
"ec2_networkacl_allow_ingress_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
|
||||
@@ -553,8 +553,8 @@
|
||||
"Description": "Ensure that ec2 security groups do not allow ingress from internet to common ports",
|
||||
"Checks": [
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"elbv2_ssl_listeners",
|
||||
"ssm_documents_set_as_public",
|
||||
"vpc_subnet_no_public_ip_by_default",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306",
|
||||
"s3_account_level_public_access_blocks"
|
||||
|
||||
@@ -253,8 +253,8 @@
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
|
||||
|
||||
@@ -12,7 +12,7 @@ from prowler.lib.logger import logger
|
||||
|
||||
timestamp = datetime.today()
|
||||
timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
|
||||
prowler_version = "5.12.0"
|
||||
prowler_version = "5.13.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://prowler.com/wp-content/uploads/logo-html.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
@@ -32,6 +32,7 @@ class Provider(str, Enum):
|
||||
GITHUB = "github"
|
||||
IAC = "iac"
|
||||
NHN = "nhn"
|
||||
MONGODBATLAS = "mongodbatlas"
|
||||
|
||||
|
||||
# Compliance
|
||||
|
||||
@@ -581,3 +581,8 @@ m365:
|
||||
github:
|
||||
# github.repository_inactive_not_archived --> CIS recommends 180 days (6 months)
|
||||
inactive_not_archived_days_threshold: 180
|
||||
|
||||
# MongoDB Atlas Configuration
|
||||
mongodbatlas:
|
||||
# mongodbatlas.organizations_service_account_secrets_expiration --> Maximum hours for service account secrets validity
|
||||
max_service_account_secret_validity_hours: 8
|
||||
@@ -0,0 +1,32 @@
|
||||
### Account, Check and/or Region can be * to apply for all the cases.
|
||||
### Account == MongoDB Atlas Organization ID and Region == MongoDB Atlas Cluster Region
|
||||
### Resources and tags are lists that can have either Regex or Keywords.
|
||||
### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
|
||||
### Use an alternation Regex to match one of multiple tags with "ORed" logic.
|
||||
### For each check you can except Accounts, Regions, Resources and/or Tags.
|
||||
########################### MONGODB ATLAS MUTELIST EXAMPLE ###########################
|
||||
Mutelist:
|
||||
Accounts:
|
||||
"your-organization-id-here":
|
||||
Checks:
|
||||
"clusters_authentication_enabled":
|
||||
Regions:
|
||||
- "US_EAST_1"
|
||||
Resources:
|
||||
- "test-cluster"
|
||||
Description: "Mute clusters_authentication_enabled check for test-cluster in US_EAST_1 region"
|
||||
"projects_auditing_enabled":
|
||||
Regions:
|
||||
- "*"
|
||||
Resources:
|
||||
- "*"
|
||||
Description: "Mute projects_auditing_enabled check for all resources in all regions"
|
||||
|
||||
"*":
|
||||
Checks:
|
||||
"clusters_backup_enabled":
|
||||
Regions:
|
||||
- "WESTERN_EUROPE"
|
||||
Resources:
|
||||
- "*"
|
||||
Description: "Mute clusters_backup_enabled check for all clusters in all regions"
|
||||
@@ -637,6 +637,10 @@ def execute(
|
||||
)
|
||||
elif global_provider.type == "m365":
|
||||
is_finding_muted_args["tenant_id"] = global_provider.identity.tenant_id
|
||||
elif global_provider.type == "mongodbatlas":
|
||||
is_finding_muted_args["organization_id"] = (
|
||||
global_provider.identity.organization_id
|
||||
)
|
||||
for finding in check_findings:
|
||||
if global_provider.type == "azure":
|
||||
is_finding_muted_args["subscription_id"] = (
|
||||
|
||||
@@ -717,6 +717,31 @@ class CheckReportNHN(Check_Report):
|
||||
self.location = getattr(resource, "location", "kr1")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckReportMongoDBAtlas(Check_Report):
|
||||
"""Contains the MongoDB Atlas Check's finding information."""
|
||||
|
||||
resource_name: str
|
||||
resource_id: str
|
||||
project_id: str
|
||||
location: str
|
||||
|
||||
def __init__(self, metadata: Dict, resource: Any) -> None:
|
||||
"""Initialize the MongoDB Atlas Check's finding information.
|
||||
|
||||
Args:
|
||||
metadata: The metadata of the check.
|
||||
resource: Basic information about the resource. Defaults to None.
|
||||
"""
|
||||
super().__init__(metadata, resource)
|
||||
self.resource_name = getattr(
|
||||
resource, "name", getattr(resource, "resource_name", "")
|
||||
)
|
||||
self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", ""))
|
||||
self.project_id = getattr(resource, "project_id", "")
|
||||
self.location = getattr(resource, "location", self.project_id)
|
||||
|
||||
|
||||
# Testing Pending
|
||||
def load_check_metadata(metadata_file: str) -> CheckMetadata:
|
||||
"""
|
||||
|
||||
@@ -26,10 +26,10 @@ class ProwlerArgumentParser:
|
||||
self.parser = argparse.ArgumentParser(
|
||||
prog="prowler",
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,dashboard,iac} ...",
|
||||
usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,mongodbatlas,dashboard,iac} ...",
|
||||
epilog="""
|
||||
Available Cloud Providers:
|
||||
{aws,azure,gcp,kubernetes,m365,github,iac,nhn}
|
||||
{aws,azure,gcp,kubernetes,m365,github,iac,nhn,mongodbatlas}
|
||||
aws AWS Provider
|
||||
azure Azure Provider
|
||||
gcp GCP Provider
|
||||
@@ -38,6 +38,7 @@ Available Cloud Providers:
|
||||
github GitHub Provider
|
||||
iac IaC Provider (Preview)
|
||||
nhn NHN Provider (Unofficial)
|
||||
mongodbatlas MongoDB Atlas Provider
|
||||
|
||||
Available components:
|
||||
dashboard Local dashboard
|
||||
@@ -234,6 +235,11 @@ Detailed documentation at https://docs.prowler.com
|
||||
nargs="+",
|
||||
help="Checks to exclude",
|
||||
)
|
||||
exclude_checks_parser.add_argument(
|
||||
"--excluded-checks-file",
|
||||
nargs="?",
|
||||
help="JSON file containing the checks to be excluded. See config/checklist_example.json",
|
||||
)
|
||||
exclude_checks_parser.add_argument(
|
||||
"--excluded-service",
|
||||
"--excluded-services",
|
||||
|
||||
@@ -278,6 +278,18 @@ class Finding(BaseModel):
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["region"] = check_output.location
|
||||
|
||||
elif provider.type == "mongodbatlas":
|
||||
output_data["auth_method"] = "api_key"
|
||||
output_data["account_uid"] = get_nested_attribute(
|
||||
provider, "identity.organization_id"
|
||||
)
|
||||
output_data["account_name"] = get_nested_attribute(
|
||||
provider, "identity.organization_name"
|
||||
)
|
||||
output_data["resource_name"] = check_output.resource_name
|
||||
output_data["resource_uid"] = check_output.resource_id
|
||||
output_data["region"] = check_output.location
|
||||
|
||||
elif provider.type == "nhn":
|
||||
output_data["auth_method"] = (
|
||||
f"passwordCredentials: username={get_nested_attribute(provider, '_identity.username')}, "
|
||||
|
||||
@@ -745,6 +745,51 @@ class HTML(Output):
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_mongodbatlas_assessment_summary(provider: Provider) -> str:
|
||||
"""
|
||||
get_mongodbatlas_assessment_summary gets the HTML assessment summary for the provider
|
||||
|
||||
Args:
|
||||
provider (Provider): the provider object
|
||||
|
||||
Returns:
|
||||
str: the HTML assessment summary
|
||||
"""
|
||||
try:
|
||||
return f"""
|
||||
<div class="col-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
MongoDB Atlas Assessment Summary
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>MongoDB Atlas organization:</b> {provider.identity.organization_name}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
MongoDB Atlas Credentials
|
||||
</div>
|
||||
<ul class="list-group
|
||||
list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<b>MongoDB Atlas authentication method:</b> API Key
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>"""
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_iac_assessment_summary(provider: Provider) -> str:
|
||||
"""
|
||||
|
||||
@@ -79,6 +79,7 @@ class Jira:
|
||||
- test_connection: Test the connection to Jira and return a Connection object
|
||||
- get_projects: Get the projects from Jira
|
||||
- get_available_issue_types: Get the available issue types for a project
|
||||
- get_available_issue_labels: Get the available labels for a project
|
||||
- send_findings: Send the findings to Jira and create an issue
|
||||
|
||||
Raises:
|
||||
@@ -135,6 +136,11 @@ class Jira:
|
||||
}
|
||||
TOKEN_URL = "https://auth.atlassian.com/oauth/token"
|
||||
API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
|
||||
HEADER_TEMPLATE = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Force-Accept-Language": "true",
|
||||
"Accept-Language": "en",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -199,6 +205,31 @@ class Jira:
|
||||
def using_basic_auth(self):
|
||||
return self._using_basic_auth
|
||||
|
||||
def get_headers(
|
||||
self, access_token: str = None, content_type_json: bool = False
|
||||
) -> dict:
|
||||
"""Get headers for API requests
|
||||
|
||||
Args:
|
||||
access_token: The access token to use for authorization
|
||||
content_type_json: Whether to include Content-Type: application/json
|
||||
|
||||
Returns:
|
||||
dict: Headers for API requests
|
||||
"""
|
||||
headers = self.HEADER_TEMPLATE.copy()
|
||||
|
||||
if not content_type_json:
|
||||
headers.pop("Content-Type", None)
|
||||
|
||||
if access_token:
|
||||
if self._using_basic_auth:
|
||||
headers["Authorization"] = f"Basic {access_token}"
|
||||
else:
|
||||
headers["Authorization"] = f"Bearer {access_token}"
|
||||
|
||||
return headers
|
||||
|
||||
def get_params(self, state_encoded):
|
||||
return {
|
||||
**self.PARAMS_TEMPLATE,
|
||||
@@ -302,7 +333,7 @@ class Jira:
|
||||
"redirect_uri": self.redirect_uri,
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
headers = self.get_headers(content_type_json=True)
|
||||
response = requests.post(self.TOKEN_URL, json=body, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -351,7 +382,7 @@ class Jira:
|
||||
"""
|
||||
try:
|
||||
if self._using_basic_auth:
|
||||
headers = {"Authorization": f"Basic {access_token}"}
|
||||
headers = self.get_headers(access_token)
|
||||
response = requests.get(
|
||||
f"https://{domain}.atlassian.net/_edge/tenant_info",
|
||||
headers=headers,
|
||||
@@ -359,7 +390,7 @@ class Jira:
|
||||
response = response.json()
|
||||
return response.get("cloudId")
|
||||
else:
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
headers = self.get_headers(access_token)
|
||||
response = requests.get(self.API_TOKEN_URL, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -377,7 +408,7 @@ class Jira:
|
||||
)
|
||||
else:
|
||||
response_error = f"Failed to get cloud id: {response.status_code} - {response.json()}"
|
||||
logger.warning(response_error)
|
||||
logger.error(response_error)
|
||||
raise JiraGetCloudIDResponseError(
|
||||
message=response_error, file=os.path.basename(__file__)
|
||||
)
|
||||
@@ -441,7 +472,7 @@ class Jira:
|
||||
"refresh_token": self._refresh_token,
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
headers = self.get_headers(content_type_json=True)
|
||||
response = requests.post(url, json=body, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -454,7 +485,7 @@ class Jira:
|
||||
return self._access_token
|
||||
else:
|
||||
response_error = f"Failed to refresh access token: {response.status_code} - {response.json()}"
|
||||
logger.warning(response_error)
|
||||
logger.error(response_error)
|
||||
raise JiraRefreshTokenResponseError(
|
||||
message=response_error, file=os.path.basename(__file__)
|
||||
)
|
||||
@@ -581,10 +612,7 @@ class Jira:
|
||||
if not access_token:
|
||||
return ValueError("Failed to get access token")
|
||||
|
||||
if self._using_basic_auth:
|
||||
headers = {"Authorization": f"Basic {access_token}"}
|
||||
else:
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
headers = self.get_headers(access_token)
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project",
|
||||
@@ -651,10 +679,7 @@ class Jira:
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
if self._using_basic_auth:
|
||||
headers = {"Authorization": f"Basic {access_token}"}
|
||||
else:
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
headers = self.get_headers(access_token)
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue/createmeta?projectKeys={project_key}&expand=projects.issuetypes.fields",
|
||||
@@ -672,7 +697,7 @@ class Jira:
|
||||
return [issue_type["name"] for issue_type in issue_types]
|
||||
else:
|
||||
response_error = f"Failed to get available issue types: {response.status_code} - {response.text}"
|
||||
logger.warning(response_error)
|
||||
logger.error(response_error)
|
||||
raise JiraGetAvailableIssueTypesResponseError(
|
||||
message=response_error, file=os.path.basename(__file__)
|
||||
)
|
||||
@@ -699,10 +724,7 @@ class Jira:
|
||||
if not access_token:
|
||||
return ValueError("Failed to get access token")
|
||||
|
||||
if self._using_basic_auth:
|
||||
headers = {"Authorization": f"Basic {access_token}"}
|
||||
else:
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
headers = self.get_headers(access_token)
|
||||
|
||||
response = requests.get(
|
||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project",
|
||||
@@ -726,7 +748,7 @@ class Jira:
|
||||
if project_response.status_code == 200:
|
||||
project_metadata = project_response.json()
|
||||
if len(project_metadata["projects"]) == 0:
|
||||
logger.warning(
|
||||
logger.error(
|
||||
f"No project metadata found for project {project['key']}, setting empty issue types"
|
||||
)
|
||||
issue_types = []
|
||||
@@ -789,6 +811,9 @@ class Jira:
|
||||
return "#FF0000"
|
||||
if status == "MUTED":
|
||||
return "#FFA500"
|
||||
if status == "MANUAL":
|
||||
return "#FFFF00"
|
||||
return "#000000"
|
||||
|
||||
@staticmethod
|
||||
def get_severity_color(severity: str) -> str:
|
||||
@@ -814,27 +839,30 @@ class Jira:
|
||||
|
||||
@staticmethod
|
||||
def get_adf_description(
|
||||
check_id: str = None,
|
||||
check_title: str = None,
|
||||
severity: str = None,
|
||||
severity_color: str = None,
|
||||
status: str = None,
|
||||
status_color: str = None,
|
||||
status_extended: str = None,
|
||||
provider: str = None,
|
||||
region: str = None,
|
||||
resource_uid: str = None,
|
||||
resource_name: str = None,
|
||||
risk: str = None,
|
||||
recommendation_text: str = None,
|
||||
recommendation_url: str = None,
|
||||
remediation_code_native_iac: str = None,
|
||||
remediation_code_terraform: str = None,
|
||||
remediation_code_cli: str = None,
|
||||
remediation_code_other: str = None,
|
||||
resource_tags: dict = None,
|
||||
compliance: dict = None,
|
||||
check_id: str = "",
|
||||
check_title: str = "",
|
||||
severity: str = "",
|
||||
severity_color: str = "",
|
||||
status: str = "",
|
||||
status_color: str = "",
|
||||
status_extended: str = "",
|
||||
provider: str = "",
|
||||
region: str = "",
|
||||
resource_uid: str = "",
|
||||
resource_name: str = "",
|
||||
risk: str = "",
|
||||
recommendation_text: str = "",
|
||||
recommendation_url: str = "",
|
||||
remediation_code_native_iac: str = "",
|
||||
remediation_code_terraform: str = "",
|
||||
remediation_code_cli: str = "",
|
||||
remediation_code_other: str = "",
|
||||
resource_tags: dict = "",
|
||||
compliance: dict = "",
|
||||
finding_url: str = "",
|
||||
tenant_info: str = "",
|
||||
) -> dict:
|
||||
|
||||
table_rows = [
|
||||
{
|
||||
"type": "tableRow",
|
||||
@@ -1411,6 +1439,93 @@ class Jira:
|
||||
}
|
||||
)
|
||||
|
||||
if finding_url:
|
||||
table_rows.append(
|
||||
{
|
||||
"type": "tableRow",
|
||||
"content": [
|
||||
{
|
||||
"type": "tableCell",
|
||||
"attrs": {"colwidth": [1]},
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Finding URL",
|
||||
"marks": [{"type": "strong"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "tableCell",
|
||||
"attrs": {"colwidth": [3]},
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": finding_url,
|
||||
"marks": [
|
||||
{
|
||||
"type": "link",
|
||||
"attrs": {"href": finding_url},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
if tenant_info:
|
||||
table_rows.append(
|
||||
{
|
||||
"type": "tableRow",
|
||||
"content": [
|
||||
{
|
||||
"type": "tableCell",
|
||||
"attrs": {"colwidth": [1]},
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Tenant Info",
|
||||
"marks": [{"type": "strong"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "tableCell",
|
||||
"attrs": {"colwidth": [3]},
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": tenant_info,
|
||||
"marks": [{"type": "code"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"type": "doc",
|
||||
"version": 1,
|
||||
@@ -1437,6 +1552,9 @@ class Jira:
|
||||
findings: list[Finding] = None,
|
||||
project_key: str = None,
|
||||
issue_type: str = None,
|
||||
issue_labels: list[str] = None,
|
||||
finding_url: str = None,
|
||||
tenant_info: str = None,
|
||||
):
|
||||
"""
|
||||
Send the findings to Jira
|
||||
@@ -1445,6 +1563,9 @@ class Jira:
|
||||
- findings: The findings to send
|
||||
- project_key: The project key
|
||||
- issue_type: The issue type
|
||||
- issue_labels: The issue labels
|
||||
- finding_url: The finding URL
|
||||
- tenant_info: The tenant info
|
||||
|
||||
Raises:
|
||||
- JiraRefreshTokenError: Failed to refresh the access token
|
||||
@@ -1479,16 +1600,7 @@ class Jira:
|
||||
message="The issue type is invalid", file=os.path.basename(__file__)
|
||||
)
|
||||
|
||||
if self._using_basic_auth:
|
||||
headers = {
|
||||
"Authorization": f"Basic {access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
else:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
headers = self.get_headers(access_token, content_type_json=True)
|
||||
|
||||
for finding in findings:
|
||||
status_color = self.get_color_from_status(finding.status.value)
|
||||
@@ -1516,15 +1628,30 @@ class Jira:
|
||||
remediation_code_other=finding.metadata.Remediation.Code.Other,
|
||||
resource_tags=finding.resource_tags,
|
||||
compliance=finding.compliance,
|
||||
finding_url=finding_url,
|
||||
tenant_info=tenant_info,
|
||||
)
|
||||
summary_parts = ["[Prowler]"]
|
||||
if finding.metadata.Severity.value:
|
||||
summary_parts.append(finding.metadata.Severity.value.upper())
|
||||
if finding.metadata.CheckID:
|
||||
summary_parts.append(finding.metadata.CheckID)
|
||||
if finding.resource_uid:
|
||||
summary_parts.append(finding.resource_uid)
|
||||
|
||||
summary = " - ".join(summary_parts[1:])
|
||||
summary = f"{summary_parts[0]} {summary}"
|
||||
|
||||
payload = {
|
||||
"fields": {
|
||||
"project": {"key": project_key},
|
||||
"summary": f"[Prowler] {finding.metadata.Severity.value.upper()} - {finding.metadata.CheckID} - {finding.resource_uid}",
|
||||
"summary": summary,
|
||||
"description": adf_description,
|
||||
"issuetype": {"name": issue_type},
|
||||
}
|
||||
}
|
||||
if issue_labels:
|
||||
payload["fields"]["labels"] = issue_labels
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
|
||||
@@ -1533,7 +1660,15 @@ class Jira:
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
response_json = response.json()
|
||||
try:
|
||||
response_json = response.json()
|
||||
except (ValueError, requests.exceptions.JSONDecodeError):
|
||||
response_error = f"Failed to send finding: {response.status_code} - {response.text}"
|
||||
logger.error(response_error)
|
||||
raise JiraSendFindingsResponseError(
|
||||
message=response_error, file=os.path.basename(__file__)
|
||||
)
|
||||
|
||||
# Check if the error is due to required custom fields
|
||||
if response.status_code == 400 and "errors" in response_json:
|
||||
errors = response_json.get("errors", {})
|
||||
@@ -1556,12 +1691,18 @@ class Jira:
|
||||
)
|
||||
|
||||
response_error = f"Failed to send finding: {response.status_code} - {response_json}"
|
||||
logger.warning(response_error)
|
||||
logger.error(response_error)
|
||||
raise JiraSendFindingsResponseError(
|
||||
message=response_error, file=os.path.basename(__file__)
|
||||
)
|
||||
else:
|
||||
logger.info(f"Finding sent successfully: {response.json()}")
|
||||
try:
|
||||
response_json = response.json()
|
||||
logger.info(f"Finding sent successfully: {response_json}")
|
||||
except (ValueError, requests.exceptions.JSONDecodeError):
|
||||
logger.info(
|
||||
f"Finding sent successfully: Status {response.status_code}"
|
||||
)
|
||||
except JiraRequiredCustomFieldsError as custom_fields_error:
|
||||
raise custom_fields_error
|
||||
except JiraRefreshTokenError as refresh_error:
|
||||
@@ -1574,3 +1715,201 @@ class Jira:
|
||||
message="Failed to create an issue in Jira",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
def send_finding(
|
||||
self,
|
||||
check_id: str = "",
|
||||
check_title: str = "",
|
||||
severity: str = "",
|
||||
status: str = "",
|
||||
status_extended: str = "",
|
||||
provider: str = "",
|
||||
region: str = "",
|
||||
resource_uid: str = "",
|
||||
resource_name: str = "",
|
||||
risk: str = "",
|
||||
recommendation_text: str = "",
|
||||
recommendation_url: str = "",
|
||||
remediation_code_native_iac: str = "",
|
||||
remediation_code_terraform: str = "",
|
||||
remediation_code_cli: str = "",
|
||||
remediation_code_other: str = "",
|
||||
resource_tags: dict = "",
|
||||
compliance: dict = "",
|
||||
project_key: str = "",
|
||||
issue_type: str = "",
|
||||
issue_labels: list[str] = "",
|
||||
finding_url: str = "",
|
||||
tenant_info: str = "",
|
||||
) -> bool:
|
||||
"""
|
||||
Send the finding to Jira
|
||||
|
||||
Args:
|
||||
- check_id: The check ID
|
||||
- check_title: The check title
|
||||
- severity: The severity
|
||||
- status: The status
|
||||
- status_extended: The status extended
|
||||
- provider: The provider
|
||||
- region: The region
|
||||
- resource_uid: The resource UID
|
||||
- resource_name: The resource name
|
||||
- risk: The risk
|
||||
- recommendation_text: The recommendation text
|
||||
- recommendation_url: The recommendation URL
|
||||
- remediation_code_native_iac: The remediation code native IAC
|
||||
- remediation_code_terraform: The remediation code terraform
|
||||
- remediation_code_cli: The remediation code CLI
|
||||
- remediation_code_other: The remediation code other
|
||||
- resource_tags: The resource tags
|
||||
- compliance: The compliance
|
||||
- project_key: The project key
|
||||
- issue_type: The issue type
|
||||
- issue_labels: The issue labels
|
||||
- finding_url: The finding URL
|
||||
- tenant_info: The tenant info
|
||||
|
||||
Raises:
|
||||
- JiraRefreshTokenError: Failed to refresh the access token
|
||||
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
|
||||
- JiraCreateIssueError: Failed to create an issue in Jira
|
||||
- JiraSendFindingsResponseError: Failed to send the finding to Jira
|
||||
- JiraRequiredCustomFieldsError: Jira project requires custom fields that are not supported
|
||||
|
||||
Returns:
|
||||
- True if the finding was sent successfully
|
||||
- False if the finding was not sent successfully
|
||||
"""
|
||||
try:
|
||||
access_token = self.get_access_token()
|
||||
|
||||
if not access_token:
|
||||
raise JiraNoTokenError(
|
||||
message="No token was found",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
projects = self.get_projects()
|
||||
|
||||
if project_key not in projects:
|
||||
logger.error("The project key is invalid")
|
||||
raise JiraInvalidProjectKeyError(
|
||||
message="The project key is invalid",
|
||||
file=os.path.basename(__file__),
|
||||
)
|
||||
|
||||
available_issue_types = self.get_available_issue_types(project_key)
|
||||
|
||||
if issue_type not in available_issue_types:
|
||||
logger.error("The issue type is invalid")
|
||||
raise JiraInvalidIssueTypeError(
|
||||
message="The issue type is invalid", file=os.path.basename(__file__)
|
||||
)
|
||||
|
||||
headers = self.get_headers(access_token, content_type_json=True)
|
||||
|
||||
status_color = self.get_color_from_status(status)
|
||||
severity_color = self.get_severity_color(severity.lower())
|
||||
adf_description = self.get_adf_description(
|
||||
check_id=check_id,
|
||||
check_title=check_title,
|
||||
severity=severity.upper(),
|
||||
severity_color=severity_color,
|
||||
status=status,
|
||||
status_color=status_color,
|
||||
status_extended=status_extended,
|
||||
provider=provider,
|
||||
region=region,
|
||||
resource_uid=resource_uid,
|
||||
resource_name=resource_name,
|
||||
risk=risk,
|
||||
recommendation_text=recommendation_text,
|
||||
recommendation_url=recommendation_url,
|
||||
remediation_code_native_iac=remediation_code_native_iac,
|
||||
remediation_code_terraform=remediation_code_terraform,
|
||||
remediation_code_cli=remediation_code_cli,
|
||||
remediation_code_other=remediation_code_other,
|
||||
resource_tags=resource_tags,
|
||||
compliance=compliance,
|
||||
finding_url=finding_url,
|
||||
tenant_info=tenant_info,
|
||||
)
|
||||
|
||||
summary_parts = ["[Prowler]"]
|
||||
if severity:
|
||||
summary_parts.append(severity.upper())
|
||||
if check_id:
|
||||
summary_parts.append(check_id)
|
||||
if resource_uid:
|
||||
summary_parts.append(resource_uid)
|
||||
summary = " - ".join(summary_parts[1:])
|
||||
summary = f"{summary_parts[0]} {summary}"
|
||||
|
||||
payload = {
|
||||
"fields": {
|
||||
"project": {"key": project_key},
|
||||
"summary": summary,
|
||||
"description": adf_description,
|
||||
"issuetype": {"name": issue_type},
|
||||
}
|
||||
}
|
||||
if issue_labels:
|
||||
payload["fields"]["labels"] = issue_labels
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
try:
|
||||
response_json = response.json()
|
||||
except (ValueError, requests.exceptions.JSONDecodeError):
|
||||
response_error = f"Failed to send finding: {response.status_code} - {response.text}"
|
||||
logger.error(response_error)
|
||||
return False
|
||||
|
||||
# Check if the error is due to required custom fields
|
||||
if response.status_code == 400 and "errors" in response_json:
|
||||
errors = response_json.get("errors", {})
|
||||
# Look for custom field errors (fields starting with "customfield_")
|
||||
custom_field_errors = {
|
||||
k: v for k, v in errors.items() if k.startswith("customfield_")
|
||||
}
|
||||
if custom_field_errors:
|
||||
custom_fields_formatted = ", ".join(
|
||||
[f"'{k}': '{v}'" for k, v in custom_field_errors.items()]
|
||||
)
|
||||
logger.error(
|
||||
f"Jira project requires custom fields that are not supported: {custom_fields_formatted}"
|
||||
)
|
||||
return False
|
||||
|
||||
response_error = (
|
||||
f"Failed to send finding: {response.status_code} - {response_json}"
|
||||
)
|
||||
logger.error(response_error)
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
response_json = response.json()
|
||||
logger.info(f"Finding sent successfully: {response_json}")
|
||||
except (ValueError, requests.exceptions.JSONDecodeError):
|
||||
logger.info(
|
||||
f"Finding sent successfully: Status {response.status_code}"
|
||||
)
|
||||
return True
|
||||
except JiraRequiredCustomFieldsError as custom_fields_error:
|
||||
logger.error(f"Custom fields error: {custom_fields_error}")
|
||||
return False
|
||||
except JiraRefreshTokenError as refresh_error:
|
||||
logger.error(f"Token refresh error: {refresh_error}")
|
||||
return False
|
||||
except JiraRefreshTokenResponseError as response_error:
|
||||
logger.error(f"Token response error: {response_error}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send finding: {e}")
|
||||
return False
|
||||
@@ -20,6 +20,8 @@ def stdout_report(finding, color, verbose, status, fix):
|
||||
details = finding.owner
|
||||
if finding.check_metadata.Provider == "m365":
|
||||
details = finding.location
|
||||
if finding.check_metadata.Provider == "mongodbatlas":
|
||||
details = finding.location
|
||||
if finding.check_metadata.Provider == "nhn":
|
||||
details = finding.location
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ def display_summary_table(
|
||||
elif provider.type == "m365":
|
||||
entity_type = "Tenant Domain"
|
||||
audited_entities = provider.identity.tenant_domain
|
||||
elif provider.type == "mongodbatlas":
|
||||
entity_type = "Organization"
|
||||
audited_entities = provider.identity.organization_name
|
||||
elif provider.type == "nhn":
|
||||
entity_type = "Tenant Domain"
|
||||
audited_entities = provider.identity.tenant_domain
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from boto3 import Session
|
||||
from botocore.client import ClientError
|
||||
@@ -219,7 +219,7 @@ class SecurityHub:
|
||||
session: Session,
|
||||
aws_account_id: str,
|
||||
aws_partition: str,
|
||||
) -> tuple[str, Session | None]:
|
||||
) -> tuple[str, Union[Session, None]]:
|
||||
"""
|
||||
Check if Security Hub is enabled in a specific region and if Prowler integration is active.
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ class cognito_user_pool_self_registration_disabled(Check):
|
||||
report.status_extended = (
|
||||
f"User pool {user_pool.id} has self registration disabled."
|
||||
)
|
||||
if not user_pool.admin_create_user_config.allow_admin_create_user_only:
|
||||
if (
|
||||
user_pool.admin_create_user_config
|
||||
and not user_pool.admin_create_user_config.allow_admin_create_user_only
|
||||
):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"User pool {user_pool.id} has self registration enabled."
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21",
|
||||
"CheckID": "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
|
||||
"CheckTitle": "Ensure no security groups allow ingress from 0.0.0.0/0 or ::/0 to FTP ports 20 or 21.",
|
||||
"CheckAliases": [
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21"
|
||||
],
|
||||
"CheckType": [
|
||||
"Infrastructure Security"
|
||||
],
|
||||
@@ -7,7 +7,7 @@ from prowler.providers.aws.services.ec2.lib.security_groups import check_securit
|
||||
from prowler.providers.aws.services.vpc.vpc_client import vpc_client
|
||||
|
||||
|
||||
class ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21(Check):
|
||||
class ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21(Check):
|
||||
def execute(self):
|
||||
findings = []
|
||||
check_ports = [20, 21]
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018",
|
||||
"CheckID": "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
|
||||
"CheckTitle": "Ensure no security groups allow ingress from 0.0.0.0/0 or ::/0 to MongoDB ports 27017 and 27018.",
|
||||
"CheckAliases": [
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018"
|
||||
],
|
||||
"CheckType": [
|
||||
"Infrastructure Security"
|
||||
],
|
||||
@@ -7,7 +7,9 @@ from prowler.providers.aws.services.ec2.lib.security_groups import check_securit
|
||||
from prowler.providers.aws.services.vpc.vpc_client import vpc_client
|
||||
|
||||
|
||||
class ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018(Check):
|
||||
class ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018(
|
||||
Check
|
||||
):
|
||||
def execute(self):
|
||||
findings = []
|
||||
check_ports = [27017, 27018]
|
||||
@@ -32,9 +32,10 @@ class firehose_stream_encrypted_at_rest(Check):
|
||||
source_stream = kinesis_client.streams.get(
|
||||
stream.source.kinesis_stream.kinesis_stream_arn
|
||||
)
|
||||
if source_stream.encrypted_at_rest != EncryptionType.NONE:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled but the source stream {source_stream.name} has at rest encryption enabled."
|
||||
if source_stream:
|
||||
if source_stream.encrypted_at_rest != EncryptionType.NONE:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled but the source stream {source_stream.name} has at rest encryption enabled."
|
||||
|
||||
# Check if the stream has encryption enabled directly
|
||||
elif stream.kms_encryption == EncryptionStatus.ENABLED:
|
||||
|
||||
@@ -36,7 +36,7 @@ class MemoryDB(AWSService):
|
||||
region=regional_client.region,
|
||||
security_groups=[
|
||||
sg["SecurityGroupId"]
|
||||
for sg in cluster["SecurityGroups"]
|
||||
for sg in cluster.get("SecurityGroups", [])
|
||||
if sg["Status"] == "active"
|
||||
],
|
||||
tls_enabled=cluster["TLSEnabled"],
|
||||
|
||||
@@ -79,7 +79,7 @@ class OpenSearchService(AWSService):
|
||||
"AdvancedSecurityOptions"
|
||||
].get("Enabled", False)
|
||||
cluster_config = describe_domain["DomainStatus"].get("ClusterConfig", {})
|
||||
domain.instance_count = cluster_config.get("InstanceCount", None)
|
||||
domain.instance_count = cluster_config.get("InstanceCount", 0)
|
||||
domain.zone_awareness_enabled = cluster_config.get(
|
||||
"ZoneAwarenessEnabled", False
|
||||
)
|
||||
@@ -155,10 +155,10 @@ class OpenSearchDomain(BaseModel):
|
||||
saml_enabled: bool = None
|
||||
update_available: bool = None
|
||||
version: str = None
|
||||
instance_count: Optional[int]
|
||||
instance_count: int = 0
|
||||
zone_awareness_enabled: Optional[bool]
|
||||
tags: Optional[list] = []
|
||||
advanced_settings_enabled: bool = None
|
||||
dedicated_master_enabled: Optional[bool]
|
||||
dedicated_master_count: Optional[int]
|
||||
dedicated_master_count: int = 0
|
||||
tags: Optional[list] = []
|
||||
@@ -261,6 +261,15 @@ class Provider(ABC):
|
||||
personal_access_token=arguments.personal_access_token,
|
||||
oauth_app_token=arguments.oauth_app_token,
|
||||
)
|
||||
elif "mongodbatlas" in provider_class_name.lower():
|
||||
provider_class(
|
||||
atlas_public_key=arguments.atlas_public_key,
|
||||
atlas_private_key=arguments.atlas_private_key,
|
||||
atlas_project_id=arguments.atlas_project_id,
|
||||
config_path=arguments.config_file,
|
||||
mutelist_path=arguments.mutelist_file,
|
||||
fixer_config=fixer_config,
|
||||
)
|
||||
|
||||
except TypeError as error:
|
||||
logger.critical(
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "Conditional Access Policy",
|
||||
"Description": "Ensure ",
|
||||
"Description": "This check verifies that phishing-resistant MFA strength is required for all administrator accounts. Phishing-resistant MFA includes authentication methods that are resistant to phishing attacks and MFA fatigue attacks compared to weaker methods like SMS or push notifications.",
|
||||
"Risk": "Administrators using weaker MFA methods, such as SMS or push notifications, are vulnerable to phishing attacks and MFA fatigue attacks. Attackers can intercept codes or trick users into approving fraudulent authentication requests, leading to unauthorized access to critical systems.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-admin-phish-resistant-mfa",
|
||||
"Remediation": {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Supported encryption providers
|
||||
ATLAS_ENCRYPTION_PROVIDERS = ["AWS", "AZURE", "GCP", "NONE"]
|
||||
@@ -0,0 +1,118 @@
|
||||
from prowler.exceptions.exceptions import ProwlerException
|
||||
|
||||
|
||||
# Exceptions codes from 8000 to 8999 are reserved for MongoDB Atlas exceptions
|
||||
class MongoDBAtlasBaseException(ProwlerException):
|
||||
"""Base class for MongoDB Atlas Errors."""
|
||||
|
||||
MONGODBATLAS_ERROR_CODES = {
|
||||
(8000, "MongoDBAtlasCredentialsError"): {
|
||||
"message": "MongoDB Atlas credentials not found or invalid",
|
||||
"remediation": "Check the MongoDB Atlas API credentials and ensure they are properly set.",
|
||||
},
|
||||
(8001, "MongoDBAtlasAuthenticationError"): {
|
||||
"message": "MongoDB Atlas authentication failed",
|
||||
"remediation": "Check the MongoDB Atlas API credentials and ensure they are valid.",
|
||||
},
|
||||
(8002, "MongoDBAtlasSessionError"): {
|
||||
"message": "MongoDB Atlas session setup failed",
|
||||
"remediation": "Check the session setup and ensure it is properly configured.",
|
||||
},
|
||||
(8003, "MongoDBAtlasIdentityError"): {
|
||||
"message": "MongoDB Atlas identity setup failed",
|
||||
"remediation": "Check credentials and ensure they are properly set up for MongoDB Atlas.",
|
||||
},
|
||||
(8004, "MongoDBAtlasAPIError"): {
|
||||
"message": "MongoDB Atlas API call failed",
|
||||
"remediation": "Check the API request and ensure it is properly formatted.",
|
||||
},
|
||||
(8005, "MongoDBAtlasRateLimitError"): {
|
||||
"message": "MongoDB Atlas API rate limit exceeded",
|
||||
"remediation": "Reduce the number of API requests or wait before making more requests.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
provider = "MongoDB Atlas"
|
||||
error_info = self.MONGODBATLAS_ERROR_CODES.get((code, self.__class__.__name__))
|
||||
if message:
|
||||
error_info["message"] = message
|
||||
super().__init__(
|
||||
code=code,
|
||||
source=provider,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
error_info=error_info,
|
||||
)
|
||||
|
||||
|
||||
class MongoDBAtlasCredentialsError(MongoDBAtlasBaseException):
|
||||
"""Exception for MongoDB Atlas credentials errors"""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
code=8000,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class MongoDBAtlasAuthenticationError(MongoDBAtlasBaseException):
|
||||
"""Exception for MongoDB Atlas authentication errors"""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
code=8001,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class MongoDBAtlasSessionError(MongoDBAtlasBaseException):
|
||||
"""Exception for MongoDB Atlas session setup errors"""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
code=8002,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class MongoDBAtlasIdentityError(MongoDBAtlasBaseException):
|
||||
"""Exception for MongoDB Atlas identity setup errors"""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
code=8003,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class MongoDBAtlasAPIError(MongoDBAtlasBaseException):
|
||||
"""Exception for MongoDB Atlas API errors"""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
code=8004,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class MongoDBAtlasRateLimitError(MongoDBAtlasBaseException):
|
||||
"""Exception for MongoDB Atlas rate limit errors"""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
code=8005,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
def init_parser(self):
|
||||
"""Initialize the MongoDB Atlas Provider CLI parser"""
|
||||
mongodbatlas_parser = self.subparsers.add_parser(
|
||||
"mongodbatlas",
|
||||
parents=[self.common_providers_parser],
|
||||
help="MongoDB Atlas Provider",
|
||||
)
|
||||
|
||||
mongodbatlas_auth_subparser = mongodbatlas_parser.add_argument_group(
|
||||
"Authentication Modes"
|
||||
)
|
||||
|
||||
mongodbatlas_auth_subparser.add_argument(
|
||||
"--atlas-public-key",
|
||||
nargs="?",
|
||||
help="MongoDB Atlas API public key",
|
||||
default=None,
|
||||
metavar="ATLAS_PUBLIC_KEY",
|
||||
)
|
||||
|
||||
mongodbatlas_auth_subparser.add_argument(
|
||||
"--atlas-private-key",
|
||||
nargs="?",
|
||||
help="MongoDB Atlas API private key",
|
||||
default=None,
|
||||
metavar="ATLAS_PRIVATE_KEY",
|
||||
)
|
||||
|
||||
mongodbatlas_filters_subparser = mongodbatlas_parser.add_argument_group(
|
||||
"Optional Filters"
|
||||
)
|
||||
|
||||
mongodbatlas_filters_subparser.add_argument(
|
||||
"--atlas-project-id",
|
||||
nargs="?",
|
||||
help="MongoDB Atlas Project ID to filter scans to a specific project",
|
||||
default=None,
|
||||
metavar="ATLAS_PROJECT_ID",
|
||||
)
|
||||
|
||||
|
||||
def validate_arguments(arguments):
|
||||
"""Validate MongoDB Atlas provider arguments"""
|
||||
# No specific validation needed for MongoDB Atlas arguments currently
|
||||
return (True, "")
|
||||
@@ -0,0 +1,30 @@
|
||||
from prowler.lib.check.models import CheckReportMongoDBAtlas
|
||||
from prowler.lib.mutelist.mutelist import Mutelist
|
||||
from prowler.lib.outputs.utils import unroll_dict, unroll_tags
|
||||
|
||||
|
||||
class MongoDBAtlasMutelist(Mutelist):
|
||||
"""MongoDB Atlas Mutelist class"""
|
||||
|
||||
def is_finding_muted(
|
||||
self,
|
||||
finding: CheckReportMongoDBAtlas,
|
||||
organization_id: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a finding is muted in the MongoDB Atlas mutelist.
|
||||
|
||||
Args:
|
||||
finding: The CheckReportMongoDBAtlas finding
|
||||
organization_id: The organization/project id
|
||||
|
||||
Returns:
|
||||
bool: True if the finding is muted, False otherwise
|
||||
"""
|
||||
return self.is_muted(
|
||||
organization_id,
|
||||
finding.check_metadata.CheckID,
|
||||
finding.location, # TODO: Study regions in MongoDB Atlas
|
||||
finding.resource_name,
|
||||
unroll_dict(unroll_tags(finding.resource_tags)),
|
||||
)
|
||||