mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(overviews): Add API overview endpoints for findings and severity (#5910)
This commit is contained in:
committed by
GitHub
parent
f576b24fc8
commit
b69a0d5137
@@ -4,47 +4,48 @@ from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django_filters.rest_framework import (
|
||||
BaseInFilter,
|
||||
FilterSet,
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
UUIDFilter,
|
||||
DateFilter,
|
||||
ChoiceFilter,
|
||||
DateFilter,
|
||||
FilterSet,
|
||||
UUIDFilter,
|
||||
)
|
||||
from rest_framework_json_api.django_filters.backends import DjangoFilterBackend
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.db_utils import (
|
||||
ProviderEnumField,
|
||||
FindingDeltaEnumField,
|
||||
StatusEnumField,
|
||||
SeverityEnumField,
|
||||
InvitationStateEnumField,
|
||||
ProviderEnumField,
|
||||
SeverityEnumField,
|
||||
StatusEnumField,
|
||||
)
|
||||
from api.models import (
|
||||
User,
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Scan,
|
||||
Task,
|
||||
StateChoices,
|
||||
Finding,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
ComplianceOverview,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
from api.uuid_utils import (
|
||||
datetime_to_uuid7,
|
||||
uuid7_start,
|
||||
transform_into_uuid7,
|
||||
uuid7_end,
|
||||
uuid7_range,
|
||||
transform_into_uuid7,
|
||||
uuid7_start,
|
||||
)
|
||||
from api.v1.serializers import TaskBase
|
||||
|
||||
@@ -57,6 +58,13 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_filterset_class(self, view, queryset=None):
|
||||
# Check if the view has 'get_filterset_class' method
|
||||
if hasattr(view, "get_filterset_class"):
|
||||
return view.get_filterset_class()
|
||||
# Fallback to the default implementation
|
||||
return super().get_filterset_class(view, queryset)
|
||||
|
||||
|
||||
class UUIDInFilter(BaseInFilter, UUIDFilter):
|
||||
pass
|
||||
@@ -482,3 +490,28 @@ class ComplianceOverviewFilter(FilterSet):
|
||||
"version": ["exact", "icontains"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
}
|
||||
|
||||
|
||||
class ScanSummaryFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
|
||||
provider_type = ChoiceFilter(
|
||||
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
|
||||
)
|
||||
provider_type__in = ChoiceInFilter(
|
||||
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
|
||||
)
|
||||
region = CharFilter(field_name="region")
|
||||
muted_findings = BooleanFilter(method="filter_muted_findings")
|
||||
|
||||
def filter_muted_findings(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset.exclude(muted__gt=0)
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = ScanSummary
|
||||
fields = {
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
}
|
||||
|
||||
@@ -22,36 +22,36 @@ from uuid6 import uuid7
|
||||
|
||||
import api.rls
|
||||
from api.db_utils import (
|
||||
PostgresEnumMigration,
|
||||
MemberRoleEnumField,
|
||||
DB_PROWLER_PASSWORD,
|
||||
DB_PROWLER_USER,
|
||||
POSTGRES_TENANT_VAR,
|
||||
POSTGRES_USER_VAR,
|
||||
TASK_RUNNER_DB_TABLE,
|
||||
InvitationStateEnum,
|
||||
InvitationStateEnumField,
|
||||
MemberRoleEnum,
|
||||
MemberRoleEnumField,
|
||||
PostgresEnumMigration,
|
||||
ProviderEnum,
|
||||
ProviderEnumField,
|
||||
ProviderSecretTypeEnum,
|
||||
ProviderSecretTypeEnumField,
|
||||
ScanTriggerEnum,
|
||||
StateEnumField,
|
||||
StateEnum,
|
||||
ScanTriggerEnumField,
|
||||
InvitationStateEnum,
|
||||
InvitationStateEnumField,
|
||||
StateEnum,
|
||||
StateEnumField,
|
||||
register_enum,
|
||||
DB_PROWLER_USER,
|
||||
DB_PROWLER_PASSWORD,
|
||||
TASK_RUNNER_DB_TABLE,
|
||||
POSTGRES_TENANT_VAR,
|
||||
POSTGRES_USER_VAR,
|
||||
)
|
||||
from api.models import (
|
||||
Provider,
|
||||
Scan,
|
||||
StateChoices,
|
||||
Finding,
|
||||
StatusChoices,
|
||||
SeverityChoices,
|
||||
Membership,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderSecret,
|
||||
Scan,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
)
|
||||
|
||||
DB_NAME = settings.DATABASES["default"]["NAME"]
|
||||
@@ -289,7 +289,8 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
# Enable tenants RLS based on memberships
|
||||
migrations.RunSQL(f"""
|
||||
migrations.RunSQL(
|
||||
f"""
|
||||
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy for SELECT
|
||||
@@ -364,7 +365,8 @@ class Migration(migrations.Migration):
|
||||
FOR INSERT
|
||||
TO {DB_PROWLER_USER}
|
||||
WITH CHECK (true);
|
||||
"""),
|
||||
"""
|
||||
),
|
||||
# Create and register ProviderEnum type
|
||||
migrations.RunPython(
|
||||
ProviderEnumMigration.create_enum_type,
|
||||
@@ -1482,4 +1484,81 @@ class Migration(migrations.Migration):
|
||||
name="comp_ov_cp_id_req_fail_idx",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ScanSummary",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("check_id", models.CharField(max_length=100)),
|
||||
("service", models.TextField()),
|
||||
(
|
||||
"severity",
|
||||
api.db_utils.SeverityEnumField(
|
||||
choices=[
|
||||
("critical", "Critical"),
|
||||
("high", "High"),
|
||||
("medium", "Medium"),
|
||||
("low", "Low"),
|
||||
("informational", "Informational"),
|
||||
]
|
||||
),
|
||||
),
|
||||
("region", models.TextField()),
|
||||
("_pass", models.IntegerField(db_column="pass", default=0)),
|
||||
("fail", models.IntegerField(default=0)),
|
||||
("muted", models.IntegerField(default=0)),
|
||||
("total", models.IntegerField(default=0)),
|
||||
("new", models.IntegerField(default=0)),
|
||||
("changed", models.IntegerField(default=0)),
|
||||
("unchanged", models.IntegerField(default=0)),
|
||||
("fail_new", models.IntegerField(default=0)),
|
||||
("fail_changed", models.IntegerField(default=0)),
|
||||
("pass_new", models.IntegerField(default=0)),
|
||||
("pass_changed", models.IntegerField(default=0)),
|
||||
("muted_new", models.IntegerField(default=0)),
|
||||
("muted_changed", models.IntegerField(default=0)),
|
||||
(
|
||||
"scan",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="aggregations",
|
||||
related_query_name="aggregation",
|
||||
to="api.scan",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "scan_summaries",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="scansummary",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_scansummary",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="scansummary",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant", "scan", "check_id", "service", "severity", "region"),
|
||||
name="unique_scan_summary",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import re
|
||||
from uuid import uuid4, UUID
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from django.conf import settings
|
||||
@@ -11,35 +11,33 @@ from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_results.models import TaskResult
|
||||
from prowler.lib.check.models import Severity
|
||||
from psqlextra.models import PostgresPartitionedModel
|
||||
from psqlextra.types import PostgresPartitioningMethod
|
||||
from uuid6 import uuid7
|
||||
|
||||
from api.db_utils import (
|
||||
MemberRoleEnumField,
|
||||
enum_to_choices,
|
||||
ProviderEnumField,
|
||||
StateEnumField,
|
||||
ScanTriggerEnumField,
|
||||
FindingDeltaEnumField,
|
||||
SeverityEnumField,
|
||||
StatusEnumField,
|
||||
CustomUserManager,
|
||||
ProviderSecretTypeEnumField,
|
||||
FindingDeltaEnumField,
|
||||
InvitationStateEnumField,
|
||||
one_week_from_now,
|
||||
MemberRoleEnumField,
|
||||
ProviderEnumField,
|
||||
ProviderSecretTypeEnumField,
|
||||
ScanTriggerEnumField,
|
||||
SeverityEnumField,
|
||||
StateEnumField,
|
||||
StatusEnumField,
|
||||
enum_to_choices,
|
||||
generate_random_token,
|
||||
one_week_from_now,
|
||||
)
|
||||
from api.exceptions import ModelValidationError
|
||||
from api.rls import (
|
||||
RowLevelSecurityProtectedModel,
|
||||
)
|
||||
from api.rls import (
|
||||
Tenant,
|
||||
RowLevelSecurityConstraint,
|
||||
BaseSecurityConstraint,
|
||||
RowLevelSecurityConstraint,
|
||||
RowLevelSecurityProtectedModel,
|
||||
Tenant,
|
||||
)
|
||||
from prowler.lib.check.models import Severity
|
||||
|
||||
fernet = Fernet(settings.SECRETS_ENCRYPTION_KEY.encode())
|
||||
|
||||
@@ -856,3 +854,51 @@ class ComplianceOverview(RowLevelSecurityProtectedModel):
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "compliance-overviews"
|
||||
|
||||
|
||||
class ScanSummary(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
check_id = models.CharField(max_length=100, blank=False, null=False)
|
||||
service = models.TextField(blank=False)
|
||||
severity = SeverityEnumField(choices=SeverityChoices)
|
||||
region = models.TextField(blank=False)
|
||||
_pass = models.IntegerField(db_column="pass", default=0)
|
||||
fail = models.IntegerField(default=0)
|
||||
muted = models.IntegerField(default=0)
|
||||
total = models.IntegerField(default=0)
|
||||
new = models.IntegerField(default=0)
|
||||
changed = models.IntegerField(default=0)
|
||||
unchanged = models.IntegerField(default=0)
|
||||
|
||||
fail_new = models.IntegerField(default=0)
|
||||
fail_changed = models.IntegerField(default=0)
|
||||
pass_new = models.IntegerField(default=0)
|
||||
pass_changed = models.IntegerField(default=0)
|
||||
muted_new = models.IntegerField(default=0)
|
||||
muted_changed = models.IntegerField(default=0)
|
||||
|
||||
scan = models.ForeignKey(
|
||||
Scan,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="aggregations",
|
||||
related_query_name="aggregation",
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "scan_summaries"
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant", "scan", "check_id", "service", "severity", "region"),
|
||||
name="unique_scan_summary",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "scan-summaries"
|
||||
|
||||
@@ -780,16 +780,332 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OpenApiResponseResponse'
|
||||
description: ''
|
||||
/api/v1/overviews/findings:
|
||||
get:
|
||||
operationId: overviews_findings_retrieve
|
||||
description: Fetch aggregated findings data across all providers, grouped by
|
||||
various metrics such as passed, failed, muted, and total findings. This endpoint
|
||||
calculates summary statistics based on the latest scans for each provider
|
||||
and applies any provided filters, such as region, provider type, and scan
|
||||
date.
|
||||
summary: Get aggregated findings data
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[findings-overview]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- new
|
||||
- changed
|
||||
- unchanged
|
||||
- fail_new
|
||||
- fail_changed
|
||||
- pass_new
|
||||
- pass_changed
|
||||
- muted_new
|
||||
- muted_changed
|
||||
- total
|
||||
- fail
|
||||
- muted
|
||||
- pass
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: filter[inserted_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[inserted_at__date]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[inserted_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[muted_findings]
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[region]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- new
|
||||
- -new
|
||||
- changed
|
||||
- -changed
|
||||
- unchanged
|
||||
- -unchanged
|
||||
- fail_new
|
||||
- -fail_new
|
||||
- fail_changed
|
||||
- -fail_changed
|
||||
- pass_new
|
||||
- -pass_new
|
||||
- pass_changed
|
||||
- -pass_changed
|
||||
- muted_new
|
||||
- -muted_new
|
||||
- muted_changed
|
||||
- -muted_changed
|
||||
- total
|
||||
- -total
|
||||
- fail
|
||||
- -fail
|
||||
- muted
|
||||
- -muted
|
||||
- pass
|
||||
- -pass
|
||||
explode: false
|
||||
tags:
|
||||
- Overview
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OverviewFindingResponse'
|
||||
description: ''
|
||||
/api/v1/overviews/findings_severity:
|
||||
get:
|
||||
operationId: overviews_findings_severity_retrieve
|
||||
description: Retrieve an aggregated summary of findings grouped by severity
|
||||
levels, such as low, medium, high, and critical. The response includes the
|
||||
total count of findings for each severity, considering only the latest scans
|
||||
for each provider. Additional filters can be applied to narrow down results
|
||||
by region, provider type, or other attributes.
|
||||
summary: Get findings data by severity
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[findings-severity-overview]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- critical
|
||||
- high
|
||||
- medium
|
||||
- low
|
||||
- informational
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: filter[inserted_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[inserted_at__date]
|
||||
schema:
|
||||
type: string
|
||||
format: date
|
||||
- in: query
|
||||
name: filter[inserted_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[muted_findings]
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: filter[provider_type]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
description: |-
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
- in: query
|
||||
name: filter[provider_type__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
description: |-
|
||||
Multiple values may be separated by commas.
|
||||
|
||||
* `aws` - AWS
|
||||
* `azure` - Azure
|
||||
* `gcp` - GCP
|
||||
* `kubernetes` - Kubernetes
|
||||
explode: false
|
||||
style: form
|
||||
- in: query
|
||||
name: filter[region]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[region__in]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Multiple values may be separated by commas.
|
||||
explode: false
|
||||
style: form
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- -id
|
||||
- critical
|
||||
- -critical
|
||||
- high
|
||||
- -high
|
||||
- medium
|
||||
- -medium
|
||||
- low
|
||||
- -low
|
||||
- informational
|
||||
- -informational
|
||||
explode: false
|
||||
tags:
|
||||
- Overview
|
||||
security:
|
||||
- jwtAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OverviewSeverityResponse'
|
||||
description: ''
|
||||
/api/v1/overviews/providers:
|
||||
get:
|
||||
operationId: overviews_providers_retrieve
|
||||
description: Fetch aggregated summaries of the latest findings and resources
|
||||
for each provider. This includes counts of passed, failed, and manual findings,
|
||||
as well as the total number of resources managed by each provider.
|
||||
summary: List aggregated overview data for providers
|
||||
description: Retrieve an aggregated overview of findings and resources grouped
|
||||
by providers. The response includes the count of passed, failed, and manual
|
||||
findings, along with the total number of resources managed by each provider.
|
||||
Only the latest findings for each provider are considered in the aggregation
|
||||
to ensure accurate and up-to-date insights.
|
||||
summary: Get aggregated provider data
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[provider-overviews]
|
||||
name: fields[providers-overview]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
@@ -4400,6 +4716,77 @@ components:
|
||||
$ref: '#/components/schemas/Membership'
|
||||
required:
|
||||
- data
|
||||
OverviewFinding:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/OverviewFindingTypeEnum'
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
id: {}
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
default: n/a
|
||||
new:
|
||||
type: integer
|
||||
changed:
|
||||
type: integer
|
||||
unchanged:
|
||||
type: integer
|
||||
fail_new:
|
||||
type: integer
|
||||
fail_changed:
|
||||
type: integer
|
||||
pass_new:
|
||||
type: integer
|
||||
pass_changed:
|
||||
type: integer
|
||||
muted_new:
|
||||
type: integer
|
||||
muted_changed:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
fail:
|
||||
type: integer
|
||||
muted:
|
||||
type: integer
|
||||
pass:
|
||||
type: integer
|
||||
required:
|
||||
- new
|
||||
- changed
|
||||
- unchanged
|
||||
- fail_new
|
||||
- fail_changed
|
||||
- pass_new
|
||||
- pass_changed
|
||||
- muted_new
|
||||
- muted_changed
|
||||
- total
|
||||
- fail
|
||||
- muted
|
||||
- pass
|
||||
OverviewFindingResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/OverviewFinding'
|
||||
required:
|
||||
- data
|
||||
OverviewFindingTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- findings-overview
|
||||
OverviewProvider:
|
||||
type: object
|
||||
required:
|
||||
@@ -4449,7 +4836,54 @@ components:
|
||||
OverviewProviderTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- provider-overviews
|
||||
- providers-overview
|
||||
OverviewSeverity:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/OverviewSeverityTypeEnum'
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
id: {}
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
default: n/a
|
||||
critical:
|
||||
type: integer
|
||||
high:
|
||||
type: integer
|
||||
medium:
|
||||
type: integer
|
||||
low:
|
||||
type: integer
|
||||
informational:
|
||||
type: integer
|
||||
required:
|
||||
- critical
|
||||
- high
|
||||
- medium
|
||||
- low
|
||||
- informational
|
||||
OverviewSeverityResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/OverviewSeverity'
|
||||
required:
|
||||
- data
|
||||
OverviewSeverityTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- findings-severity-overview
|
||||
PaginatedComplianceOverviewList:
|
||||
type: object
|
||||
required:
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import timezone, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import ANY, Mock, patch
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from api.models import (
|
||||
User,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderGroupMembership,
|
||||
Scan,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
Scan,
|
||||
User,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
from conftest import (
|
||||
API_JSON_CONTENT_TYPE,
|
||||
TEST_PASSWORD,
|
||||
TEST_USER,
|
||||
)
|
||||
|
||||
TODAY = str(datetime.today().date())
|
||||
|
||||
@@ -1794,7 +1789,7 @@ class TestScanViewSet:
|
||||
],
|
||||
)
|
||||
@patch("api.v1.views.Task.objects.get")
|
||||
@patch("api.v1.views.perform_scan_task.delay")
|
||||
@patch("api.v1.views.perform_scan_task.apply_async")
|
||||
def test_scans_create_valid(
|
||||
self,
|
||||
mock_perform_scan_task,
|
||||
@@ -3009,9 +3004,9 @@ class TestInvitationViewSet:
|
||||
response = authenticated_client.get(
|
||||
reverse("invitation-list"),
|
||||
{
|
||||
f"filter[{filter_name}]": filter_value
|
||||
if filter_name != "inviter"
|
||||
else str(user.id)
|
||||
f"filter[{filter_name}]": (
|
||||
filter_value if filter_name != "inviter" else str(user.id)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3262,3 +3257,5 @@ class TestOverviewViewSet:
|
||||
assert response.json()["data"][0]["attributes"]["resources"]["total"] == len(
|
||||
resources_fixture
|
||||
)
|
||||
|
||||
# TODO Add more tests for the rest of overviews
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
@@ -14,24 +14,23 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from api.models import (
|
||||
StateChoices,
|
||||
User,
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderGroupMembership,
|
||||
Scan,
|
||||
Task,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Finding,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
ComplianceOverview,
|
||||
Scan,
|
||||
StateChoices,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
|
||||
|
||||
# Tokens
|
||||
|
||||
|
||||
@@ -1234,7 +1233,7 @@ class OverviewProviderSerializer(serializers.Serializer):
|
||||
resources = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-overviews"
|
||||
resource_name = "providers-overview"
|
||||
|
||||
def get_root_meta(self, _resource, _many):
|
||||
return {"version": "v1"}
|
||||
@@ -1270,3 +1269,45 @@ class OverviewProviderSerializer(serializers.Serializer):
|
||||
return {
|
||||
"total": obj["total_resources"],
|
||||
}
|
||||
|
||||
|
||||
class OverviewFindingSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(default="n/a")
|
||||
new = serializers.IntegerField()
|
||||
changed = serializers.IntegerField()
|
||||
unchanged = serializers.IntegerField()
|
||||
fail_new = serializers.IntegerField()
|
||||
fail_changed = serializers.IntegerField()
|
||||
pass_new = serializers.IntegerField()
|
||||
pass_changed = serializers.IntegerField()
|
||||
muted_new = serializers.IntegerField()
|
||||
muted_changed = serializers.IntegerField()
|
||||
total = serializers.IntegerField()
|
||||
_pass = serializers.IntegerField()
|
||||
fail = serializers.IntegerField()
|
||||
muted = serializers.IntegerField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "findings-overview"
|
||||
|
||||
def get_root_meta(self, _resource, _many):
|
||||
return {"version": "v1"}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["pass"] = self.fields.pop("_pass")
|
||||
|
||||
|
||||
class OverviewSeveritySerializer(serializers.Serializer):
|
||||
id = serializers.CharField(default="n/a")
|
||||
critical = serializers.IntegerField()
|
||||
high = serializers.IntegerField()
|
||||
medium = serializers.IntegerField()
|
||||
low = serializers.IntegerField()
|
||||
informational = serializers.IntegerField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "findings-severity-overview"
|
||||
|
||||
def get_root_meta(self, _resource, _many):
|
||||
return {"version": "v1"}
|
||||
|
||||
+190
-67
@@ -2,20 +2,20 @@ from celery.result import AsyncResult
|
||||
from django.conf import settings as django_settings
|
||||
from django.contrib.postgres.search import SearchQuery
|
||||
from django.db import transaction
|
||||
from django.db.models import Prefetch, Subquery, OuterRef, Count, Q, F
|
||||
from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema,
|
||||
extend_schema_view,
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
OpenApiTypes,
|
||||
extend_schema,
|
||||
extend_schema_view,
|
||||
)
|
||||
from drf_spectacular.views import SpectacularAPIView
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import (
|
||||
MethodNotAllowed,
|
||||
@@ -23,82 +23,87 @@ from rest_framework.exceptions import (
|
||||
PermissionDenied,
|
||||
ValidationError,
|
||||
)
|
||||
from rest_framework.generics import get_object_or_404, GenericAPIView
|
||||
from rest_framework.generics import GenericAPIView, get_object_or_404
|
||||
from rest_framework_json_api.views import Response
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken
|
||||
from rest_framework_simplejwt.exceptions import TokenError
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||
from tasks.beat import schedule_provider_scan
|
||||
from tasks.tasks import (
|
||||
check_provider_connection_task,
|
||||
delete_provider_task,
|
||||
perform_scan_summary_task,
|
||||
perform_scan_task,
|
||||
)
|
||||
|
||||
from api.base_views import BaseTenantViewset, BaseRLSViewSet, BaseUserViewset
|
||||
from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset
|
||||
from api.db_router import MainRouter
|
||||
from api.filters import (
|
||||
ComplianceOverviewFilter,
|
||||
FindingFilter,
|
||||
InvitationFilter,
|
||||
MembershipFilter,
|
||||
ProviderFilter,
|
||||
ProviderGroupFilter,
|
||||
TenantFilter,
|
||||
MembershipFilter,
|
||||
ScanFilter,
|
||||
TaskFilter,
|
||||
ResourceFilter,
|
||||
FindingFilter,
|
||||
ProviderSecretFilter,
|
||||
InvitationFilter,
|
||||
ResourceFilter,
|
||||
ScanFilter,
|
||||
ScanSummaryFilter,
|
||||
TaskFilter,
|
||||
TenantFilter,
|
||||
UserFilter,
|
||||
ComplianceOverviewFilter,
|
||||
)
|
||||
from api.models import (
|
||||
StatusChoices,
|
||||
User,
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderGroupMembership,
|
||||
Scan,
|
||||
Task,
|
||||
Resource,
|
||||
Finding,
|
||||
ProviderSecret,
|
||||
Invitation,
|
||||
ComplianceOverview,
|
||||
Resource,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StatusChoices,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
from api.pagination import ComplianceOverviewPagination
|
||||
from api.rls import Tenant
|
||||
from api.utils import validate_invitation
|
||||
from api.uuid_utils import datetime_to_uuid7
|
||||
from api.v1.serializers import (
|
||||
TokenSerializer,
|
||||
TokenRefreshSerializer,
|
||||
UserSerializer,
|
||||
UserCreateSerializer,
|
||||
UserUpdateSerializer,
|
||||
ComplianceOverviewFullSerializer,
|
||||
ComplianceOverviewSerializer,
|
||||
FindingSerializer,
|
||||
InvitationAcceptSerializer,
|
||||
InvitationCreateSerializer,
|
||||
InvitationSerializer,
|
||||
InvitationUpdateSerializer,
|
||||
MembershipSerializer,
|
||||
OverviewFindingSerializer,
|
||||
OverviewProviderSerializer,
|
||||
OverviewSeveritySerializer,
|
||||
ProviderCreateSerializer,
|
||||
ProviderGroupMembershipUpdateSerializer,
|
||||
ProviderGroupSerializer,
|
||||
ProviderGroupUpdateSerializer,
|
||||
ProviderGroupMembershipUpdateSerializer,
|
||||
ProviderSerializer,
|
||||
ProviderCreateSerializer,
|
||||
ProviderUpdateSerializer,
|
||||
TenantSerializer,
|
||||
TaskSerializer,
|
||||
ScanSerializer,
|
||||
ScanCreateSerializer,
|
||||
ScanUpdateSerializer,
|
||||
ResourceSerializer,
|
||||
FindingSerializer,
|
||||
ProviderSecretCreateSerializer,
|
||||
ProviderSecretSerializer,
|
||||
ProviderSecretUpdateSerializer,
|
||||
ProviderSecretCreateSerializer,
|
||||
InvitationSerializer,
|
||||
InvitationCreateSerializer,
|
||||
InvitationUpdateSerializer,
|
||||
InvitationAcceptSerializer,
|
||||
ComplianceOverviewSerializer,
|
||||
ComplianceOverviewFullSerializer,
|
||||
OverviewProviderSerializer,
|
||||
)
|
||||
from tasks.beat import schedule_provider_scan
|
||||
from tasks.tasks import (
|
||||
check_provider_connection_task,
|
||||
delete_provider_task,
|
||||
perform_scan_task,
|
||||
ProviderSerializer,
|
||||
ProviderUpdateSerializer,
|
||||
ResourceSerializer,
|
||||
ScanCreateSerializer,
|
||||
ScanSerializer,
|
||||
ScanUpdateSerializer,
|
||||
TaskSerializer,
|
||||
TenantSerializer,
|
||||
TokenRefreshSerializer,
|
||||
TokenSerializer,
|
||||
UserCreateSerializer,
|
||||
UserSerializer,
|
||||
UserUpdateSerializer,
|
||||
)
|
||||
|
||||
CACHE_DECORATOR = cache_control(
|
||||
@@ -803,12 +808,18 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
with transaction.atomic():
|
||||
scan = input_serializer.save()
|
||||
with transaction.atomic():
|
||||
task = perform_scan_task.delay(
|
||||
tenant_id=request.tenant_id,
|
||||
scan_id=str(scan.id),
|
||||
provider_id=str(scan.provider_id),
|
||||
# Disabled for now
|
||||
# checks_to_execute=scan.scanner_args.get("checks_to_execute"),
|
||||
task = perform_scan_task.apply_async(
|
||||
kwargs={
|
||||
"tenant_id": request.tenant_id,
|
||||
"scan_id": str(scan.id),
|
||||
"provider_id": str(scan.provider_id),
|
||||
# Disabled for now
|
||||
# checks_to_execute=scan.scanner_args.get("checks_to_execute"),
|
||||
},
|
||||
link=perform_scan_summary_task.si(
|
||||
tenant_id=request.tenant_id,
|
||||
scan_id=str(scan.id),
|
||||
),
|
||||
)
|
||||
|
||||
scan.task_id = task.id
|
||||
@@ -1295,26 +1306,67 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
|
||||
@extend_schema(tags=["Overview"])
|
||||
@extend_schema_view(
|
||||
providers=extend_schema(
|
||||
summary="List aggregated overview data for providers",
|
||||
description="Fetch aggregated summaries of the latest findings and resources for each provider. "
|
||||
"This includes counts of passed, failed, and manual findings, as well as the total number "
|
||||
"of resources managed by each provider.",
|
||||
summary="Get aggregated provider data",
|
||||
description=(
|
||||
"Retrieve an aggregated overview of findings and resources grouped by providers. "
|
||||
"The response includes the count of passed, failed, and manual findings, along with "
|
||||
"the total number of resources managed by each provider. Only the latest findings for "
|
||||
"each provider are considered in the aggregation to ensure accurate and up-to-date insights."
|
||||
),
|
||||
),
|
||||
findings=extend_schema(
|
||||
summary="Get aggregated findings data",
|
||||
description=(
|
||||
"Fetch aggregated findings data across all providers, grouped by various metrics such as "
|
||||
"passed, failed, muted, and total findings. This endpoint calculates summary statistics "
|
||||
"based on the latest scans for each provider and applies any provided filters, such as "
|
||||
"region, provider type, and scan date."
|
||||
),
|
||||
filters=True,
|
||||
),
|
||||
findings_severity=extend_schema(
|
||||
summary="Get findings data by severity",
|
||||
description=(
|
||||
"Retrieve an aggregated summary of findings grouped by severity levels, such as low, medium, "
|
||||
"high, and critical. The response includes the total count of findings for each severity, "
|
||||
"considering only the latest scans for each provider. Additional filters can be applied to "
|
||||
"narrow down results by region, provider type, or other attributes."
|
||||
),
|
||||
filters=True,
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
class OverviewViewSet(BaseRLSViewSet):
|
||||
queryset = ComplianceOverview.objects.all()
|
||||
http_method_names = ["get"]
|
||||
ordering = ["compliance_id"]
|
||||
ordering = ["-id"]
|
||||
|
||||
def get_queryset(self):
|
||||
return Finding.objects.all()
|
||||
if self.action == "providers":
|
||||
return Finding.objects.all()
|
||||
elif self.action == "findings":
|
||||
return ScanSummary.objects.all()
|
||||
elif self.action == "findings_severity":
|
||||
return ScanSummary.objects.all()
|
||||
else:
|
||||
return super().get_queryset()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "providers":
|
||||
return OverviewProviderSerializer
|
||||
elif self.action == "findings":
|
||||
return OverviewFindingSerializer
|
||||
elif self.action == "findings_severity":
|
||||
return OverviewSeveritySerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def get_filterset_class(self):
|
||||
if self.action == "providers":
|
||||
return None
|
||||
elif self.action in ["findings", "findings_severity"]:
|
||||
return ScanSummaryFilter
|
||||
return None
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def list(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(method="GET")
|
||||
@@ -1366,7 +1418,7 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
for res in resources_aggregated
|
||||
if res["provider__provider"] == provider
|
||||
),
|
||||
0, # Default to 0 if no resources are found
|
||||
0,
|
||||
)
|
||||
overview.append(
|
||||
{
|
||||
@@ -1382,3 +1434,74 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
serializer = OverviewProviderSerializer(overview, many=True)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings")
|
||||
def findings(self, request):
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(provider_id=OuterRef("scan__provider_id"))
|
||||
.order_by("-id")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
annotated_queryset = filtered_queryset.annotate(
|
||||
latest_scan_id=Subquery(latest_scan_subquery)
|
||||
)
|
||||
|
||||
filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id"))
|
||||
|
||||
aggregated_totals = filtered_queryset.aggregate(
|
||||
_pass=Sum("_pass") or 0,
|
||||
fail=Sum("fail") or 0,
|
||||
muted=Sum("muted") or 0,
|
||||
total=Sum("total") or 0,
|
||||
new=Sum("new") or 0,
|
||||
changed=Sum("changed") or 0,
|
||||
unchanged=Sum("unchanged") or 0,
|
||||
fail_new=Sum("fail_new") or 0,
|
||||
fail_changed=Sum("fail_changed") or 0,
|
||||
pass_new=Sum("pass_new") or 0,
|
||||
pass_changed=Sum("pass_changed") or 0,
|
||||
muted_new=Sum("muted_new") or 0,
|
||||
muted_changed=Sum("muted_changed") or 0,
|
||||
)
|
||||
|
||||
for key in aggregated_totals:
|
||||
if aggregated_totals[key] is None:
|
||||
aggregated_totals[key] = 0
|
||||
|
||||
serializer = self.get_serializer(aggregated_totals)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="findings_severity")
|
||||
def findings_severity(self, request):
|
||||
queryset = self.get_queryset()
|
||||
filtered_queryset = self.filter_queryset(queryset)
|
||||
|
||||
latest_scan_subquery = (
|
||||
Scan.objects.filter(provider_id=OuterRef("scan__provider_id"))
|
||||
.order_by("-id")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
||||
annotated_queryset = filtered_queryset.annotate(
|
||||
latest_scan_id=Subquery(latest_scan_subquery)
|
||||
)
|
||||
|
||||
filtered_queryset = annotated_queryset.filter(scan_id=F("latest_scan_id"))
|
||||
|
||||
severity_counts = (
|
||||
filtered_queryset.values("severity")
|
||||
.annotate(count=Sum("total"))
|
||||
.order_by("severity")
|
||||
)
|
||||
|
||||
severity_data = {sev[0]: 0 for sev in SeverityChoices}
|
||||
|
||||
for item in severity_counts:
|
||||
severity_data[item["severity"]] = item["count"]
|
||||
|
||||
serializer = OverviewSeveritySerializer(severity_data)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -3,8 +3,7 @@ from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from celery.utils.log import get_task_logger
|
||||
from prowler.lib.outputs.finding import Finding as ProwlerFinding
|
||||
from prowler.lib.scan.scan import Scan as ProwlerScan
|
||||
from django.db.models import Case, Count, IntegerField, Sum, When
|
||||
|
||||
from api.compliance import (
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
|
||||
@@ -12,17 +11,20 @@ from api.compliance import (
|
||||
)
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.models import (
|
||||
Provider,
|
||||
Scan,
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Provider,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
StatusChoices as FindingStatus,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
StateChoices,
|
||||
ComplianceOverview,
|
||||
)
|
||||
from api.models import StatusChoices as FindingStatus
|
||||
from api.utils import initialize_prowler_provider
|
||||
from api.v1.serializers import ScanTaskSerializer
|
||||
from prowler.lib.outputs.finding import Finding as ProwlerFinding
|
||||
from prowler.lib.scan.scan import Scan as ProwlerScan
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
@@ -268,7 +270,7 @@ def perform_prowler_scan(
|
||||
scan_instance.unique_resource_count = len(unique_resources)
|
||||
scan_instance.save()
|
||||
|
||||
if generate_compliance:
|
||||
if exception is None and generate_compliance:
|
||||
try:
|
||||
regions = prowler_provider.get_regions()
|
||||
except AttributeError:
|
||||
@@ -321,3 +323,155 @@ def perform_prowler_scan(
|
||||
|
||||
serializer = ScanTaskSerializer(instance=scan_instance)
|
||||
return serializer.data
|
||||
|
||||
|
||||
def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
"""
|
||||
Aggregates findings for a given scan and stores the results in the ScanSummary table.
|
||||
|
||||
This function retrieves all findings associated with a given `scan_id` and calculates various
|
||||
metrics such as counts of failed, passed, and muted findings, as well as their deltas (new,
|
||||
changed, unchanged). The results are grouped by `check_id`, `service`, `severity`, and `region`.
|
||||
These aggregated metrics are then stored in the `ScanSummary` table.
|
||||
|
||||
Args:
|
||||
tenant_id (str): The ID of the tenant to which the scan belongs.
|
||||
scan_id (str): The ID of the scan for which findings need to be aggregated.
|
||||
|
||||
Aggregated Metrics:
|
||||
- fail: Total number of failed findings.
|
||||
- _pass: Total number of passed findings.
|
||||
- muted: Total number of muted findings.
|
||||
- total: Total number of findings.
|
||||
- new: Total number of new findings.
|
||||
- changed: Total number of changed findings.
|
||||
- unchanged: Total number of unchanged findings.
|
||||
- fail_new: Failed findings with a delta of 'new'.
|
||||
- fail_changed: Failed findings with a delta of 'changed'.
|
||||
- pass_new: Passed findings with a delta of 'new'.
|
||||
- pass_changed: Passed findings with a delta of 'changed'.
|
||||
- muted_new: Muted findings with a delta of 'new'.
|
||||
- muted_changed: Muted findings with a delta of 'changed'.
|
||||
"""
|
||||
with tenant_transaction(tenant_id):
|
||||
findings = Finding.objects.filter(scan_id=scan_id)
|
||||
|
||||
aggregation = findings.values(
|
||||
"check_id",
|
||||
"resources__service",
|
||||
"severity",
|
||||
"resources__region",
|
||||
).annotate(
|
||||
fail=Sum(
|
||||
Case(
|
||||
When(status="FAIL", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
_pass=Sum(
|
||||
Case(
|
||||
When(status="PASS", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
muted=Sum(
|
||||
Case(
|
||||
When(status="MUTED", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
total=Count("id"),
|
||||
new=Sum(
|
||||
Case(
|
||||
When(delta="new", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
unchanged=Sum(
|
||||
Case(
|
||||
When(delta__isnull=True, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
fail_new=Sum(
|
||||
Case(
|
||||
When(delta="new", status="FAIL", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
fail_changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", status="FAIL", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
pass_new=Sum(
|
||||
Case(
|
||||
When(delta="new", status="PASS", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
pass_changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", status="PASS", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
muted_new=Sum(
|
||||
Case(
|
||||
When(delta="new", status="MUTED", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
muted_changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", status="MUTED", then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
with tenant_transaction(tenant_id):
|
||||
scan_aggregations = {
|
||||
ScanSummary(
|
||||
tenant_id=tenant_id,
|
||||
scan_id=scan_id,
|
||||
check_id=agg["check_id"],
|
||||
service=agg["resources__service"],
|
||||
severity=agg["severity"],
|
||||
region=agg["resources__region"],
|
||||
fail=agg["fail"],
|
||||
_pass=agg["_pass"],
|
||||
muted=agg["muted"],
|
||||
total=agg["total"],
|
||||
new=agg["new"],
|
||||
changed=agg["changed"],
|
||||
unchanged=agg["unchanged"],
|
||||
fail_new=agg["fail_new"],
|
||||
fail_changed=agg["fail_changed"],
|
||||
pass_new=agg["pass_new"],
|
||||
pass_changed=agg["pass_changed"],
|
||||
muted_new=agg["muted_new"],
|
||||
muted_changed=agg["muted_changed"],
|
||||
)
|
||||
for agg in aggregation
|
||||
}
|
||||
ScanSummary.objects.bulk_create(scan_aggregations, batch_size=3000)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from celery import shared_task
|
||||
from config.celery import RLSTask
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
from tasks.jobs.deletion import delete_instance
|
||||
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
|
||||
|
||||
from api.db_utils import tenant_transaction
|
||||
from api.decorators import set_tenant
|
||||
from api.models import Provider, Scan
|
||||
from config.celery import RLSTask
|
||||
from tasks.jobs.connection import check_provider_connection
|
||||
from tasks.jobs.deletion import delete_instance
|
||||
from tasks.jobs.scan import perform_prowler_scan
|
||||
|
||||
|
||||
@shared_task(base=RLSTask, name="provider-connection-check")
|
||||
@@ -110,3 +110,8 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
|
||||
scan_id=str(scan_instance.id),
|
||||
provider_id=provider_id,
|
||||
)
|
||||
|
||||
|
||||
@shared_task(name="scan-summary")
|
||||
def perform_scan_summary_task(tenant_id: str, scan_id: str):
|
||||
return aggregate_findings(tenant_id=tenant_id, scan_id=scan_id)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from api.models import (
|
||||
StateChoices,
|
||||
Severity,
|
||||
Finding,
|
||||
Resource,
|
||||
StatusChoices,
|
||||
Provider,
|
||||
)
|
||||
from tasks.jobs.scan import (
|
||||
perform_prowler_scan,
|
||||
_create_finding_delta,
|
||||
_store_resources,
|
||||
perform_prowler_scan,
|
||||
)
|
||||
|
||||
from api.models import (
|
||||
Finding,
|
||||
Provider,
|
||||
Resource,
|
||||
Severity,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
)
|
||||
|
||||
|
||||
@@ -358,3 +358,6 @@ class TestPerformScan:
|
||||
|
||||
assert resource == resource_instance
|
||||
assert resource_uid_tuple == (resource_instance.uid, resource_instance.region)
|
||||
|
||||
|
||||
# TODO Add tests for aggregations
|
||||
|
||||
Reference in New Issue
Block a user