Skip to content

Commit 5f3dd44

Browse files
committed
feat(attack-surfaces): add check ID list to response
1 parent eebc056 commit 5f3dd44

File tree

5 files changed

+96
-17
lines changed

5 files changed

+96
-17
lines changed

api/src/backend/api/apps.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def ready(self):
4040
self._ensure_crypto_keys()
4141

4242
load_prowler_compliance()
43+
self._initialize_attack_surface_mapping()
4344

4445
def _ensure_crypto_keys(self):
4546
"""
@@ -167,3 +168,13 @@ def _generate_jwt_keys(self):
167168
f"Error generating JWT keys: {e}. Please set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' manually."
168169
)
169170
raise e
171+
172+
def _initialize_attack_surface_mapping(self):
173+
from tasks.jobs.scan import ( # noqa: F401
174+
_get_attack_surface_mapping_from_provider,
175+
)
176+
177+
from api.models import Provider # noqa: F401
178+
179+
for provider_type, _label in Provider.ProviderChoices.choices:
180+
_get_attack_surface_mapping_from_provider(provider_type)

api/src/backend/api/specs/v1.yaml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4502,7 +4502,7 @@ paths:
45024502
operationId: overviews_attack_surfaces_retrieve
45034503
description: Retrieve aggregated attack surface metrics from latest completed
45044504
scans per provider.
4505-
summary: Get Attack surface overview
4505+
summary: Get attack surface overview
45064506
parameters:
45074507
- in: query
45084508
name: fields[attack-surface-overviews]
@@ -4515,30 +4515,31 @@ paths:
45154515
- total_findings
45164516
- failed_findings
45174517
- muted_failed_findings
4518+
- check_ids
45184519
description: endpoint return only specific fields in the response on a per-type
45194520
basis by including a fields[TYPE] query parameter.
45204521
explode: false
4522+
- in: query
4523+
name: filter[provider_id.in]
4524+
schema:
4525+
type: string
4526+
description: Filter by multiple provider IDs (comma-separated UUIDs)
45214527
- in: query
45224528
name: filter[provider_id]
45234529
schema:
45244530
type: string
45254531
format: uuid
45264532
description: Filter by specific provider ID
45274533
- in: query
4528-
name: filter[provider_id__in]
4534+
name: filter[provider_type.in]
45294535
schema:
45304536
type: string
4531-
description: Filter by multiple provider IDs (comma-separated UUIDs)
4537+
description: Filter by multiple provider types (comma-separated)
45324538
- in: query
45334539
name: filter[provider_type]
45344540
schema:
45354541
type: string
45364542
description: Filter by provider type (aws, azure, gcp, etc.)
4537-
- in: query
4538-
name: filter[provider_type__in]
4539-
schema:
4540-
type: string
4541-
description: Filter by multiple provider types (comma-separated)
45424543
tags:
45434544
- Overview
45444545
security:
@@ -10697,6 +10698,11 @@ components:
1069710698
type: integer
1069810699
muted_failed_findings:
1069910700
type: integer
10701+
check_ids:
10702+
type: array
10703+
items:
10704+
type: string
10705+
readOnly: true
1070010706
required:
1070110707
- id
1070210708
- total_findings

api/src/backend/api/tests/test_views.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6866,6 +6866,7 @@ def test_overview_attack_surface_no_data(self, authenticated_client):
68666866
assert item["attributes"]["total_findings"] == 0
68676867
assert item["attributes"]["failed_findings"] == 0
68686868
assert item["attributes"]["muted_failed_findings"] == 0
6869+
assert item["attributes"]["check_ids"] == []
68696870

68706871
def test_overview_attack_surface_with_data(
68716872
self,
@@ -6877,6 +6878,13 @@ def test_overview_attack_surface_with_data(
68776878
tenant = tenants_fixture[0]
68786879
provider = providers_fixture[0]
68796880

6881+
mapping = {
6882+
"internet-exposed": {"aws-check-1", "aws-check-2"},
6883+
"secrets": {"aws-secret-check"},
6884+
"privilege-escalation": {"aws-priv-check"},
6885+
"ec2-imdsv1": {"aws-imdsv1-check"},
6886+
}
6887+
68806888
scan = Scan.objects.create(
68816889
name="attack-surface-scan",
68826890
provider=provider,
@@ -6902,18 +6910,31 @@ def test_overview_attack_surface_with_data(
69026910
muted_failed=2,
69036911
)
69046912

6905-
response = authenticated_client.get(reverse("overview-attack-surface"))
6913+
with patch(
6914+
"api.v1.views._get_attack_surface_mapping_from_provider",
6915+
return_value=mapping,
6916+
):
6917+
response = authenticated_client.get(reverse("overview-attack-surface"))
69066918
assert response.status_code == status.HTTP_200_OK
69076919
data = response.json()["data"]
69086920
assert len(data) == 4
69096921

69106922
results_by_type = {item["id"]: item["attributes"] for item in data}
69116923
assert results_by_type["internet-exposed"]["total_findings"] == 20
69126924
assert results_by_type["internet-exposed"]["failed_findings"] == 10
6925+
assert set(results_by_type["internet-exposed"]["check_ids"]) == {
6926+
"aws-check-1",
6927+
"aws-check-2",
6928+
}
69136929
assert results_by_type["secrets"]["total_findings"] == 15
69146930
assert results_by_type["secrets"]["failed_findings"] == 8
6931+
assert set(results_by_type["secrets"]["check_ids"]) == {"aws-secret-check"}
69156932
assert results_by_type["privilege-escalation"]["total_findings"] == 0
6933+
assert set(results_by_type["privilege-escalation"]["check_ids"]) == {
6934+
"aws-priv-check"
6935+
}
69166936
assert results_by_type["ec2-imdsv1"]["total_findings"] == 0
6937+
assert set(results_by_type["ec2-imdsv1"]["check_ids"]) == {"aws-imdsv1-check"}
69176938

69186939
def test_overview_attack_surface_provider_filter(
69196940
self,
@@ -6940,6 +6961,13 @@ def test_overview_attack_surface_provider_filter(
69406961
tenant=tenant,
69416962
)
69426963

6964+
mapping = {
6965+
"internet-exposed": {"shared-check", "shared-check"},
6966+
"secrets": set(),
6967+
"privilege-escalation": {"priv-check"},
6968+
"ec2-imdsv1": {"imdsv1-check"},
6969+
}
6970+
69436971
create_attack_surface_overview(
69446972
tenant,
69456973
scan1,
@@ -6957,15 +6985,20 @@ def test_overview_attack_surface_provider_filter(
69576985
muted_failed=3,
69586986
)
69596987

6960-
response = authenticated_client.get(
6961-
reverse("overview-attack-surface"),
6962-
{"filter[provider_id]": str(provider1.id)},
6963-
)
6988+
with patch(
6989+
"api.v1.views._get_attack_surface_mapping_from_provider",
6990+
return_value=mapping,
6991+
):
6992+
response = authenticated_client.get(
6993+
reverse("overview-attack-surface"),
6994+
{"filter[provider_id]": str(provider1.id)},
6995+
)
69646996
assert response.status_code == status.HTTP_200_OK
69656997
data = response.json()["data"]
69666998
results_by_type = {item["id"]: item["attributes"] for item in data}
69676999
assert results_by_type["internet-exposed"]["total_findings"] == 10
69687000
assert results_by_type["internet-exposed"]["failed_findings"] == 5
7001+
assert results_by_type["internet-exposed"]["check_ids"] == ["shared-check"]
69697002

69707003
def test_overview_services_region_filter(
69717004
self, authenticated_client, scan_summaries_fixture

api/src/backend/api/v1/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,9 @@ class AttackSurfaceOverviewSerializer(BaseSerializerV1):
22262226
total_findings = serializers.IntegerField()
22272227
failed_findings = serializers.IntegerField()
22282228
muted_failed_findings = serializers.IntegerField()
2229+
check_ids = serializers.ListField(
2230+
child=serializers.CharField(), allow_empty=True, default=list, read_only=True
2231+
)
22292232

22302233
class JSONAPIMeta:
22312234
resource_name = "attack-surface-overviews"

api/src/backend/api/v1/views.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
7575
from tasks.beat import schedule_provider_scan
7676
from tasks.jobs.export import get_s3_client
77+
from tasks.jobs.scan import _get_attack_surface_mapping_from_provider
7778
from tasks.tasks import (
7879
backfill_scan_resource_summaries_task,
7980
check_integration_connection_task,
@@ -3893,8 +3894,8 @@ def attributes(self, request):
38933894
filters=True,
38943895
),
38953896
attack_surface=extend_schema(
3896-
summary="Retrieve attack surface overview",
3897-
description="Returns aggregated attack surface metrics from latest completed scans per provider.",
3897+
summary="Get attack surface overview",
3898+
description="Retrieve aggregated attack surface metrics from latest completed scans per provider.",
38983899
tags=["Overview"],
38993900
parameters=[
39003901
OpenApiParameter(
@@ -4056,6 +4057,19 @@ def _latest_scan_ids_for_allowed_providers(self, tenant_id):
40564057
.values_list("id", flat=True)
40574058
)
40584059

4060+
def _attack_surface_check_ids_by_provider_types(self, provider_types):
4061+
check_ids_by_type = {
4062+
attack_surface_type: set()
4063+
for attack_surface_type in AttackSurfaceOverview.AttackSurfaceTypeChoices.values
4064+
}
4065+
for provider_type in provider_types:
4066+
attack_surface_mapping = _get_attack_surface_mapping_from_provider(
4067+
provider_type=provider_type
4068+
)
4069+
for attack_surface_type, check_ids in attack_surface_mapping.items():
4070+
check_ids_by_type[attack_surface_type].update(check_ids)
4071+
return check_ids_by_type
4072+
40594073
@action(detail=False, methods=["get"], url_name="providers")
40604074
def providers(self, request):
40614075
tenant_id = self.request.tenant_id
@@ -4566,7 +4580,14 @@ def attack_surface(self, request):
45664580
filtered_queryset = self._apply_filterset(
45674581
base_queryset, AttackSurfaceOverviewFilter
45684582
)
4569-
4583+
provider_types = list(
4584+
filtered_queryset.values_list(
4585+
"scan__provider__provider", flat=True
4586+
).distinct()
4587+
)
4588+
attack_surface_check_ids = self._attack_surface_check_ids_by_provider_types(
4589+
provider_types
4590+
)
45704591
# Aggregate attack surface data
45714592
aggregation = filtered_queryset.values("attack_surface_type").annotate(
45724593
total_findings=Coalesce(Sum("total_findings"), 0),
@@ -4590,7 +4611,12 @@ def attack_surface(self, request):
45904611
}
45914612

45924613
response_data = [
4593-
{"attack_surface_type": key, **value} for key, value in results.items()
4614+
{
4615+
"attack_surface_type": key,
4616+
**value,
4617+
"check_ids": attack_surface_check_ids.get(key, []),
4618+
}
4619+
for key, value in results.items()
45944620
]
45954621

45964622
return Response(

0 commit comments

Comments
 (0)