Skip to content

Conversation

@vicferpoy
Copy link
Member

@vicferpoy vicferpoy commented Nov 25, 2025

Description

A new endpoint has been added to retrieve an overview of the attack surfaces in the tenant. In essence, these are just category filters. Given Prowler's attack surface definition, we do not have a set of defined check IDs for every category, for every provider type. That's the reason behind having some hardcoded values for now.

In addition, some refactoring was included in the PR to DRY some parts of the code.

{
  "data": [
    {
      "type": "attack-surface-overviews",
      "id": "internet-exposed",
      "attributes": {
        "total_findings": 118,
        "failed_findings": 3,
        "muted_failed_findings": 0,
        "check_ids": [
          "ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
          "rds_snapshots_public_access",
          "neptune_cluster_public_snapshot",
          "ec2_instance_port_elasticsearch_kibana_exposed_to_internet",
          "elbv2_internet_facing",
          "emr_cluster_publicly_accesible",
          "ecr_repositories_not_publicly_accessible",
          "emr_cluster_master_nodes_no_public_ip",
          "ec2_instance_port_telnet_exposed_to_internet",
          "ec2_instance_port_ssh_exposed_to_internet",
          "documentdb_cluster_public_snapshot",
          "elb_internet_facing",
          "awslambda_function_not_publicly_accessible",
          "glue_data_catalogs_not_publicly_accessible",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
          "emr_cluster_account_public_block_enabled",
          "sqs_queues_not_publicly_accessible",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483",
          "ec2_networkacl_allow_ingress_any_port",
          "apigateway_restapi_public_with_authorizer",
          "s3_bucket_policy_public_write_access",
          "lightsail_instance_public",
          "ec2_securitygroup_allow_ingress_from_internet_to_any_port",
          "ec2_instance_port_ldap_exposed_to_internet",
          "s3_bucket_public_access",
          "ec2_instance_port_redis_exposed_to_internet",
          "eventbridge_bus_exposed",
          "accessanalyzer_enabled_without_findings",
          "lightsail_database_public",
          "rds_instance_no_public_access",
          "cloudwatch_log_group_not_publicly_accessible",
          "ec2_instance_port_memcached_exposed_to_internet",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23",
          "sns_topics_not_publicly_accessible",
          "ec2_instance_port_rdp_exposed_to_internet",
          "ec2_instance_port_sqlserver_exposed_to_internet",
          "ec2_networkacl_allow_ingress_tcp_port_22",
          "ec2_instance_account_imdsv2_enabled",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306",
          "ec2_instance_internet_facing_with_instance_profile",
          "ec2_instance_port_oracle_exposed_to_internet",
          "ec2_instance_port_kafka_exposed_to_internet",
          "ec2_networkacl_unused",
          "ec2_instance_port_mysql_exposed_to_internet",
          "elasticache_cluster_uses_public_subnet",
          "eks_cluster_private_nodes_enabled",
          "opensearch_service_domains_not_publicly_accessible",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601",
          "codebuild_project_not_publicly_accessible",
          "ec2_elastic_ip_shodan",
          "ec2_securitygroup_allow_wide_open_public_ipv4",
          "ec2_instance_port_mongodb_exposed_to_internet",
          "eks_cluster_not_publicly_accessible",
          "cloudfront_distributions_geo_restrictions_enabled",
          "redshift_cluster_public_access",
          "ecs_service_no_assign_public_ip",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379",
          "sagemaker_notebook_instance_without_direct_internet_access_configured",
          "autoscaling_group_launch_configuration_no_public_ip",
          "awslambda_function_url_cors_policy",
          "neptune_cluster_uses_public_subnet",
          "ec2_ebs_snapshot_account_block_public_access",
          "efs_mount_target_not_publicly_accessible",
          "ec2_instance_port_ftp_exposed_to_internet",
          "secretsmanager_not_publicly_accessible",
          "mq_broker_not_publicly_accessible",
          "ec2_ebs_public_snapshot",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_ftp_20_21",
          "ecs_task_set_no_assign_public_ip",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mongodb_27017_27018",
          "ses_identity_not_publicly_accessible",
          "cloudfront_distributions_https_enabled",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434",
          "ssm_documents_set_as_public",
          "s3_bucket_public_list_acl",
          "ec2_instance_port_cifs_exposed_to_internet",
          "appstream_fleet_default_internet_access_disabled",
          "awslambda_function_url_public",
          "ec2_instance_port_postgresql_exposed_to_internet",
          "cloudfront_distributions_using_waf",
          "dms_instance_no_public_access",
          "kafka_cluster_is_public",
          "apigateway_restapi_public",
          "ec2_instance_public_ip",
          "fms_policy_compliant",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888",
          "ec2_instance_port_kerberos_exposed_to_internet",
          "ec2_ami_public",
          "glacier_vaults_policy_public_access",
          "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211",
          "cloudtrail_logs_s3_bucket_is_not_publicly_accessible",
          "s3_bucket_public_write_acl",
          "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
          "ec2_networkacl_allow_ingress_tcp_port_3389",
          "ec2_instance_port_cassandra_exposed_to_internet"
        ]
      }
    },
    {
      "type": "attack-surface-overviews",
      "id": "secrets",
      "attributes": {
        "total_findings": 9,
        "failed_findings": 0,
        "muted_failed_findings": 0,
        "check_ids": [
          "autoscaling_find_secrets_ec2_launch_configuration",
          "secretsmanager_secret_unused",
          "ec2_instance_secrets_user_data",
          "ecs_task_definitions_no_environment_secrets",
          "ec2_launch_template_no_secrets",
          "autoscaling_group_launch_configuration_requires_imdsv2",
          "cloudformation_stack_outputs_find_secrets",
          "awslambda_function_no_secrets_in_variables",
          "codebuild_project_source_repo_url_no_sensitive_credentials",
          "cloudwatch_log_group_no_secrets_in_logs",
          "ssm_document_secrets",
          "awslambda_function_no_secrets_in_code",
          "secretsmanager_secret_rotated_periodically",
          "codebuild_project_no_secrets_in_variables"
        ]
      }
    },
    {
      "type": "attack-surface-overviews",
      "id": "privilege-escalation",
      "attributes": {
        "total_findings": 19,
        "failed_findings": 1,
        "muted_failed_findings": 0,
        "check_ids": [
          "iam_inline_policy_allows_privilege_escalation",
          "iam_policy_allows_privilege_escalation"
        ]
      }
    },
    {
      "type": "attack-surface-overviews",
      "id": "ec2-imdsv1",
      "attributes": {
        "total_findings": 0,
        "failed_findings": 0,
        "muted_failed_findings": 0,
        "check_ids": [
          "ec2_instance_imdsv2_enabled"
        ]
      }
    }
  ],
  "meta": {
    "version": "v1"
  }
}

