Skip to content

Commit 07e82bd

Browse files
authored
feat(attack-surfaces): add new endpoints to retrieve overview data (#9309)
1 parent 4661e01 commit 07e82bd

File tree

16 files changed

+1388
-92
lines changed

16 files changed

+1388
-92
lines changed

api/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to the **Prowler API** are documented in this file.
44

55
## [1.16.0] (Unreleased)
66

7+
### Added
8+
- New endpoint to retrieve an overview of the attack surfaces [(#9309)](https://github.com/prowler-cloud/prowler/pull/9309)
9+
710
### Changed
811
- Restore the compliance overview endpoint's mandatory filters [(#9330)](https://github.com/prowler-cloud/prowler/pull/9330)
912

api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ name = "prowler-api"
4444
package-mode = false
4545
# Needed for the SDK compatibility
4646
requires-python = ">=3.11,<3.13"
47-
version = "1.15.0"
47+
version = "1.16.0"
4848

4949
[project.scripts]
5050
celery = "src.backend.config.settings.celery"

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/filters.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
StatusEnumField,
2424
)
2525
from api.models import (
26+
AttackSurfaceOverview,
2627
ComplianceRequirementOverview,
2728
Finding,
2829
Integration,
@@ -1013,3 +1014,22 @@ class Meta:
10131014
"inserted_at": ["date", "gte", "lte"],
10141015
"overall_score": ["exact", "gte", "lte"],
10151016
}
1017+
1018+
1019+
class AttackSurfaceOverviewFilter(FilterSet):
1020+
"""Filter for attack surface overview aggregations by provider."""
1021+
1022+
provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
1023+
provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
1024+
provider_type = ChoiceFilter(
1025+
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
1026+
)
1027+
provider_type__in = ChoiceInFilter(
1028+
field_name="scan__provider__provider",
1029+
choices=Provider.ProviderChoices.choices,
1030+
lookup_expr="in",
1031+
)
1032+
1033+
class Meta:
1034+
model = AttackSurfaceOverview
1035+
fields = {}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Generated by Django 5.1.14 on 2025-11-19 13:03
2+
3+
import uuid
4+
5+
import django.db.models.deletion
6+
from django.db import migrations, models
7+
8+
import api.rls
9+
10+
11+
class Migration(migrations.Migration):
12+
dependencies = [
13+
("api", "0059_compliance_overview_summary"),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name="AttackSurfaceOverview",
19+
fields=[
20+
(
21+
"id",
22+
models.UUIDField(
23+
default=uuid.uuid4,
24+
editable=False,
25+
primary_key=True,
26+
serialize=False,
27+
),
28+
),
29+
("inserted_at", models.DateTimeField(auto_now_add=True)),
30+
(
31+
"attack_surface_type",
32+
models.CharField(
33+
choices=[
34+
("internet-exposed", "Internet Exposed"),
35+
("secrets", "Exposed Secrets"),
36+
("privilege-escalation", "Privilege Escalation"),
37+
("ec2-imdsv1", "EC2 IMDSv1 Enabled"),
38+
],
39+
max_length=50,
40+
),
41+
),
42+
("total_findings", models.IntegerField(default=0)),
43+
("failed_findings", models.IntegerField(default=0)),
44+
("muted_failed_findings", models.IntegerField(default=0)),
45+
],
46+
options={
47+
"db_table": "attack_surface_overviews",
48+
"abstract": False,
49+
},
50+
),
51+
migrations.AddField(
52+
model_name="attacksurfaceoverview",
53+
name="scan",
54+
field=models.ForeignKey(
55+
on_delete=django.db.models.deletion.CASCADE,
56+
related_name="attack_surface_overviews",
57+
related_query_name="attack_surface_overview",
58+
to="api.scan",
59+
),
60+
),
61+
migrations.AddField(
62+
model_name="attacksurfaceoverview",
63+
name="tenant",
64+
field=models.ForeignKey(
65+
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
66+
),
67+
),
68+
migrations.AddIndex(
69+
model_name="attacksurfaceoverview",
70+
index=models.Index(
71+
fields=["tenant_id", "scan_id"], name="attack_surf_tenant_scan_idx"
72+
),
73+
),
74+
migrations.AddConstraint(
75+
model_name="attacksurfaceoverview",
76+
constraint=models.UniqueConstraint(
77+
fields=("tenant_id", "scan_id", "attack_surface_type"),
78+
name="unique_attack_surface_per_scan",
79+
),
80+
),
81+
migrations.AddConstraint(
82+
model_name="attacksurfaceoverview",
83+
constraint=api.rls.RowLevelSecurityConstraint(
84+
"tenant_id",
85+
name="rls_on_attacksurfaceoverview",
86+
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
87+
),
88+
),
89+
]

api/src/backend/api/models.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2405,3 +2405,63 @@ class Meta(RowLevelSecurityProtectedModel.Meta):
24052405

24062406
class JSONAPIMeta:
24072407
resource_name = "threatscore-snapshots"
2408+
2409+
2410+
class AttackSurfaceOverview(RowLevelSecurityProtectedModel):
2411+
"""
2412+
Pre-aggregated attack surface metrics per scan.
2413+
2414+
Stores counts for each attack surface type (internet-exposed, secrets,
2415+
privilege-escalation, ec2-imdsv1) to enable fast overview queries.
2416+
"""
2417+
2418+
class AttackSurfaceTypeChoices(models.TextChoices):
2419+
INTERNET_EXPOSED = "internet-exposed", _("Internet Exposed")
2420+
SECRETS = "secrets", _("Exposed Secrets")
2421+
PRIVILEGE_ESCALATION = "privilege-escalation", _("Privilege Escalation")
2422+
EC2_IMDSV1 = "ec2-imdsv1", _("EC2 IMDSv1 Enabled")
2423+
2424+
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
2425+
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
2426+
2427+
scan = models.ForeignKey(
2428+
Scan,
2429+
on_delete=models.CASCADE,
2430+
related_name="attack_surface_overviews",
2431+
related_query_name="attack_surface_overview",
2432+
)
2433+
2434+
attack_surface_type = models.CharField(
2435+
max_length=50,
2436+
choices=AttackSurfaceTypeChoices.choices,
2437+
)
2438+
2439+
# Finding counts
2440+
total_findings = models.IntegerField(default=0) # All findings (PASS + FAIL)
2441+
failed_findings = models.IntegerField(default=0) # Non-muted failed findings
2442+
muted_failed_findings = models.IntegerField(default=0) # Muted failed findings
2443+
2444+
class Meta(RowLevelSecurityProtectedModel.Meta):
2445+
db_table = "attack_surface_overviews"
2446+
2447+
constraints = [
2448+
models.UniqueConstraint(
2449+
fields=("tenant_id", "scan_id", "attack_surface_type"),
2450+
name="unique_attack_surface_per_scan",
2451+
),
2452+
RowLevelSecurityConstraint(
2453+
field="tenant_id",
2454+
name="rls_on_%(class)s",
2455+
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
2456+
),
2457+
]
2458+
2459+
indexes = [
2460+
models.Index(
2461+
fields=["tenant_id", "scan_id"],
2462+
name="attack_surf_tenant_scan_idx",
2463+
),
2464+
]
2465+
2466+
class JSONAPIMeta:
2467+
resource_name = "attack-surface-overviews"

api/src/backend/api/rbac/permissions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ def get_providers(role: Role) -> QuerySet[Provider]:
6565
A QuerySet of Provider objects filtered by the role's provider groups.
6666
If the role has no provider groups, returns an empty queryset.
6767
"""
68-
tenant = role.tenant
68+
tenant_id = role.tenant_id
6969
provider_groups = role.provider_groups.all()
7070
if not provider_groups.exists():
7171
return Provider.objects.none()
7272

7373
return Provider.objects.filter(
74-
tenant=tenant, provider_groups__in=provider_groups
74+
tenant_id=tenant_id, provider_groups__in=provider_groups
7575
).distinct()

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

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.0.3
22
info:
33
title: Prowler API
4-
version: 1.15.0
4+
version: 1.16.0
55
description: |-
66
Prowler API specification.
77

@@ -4497,6 +4497,60 @@ paths:
44974497
responses:
44984498
'204':
44994499
description: No response body
4500+
/api/v1/overviews/attack-surfaces:
4501+
get:
4502+
operationId: overviews_attack_surfaces_retrieve
4503+
description: Retrieve aggregated attack surface metrics from latest completed
4504+
scans per provider.
4505+
summary: Get attack surface overview
4506+
parameters:
4507+
- in: query
4508+
name: fields[attack-surface-overviews]
4509+
schema:
4510+
type: array
4511+
items:
4512+
type: string
4513+
enum:
4514+
- id
4515+
- total_findings
4516+
- failed_findings
4517+
- muted_failed_findings
4518+
- check_ids
4519+
description: endpoint return only specific fields in the response on a per-type
4520+
basis by including a fields[TYPE] query parameter.
4521+
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)
4527+
- in: query
4528+
name: filter[provider_id]
4529+
schema:
4530+
type: string
4531+
format: uuid
4532+
description: Filter by specific provider ID
4533+
- in: query
4534+
name: filter[provider_type.in]
4535+
schema:
4536+
type: string
4537+
description: Filter by multiple provider types (comma-separated)
4538+
- in: query
4539+
name: filter[provider_type]
4540+
schema:
4541+
type: string
4542+
description: Filter by provider type (aws, azure, gcp, etc.)
4543+
tags:
4544+
- Overview
4545+
security:
4546+
- JWT or API Key: []
4547+
responses:
4548+
'200':
4549+
content:
4550+
application/vnd.api+json:
4551+
schema:
4552+
$ref: '#/components/schemas/AttackSurfaceOverviewResponse'
4553+
description: ''
45004554
/api/v1/overviews/findings:
45014555
get:
45024556
operationId: overviews_findings_retrieve
@@ -10618,6 +10672,49 @@ paths:
1061810672
description: ''
1061910673
components:
1062010674
schemas:
10675+
AttackSurfaceOverview:
10676+
type: object
10677+
required:
10678+
- type
10679+
- id
10680+
additionalProperties: false
10681+
properties:
10682+
type:
10683+
type: string
10684+
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
10685+
member is used to describe resource objects that share common attributes
10686+
and relationships.
10687+
enum:
10688+
- attack-surface-overviews
10689+
id: {}
10690+
attributes:
10691+
type: object
10692+
properties:
10693+
id:
10694+
type: string
10695+
total_findings:
10696+
type: integer
10697+
failed_findings:
10698+
type: integer
10699+
muted_failed_findings:
10700+
type: integer
10701+
check_ids:
10702+
type: array
10703+
items:
10704+
type: string
10705+
readOnly: true
10706+
required:
10707+
- id
10708+
- total_findings
10709+
- failed_findings
10710+
- muted_failed_findings
10711+
AttackSurfaceOverviewResponse:
10712+
type: object
10713+
properties:
10714+
data:
10715+
$ref: '#/components/schemas/AttackSurfaceOverview'
10716+
required:
10717+
- data
1062110718
ComplianceOverview:
1062210719
type: object
1062310720
required:

0 commit comments

Comments
 (0)