From 4fa88aa22616c9355d196ec64e77de587675da57 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Fri, 3 Jan 2025 11:41:40 +0800 Subject: [PATCH 1/7] perf: account risk filter --- apps/accounts/filters.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index 781312609..514274700 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # -from django.db.models import Q, F, Value, CharField -from django.db.models.functions import Concat +from django.db.models import Q, Exists, OuterRef from django.utils import timezone from django_filters import rest_framework as drf_filters @@ -63,20 +62,15 @@ class AccountFilterSet(BaseFilterSet): if not value: return queryset - asset_usernames = AccountRisk.objects.filter(risk=value). \ - values_list( - Concat( - F('asset_id'), Value('-'), F('username'), - output_field=CharField() - ), flat=True - ) - - queryset = queryset.annotate( - asset_username=Concat( - F('asset_id'), Value('-'), F('username'), - output_field=CharField() + queryset = queryset.filter( + Exists( + AccountRisk.objects.filter( + risk=value, + asset_id=OuterRef('asset_id'), + username=OuterRef('username') + ) ) - ).filter(asset_username__in=asset_usernames) + ) return queryset @staticmethod From 024e2f6d7910fb49763ce03d37682288a88d197e Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Fri, 3 Jan 2025 14:45:54 +0800 Subject: [PATCH 2/7] perf: Copy move account failed message --- apps/accounts/api/account/account.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index a9edc25c2..6aa3d330e 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -1,4 +1,6 @@ +from django.db import transaction from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ from rest_framework.decorators import action from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.response import Response @@ -14,10 +16,12 @@ from authentication.permissions import UserConfirmation, ConfirmType from common.api.mixin import ExtraFilterFieldsMixin from common.drf.filters import AttrRulesFilterBackend from common.permissions import IsValidUser -from common.utils import lazyproperty +from common.utils import lazyproperty, get_logger from orgs.mixins.api import OrgBulkModelViewSet from rbac.permissions import RBACPermission +logger = get_logger(__file__) + __all__ = [ 'AccountViewSet', 'AccountSecretsViewSet', 'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi', @@ -109,10 +113,12 @@ class AccountViewSet(OrgBulkModelViewSet): account_data['asset'] = asset creation_results[asset] = {'state': 'created'} try: - self.model.objects.create(**account_data) - success_count += 1 + with transaction.atomic(): + self.model.objects.create(**account_data) + success_count += 1 except Exception as e: - creation_results[asset] = {'error': str(e), 'state': 'error'} + logger.debug(f'{ "Move" if move else "Copy" } to assets error: {e}') + creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'} results = [{'asset': str(asset), **res} for asset, res in creation_results.items()] From afc1d6610f3e209fbbf328068fc48ffa861cf2e3 Mon Sep 17 00:00:00 2001 From: wangruidong <940853815@qq.com> Date: Fri, 3 Jan 2025 15:05:43 +0800 Subject: [PATCH 3/7] fix: gather account sync account to asset --- apps/accounts/automations/gather_account/manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index fb0c819fa..c52a72a35 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -358,7 +358,6 @@ class GatherAccountsManager(AccountBasePlaybookManager): for asset, accounts_data in self.asset_account_info.items(): ori_users = self.ori_asset_usernames[str(asset.id)] with tmp_to_org(asset.org_id): - gathered_accounts = [] for d in accounts_data: username = d["username"] ori_account = self.ori_gathered_accounts_mapper.get( @@ -374,6 +373,9 @@ class GatherAccountsManager(AccountBasePlaybookManager): self.create_gathered_account.finish() self.update_gathered_account.finish() self.update_gather_accounts_status(asset) + if not self.is_sync_account: + continue + gathered_accounts = GatheredAccount.objects.filter(asset=asset) GatheredAccount.sync_accounts(gathered_accounts, self.is_sync_account) # 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步 time.sleep(0.5) From 83dc15d441d28be51f86a23b3da0126b99547780 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Fri, 3 Jan 2025 17:47:18 +0800 Subject: [PATCH 4/7] perf: Pam dashboard --- apps/accounts/api/account/__init__.py | 3 +- apps/accounts/api/account/pam_dashboard.py | 42 +++++++++++++++++++ .../automations/check_account/manager.py | 10 ++--- .../automations/gather_account/manager.py | 6 +-- apps/accounts/filters.py | 15 ++----- apps/accounts/models/account.py | 17 ++++++++ apps/accounts/risk_handlers.py | 8 ++-- apps/accounts/urls.py | 1 + 8 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 apps/accounts/api/account/pam_dashboard.py diff --git a/apps/accounts/api/account/__init__.py b/apps/accounts/api/account/__init__.py index 2b1f578e7..c2221a379 100644 --- a/apps/accounts/api/account/__init__.py +++ b/apps/accounts/api/account/__init__.py @@ -1,5 +1,6 @@ from .account import * +from .application import * +from .pam_dashboard import * from .task import * from .template import * from .virtual import * -from .application import * diff --git a/apps/accounts/api/account/pam_dashboard.py b/apps/accounts/api/account/pam_dashboard.py new file mode 100644 index 000000000..9fb00dda6 --- /dev/null +++ b/apps/accounts/api/account/pam_dashboard.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +from django.http.response import JsonResponse +from rest_framework.views import APIView + +from accounts.models import Account, RiskChoice + +__all__ = ['PamDashboardApi'] + + +class PamDashboardApi(APIView): + http_method_names = ['get'] + rbac_perms = { + 'GET': 'accounts.view_account', + } + + def get(self, request, *args, **kwargs): + query_params = self.request.query_params + data = {} + + account_count = Account.objects.count() + privileged_account_count = Account.objects.filter(privileged=True).count() + + if query_params.get('total_privileged_accounts'): + data['total_privileged_accounts'] = privileged_account_count + + if query_params.get('total_ordinary_accounts'): + data['total_ordinary_accounts'] = account_count - privileged_account_count + + if query_params.get('total_unmanaged_accounts'): + data['total_unmanaged_accounts'] = Account.get_risks(RiskChoice.new_found).count() + + if query_params.get('total_unavailable_accounts'): + data['total_unavailable_accounts'] = Account.objects.filter(is_active=False).count() + + if query_params.get('total_weak_password_accounts'): + data['total_weak_password_accounts'] = Account.get_risks(RiskChoice.weak_password) + + if query_params.get('total_long_time_change_password_accounts'): + data['total_long_time_change_password_accounts'] = Account.get_risks(RiskChoice.long_time_password) + + return JsonResponse(data, status=200) diff --git a/apps/accounts/automations/check_account/manager.py b/apps/accounts/automations/check_account/manager.py index 9d802bd3e..162f40b2c 100644 --- a/apps/accounts/automations/check_account/manager.py +++ b/apps/accounts/automations/check_account/manager.py @@ -3,7 +3,7 @@ from collections import defaultdict from django.utils import timezone -from accounts.models import Account, AccountRisk +from accounts.models import Account, AccountRisk, RiskChoice from assets.automations.base.manager import BaseManager from common.decorators import bulk_create_decorator, bulk_update_decorator from common.utils.strings import color_fmt @@ -69,12 +69,12 @@ def check_account_secrets(accounts, assets): if is_weak_password(account.secret): print(tmpl % (account, color_fmt("weak", "red"))) - summary["weak_password"] += 1 - result["weak_password"].append(result_item) + summary[RiskChoice.weak_password] += 1 + result[RiskChoice.weak_password].append(result_item) risks.append( { "account": account, - "risk": "weak_password", + "risk": RiskChoice.weak_password, } ) else: @@ -143,7 +143,7 @@ class CheckAccountManager(BaseManager): "\n---\nSummary: \nok: %s, weak password: %s, no secret: %s, using time: %ss" % ( self.summary["ok"], - self.summary["weak_password"], + self.summary[RiskChoice.weak_password], self.summary["no_secret"], int(self.duration), ) diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index c52a72a35..af085926b 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -4,7 +4,7 @@ from collections import defaultdict from django.utils import timezone from accounts.const import AutomationTypes -from accounts.models import GatheredAccount, Account, AccountRisk +from accounts.models import GatheredAccount, Account, AccountRisk, RiskChoice from common.const import ConfirmOrIgnore from common.decorators import bulk_create_decorator, bulk_update_decorator from common.utils import get_logger @@ -68,7 +68,7 @@ class AnalyseAccountRisk: {"field": "date_last_login", "risk": "long_time_no_login", "delta": long_time}, { "field": "date_password_change", - "risk": "long_time_password", + "risk": RiskChoice.long_time_password, "delta": long_time, }, { @@ -164,7 +164,7 @@ class AnalyseAccountRisk: self._create_risk( dict( **basic, - risk="new_found", + risk=RiskChoice.new_found, details=[{"datetime": self.now.isoformat()}], ) ) diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index 514274700..df224b707 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- # -from django.db.models import Q, Exists, OuterRef +from django.db.models import Q from django.utils import timezone from django_filters import rest_framework as drf_filters from assets.models import Node from common.drf.filters import BaseFilterSet from common.utils.timezone import local_zero_hour, local_now -from .models import Account, GatheredAccount, ChangeSecretRecord, AccountRisk +from .models import Account, GatheredAccount, ChangeSecretRecord class AccountFilterSet(BaseFilterSet): @@ -62,16 +62,7 @@ class AccountFilterSet(BaseFilterSet): if not value: return queryset - queryset = queryset.filter( - Exists( - AccountRisk.objects.filter( - risk=value, - asset_id=OuterRef('asset_id'), - username=OuterRef('username') - ) - ) - ) - return queryset + return Account.get_risks(queryset, value) @staticmethod def filter_latest(queryset, name, value): diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index b5cd382fa..0fe19a20a 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Exists, OuterRef from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords @@ -165,6 +166,22 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount): return escape(value) + @classmethod + def get_risks(cls, queryset=None, risk_type=None): + from accounts.models import AccountRisk + subquery = AccountRisk.objects.filter( + asset_id=OuterRef('asset_id'), + username=OuterRef('username') + ) + + if risk_type: + subquery = subquery.filter(risk=risk_type) + + if queryset is None: + queryset = cls.objects.all() + + return queryset.filter(Exists(subquery)) + def replace_history_model_with_mixin(): """ diff --git a/apps/accounts/risk_handlers.py b/apps/accounts/risk_handlers.py index e2911da7e..2dd8c8d53 100644 --- a/apps/accounts/risk_handlers.py +++ b/apps/accounts/risk_handlers.py @@ -1,15 +1,13 @@ +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from accounts.const import AutomationTypes -from common.const import ConfirmOrIgnore from accounts.models import ( GatheredAccount, AccountRisk, SecretType, - AutomationExecution, + AutomationExecution, RiskChoice, ) -from django.utils import timezone - from common.const import ConfirmOrIgnore TYPE_CHOICES = [ @@ -83,7 +81,7 @@ class RiskHandler: GatheredAccount.objects.filter(asset=self.asset, username=self.username).update( present=True, status=ConfirmOrIgnore.confirmed ) - self.risk = "new_found" + self.risk = RiskChoice.new_found def handle_disable_remote(self): pass diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index b9cfea710..f196e1146 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -49,6 +49,7 @@ urlpatterns = [ path('push-account//nodes/', api.PushAccountNodeAddRemoveApi.as_view(), name='push-account-add-or-remove-node'), path('push-account//assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'), + path('pam-dashboard/', api.PamDashboardApi.as_view(), name='pam-dashboard'), path('change-secret-dashboard/', api.ChangeSecretDashboardApi.as_view(), name='change-secret-dashboard'), ] From 2273275c235d788460e296cef8ae7104ff2ce232 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Fri, 3 Jan 2025 18:37:43 +0800 Subject: [PATCH 5/7] perf: Account dashboard total accounts --- apps/accounts/api/account/pam_dashboard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/accounts/api/account/pam_dashboard.py b/apps/accounts/api/account/pam_dashboard.py index 9fb00dda6..fec642961 100644 --- a/apps/accounts/api/account/pam_dashboard.py +++ b/apps/accounts/api/account/pam_dashboard.py @@ -20,6 +20,9 @@ class PamDashboardApi(APIView): account_count = Account.objects.count() privileged_account_count = Account.objects.filter(privileged=True).count() + + if query_params.get('total_accounts'): + data['total_accounts'] = account_count if query_params.get('total_privileged_accounts'): data['total_privileged_accounts'] = privileged_account_count From ff9adb45b7051da935688acca9a00012fa717bf3 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 6 Jan 2025 16:52:12 +0800 Subject: [PATCH 6/7] perf: Pam dashboard --- apps/accounts/api/account/pam_dashboard.py | 14 +++++++++++--- apps/accounts/models/account.py | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/accounts/api/account/pam_dashboard.py b/apps/accounts/api/account/pam_dashboard.py index fec642961..b857b56ee 100644 --- a/apps/accounts/api/account/pam_dashboard.py +++ b/apps/accounts/api/account/pam_dashboard.py @@ -4,6 +4,7 @@ from django.http.response import JsonResponse from rest_framework.views import APIView from accounts.models import Account, RiskChoice +from common.utils.timezone import local_monday __all__ = ['PamDashboardApi'] @@ -24,6 +25,10 @@ class PamDashboardApi(APIView): if query_params.get('total_accounts'): data['total_accounts'] = account_count + if query_params.get('total_week_add_accounts'): + monday_time = local_monday() + data['total_week_add_accounts'] = Account.objects.filter(date_created__gte=monday_time).count() + if query_params.get('total_privileged_accounts'): data['total_privileged_accounts'] = privileged_account_count @@ -31,15 +36,18 @@ class PamDashboardApi(APIView): data['total_ordinary_accounts'] = account_count - privileged_account_count if query_params.get('total_unmanaged_accounts'): - data['total_unmanaged_accounts'] = Account.get_risks(RiskChoice.new_found).count() + data['total_unmanaged_accounts'] = Account.get_risks(risk_type=RiskChoice.new_found).count() if query_params.get('total_unavailable_accounts'): data['total_unavailable_accounts'] = Account.objects.filter(is_active=False).count() + if query_params.get('total_long_time_no_login_accounts'): + data['total_long_time_no_login_accounts'] = Account.get_risks(risk_type=RiskChoice.long_time_no_login).count() + if query_params.get('total_weak_password_accounts'): - data['total_weak_password_accounts'] = Account.get_risks(RiskChoice.weak_password) + data['total_weak_password_accounts'] = Account.get_risks(risk_type=RiskChoice.weak_password).count() if query_params.get('total_long_time_change_password_accounts'): - data['total_long_time_change_password_accounts'] = Account.get_risks(RiskChoice.long_time_password) + data['total_long_time_change_password_accounts'] = Account.get_risks(risk_type=RiskChoice.long_time_password).count() return JsonResponse(data, status=200) diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index 0fe19a20a..f7c759625 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -168,6 +168,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount): @classmethod def get_risks(cls, queryset=None, risk_type=None): + # TODO 数据量大时,子查询性能不佳,考虑用原生sql或者在模型层面做出改动 from accounts.models import AccountRisk subquery = AccountRisk.objects.filter( asset_id=OuterRef('asset_id'), From 35e43514adc6df98ce15e2e48d48ad56f3f18c35 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 6 Jan 2025 17:20:57 +0800 Subject: [PATCH 7/7] perf: Change secret filter account secret_reset --- apps/accounts/automations/base/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/accounts/automations/base/manager.py b/apps/accounts/automations/base/manager.py index b86963e7c..1e261a89b 100644 --- a/apps/accounts/automations/base/manager.py +++ b/apps/accounts/automations/base/manager.py @@ -54,7 +54,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager): asset = privilege_account.asset accounts = asset.accounts.all() - accounts = accounts.filter(id__in=self.account_ids) + accounts = accounts.filter(id__in=self.account_ids, secret_reset=True) if self.secret_type: accounts = accounts.filter(secret_type=self.secret_type)