Query plans

Note

Some sequential scans for provider and scans are due to the lack of enough sample data in the test lab. More tenants would be needed.

GET /overviews/attack-surfaces
SELECT "attack_surface_overviews"."attack_surface_type",
       COALESCE(SUM("attack_surface_overviews"."total_findings"), 0) AS "total_findings",
       COALESCE(SUM("attack_surface_overviews"."failed_findings"), 0) AS "failed_findings",
       COALESCE(SUM("attack_surface_overviews"."muted_failed_findings"), 0) AS "muted_failed_findings"
FROM "attack_surface_overviews"
WHERE ("attack_surface_overviews"."scan_id" IN
         (SELECT DISTINCT ON (U0."provider_id") U0."id"
          FROM "scans" U0
          WHERE (U0."state" = completed
                 AND U0."tenant_id" = 7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb)
          ORDER BY U0."provider_id" ASC,
                   U0."inserted_at" DESC)
       AND "attack_surface_overviews"."tenant_id" = 7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb)
GROUP BY "attack_surface_overviews"."attack_surface_type"
14.34ms
0 joins
Query Plan
GroupAggregate  (cost=10.85..10.88 rows=1 width=142)
  Group Key: attack_surface_overviews.attack_surface_type
  ->  Sort  (cost=10.85..10.86 rows=1 width=130)
        Sort Key: attack_surface_overviews.attack_surface_type
        ->  Nested Loop Semi Join  (cost=5.42..10.84 rows=1 width=130)
              Join Filter: (attack_surface_overviews.scan_id = u0.id)
              ->  Bitmap Heap Scan on attack_surface_overviews  (cost=4.16..9.53 rows=1 width=146)
                    Recheck Cond: (tenant_id = '7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb'::uuid)
                    Filter: CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END
                    ->  Bitmap Index Scan on attack_surface_overviews_tenant_id_5e919d1b  (cost=0.00..4.16 rows=2 width=0)
                          Index Cond: (tenant_id = '7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb'::uuid)
              ->  Unique  (cost=1.26..1.27 rows=2 width=40)
                    ->  Sort  (cost=1.26..1.26 rows=2 width=40)
                          Sort Key: u0.provider_id, u0.inserted_at DESC
                          ->  Seq Scan on scans u0  (cost=0.00..1.25 rows=2 width=40)
                                Filter: ((tenant_id = '7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb'::uuid) AND CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END AND (state = 'completed'::state))
