perf: update pam

pull/14556/head
ibuler 2024-11-27 19:33:58 +08:00
parent 7afd25ca9c
commit 0fd5a2c4d9
24 changed files with 2550 additions and 1375 deletions

View File

@ -2,6 +2,10 @@
# #
from django.db.models import Q, Count from django.db.models import Q, Count
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed
from operator import itemgetter
from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
@ -15,6 +19,8 @@ __all__ = [
'AccountRiskViewSet', 'CheckAccountEngineViewSet', 'AccountRiskViewSet', 'CheckAccountEngineViewSet',
] ]
from ...risk_handlers import RiskHandler
class CheckAccountAutomationViewSet(OrgBulkModelViewSet): class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
model = CheckAccountAutomation model = CheckAccountAutomation
@ -46,6 +52,7 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
serializer_classes = { serializer_classes = {
'default': serializers.AccountRiskSerializer, 'default': serializers.AccountRiskSerializer,
'assets': serializers.AssetRiskSerializer, 'assets': serializers.AssetRiskSerializer,
'handle': serializers.HandleRiskSerializer
} }
ordering_fields = ( ordering_fields = (
'asset', 'risk', 'status', 'username', 'date_created' 'asset', 'risk', 'status', 'username', 'date_created'
@ -53,9 +60,15 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
ordering = ('-asset', 'date_created') ordering = ('-asset', 'date_created')
rbac_perms = { rbac_perms = {
'sync_accounts': 'assets.add_accountrisk', 'sync_accounts': 'assets.add_accountrisk',
'assets': 'accounts.view_accountrisk' 'assets': 'accounts.view_accountrisk',
'handle': 'accounts.change_accountrisk'
} }
http_method_names = ['get', 'head', 'options']
def update(self, request, *args, **kwargs):
raise MethodNotAllowed('PUT')
def create(self, request, *args, **kwargs):
raise MethodNotAllowed('POST')
@action(methods=['get'], detail=False, url_path='assets') @action(methods=['get'], detail=False, url_path='assets')
def assets(self, request, *args, **kwargs): def assets(self, request, *args, **kwargs):
@ -72,6 +85,32 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
) )
return self.get_paginated_response_from_queryset(queryset) return self.get_paginated_response_from_queryset(queryset)
@action(methods=['post'], detail=False, url_path='handle')
def handle(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
asset, username, act, risk = itemgetter('asset', 'username', 'action', 'risk')(serializer.validated_data)
handler = RiskHandler(asset=asset, username=username)
data = handler.handle(act, risk)
if not data:
data = {'message': 'Success'}
return Response(data)
# 处理风险
def handle_add_account(self):
pass
def handle_disable_remote(self):
pass
def handle_delete_remote(self):
pass
def handle_delete_both(self):
pass
class CheckAccountEngineViewSet(JMSModelViewSet): class CheckAccountEngineViewSet(JMSModelViewSet):
search_fields = ('name',) search_fields = ('name',)

View File

@ -13,18 +13,22 @@ from accounts.filters import GatheredAccountFilterSet
from accounts.models import GatherAccountsAutomation, AutomationExecution from accounts.models import GatherAccountsAutomation, AutomationExecution
from accounts.models import GatheredAccount from accounts.models import GatheredAccount
from assets.models import Asset from assets.models import Asset
from accounts.tasks.common import quickstart_automation_by_snapshot
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from .base import AutomationExecutionViewSet from .base import AutomationExecutionViewSet
__all__ = [ __all__ = [
'GatherAccountsAutomationViewSet', 'GatherAccountsExecutionViewSet', "GatherAccountsAutomationViewSet",
'GatheredAccountViewSet' "GatherAccountsExecutionViewSet",
"GatheredAccountViewSet",
] ]
from ...risk_handlers import RiskHandler
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet): class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
model = GatherAccountsAutomation model = GatherAccountsAutomation
filterset_fields = ('name',) filterset_fields = ("name",)
search_fields = filterset_fields search_fields = filterset_fields
serializer_class = serializers.GatherAccountAutomationSerializer serializer_class = serializers.GatherAccountAutomationSerializer
@ -47,52 +51,56 @@ class GatherAccountsExecutionViewSet(AutomationExecutionViewSet):
class GatheredAccountViewSet(OrgBulkModelViewSet): class GatheredAccountViewSet(OrgBulkModelViewSet):
model = GatheredAccount model = GatheredAccount
search_fields = ('username',) search_fields = ("username",)
filterset_class = GatheredAccountFilterSet filterset_class = GatheredAccountFilterSet
ordering = ("status",)
serializer_classes = { serializer_classes = {
'default': serializers.GatheredAccountSerializer, "default": serializers.GatheredAccountSerializer,
'status': serializers.GatheredAccountActionSerializer, "status": serializers.GatheredAccountActionSerializer,
} }
rbac_perms = { rbac_perms = {
'sync_accounts': 'assets.add_gatheredaccount', "sync_accounts": "assets.add_gatheredaccount",
'discover': 'assets.add_gatheredaccount', "discover": "assets.add_gatheredaccount",
'status': 'assets.change_gatheredaccount', "status": "assets.change_gatheredaccount",
} }
@action(methods=['put'], detail=True, url_path='status') @action(methods=["put"], detail=True, url_path="status")
def status(self, request, *args, **kwargs): def status(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
instance.status = request.data.get('status') instance.status = request.data.get("status")
instance.save(update_fields=['status']) instance.save(update_fields=["status"])
if instance.status == 'confirmed': if instance.status == "confirmed":
GatheredAccount.sync_accounts([instance]) GatheredAccount.sync_accounts([instance])
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
@action(methods=['get'], detail=False, url_path='discover') @action(methods=["post"], detail=False, url_path="delete-remote")
def discover(self, request, *args, **kwargs): def delete_remote(self, request, *args, **kwargs):
asset_id = request.query_params.get('asset_id') asset_id = request.data.get("asset_id")
if not asset_id: username = request.data.get("username")
return Response(status=status.HTTP_400_BAD_REQUEST, data={'asset_id': 'This field is required.'})
asset = get_object_or_404(Asset, pk=asset_id) asset = get_object_or_404(Asset, pk=asset_id)
handler = RiskHandler(asset, username)
handler.handle_delete_remote()
return Response(status=status.HTTP_200_OK)
@action(methods=["get"], detail=False, url_path="discover")
def discover(self, request, *args, **kwargs):
asset_id = request.query_params.get("asset_id")
if not asset_id:
return Response(status=400, data={"asset_id": "This field is required."})
get_object_or_404(Asset, pk=asset_id)
execution = AutomationExecution() execution = AutomationExecution()
execution.snapshot = { execution.snapshot = {
'assets': [asset_id], "assets": [asset_id],
'nodes': [], "nodes": [],
'type': 'gather_accounts', "type": "gather_accounts",
'is_sync_account': False, "is_sync_account": False,
'check_risk': True, "check_risk": True,
'name': 'Adhoc gather accounts: {}'.format(asset_id), "name": "Adhoc gather accounts: {}".format(asset_id),
} }
execution.save() execution.save()
execution.start() execution.start()
report = execution.manager.gen_report() report = execution.manager.gen_report()
return HttpResponse(report) return HttpResponse(report)
@action(methods=['post'], detail=False, url_path='sync-accounts')
def sync_accounts(self, request, *args, **kwargs):
gathered_account_ids = request.data.get('gathered_account_ids')
gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids).filter(status='')
self.model.sync_accounts(gathered_accounts)
return Response(status=status.HTTP_201_CREATED)

