diff --git a/apps/accounts/api/automations/check_account.py b/apps/accounts/api/automations/check_account.py index 7c975b586..702899811 100644 --- a/apps/accounts/api/automations/check_account.py +++ b/apps/accounts/api/automations/check_account.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from django.db.models import Q, Count +from django.http import HttpResponse from rest_framework.decorators import action from accounts import serializers @@ -28,8 +29,9 @@ class CheckAccountExecutionViewSet(AutomationExecutionViewSet): ("list", "accounts.view_checkaccountexecution"), ("retrieve", "accounts.view_checkaccountsexecution"), ("create", "accounts.add_checkaccountexecution"), + ("report", "accounts.view_checkaccountsexecution"), ) - + ordering = ('-date_created',) tp = AutomationTypes.check_account def get_queryset(self): @@ -37,6 +39,12 @@ class CheckAccountExecutionViewSet(AutomationExecutionViewSet): queryset = queryset.filter(automation__type=self.tp) return queryset + @action(methods=['get'], detail=True, url_path='report') + def report(self, request, *args, **kwargs): + execution = self.get_object() + report = execution.manager.gen_report() + return HttpResponse(report) + class AccountRiskViewSet(OrgBulkModelViewSet): model = AccountRisk diff --git a/apps/accounts/automations/base/manager.py b/apps/accounts/automations/base/manager.py index 401304597..652a8aa89 100644 --- a/apps/accounts/automations/base/manager.py +++ b/apps/accounts/automations/base/manager.py @@ -1,3 +1,5 @@ +from django.template.loader import render_to_string + from accounts.automations.methods import platform_automation_methods from assets.automations.base.manager import BasePlaybookManager from common.utils import get_logger @@ -6,7 +8,16 @@ logger = get_logger(__name__) class AccountBasePlaybookManager(BasePlaybookManager): + template_path = '' @property def platform_automation_methods(self): return platform_automation_methods + + def gen_report(self): + context = { + 'execution': self.execution, + 'summary': self.execution.summary, + 'result': self.execution.result + } + return render_to_string(self.template_path, context) diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py index d4ca1c2f3..294826802 100644 --- a/apps/accounts/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -15,7 +15,6 @@ from assets.const import HostTypes from common.utils import get_logger from common.utils.file import encrypt_and_compress_zip_file from common.utils.timezone import local_now_filename -from users.models import User from ..base.manager import AccountBasePlaybookManager from ...utils import SecretGenerator @@ -247,7 +246,6 @@ class ChangeSecretManager(AccountBasePlaybookManager): ] recipients = self.execution.recipients - recipients = User.objects.filter(id__in=list(recipients.keys())) if not recipients: return diff --git a/apps/accounts/automations/check_account/manager.py b/apps/accounts/automations/check_account/manager.py index fee2db3e5..1a5cc44b0 100644 --- a/apps/accounts/automations/check_account/manager.py +++ b/apps/accounts/automations/check_account/manager.py @@ -2,9 +2,14 @@ import re import time from collections import defaultdict +from django.template.loader import render_to_string from django.utils import timezone +from premailer import transform from accounts.models import Account, AccountRisk +from common.db.utils import safe_db_connection +from common.tasks import send_mail_async +from common.utils.strings import color_fmt def is_weak_password(password): @@ -30,7 +35,6 @@ def is_weak_password(password): or not re.search(r'[0-9]', password) or not re.search(r'[\W_]', password)): return True - return False @@ -38,27 +42,34 @@ def check_account_secrets(accounts, assets): now = timezone.now().isoformat() risks = [] tmpl = "Check account %s: %s" - RED = "\033[31m" - GREEN = "\033[32m" - RESET = "\033[0m" # 还原默认颜色 - summary = defaultdict(int) + result = defaultdict(list) + summary['accounts'] = len(accounts) + summary['assets'] = len(assets) + for account in accounts: + result_item = { + 'asset': str(account.asset), + 'username': account.username, + } if not account.secret: print(tmpl % (account, "no secret")) summary['no_secret'] += 1 + result['no_secret'].append(result_item) continue if is_weak_password(account.secret): - print(tmpl % (account, f"{RED}weak{RESET}")) - summary['weak'] += 1 + print(tmpl % (account, color_fmt("weak", "red"))) + summary['weak_password'] += 1 + result['weak_password'].append(result_item) risks.append({ 'account': account, 'risk': 'weak_password', }) else: summary['ok'] += 1 - print(tmpl % (account, f"{GREEN}ok{RESET}")) + result['ok'].append(result_item) + print(tmpl % (account, color_fmt("ok", "green"))) origin_risks = AccountRisk.objects.filter(asset__in=assets) origin_risks_dict = {f'{r.asset_id}_{r.username}_{r.risk}': r for r in origin_risks} @@ -77,7 +88,7 @@ def check_account_secrets(accounts, assets): risk=d['risk'], details=[{'datetime': now}], ) - return summary + return summary, result class CheckAccountManager: @@ -90,9 +101,12 @@ class CheckAccountManager: self.timedelta = 0 self.assets = [] self.summary = {} + self.result = defaultdict(list) def pre_run(self): self.assets = self.execution.get_all_assets() + self.execution.date_start = timezone.now() + self.execution.save(update_fields=['date_start']) def batch_run(self, batch_size=100): for engine in self.execution.snapshot.get('engines', []): @@ -104,18 +118,57 @@ class CheckAccountManager: for i in range(0, len(self.assets), batch_size): _assets = self.assets[i:i + batch_size] accounts = Account.objects.filter(asset__in=_assets) - summary = handle(accounts, _assets) - self.summary.update(summary) + summary, result = handle(accounts, _assets) - def after_run(self): + for k, v in summary.items(): + self.summary[k] = self.summary.get(k, 0) + v + for k, v in result.items(): + self.result[k].extend(v) + + def _update_execution_and_summery(self): self.date_end = timezone.now() self.time_end = time.time() - self.timedelta = self.time_end - self.time_start - tmpl = "\n-\nSummary: ok: %s, weak: %s, no_secret: %s, using time: %ss" % ( - self.summary['ok'], self.summary['weak'], self.summary['no_secret'], self.timedelta + self.duration = self.time_end - self.time_start + self.execution.date_finished = timezone.now() + self.execution.status = 'success' + self.execution.summary = self.summary + self.execution.result = self.result + + with safe_db_connection(): + self.execution.save(update_fields=['date_finished', 'status', 'summary', 'result']) + + def after_run(self): + self._update_execution_and_summery() + self._send_report() + + tmpl = "\n---\nSummary: \nok: %s, weak password: %s, no secret: %s, using time: %ss" % ( + self.summary['ok'], self.summary['weak_password'], self.summary['no_secret'], int(self.timedelta) ) print(tmpl) + def gen_report(self): + template_path = 'accounts/check_account_report.html' + context = { + 'execution': self.execution, + 'summary': self.execution.summary, + 'result': self.execution.result + } + data = render_to_string(template_path, context) + return data + + def _send_report(self): + recipients = self.execution.recipients + if not recipients: + return + + report = self.gen_report() + report = transform(report) + print("Send resport to: {}".format([str(r) for r in recipients])) + subject = f'Check account automation {self.execution.id} finished' + emails = [r.email for r in recipients if r.email] + + send_mail_async(subject, report, emails, html_message=report) + def run(self,): self.pre_run() self.batch_run() diff --git a/apps/accounts/automations/endpoint.py b/apps/accounts/automations/endpoint.py index 348db03d3..db600e791 100644 --- a/apps/accounts/automations/endpoint.py +++ b/apps/accounts/automations/endpoint.py @@ -18,7 +18,6 @@ class ExecutionManager: AutomationTypes.gather_accounts: GatherAccountsManager, AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager, AutomationTypes.check_account: CheckAccountManager, - # TODO 后期迁移到自动化策略中 'backup_account': AccountBackupManager, } @@ -28,3 +27,6 @@ class ExecutionManager: def run(self, *args, **kwargs): return self._runner.run(*args, **kwargs) + + def __getattr__(self, item): + return getattr(self._runner, item) diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index 956a3f5c8..1b18d5dd8 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -9,7 +9,6 @@ from common.const import ConfirmOrIgnore from common.utils import get_logger from common.utils.strings import get_text_diff from orgs.utils import tmp_to_org -from users.models import User from .filter import GatherAccountsFilter from ..base.manager import AccountBasePlaybookManager from ...notifications import GatherAccountChangeMsg @@ -313,10 +312,7 @@ class GatherAccountsManager(AccountBasePlaybookManager): if not self.asset_usernames_mapper or not recipients: return None, None - users = User.objects.filter(id__in=recipients) - if not users.exists(): - return users, None - + users = recipients asset_ids = self.asset_usernames_mapper.keys() assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts') gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, remote_present=True) diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py index 12f630923..18aca5134 100644 --- a/apps/accounts/automations/push_account/manager.py +++ b/apps/accounts/automations/push_account/manager.py @@ -7,68 +7,6 @@ logger = get_logger(__name__) class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager): - @classmethod def method_type(cls): return AutomationTypes.push_account - - # @classmethod - # def trigger_by_asset_create(cls, asset): - # automations = PushAccountAutomation.objects.filter( - # triggers__contains=TriggerChoice.on_asset_create - # ) - # account_automation_map = {auto.username: auto for auto in automations} - # - # util = AssetPermissionUtil() - # permissions = util.get_permissions_for_assets([asset], with_node=True) - # account_permission_map = defaultdict(list) - # for permission in permissions: - # for account in permission.accounts: - # account_permission_map[account].append(permission) - # - # username_automation_map = {} - # for username, automation in account_automation_map.items(): - # if username != '@USER': - # username_automation_map[username] = automation - # continue - # - # asset_permissions = account_permission_map.get(username) - # if not asset_permissions: - # continue - # asset_permissions = util.get_permissions([p.id for p in asset_permissions]) - # usernames = asset_permissions.values_list('users__username', flat=True).distinct() - # for _username in usernames: - # username_automation_map[_username] = automation - # - # asset_usernames_exists = asset.accounts.values_list('username', flat=True) - # accounts_to_create = [] - # accounts_to_push = [] - # for username, automation in username_automation_map.items(): - # if username in asset_usernames_exists: - # continue - # - # if automation.secret_strategy != SecretStrategy.custom: - # secret_generator = SecretGenerator( - # automation.secret_strategy, automation.secret_type, - # automation.password_rules - # ) - # secret = secret_generator.get_secret() - # else: - # secret = automation.secret - # - # account = Account( - # username=username, secret=secret, - # asset=asset, secret_type=automation.secret_type, - # comment='Create by account creation {}'.format(automation.name), - # ) - # accounts_to_create.append(account) - # if automation.action == 'create_and_push': - # accounts_to_push.append(account) - # else: - # accounts_to_create.append(account) - # - # logger.debug(f'Create account {account} for asset {asset}') - - # @classmethod - # def trigger_by_permission_accounts_change(cls): - # pass diff --git a/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py b/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py index c4c18f232..fc737791f 100644 --- a/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py +++ b/apps/accounts/migrations/0008_remove_accountrisk_confirmed_accountrisk_status_and_more.py @@ -48,13 +48,13 @@ class Migration(migrations.Migration): ), ), migrations.AddField( - model_name='changesecretautomation', - name='check_conn_after_change', - field=models.BooleanField(default=True, verbose_name='Check connection after change'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='check_conn_after_change', - field=models.BooleanField(default=True, verbose_name='Check connection after change'), - ), + model_name='changesecretautomation', + name='check_conn_after_change', + field=models.BooleanField(default=True, verbose_name='Check connection after change'), + ), + migrations.AddField( + model_name='pushaccountautomation', + name='check_conn_after_change', + field=models.BooleanField(default=True, verbose_name='Check connection after change'), + ), ] diff --git a/apps/accounts/migrations/0012_checkaccountautomation_checkaccountengine_and_more.py b/apps/accounts/migrations/0012_accountcheckengine_accountcheckautomation_engines.py similarity index 99% rename from apps/accounts/migrations/0012_checkaccountautomation_checkaccountengine_and_more.py rename to apps/accounts/migrations/0012_accountcheckengine_accountcheckautomation_engines.py index 3ec3e4e50..375469b9d 100644 --- a/apps/accounts/migrations/0012_checkaccountautomation_checkaccountengine_and_more.py +++ b/apps/accounts/migrations/0012_accountcheckengine_accountcheckautomation_engines.py @@ -5,7 +5,6 @@ import django.db.models.deletion import uuid - def init_account_check_engine(apps, schema_editor): data = [ { @@ -26,7 +25,6 @@ def init_account_check_engine(apps, schema_editor): model_cls.objects.create(**item) - class Migration(migrations.Migration): dependencies = [ diff --git a/apps/accounts/migrations/0013_checkaccountautomation_recipients.py b/apps/accounts/migrations/0013_checkaccountautomation_recipients.py new file mode 100644 index 000000000..4197379b2 --- /dev/null +++ b/apps/accounts/migrations/0013_checkaccountautomation_recipients.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.13 on 2024-11-15 03:00 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("accounts", "0012_accountcheckengine_accountcheckautomation_engines"), + ] + + operations = [ + migrations.AddField( + model_name="checkaccountautomation", + name="recipients", + field=models.ManyToManyField( + blank=True, to=settings.AUTH_USER_MODEL, verbose_name="Recipient" + ), + ), + ] diff --git a/apps/accounts/models/automations/base.py b/apps/accounts/models/automations/base.py index 837830759..8a2464c7e 100644 --- a/apps/accounts/models/automations/base.py +++ b/apps/accounts/models/automations/base.py @@ -42,10 +42,11 @@ class AutomationExecution(AssetAutomationExecution): ('add_pushaccountexecution', _('Can add push account execution')), ] - def start(self): + @property + def manager(self): from accounts.automations.endpoint import ExecutionManager manager = ExecutionManager(execution=self) - return manager.run() + return manager class ChangeSecretMixin(SecretWithRandomMixin): diff --git a/apps/accounts/models/automations/change_secret.py b/apps/accounts/models/automations/change_secret.py index 6d1c22715..dff4d5cc4 100644 --- a/apps/accounts/models/automations/change_secret.py +++ b/apps/accounts/models/automations/change_secret.py @@ -24,10 +24,7 @@ class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation): def to_attr_json(self): attr_json = super().to_attr_json() attr_json.update({ - 'recipients': { - str(recipient.id): (str(recipient), bool(recipient.secret_key)) - for recipient in self.recipients.all() - } + 'recipients': [str(r.id) for r in self.recipients.all()] }) return attr_json diff --git a/apps/accounts/models/automations/check_account.py b/apps/accounts/models/automations/check_account.py index 71b7c543d..2f3657764 100644 --- a/apps/accounts/models/automations/check_account.py +++ b/apps/accounts/models/automations/check_account.py @@ -15,11 +15,16 @@ __all__ = ['CheckAccountAutomation', 'AccountRisk', 'RiskChoice', 'CheckAccountE class CheckAccountAutomation(AccountBaseAutomation): engines = models.ManyToManyField('CheckAccountEngine', related_name='check_automations', verbose_name=_('Engines')) + recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True) + + def get_report_template(self): + return 'accounts/check_account_report.html' def to_attr_json(self): attr_json = super().to_attr_json() attr_json.update({ 'engines': [engine.slug for engine in self.engines.all()], + 'recipients': [str(user.id) for user in self.recipients.all()] }) return attr_json diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py index b575d4f42..dca398b92 100644 --- a/apps/accounts/models/automations/gather_account.py +++ b/apps/accounts/models/automations/gather_account.py @@ -9,7 +9,7 @@ from common.utils.timezone import is_date_more_than from orgs.mixins.models import JMSOrgBaseModel from .base import AccountBaseAutomation -__all__ = ['GatherAccountsAutomation', 'GatheredAccount', ] +__all__ = ['GatherAccountsAutomation', 'GatheredAccount',] class GatheredAccount(JMSOrgBaseModel): diff --git a/apps/accounts/serializers/automations/check_account.py b/apps/accounts/serializers/automations/check_account.py index 1f0e10aa6..32e50e88b 100644 --- a/apps/accounts/serializers/automations/check_account.py +++ b/apps/accounts/serializers/automations/check_account.py @@ -58,7 +58,7 @@ class CheckAccountAutomationSerializer(BaseAutomationSerializer): model = CheckAccountAutomation read_only_fields = BaseAutomationSerializer.Meta.read_only_fields fields = BaseAutomationSerializer.Meta.fields \ - + ['engines'] + read_only_fields + + ['engines', 'recipients'] + read_only_fields extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs @property diff --git a/apps/accounts/templates/accounts/check_account_report.html b/apps/accounts/templates/accounts/check_account_report.html new file mode 100644 index 000000000..820cecd97 --- /dev/null +++ b/apps/accounts/templates/accounts/check_account_report.html @@ -0,0 +1,108 @@ +{% load i18n %} + +
{% trans 'The following is a summary of the account check tasks. Please review and handle them' %}
+任务汇总: | +|
---|---|
{% trans 'Task name' %}: | +{{ execution.automation.name }} | +
{% trans 'Date start' %}: | +{{ execution.date_start }} | +
{% trans 'Date end' %}: | +{{ execution.date_finished }} | +
{% trans 'Time using' %}: | +{{ execution.duration }}s | +
{% trans 'Assets count' %}: | +{{ summary.assets }} | +
{% trans 'Account count' %}: | +{{ summary.accounts }} | +
{% trans 'Week password count' %}: | +{{ summary.weak_password }} | +
{% trans 'Ok count' %}: | +{{ summary.ok }} | +
{% trans 'No password count' %}: | +{{ summary.no_secret }} | +
{% trans 'Account check details' %}:
+{% trans 'No.' %} | +{% trans 'Asset' %} | +{% trans 'Username' %} | +{% trans 'Result' %} | +
---|---|---|---|
{{ forloop.counter }} | +{{ account.asset }} | +{{ account.username }} | +{% trans 'Week password' %} | +