GET /overviews/attack-surfaces?filter[provider_id]=
SELECT "attack_surface_overviews"."attack_surface_type",
       COALESCE(SUM("attack_surface_overviews"."total_findings"), 0) AS "total_findings",
       COALESCE(SUM("attack_surface_overviews"."failed_findings"), 0) AS "failed_findings",
       COALESCE(SUM("attack_surface_overviews"."muted_failed_findings"), 0) AS "muted_failed_findings"
FROM "attack_surface_overviews"
INNER JOIN "scans" ON ("attack_surface_overviews"."scan_id" = "scans"."id")
WHERE ("attack_surface_overviews"."scan_id" IN
         (SELECT DISTINCT ON (U0."provider_id") U0."id"
          FROM "scans" U0
          WHERE (U0."state" = completed
                 AND U0."tenant_id" = 7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb)
          ORDER BY U0."provider_id" ASC,
                   U0."inserted_at" DESC)
       AND "attack_surface_overviews"."tenant_id" = 7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb
       AND "scans"."provider_id" = aa3c854e-833b-4c7f-9608-ed50adf818d4)
GROUP BY "attack_surface_overviews"."attack_surface_type"
15.945999999999998ms
1 joins
Query Plan
GroupAggregate  (cost=10.74..10.76 rows=1 width=142)
  Group Key: attack_surface_overviews.attack_surface_type
  ->  Sort  (cost=10.74..10.74 rows=1 width=130)
        Sort Key: attack_surface_overviews.attack_surface_type
        ->  Nested Loop Semi Join  (cost=1.41..10.73 rows=1 width=130)
              Join Filter: (attack_surface_overviews.scan_id = u0.id)
              ->  Nested Loop  (cost=0.15..9.42 rows=1 width=162)
                    ->  Seq Scan on scans  (cost=0.00..1.23 rows=1 width=16)
                          Filter: ((provider_id = 'aa3c854e-833b-4c7f-9608-ed50adf818d4'::uuid) AND CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END)
                    ->  Index Scan using unique_attack_surface_per_scan on attack_surface_overviews  (cost=0.15..8.18 rows=1 width=146)
                          Index Cond: ((tenant_id = '7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb'::uuid) AND (scan_id = scans.id))
                          Filter: CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END
              ->  Unique  (cost=1.26..1.27 rows=2 width=40)
                    ->  Sort  (cost=1.26..1.26 rows=2 width=40)
                          Sort Key: u0.provider_id, u0.inserted_at DESC
                          ->  Seq Scan on scans u0  (cost=0.00..1.25 rows=2 width=40)
                                Filter: ((tenant_id = '7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb'::uuid) AND CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END AND (state = 'completed'::state))
GET /overviews/attack-surfaces?filter[provider_type__in]=
SELECT "attack_surface_overviews"."attack_surface_type",
       COALESCE(SUM("attack_surface_overviews"."total_findings"), 0) AS "total_findings",
       COALESCE(SUM("attack_surface_overviews"."failed_findings"), 0) AS "failed_findings",
       COALESCE(SUM("attack_surface_overviews"."muted_failed_findings"), 0) AS "muted_failed_findings"
FROM "attack_surface_overviews"
INNER JOIN "scans" ON ("attack_surface_overviews"."scan_id" = "scans"."id")
INNER JOIN "providers" ON ("scans"."provider_id" = "providers"."id")
WHERE ("attack_surface_overviews"."scan_id" IN
         (SELECT DISTINCT ON (U0."provider_id") U0."id"
          FROM "scans" U0
          WHERE (U0."state" = completed
                 AND U0."tenant_id" = 7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb)
          ORDER BY U0."provider_id" ASC,
                   U0."inserted_at" DESC)
       AND "attack_surface_overviews"."tenant_id" = 7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb
       AND "providers"."provider" IN (aws))
