From 7f06190c5fce117b4b03199543b83d1a49c2787f Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 4 Nov 2024 18:34:35 +0800 Subject: [PATCH] perf: update pam --- .../accounts/api/automations/check_account.py | 29 ++++++++-- ...risk_account_accountrisk_asset_and_more.py | 58 +++++++++++++++++++ .../migrations/0007_alter_accountrisk_risk.py | 34 +++++++++++ .../models/automations/check_account.py | 47 +++++++++++++-- .../serializers/automations/check_accounts.py | 26 ++++++++- apps/accounts/urls.py | 1 + 6 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 apps/accounts/migrations/0006_remove_accountrisk_account_accountrisk_asset_and_more.py create mode 100644 apps/accounts/migrations/0007_alter_accountrisk_risk.py diff --git a/apps/accounts/api/automations/check_account.py b/apps/accounts/api/automations/check_account.py index f94a53a48..0f00217e4 100644 --- a/apps/accounts/api/automations/check_account.py +++ b/apps/accounts/api/automations/check_account.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # +from django.db.models import Q, Count +from rest_framework.decorators import action from accounts import serializers from accounts.const import AutomationTypes -from accounts.models import AccountCheckAutomation -from accounts.models import AccountRisk +from accounts.models import AccountCheckAutomation, AccountRisk, RiskChoice from orgs.mixins.api import OrgBulkModelViewSet from .base import AutomationExecutionViewSet @@ -41,10 +42,28 @@ class AccountRiskViewSet(OrgBulkModelViewSet): search_fields = ('username',) serializer_classes = { 'default': serializers.AccountRiskSerializer, + 'assets': serializers.AssetRiskSerializer, } rbac_perms = { - 'sync_accounts': 'assets.add_AccountRisk', + 'sync_accounts': 'assets.add_accountrisk', + 'assets': 'accounts.view_accountrisk' } + http_method_names = ['get', 'head', 'options'] + + @action(methods=['get'], detail=False, url_path='assets') + def assets(self, request, *args, **kwargs): + annotations = { + f'{risk[0]}_count': Count('id', filter=Q(risk=risk[0])) + for risk in RiskChoice.choices + } + queryset = ( + AccountRisk.objects + .select_related('asset', 'asset__platform') # 使用 select_related 来优化 asset 和 asset__platform 的查询 + .values('asset__id', 'asset__name', 'asset__address', 'asset__platform__name') # 添加需要的字段 + .annotate(risk_total=Count('id')) # 计算风险总数 + .annotate(**annotations) # 使用上面定义的 annotations 进行计数 + ) + return self.get_paginated_response_from_queryset(queryset) class AccountCheckEngineViewSet(OrgBulkModelViewSet): @@ -60,12 +79,12 @@ class AccountCheckEngineViewSet(OrgBulkModelViewSet): 'id': 1, 'name': 'check_gathered_account', 'display_name': '检查发现的账号', - 'description': '基于自动发现的账号结果进行检查分析 ' + 'description': '基于自动发现的账号结果进行检查分析,检查 用户组、公钥、sudoers 等信息' }, { 'id': 2, 'name': 'check_account_secret', 'display_name': '检查账号密码强弱', - 'description': '基于账号密码的安全性进行检查分析' + 'description': '基于账号密码的安全性进行检查分析, 检查密码强度、泄露等信息' } ] diff --git a/apps/accounts/migrations/0006_remove_accountrisk_account_accountrisk_asset_and_more.py b/apps/accounts/migrations/0006_remove_accountrisk_account_accountrisk_asset_and_more.py new file mode 100644 index 000000000..6cccf9b8e --- /dev/null +++ b/apps/accounts/migrations/0006_remove_accountrisk_account_accountrisk_asset_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.1.13 on 2024-11-04 06:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("assets", "0006_baseautomation_start_time"), + ("accounts", "0005_account_secret_reset"), + ] + + operations = [ + migrations.RemoveField( + model_name="accountrisk", + name="account", + ), + migrations.AddField( + model_name="accountrisk", + name="asset", + field=models.ForeignKey( + default=None, + on_delete=django.db.models.deletion.CASCADE, + related_name="risks", + to="assets.asset", + verbose_name="Asset", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="accountrisk", + name="username", + field=models.CharField(default="", max_length=32, verbose_name="Username"), + preserve_default=False, + ), + migrations.AlterField( + model_name="accountrisk", + name="risk", + field=models.CharField( + choices=[ + ("zombie", "Long time no login"), + ("ghost", "Not managed"), + ("long_time_no_change", "Long time no change"), + ("weak_password", "Weak password"), + ("login_bypass", "Login bypass"), + ("group_change", "Group change"), + ("account_delete", "Account delete"), + ("password_expired", "Password expired"), + ("no_admin_account", "No admin account"), + ("password_error", "Password error"), + ("other", "Other"), + ], + max_length=128, + verbose_name="Risk", + ), + ), + ] diff --git a/apps/accounts/migrations/0007_alter_accountrisk_risk.py b/apps/accounts/migrations/0007_alter_accountrisk_risk.py new file mode 100644 index 000000000..d806f7f3b --- /dev/null +++ b/apps/accounts/migrations/0007_alter_accountrisk_risk.py @@ -0,0 +1,34 @@ +# Generated by Django 4.1.13 on 2024-11-04 06:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0006_remove_accountrisk_account_accountrisk_asset_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="accountrisk", + name="risk", + field=models.CharField( + choices=[ + ("zombie", "Long time no login"), + ("ghost", "Not managed"), + ("long_time_password", "Long time no change"), + ("weak_password", "Weak password"), + ("group_changed", "Group change"), + ("sudo_changed", "Sudo changed"), + ("account_deleted", "Account delete"), + ("password_expired", "Password expired"), + ("no_admin_account", "No admin account"), + ("password_error", "Password error"), + ("others", "Others"), + ], + max_length=128, + verbose_name="Risk", + ), + ), + ] diff --git a/apps/accounts/models/automations/check_account.py b/apps/accounts/models/automations/check_account.py index eff443035..9f10e463f 100644 --- a/apps/accounts/models/automations/check_account.py +++ b/apps/accounts/models/automations/check_account.py @@ -1,3 +1,5 @@ +from itertools import islice + from django.db import models from django.db.models import TextChoices from django.utils.translation import gettext_lazy as _ @@ -7,7 +9,7 @@ from orgs.mixins.models import JMSOrgBaseModel from .base import AccountBaseAutomation from ...const import AutomationTypes -__all__ = ['AccountCheckAutomation', 'AccountRisk'] +__all__ = ['AccountCheckAutomation', 'AccountRisk', 'RiskChoice'] class AccountCheckAutomation(AccountBaseAutomation): @@ -35,14 +37,22 @@ class AccountCheckAutomation(AccountBaseAutomation): class RiskChoice(TextChoices): - zombie = 'zombie', _('Zombie') # 好久没登录的账号 - ghost = 'ghost', _('Ghost') # 未被纳管的账号 + zombie = 'zombie', _('Long time no login') # 好久没登录的账号 + ghost = 'ghost', _('Not managed') # 未被纳管的账号 + long_time_password = 'long_time_password', _('Long time no change') weak_password = 'weak_password', _('Weak password') - longtime_no_change = 'long_time_no_change', _('Long time no change') + password_error = 'password_error', _('Password error') + password_expired = 'password_expired', _('Password expired') + group_changed = 'group_changed', _('Group change') + sudo_changed = 'sudo_changed', _('Sudo changed') + account_deleted = 'account_deleted', _('Account delete') + no_admin_account = 'no_admin_account', _('No admin account') # 为什么不叫 No privileged 呢,是因为有 privileged,但是不可用 + other = 'others', _('Others') class AccountRisk(JMSOrgBaseModel): - account = models.ForeignKey('Account', on_delete=models.CASCADE, related_name='risks', verbose_name=_('Account')) + asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, related_name='risks', verbose_name=_('Asset')) + username = models.CharField(max_length=32, verbose_name=_('Username')) risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices) confirmed = models.BooleanField(default=False, verbose_name=_('Confirmed')) @@ -50,4 +60,29 @@ class AccountRisk(JMSOrgBaseModel): verbose_name = _('Account risk') def __str__(self): - return f"{self.account} - {self.risk}" + return f"{self.username}@{self.asset} - {self.risk}" + + @classmethod + def gen_fake_data(cls, count=1000, batch_size=50): + from assets.models import Asset + from accounts.models import Account + + assets = Asset.objects.all() + accounts = Account.objects.all() + + counter = iter(range(count)) + while True: + batch = list(islice(counter, batch_size)) + if not batch: + break + + to_create = [] + for i in batch: + asset = assets[i % len(assets)] + account = accounts[i % len(accounts)] + risk = RiskChoice.choices[i % len(RiskChoice.choices)][0] + to_create.append(cls(asset=asset, username=account.username, risk=risk)) + + cls.objects.bulk_create(to_create) + + diff --git a/apps/accounts/serializers/automations/check_accounts.py b/apps/accounts/serializers/automations/check_accounts.py index a0a28f463..735643a49 100644 --- a/apps/accounts/serializers/automations/check_accounts.py +++ b/apps/accounts/serializers/automations/check_accounts.py @@ -3,7 +3,7 @@ from rest_framework import serializers from accounts.const import AutomationTypes -from accounts.models import AccountCheckAutomation, AccountRisk +from accounts.models import AccountCheckAutomation, AccountRisk, RiskChoice from common.utils import get_logger from .base import BaseAutomationSerializer @@ -12,7 +12,8 @@ logger = get_logger(__file__) __all__ = [ 'CheckAccountsAutomationSerializer', 'AccountRiskSerializer', - 'AccountCheckEngineSerializer' + 'AccountCheckEngineSerializer', + 'AssetRiskSerializer', ] @@ -22,6 +23,27 @@ class AccountRiskSerializer(serializers.ModelSerializer): fields = '__all__' +class RiskSummarySerializer(serializers.Serializer): + risk = serializers.CharField(max_length=128) + count = serializers.IntegerField() + + +class AssetRiskSerializer(serializers.Serializer): + id = serializers.CharField(max_length=128, required=False, source='asset__id') + name = serializers.CharField(max_length=128, required=False, source='asset__name') + address = serializers.CharField(max_length=128, required=False, source='asset__address') + platform = serializers.CharField(max_length=128, required=False, source='asset__platform__name') + risk_total = serializers.IntegerField() + risk_summary = serializers.SerializerMethodField() + + @staticmethod + def get_risk_summary(obj): + summary = {} + for risk in RiskChoice.choices: + summary[f'{risk[0]}_count'] = obj.get(f'{risk[0]}_count', 0) + return summary + + class CheckAccountsAutomationSerializer(BaseAutomationSerializer): class Meta: model = AccountCheckAutomation diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index e65556f73..6bfe9056b 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -25,6 +25,7 @@ router.register(r'push-account-automations', api.PushAccountAutomationViewSet, ' router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'push-account-execution') router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record') router.register(r'account-check-engines', api.AccountCheckEngineViewSet, 'account-check-engine') +router.register(r'account-risks', api.AccountRiskViewSet, 'account-risks') urlpatterns = [ path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'),