View File

@ -56,7 +56,7 @@ def get_items_diff(ori_account, d):
class AnalyseAccountRisk: class AnalyseAccountRisk:
long_time = timezone.timedelta(days=90) long_time = timezone.timedelta(days=90)
datetime_check_items = [ datetime_check_items = [
{"field": "date_last_login", "risk": "zombie", "delta": long_time}, {"field": "date_last_login", "risk": "long_time_no_login", "delta": long_time},
{ {
"field": "date_password_change", "field": "date_password_change",
"risk": "long_time_password", "risk": "long_time_password",
@ -154,7 +154,7 @@ class AnalyseAccountRisk:
else: else:
self._create_risk( self._create_risk(
dict( dict(
**basic, risk="ghost", details=[{"datetime": self.now.isoformat()}] **basic, risk="new_found", details=[{"datetime": self.now.isoformat()}]
) )
) )
@ -227,8 +227,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
for asset_id, username in accounts: for asset_id, username in accounts:
self.ori_asset_usernames[str(asset_id)].add(username) self.ori_asset_usernames[str(asset_id)].add(username)
ga_accounts = GatheredAccount.objects.filter(asset__in=assets).prefetch_related( ga_accounts = (
"asset" GatheredAccount.objects.filter(asset__in=assets)
.prefetch_related("asset")
) )
for account in ga_accounts: for account in ga_accounts:
self.ori_gathered_usernames[str(account.asset_id)].add(account.username) self.ori_gathered_usernames[str(account.asset_id)].add(account.username)
@ -315,8 +316,10 @@ class GatherAccountsManager(AccountBasePlaybookManager):
.filter(present=True) .filter(present=True)
.update(present=False) .update(present=False)
) )
queryset.filter(username__in=ori_users).filter(present=False).update( (
present=True queryset.filter(username__in=ori_users)
.filter(present=False)
.update(present=True)
) )
@bulk_create_decorator(GatheredAccount) @bulk_create_decorator(GatheredAccount)

View File

@ -1,4 +1,5 @@
import os import os
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from django.db.models import QuerySet from django.db.models import QuerySet
@ -12,10 +13,16 @@ logger = get_logger(__name__)
class RemoveAccountManager(AccountBasePlaybookManager): class RemoveAccountManager(AccountBasePlaybookManager):
super_accounts = ['root', 'administrator']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.host_account_mapper = {} self.host_account_mapper = dict()
self.host_accounts = defaultdict(list)
snapshot_account = self.execution.snapshot.get('accounts', [])
self.snapshot_asset_account_map = defaultdict(list)
for account in snapshot_account:
self.snapshot_asset_account_map[str(account['asset'])].append(account)
def prepare_runtime_dir(self): def prepare_runtime_dir(self):
path = super().prepare_runtime_dir() path = super().prepare_runtime_dir()
@ -30,28 +37,22 @@ class RemoveAccountManager(AccountBasePlaybookManager):
def method_type(cls): def method_type(cls):
return AutomationTypes.remove_account return AutomationTypes.remove_account
def get_gather_accounts(self, privilege_account, gather_accounts: QuerySet):
gather_account_ids = self.execution.snapshot['gather_accounts']
gather_accounts = gather_accounts.filter(id__in=gather_account_ids)
gather_accounts = gather_accounts.exclude(
username__in=[privilege_account.username, 'root', 'Administrator']
)
return gather_accounts
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
if host.get('error'): if host.get('error'):
return host return host
gather_accounts = asset.gatheredaccount_set.all()
gather_accounts = self.get_gather_accounts(account, gather_accounts)
inventory_hosts = [] inventory_hosts = []
accounts_to_remove = self.snapshot_asset_account_map.get(str(asset.id), [])
for gather_account in gather_accounts: for account in accounts_to_remove:
username = account.get('username')
if not username or username.lower() in self.super_accounts:
print("Super account can not be remove: ", username)
continue
h = deepcopy(host) h = deepcopy(host)
h['name'] += '(' + gather_account.username + ')' h['name'] += '(' + username + ')'
self.host_account_mapper[h['name']] = (asset, gather_account) self.host_account_mapper[h['name']] = account
h['account'] = {'username': gather_account.username} h['account'] = {'username': username}
inventory_hosts.append(h) inventory_hosts.append(h)
return inventory_hosts return inventory_hosts

View File

@ -101,7 +101,7 @@ class Migration(migrations.Migration):
name="status", name="status",
field=models.CharField( field=models.CharField(
blank=True, blank=True,
choices=[("confirmed", "Confirmed"), ("ignored", "Ignored")], choices=[("confirmed", "Confirmed"), ("ignored", "Ignored"), ("pending", "Pending")],
default="", default="",
max_length=32, max_length=32,
verbose_name="Status", verbose_name="Status",

View File

@ -19,7 +19,7 @@ class Migration(migrations.Migration):
name="status", name="status",
field=models.CharField( field=models.CharField(
blank=True, blank=True,
choices=[("confirmed", "Confirmed"), ("ignored", "Ignored")], choices=[("confirmed", "Confirmed"), ("ignored", "Ignored"), ("pending", "Pending")],
default="", default="",
max_length=32, max_length=32,
verbose_name="Status", verbose_name="Status",

View File

@ -0,0 +1,35 @@
# Generated by Django 4.1.13 on 2024-11-26 08:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0014_gatheraccountsautomation_check_risk"),
]
operations = [
migrations.AlterField(
model_name="accountrisk",
name="risk",
field=models.CharField(
choices=[
("long_time_no_login", "Long time no login"),
("new_found", "New found"),
("groups_changed", "Groups change"),
("sudoers_changed", "Sudo changed"),
("authorized_keys_changed", "Authorized keys changed"),
("account_deleted", "Account delete"),
("password_expired", "Password expired"),
("long_time_password", "Long time no change"),
("weak_password", "Weak password"),
("password_error", "Password error"),
("no_admin_account", "No admin account"),
("others", "Others"),
],
max_length=128,
verbose_name="Risk",
),
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 4.1.13 on 2024-11-27 11:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0015_alter_accountrisk_risk"),
]
operations = [
migrations.AlterField(
model_name="accountrisk",
name="status",
field=models.CharField(
blank=True,
choices=[
("", "Pending"),
("confirmed", "Confirmed"),
("ignored", "Ignored"),
],
default="",
max_length=32,
verbose_name="Status",
),
),
migrations.AlterField(
model_name="gatheredaccount",
name="status",
field=models.CharField(
blank=True,
choices=[
("", "Pending"),
("confirmed", "Confirmed"),
("ignored", "Ignored"),
],
default="",
max_length=32,
verbose_name="Status",
),
),
]

View File

@ -39,8 +39,8 @@ class CheckAccountAutomation(AccountBaseAutomation):
class RiskChoice(TextChoices): class RiskChoice(TextChoices):
# 依赖自动发现的 # 依赖自动发现的
zombie = 'zombie', _('Long time no login') # 好久没登录的账号, 禁用、删除 long_time_no_login = 'long_time_no_login', _('Long time no login') # 好久没登录的账号, 禁用、删除
ghost = 'ghost', _('Not managed') # 未被纳管的账号, 纳管, 删除, 禁用 new_found = 'new_found', _('New found') # 未被纳管的账号, 纳管, 删除, 禁用
group_changed = 'groups_changed', _('Groups change') # 组变更, 确认 group_changed = 'groups_changed', _('Groups change') # 组变更, 确认
sudo_changed = 'sudoers_changed', _('Sudo changed') # sudo 变更, 确认 sudo_changed = 'sudoers_changed', _('Sudo changed') # sudo 变更, 确认
authorized_keys_changed = 'authorized_keys_changed', _('Authorized keys changed') # authorized_keys 变更, 确认 authorized_keys_changed = 'authorized_keys_changed', _('Authorized keys changed') # authorized_keys 变更, 确认
@ -58,7 +58,7 @@ class AccountRisk(JMSOrgBaseModel):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, related_name='risks', verbose_name=_('Asset')) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, related_name='risks', verbose_name=_('Asset'))
username = models.CharField(max_length=32, verbose_name=_('Username')) username = models.CharField(max_length=32, verbose_name=_('Username'))
risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices) risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices)
status = models.CharField(max_length=32, choices=ConfirmOrIgnore.choices, default='', blank=True, verbose_name=_('Status')) status = models.CharField(max_length=32, choices=ConfirmOrIgnore.choices, default=ConfirmOrIgnore.pending, blank=True, verbose_name=_('Status'))
details = models.JSONField(default=list, verbose_name=_('Details')) details = models.JSONField(default=list, verbose_name=_('Details'))
class Meta: class Meta:

View File

@ -24,7 +24,7 @@ class GatheredAccount(JMSOrgBaseModel):
present = models.BooleanField(default=False, verbose_name=_("Present")) # 系统资产上是否还存在 present = models.BooleanField(default=False, verbose_name=_("Present")) # 系统资产上是否还存在
date_password_change = models.DateTimeField(null=True, verbose_name=_("Date change password")) date_password_change = models.DateTimeField(null=True, verbose_name=_("Date change password"))
date_password_expired = models.DateTimeField(null=True, verbose_name=_("Date password expired")) date_password_expired = models.DateTimeField(null=True, verbose_name=_("Date password expired"))
status = models.CharField(max_length=32, default='', blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Status")) status = models.CharField(max_length=32, default=ConfirmOrIgnore.pending, blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Status"))
@property @property
def address(self): def address(self):

View File

@ -0,0 +1,71 @@
from django.utils.translation import gettext_lazy as _
from accounts.models import GatheredAccount, AccountRisk, SecretType, AutomationExecution
TYPE_CHOICES = [
("ignore", _("Ignore")),
("disable_remote", _("Disable remote")),
("delete_remote", _("Delete remote")),
("delete_both", _("Delete remote")),
("add_account", _("Add account")),
("change_password_add", _("Change password and Add")),
("change_password", _("Change password")),
]
class RiskHandler:
def __init__(self, asset, username):
self.asset = asset
self.username = username
def handle(self, tp, risk=""):
attr = f"handle_{tp}"
if hasattr(self, attr):
return getattr(self, attr)(risk=risk)
else:
raise ValueError(f"Invalid risk type: {tp}")
def handle_ignore(self, risk=""):
pass
def handle_add_account(self, risk=""):
data = {
"username": self.username,
"name": self.username,
"secret_type": SecretType.PASSWORD,
"source": "collected",
}
self.asset.accounts.get_or_create(defaults=data, username=self.username)
GatheredAccount.objects.filter(asset=self.asset, username=self.username).update(
present=True, status="confirmed"
)
(
AccountRisk.objects.filter(asset=self.asset, username=self.username)
.filter(risk__in=["new_found"])
.update(status="confirmed")
)
def handle_disable_remote(self, risk=""):
pass
def handle_delete_remote(self, risk=""):
asset = self.asset
execution = AutomationExecution()
execution.snapshot = {
"assets": [str(asset.id)],
"accounts": [{"asset": str(asset.id), "username": self.username}],
"type": "remove_account",
"name": "Remove remote account: {}@{}".format(self.username, asset.name),
}
execution.save()
execution.start()
return execution
def handle_delete_both(self, risk=""):
pass
def handle_change_password_add(self, risk=""):
pass
def handle_change_password(self, risk=""):
pass

View File

@ -4,36 +4,47 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.models import CheckAccountAutomation, AccountRisk, RiskChoice, CheckAccountEngine from accounts.models import (
CheckAccountAutomation,
AccountRisk,
RiskChoice,
CheckAccountEngine,
)
from assets.models import Asset from assets.models import Asset
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.utils import get_logger from common.utils import get_logger
from .base import BaseAutomationSerializer from .base import BaseAutomationSerializer
from accounts.risk_handlers import TYPE_CHOICES
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'CheckAccountAutomationSerializer', "CheckAccountAutomationSerializer",
'AccountRiskSerializer', "AccountRiskSerializer",
'CheckAccountEngineSerializer', "CheckAccountEngineSerializer",
'AssetRiskSerializer', "AssetRiskSerializer",
"HandleRiskSerializer",
] ]
class AccountRiskSerializer(serializers.ModelSerializer): class AccountRiskSerializer(serializers.ModelSerializer):
asset = ObjectRelatedField(queryset=Asset.objects.all(), required=False,label=_("Asset")) asset = ObjectRelatedField(
risk = LabeledChoiceField(choices=RiskChoice.choices, required=False, read_only=True, label=_("Risk")) queryset=Asset.objects.all(), required=False, label=_("Asset")
)
risk = LabeledChoiceField(
choices=RiskChoice.choices, required=False, read_only=True, label=_("Risk")
)
class Meta: class Meta:
model = AccountRisk model = AccountRisk
fields = [ fields = [
'id', 'asset', 'username', 'risk', 'status', "id", "asset", "username", "risk", "status",
'date_created', 'details', "date_created", "details",
] ]
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
return queryset.select_related('asset') return queryset.select_related("asset")
class RiskSummarySerializer(serializers.Serializer): class RiskSummarySerializer(serializers.Serializer):
@ -42,10 +53,14 @@ class RiskSummarySerializer(serializers.Serializer):
class AssetRiskSerializer(serializers.Serializer): class AssetRiskSerializer(serializers.Serializer):
id = serializers.CharField(max_length=128, required=False, source='asset__id') id = serializers.CharField(max_length=128, required=False, source="asset__id")
name = serializers.CharField(max_length=128, required=False, source='asset__name') name = serializers.CharField(max_length=128, required=False, source="asset__name")
address = serializers.CharField(max_length=128, required=False, source='asset__address') address = serializers.CharField(
platform = serializers.CharField(max_length=128, required=False, source='asset__platform__name') 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_total = serializers.IntegerField()
risk_summary = serializers.SerializerMethodField() risk_summary = serializers.SerializerMethodField()
@ -53,16 +68,26 @@ class AssetRiskSerializer(serializers.Serializer):
def get_risk_summary(obj): def get_risk_summary(obj):
summary = {} summary = {}
for risk in RiskChoice.choices: for risk in RiskChoice.choices:
summary[f'{risk[0]}_count'] = obj.get(f'{risk[0]}_count', 0) summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)
return summary return summary
class HandleRiskSerializer(serializers.Serializer):
username = serializers.CharField(max_length=128)
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects)
action = serializers.ChoiceField(choices=TYPE_CHOICES)
risk = serializers.ChoiceField(choices=RiskChoice.choices, allow_null=True, allow_blank=True)
class CheckAccountAutomationSerializer(BaseAutomationSerializer): class CheckAccountAutomationSerializer(BaseAutomationSerializer):
class Meta: class Meta:
model = CheckAccountAutomation model = CheckAccountAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
fields = BaseAutomationSerializer.Meta.fields \ fields = (
+ ['engines', 'recipients'] + read_only_fields BaseAutomationSerializer.Meta.fields
+ ["engines", "recipients"]
+ read_only_fields
)
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
@property @property
@ -73,8 +98,8 @@ class CheckAccountAutomationSerializer(BaseAutomationSerializer):
class CheckAccountEngineSerializer(serializers.ModelSerializer): class CheckAccountEngineSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CheckAccountEngine model = CheckAccountEngine
fields = ['id', 'name', 'slug', 'is_active', 'comment'] fields = ["id", "name", "slug", "is_active", "comment"]
read_only_fields = ['slug'] read_only_fields = ["slug"]
extra_kwargs = { extra_kwargs = {
'is_active': {'required': False}, "is_active": {"required": False},
} }