GROUP BY "attack_surface_overviews"."attack_surface_type"
22.925ms
2 joins
Query Plan
GroupAggregate  (cost=21.02..21.04 rows=1 width=142)
  Group Key: attack_surface_overviews.attack_surface_type
  ->  Sort  (cost=21.02..21.02 rows=1 width=130)
        Sort Key: attack_surface_overviews.attack_surface_type
        ->  Nested Loop Semi Join  (cost=1.41..21.01 rows=1 width=130)
              Join Filter: (attack_surface_overviews.scan_id = u0.id)
              ->  Nested Loop  (cost=0.15..19.69 rows=1 width=162)
                    ->  Nested Loop  (cost=0.00..13.50 rows=1 width=16)
                          Join Filter: (providers.id = scans.provider_id)
                          ->  Seq Scan on providers  (cost=0.00..12.25 rows=1 width=16)
                                Filter: (CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END AND (provider = 'aws'::provider))
                          ->  Seq Scan on scans  (cost=0.00..1.20 rows=4 width=32)
                                Filter: CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END
                    ->  Index Scan using unique_attack_surface_per_scan on attack_surface_overviews  (cost=0.15..6.18 rows=1 width=146)
                          Index Cond: ((tenant_id = '7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb'::uuid) AND (scan_id = scans.id))
                          Filter: CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END
              ->  Unique  (cost=1.26..1.27 rows=2 width=40)
                    ->  Sort  (cost=1.26..1.26 rows=2 width=40)
                          Sort Key: u0.provider_id, u0.inserted_at DESC
                          ->  Seq Scan on scans u0  (cost=0.00..1.25 rows=2 width=40)
                                Filter: ((tenant_id = '7f5d2dc4-9f1e-4cab-990c-ab5b9f6f50fb'::uuid) AND CASE WHEN (current_setting('api.tenant_id'::text, true) IS NULL) THEN false ELSE (tenant_id = (current_setting('api.tenant_id'::text))::uuid) END AND (state = 'completed'::state))

Steps to review

Please add a detailed description of how to review this PR.

Checklist

UI

  • All issue/task requirements work as expected on the UI
  • Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px)
  • Screenshots/Video of the functionality flow (if applicable) - Table (640px > X < 1024px)
  • Screenshots/Video of the functionality flow (if applicable) - Desktop (X > 1024px)
  • Ensure new entries are added to CHANGELOG.md, if applicable.

API

  • Verify if API specs need to be regenerated.
  • Check if version updates are required (e.g., specs, Poetry, etc.).
  • Ensure new entries are added to CHANGELOG.md, if applicable.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@github-actions github-actions bot added component/api review-django-migrations This PR contains changes in Django migrations labels Nov 25, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 25, 2025

✅ All necessary CHANGELOG.md files have been updated.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 25, 2025

Conflict Markers Resolved

All conflict markers have been successfully resolved in this pull request.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 25, 2025

🔒 Container Security Scan

Image: prowler-api:d36b11b
Last scan: 2025-12-02 11:01:33 UTC

📊 Vulnerability Summary

Severity Count
🔴 Critical 4
Total 4

3 package(s) affected

⚠️ Action Required

Critical severity vulnerabilities detected. These should be addressed before merging:

  • Review the detailed scan results
  • Update affected packages to patched versions
  • Consider using a different base image if updates are unavailable

📋 Resources:

@codecov
Copy link

codecov bot commented Nov 25, 2025

Codecov Report

❌ Patch coverage is 98.72611% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.72%. Comparing base (3dadb26) to head (5f3dd44).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9309      +/-   ##
==========================================
- Coverage   92.96%   92.72%   -0.24%     
==========================================
  Files         126      155      +29     
  Lines        3013    21544   +18531     
==========================================
+ Hits         2801    19977   +17176     
- Misses        212     1567    +1355     
Flag Coverage Δ
api 92.72% <98.72%> (?)
prowler-py3.10-gcp ?
prowler-py3.11-gcp ?
prowler-py3.12-gcp ?
prowler-py3.9-gcp ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
prowler ∅ <ø> (∅)
api 92.72% <98.72%> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vicferpoy vicferpoy marked this pull request as ready for review November 26, 2025 16:35
@vicferpoy vicferpoy requested a review from a team as a code owner November 26, 2025 16:35
josemazo
josemazo previously approved these changes Nov 27, 2025
Copy link
Contributor

@josemazo josemazo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Copy link
Contributor

@josemazo josemazo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀🚀

@vicferpoy vicferpoy merged commit 07e82bd into master Dec 2, 2025
32 of 33 checks passed
@vicferpoy vicferpoy deleted the PROWLER-382-attack-surface-component-api branch December 2, 2025 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/api review-django-migrations This PR contains changes in Django migrations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants