diff --git a/apps/assets/const/automation.py b/apps/assets/const/automation.py index 1d48575a3..c5971073c 100644 --- a/apps/assets/const/automation.py +++ b/apps/assets/const/automation.py @@ -14,6 +14,10 @@ class Connectivity(TextChoices): NTLM_ERR = 'ntlm_err', _('NTLM credentials rejected error') CREATE_TEMPORARY_ERR = 'create_temp_err', _('Create temporary error') + @classmethod + def as_dict(cls): + return {choice.value: choice.label for choice in cls} + class AutomationTypes(TextChoices): ping = 'ping', _('Ping') diff --git a/apps/reports/api/__init__.py b/apps/reports/api/__init__.py index 3d2c30cb1..879c416a9 100644 --- a/apps/reports/api/__init__.py +++ b/apps/reports/api/__init__.py @@ -1,3 +1,4 @@ +from .accouts import * from .assets import * from .report import * from .users import * diff --git a/apps/reports/api/accouts/__init__.py b/apps/reports/api/accouts/__init__.py new file mode 100644 index 000000000..96c79ae91 --- /dev/null +++ b/apps/reports/api/accouts/__init__.py @@ -0,0 +1 @@ +from .account import * diff --git a/apps/reports/api/accouts/account.py b/apps/reports/api/accouts/account.py new file mode 100644 index 000000000..ae1f80309 --- /dev/null +++ b/apps/reports/api/accouts/account.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# +from collections import defaultdict + +from django.db.models import Count, Q, F, Value +from django.db.models.functions import Concat +from django.http import JsonResponse +from rest_framework.views import APIView + +from accounts.models import Account, AccountTemplate +from assets.const import Connectivity +from common.permissions import IsValidLicense +from common.utils import lazyproperty +from rbac.permissions import RBACPermission +from reports.api.assets.base import group_stats +from reports.mixins import DateRangeMixin + +__all__ = ['AccountStatisticApi'] + + +class AccountStatisticApi(DateRangeMixin, APIView): + http_method_names = ['get'] + # TODO: Define the required RBAC permissions for this API + rbac_perms = { + 'GET': 'accounts.view_account', + } + permission_classes = [RBACPermission, IsValidLicense] + + @lazyproperty + def base_qs(self): + return Account.objects.all() + + @lazyproperty + def template_qs(self): + return AccountTemplate.objects.all() + + def get_change_secret_account_metrics(self): + filtered_queryset = self.filter_by_date_range(self.base_qs, 'date_change_secret') + + data = defaultdict(set) + for t, _id in filtered_queryset.values_list('date_change_secret', 'id'): + date_str = str(t.date()) + data[date_str].add(_id) + + metrics = [len(data.get(str(d), set())) for d in self.date_range_list] + return metrics + + def get(self, request, *args, **kwargs): + qs = self.base_qs + + stats = qs.aggregate( + total=Count(1), + active=Count(1, filter=Q(is_active=True)), + connected=Count(1, filter=Q(connectivity=Connectivity.OK)), + su_from=Count(1, filter=Q(su_from__isnull=False)), + date_change_secret=Count(1, filter=Q(secret_reset=True)), + ) + + stats['template_total'] = self.template_qs.count() + + source_pie_data = [ + {'name': str(source), 'value': total} + for source, total in + qs.values('source').annotate( + total=Count(1) + ).values_list('source', 'total') + ] + + by_connectivity = group_stats( + qs, 'label', 'connectivity', Connectivity.as_dict(), + ) + + top_assets = qs.values('asset__name') \ + .annotate(account_count=Count('id')) \ + .order_by('-account_count')[:10] + + top_version_accounts = qs.annotate( + display_key=Concat( + F('asset__name'), + Value('('), + F('username'), + Value(')') + ) + ).values('display_key', 'version').order_by('-version')[:10] + + payload = { + 'account_stats': stats, + 'top_assets': list(top_assets), + 'top_version_accounts': list(top_version_accounts), + 'source_pie': source_pie_data, + 'by_connectivity': by_connectivity, + 'change_secret_account_metrics': { + 'dates_metrics_date': self.dates_metrics_date, + 'dates_metrics_total': self.get_change_secret_account_metrics(), + } + } + return JsonResponse(payload, status=200) diff --git a/apps/reports/api/accouts/base.py b/apps/reports/api/accouts/base.py new file mode 100644 index 000000000..f905777d4 --- /dev/null +++ b/apps/reports/api/accouts/base.py @@ -0,0 +1,21 @@ +from django.db.models import Count, F + + +def group_stats(queryset, alias, key, label_map=None): + grouped = ( + queryset + .exclude(**{f'{key}__isnull': True}) + .values(**{alias: F(key)}) + .annotate(total=Count('id')) + ) + + data = [ + { + alias: val, + 'total': cnt, + **({'label': label_map.get(val, val)} if label_map else {}) + } + for val, cnt in grouped.values_list(alias, 'total') + ] + + return data diff --git a/apps/reports/urls/api_urls.py b/apps/reports/urls/api_urls.py index e6296855d..683921993 100644 --- a/apps/reports/urls/api_urls.py +++ b/apps/reports/urls/api_urls.py @@ -9,5 +9,6 @@ urlpatterns = [ path('reports/users/', api.UserReportApi.as_view(), name='user-list'), path('reports/user-change-password/', api.UserChangeSecretApi.as_view(), name='user-change-password'), path('reports/asset-statistic/', api.AssetStatisticApi.as_view(), name='asset-statistic'), - path('reports/asset-activity/', api.AssetActivityApi.as_view(), name='asset-activity') + path('reports/asset-activity/', api.AssetActivityApi.as_view(), name='asset-activity'), + path('reports/account-statistic/', api.AccountStatisticApi.as_view(), name='account-statistic'), ]