View File

@ -41,7 +41,7 @@ def remove_accounts_task(gather_account_ids):
task_snapshot = { task_snapshot = {
'assets': [str(i.asset_id) for i in gather_accounts], 'assets': [str(i.asset_id) for i in gather_accounts],
'gather_accounts': [str(i.id) for i in gather_accounts], 'accounts': [{'asset': str(i.asset_id), 'username': i.username} for i in gather_accounts],
} }
tp = AutomationTypes.remove_account tp = AutomationTypes.remove_account

View File

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
from common.utils import ssh_key_gen, random_string from common.utils import ssh_key_gen, random_string
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
@ -26,12 +27,12 @@ class SecretGenerator:
rules = copy.deepcopy(DEFAULT_PASSWORD_RULES) rules = copy.deepcopy(DEFAULT_PASSWORD_RULES)
rules.update(password_rules) rules.update(password_rules)
rules = { rules = {
'length': rules['length'], "length": rules["length"],
'lower': rules['lowercase'], "lower": rules["lowercase"],
'upper': rules['uppercase'], "upper": rules["uppercase"],
'digit': rules['digit'], "digit": rules["digit"],
'special_char': rules['symbol'], "special_char": rules["symbol"],
'exclude_chars': rules.get('exclude_symbols', ''), "exclude_chars": rules.get("exclude_symbols", ""),
} }
return random_string(**rules) return random_string(**rules)
@ -46,10 +47,12 @@ class SecretGenerator:
def validate_password_for_ansible(password): def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """ """校验 Ansible 不支持的特殊字符"""
if password.startswith('{{') and password.endswith('}}'): if password.startswith("{{") and password.endswith("}}"):
raise serializers.ValidationError( raise serializers.ValidationError(
_('If the password starts with {{` and ends with }} `, then the password is not allowed.') _(
"If the password starts with {{` and ends with }} `, then the password is not allowed."
)
) )

View File

@ -45,3 +45,4 @@ def quickstart_automation(task_name, tp, task_snapshot=None):
trigger=Trigger.manual, **data trigger=Trigger.manual, **data
) )
execution.start() execution.start()
return execution

View File

@ -76,6 +76,7 @@ class Language(models.TextChoices):
class ConfirmOrIgnore(models.TextChoices): class ConfirmOrIgnore(models.TextChoices):
pending = '', _('Pending')
confirmed = 'confirmed', _('Confirmed') confirmed = 'confirmed', _('Confirmed')
ignored = 'ignored', _('Ignored') ignored = 'ignored', _('Ignored')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,10 @@
"AccountDeleteConfirmMsg": "Delete account, continue?", "AccountDeleteConfirmMsg": "Delete account, continue?",
"AccountExportTips": "The exported information contains sensitive information such as encrypted account numbers. the exported format is an encrypted zip file (if you have not set the encryption password, please go to personal info to set the file encryption password).", "AccountExportTips": "The exported information contains sensitive information such as encrypted account numbers. the exported format is an encrypted zip file (if you have not set the encryption password, please go to personal info to set the file encryption password).",
"AccountDiscoverDetail": "Gather account details", "AccountDiscoverDetail": "Gather account details",
"AccountDiscoverList": "Gather accounts", "AccountDiscoverList": "Discover accounts",
"AccountDiscoverTaskCreate": "Create gather accounts task", "AccountDiscoverTaskCreate": "Create discover accounts task",
"AccountDiscoverTaskList": "Gather accounts tasks", "AccountDiscoverTaskList": "Discover accounts tasks",
"AccountDiscoverTaskUpdate": "Update the gather accounts task", "AccountDiscoverTaskUpdate": "Update the discover accounts task",
"AccountList": "Accounts", "AccountList": "Accounts",
"AccountPolicy": "Account policy", "AccountPolicy": "Account policy",
"AccountPolicyHelpText": "For accounts that do not meet the requirements when creating, such as: non-compliant key types and unique key constraints, you can choose the above strategy.", "AccountPolicyHelpText": "For accounts that do not meet the requirements when creating, such as: non-compliant key types and unique key constraints, you can choose the above strategy.",
@ -546,9 +546,9 @@
"GatewayList": "Gateways", "GatewayList": "Gateways",
"GatewayPlatformHelpText": "Only platforms with names starting with Gateway can be used as gateways.", "GatewayPlatformHelpText": "Only platforms with names starting with Gateway can be used as gateways.",
"GatewayUpdate": "Update the gateway", "GatewayUpdate": "Update the gateway",
"DiscoverAccounts": "Gather accounts", "DiscoverAccounts": "Discover accounts",
"DiscoverAccountsHelpText": "Collect account information on assets. the collected account information can be imported into the system for centralized management.", "DiscoverAccountsHelpText": "Collect account information on assets. the collected account information can be imported into the system for centralized management.",
"GatheredAccountList": "Gathered accounts", "DiscoveredAccountList": "Discovered accounts",
"General": "General", "General": "General",
"GeneralAccounts": "General accounts", "GeneralAccounts": "General accounts",
"GeneralSetting": "General", "GeneralSetting": "General",

View File

@ -563,7 +563,7 @@
"GatewayUpdate": "ゲートウェイの更新", "GatewayUpdate": "ゲートウェイの更新",
"DiscoverAccounts": "アカウント収集", "DiscoverAccounts": "アカウント収集",
"DiscoverAccountsHelpText": "資産上のアカウント情報を収集します。収集したアカウント情報は、システムにインポートして一元管理が可能です", "DiscoverAccountsHelpText": "資産上のアカウント情報を収集します。収集したアカウント情報は、システムにインポートして一元管理が可能です",
"GatheredAccountList": "収集したアカウント", "DiscoveredAccountList": "収集したアカウント",
"General": "基本", "General": "基本",
"GeneralAccounts": "一般アカウント", "GeneralAccounts": "一般アカウント",
"GeneralSetting": "汎用設定", "GeneralSetting": "汎用設定",

View File

@ -548,7 +548,7 @@
"GatewayUpdate": "更新网关", "GatewayUpdate": "更新网关",
"DiscoverAccounts": "账号发现", "DiscoverAccounts": "账号发现",
"DiscoverAccountsHelpText": "收集资产上的账号信息。收集后的账号信息可以导入到系统中,方便统一管理", "DiscoverAccountsHelpText": "收集资产上的账号信息。收集后的账号信息可以导入到系统中,方便统一管理",
"GatheredAccountList": "发现的账号", "DiscoveredAccountList": "发现的账号",
"General": "基本", "General": "基本",
"GeneralAccounts": "普通账号", "GeneralAccounts": "普通账号",
"GeneralSetting": "通用配置", "GeneralSetting": "通用配置",

View File

@ -719,7 +719,7 @@
"GatewayUpdate": "更新網關", "GatewayUpdate": "更新網關",
"DiscoverAccounts": "帳號收集", "DiscoverAccounts": "帳號收集",
"DiscoverAccountsHelpText": "收集資產上的賬號資訊。收集後的賬號資訊可以導入到系統中,方便統一", "DiscoverAccountsHelpText": "收集資產上的賬號資訊。收集後的賬號資訊可以導入到系統中,方便統一",
"GatheredAccountList": "Collected accounts", "DiscoveredAccountList": "Collected accounts",
"General": "基本", "General": "基本",
"GeneralAccounts": "普通帳號", "GeneralAccounts": "普通帳號",
"GeneralSetting": "General Settings", "GeneralSetting": "General Settings",