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 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.const import AutomationTypes
@ -15,6 +19,8 @@ __all__ = [
'AccountRiskViewSet', 'CheckAccountEngineViewSet',
]
from ...risk_handlers import RiskHandler
class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
model = CheckAccountAutomation
@ -46,6 +52,7 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
serializer_classes = {
'default': serializers.AccountRiskSerializer,
'assets': serializers.AssetRiskSerializer,
'handle': serializers.HandleRiskSerializer
}
ordering_fields = (
'asset', 'risk', 'status', 'username', 'date_created'
@ -53,9 +60,15 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
ordering = ('-asset', 'date_created')
rbac_perms = {
'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')
def assets(self, request, *args, **kwargs):
@ -72,6 +85,32 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
)
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):
search_fields = ('name',)

View File

@ -13,18 +13,22 @@ from accounts.filters import GatheredAccountFilterSet
from accounts.models import GatherAccountsAutomation, AutomationExecution
from accounts.models import GatheredAccount
from assets.models import Asset
from accounts.tasks.common import quickstart_automation_by_snapshot
from orgs.mixins.api import OrgBulkModelViewSet
from .base import AutomationExecutionViewSet
__all__ = [
'GatherAccountsAutomationViewSet', 'GatherAccountsExecutionViewSet',
'GatheredAccountViewSet'
"GatherAccountsAutomationViewSet",
"GatherAccountsExecutionViewSet",
"GatheredAccountViewSet",
]
from ...risk_handlers import RiskHandler
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
model = GatherAccountsAutomation
filterset_fields = ('name',)
filterset_fields = ("name",)
search_fields = filterset_fields
serializer_class = serializers.GatherAccountAutomationSerializer
@ -47,52 +51,56 @@ class GatherAccountsExecutionViewSet(AutomationExecutionViewSet):
class GatheredAccountViewSet(OrgBulkModelViewSet):
model = GatheredAccount
search_fields = ('username',)
search_fields = ("username",)
filterset_class = GatheredAccountFilterSet
ordering = ("status",)
serializer_classes = {
'default': serializers.GatheredAccountSerializer,
'status': serializers.GatheredAccountActionSerializer,
"default": serializers.GatheredAccountSerializer,
"status": serializers.GatheredAccountActionSerializer,
}
rbac_perms = {
'sync_accounts': 'assets.add_gatheredaccount',
'discover': 'assets.add_gatheredaccount',
'status': 'assets.change_gatheredaccount',
"sync_accounts": "assets.add_gatheredaccount",
"discover": "assets.add_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):
instance = self.get_object()
instance.status = request.data.get('status')
instance.save(update_fields=['status'])
instance.status = request.data.get("status")
instance.save(update_fields=["status"])
if instance.status == 'confirmed':
if instance.status == "confirmed":
GatheredAccount.sync_accounts([instance])
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=status.HTTP_400_BAD_REQUEST, data={'asset_id': 'This field is required.'})
@action(methods=["post"], detail=False, url_path="delete-remote")
def delete_remote(self, request, *args, **kwargs):
asset_id = request.data.get("asset_id")
username = request.data.get("username")
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.snapshot = {
'assets': [asset_id],
'nodes': [],
'type': 'gather_accounts',
'is_sync_account': False,
'check_risk': True,
'name': 'Adhoc gather accounts: {}'.format(asset_id),
"assets": [asset_id],
"nodes": [],
"type": "gather_accounts",
"is_sync_account": False,
"check_risk": True,
"name": "Adhoc gather accounts: {}".format(asset_id),
}
execution.save()
execution.start()
report = execution.manager.gen_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:
long_time = timezone.timedelta(days=90)
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",
"risk": "long_time_password",
@ -154,7 +154,7 @@ class AnalyseAccountRisk:
else:
self._create_risk(
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:
self.ori_asset_usernames[str(asset_id)].add(username)
ga_accounts = GatheredAccount.objects.filter(asset__in=assets).prefetch_related(
"asset"
ga_accounts = (
GatheredAccount.objects.filter(asset__in=assets)
.prefetch_related("asset")
)
for account in ga_accounts:
self.ori_gathered_usernames[str(account.asset_id)].add(account.username)
@ -315,8 +316,10 @@ class GatherAccountsManager(AccountBasePlaybookManager):
.filter(present=True)
.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)

View File

@ -1,4 +1,5 @@
import os
from collections import defaultdict
from copy import deepcopy
from django.db.models import QuerySet
@ -12,10 +13,16 @@ logger = get_logger(__name__)
class RemoveAccountManager(AccountBasePlaybookManager):
super_accounts = ['root', 'administrator']
def __init__(self, *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):
path = super().prepare_runtime_dir()
@ -30,28 +37,22 @@ class RemoveAccountManager(AccountBasePlaybookManager):
def method_type(cls):
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):
if host.get('error'):
return host
gather_accounts = asset.gatheredaccount_set.all()
gather_accounts = self.get_gather_accounts(account, gather_accounts)
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['name'] += '(' + gather_account.username + ')'
self.host_account_mapper[h['name']] = (asset, gather_account)
h['account'] = {'username': gather_account.username}
h['name'] += '(' + username + ')'
self.host_account_mapper[h['name']] = account
h['account'] = {'username': username}
inventory_hosts.append(h)
return inventory_hosts

View File

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

View File

@ -19,7 +19,7 @@ class Migration(migrations.Migration):
name="status",
field=models.CharField(
blank=True,
choices=[("confirmed", "Confirmed"), ("ignored", "Ignored")],
choices=[("confirmed", "Confirmed"), ("ignored", "Ignored"), ("pending", "Pending")],
default="",
max_length=32,
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):
# 依赖自动发现的
zombie = 'zombie', _('Long time no login') # 好久没登录的账号, 禁用、删除
ghost = 'ghost', _('Not managed') # 未被纳管的账号, 纳管, 删除, 禁用
long_time_no_login = 'long_time_no_login', _('Long time no login') # 好久没登录的账号, 禁用、删除
new_found = 'new_found', _('New found') # 未被纳管的账号, 纳管, 删除, 禁用
group_changed = 'groups_changed', _('Groups change') # 组变更, 确认
sudo_changed = 'sudoers_changed', _('Sudo changed') # sudo 变更, 确认
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'))
username = models.CharField(max_length=32, verbose_name=_('Username'))
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'))
class Meta:

View File

@ -24,7 +24,7 @@ class GatheredAccount(JMSOrgBaseModel):
present = models.BooleanField(default=False, verbose_name=_("Present")) # 系统资产上是否还存在
date_password_change = models.DateTimeField(null=True, verbose_name=_("Date change password"))
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
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 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 common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.utils import get_logger
from .base import BaseAutomationSerializer
from accounts.risk_handlers import TYPE_CHOICES
logger = get_logger(__file__)
__all__ = [
'CheckAccountAutomationSerializer',
'AccountRiskSerializer',
'CheckAccountEngineSerializer',
'AssetRiskSerializer',
"CheckAccountAutomationSerializer",
"AccountRiskSerializer",
"CheckAccountEngineSerializer",
"AssetRiskSerializer",
"HandleRiskSerializer",
]
class AccountRiskSerializer(serializers.ModelSerializer):
asset = ObjectRelatedField(queryset=Asset.objects.all(), required=False,label=_("Asset"))
risk = LabeledChoiceField(choices=RiskChoice.choices, required=False, read_only=True, label=_("Risk"))
asset = ObjectRelatedField(
queryset=Asset.objects.all(), required=False, label=_("Asset")
)
risk = LabeledChoiceField(
choices=RiskChoice.choices, required=False, read_only=True, label=_("Risk")
)
class Meta:
model = AccountRisk
fields = [
'id', 'asset', 'username', 'risk', 'status',
'date_created', 'details',
"id", "asset", "username", "risk", "status",
"date_created", "details",
]
@classmethod
def setup_eager_loading(cls, queryset):
return queryset.select_related('asset')
return queryset.select_related("asset")
class RiskSummarySerializer(serializers.Serializer):
@ -42,10 +53,14 @@ class RiskSummarySerializer(serializers.Serializer):
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')
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()
@ -53,16 +68,26 @@ class AssetRiskSerializer(serializers.Serializer):
def get_risk_summary(obj):
summary = {}
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
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 Meta:
model = CheckAccountAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
fields = BaseAutomationSerializer.Meta.fields \
+ ['engines', 'recipients'] + read_only_fields
fields = (
BaseAutomationSerializer.Meta.fields
+ ["engines", "recipients"]
+ read_only_fields
)
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
@property
@ -73,8 +98,8 @@ class CheckAccountAutomationSerializer(BaseAutomationSerializer):
class CheckAccountEngineSerializer(serializers.ModelSerializer):
class Meta:
model = CheckAccountEngine
fields = ['id', 'name', 'slug', 'is_active', 'comment']
read_only_fields = ['slug']
fields = ["id", "name", "slug", "is_active", "comment"]
read_only_fields = ["slug"]
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 = {
'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

View File

@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
from common.utils import ssh_key_gen, random_string
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.update(password_rules)
rules = {
'length': rules['length'],
'lower': rules['lowercase'],
'upper': rules['uppercase'],
'digit': rules['digit'],
'special_char': rules['symbol'],
'exclude_chars': rules.get('exclude_symbols', ''),
"length": rules["length"],
"lower": rules["lowercase"],
"upper": rules["uppercase"],
"digit": rules["digit"],
"special_char": rules["symbol"],
"exclude_chars": rules.get("exclude_symbols", ""),
}
return random_string(**rules)
@ -46,10 +47,12 @@ class SecretGenerator:
def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """
if password.startswith('{{') and password.endswith('}}'):
"""校验 Ansible 不支持的特殊字符"""
if password.startswith("{{") and password.endswith("}}"):
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
)
execution.start()
return execution

View File

@ -76,6 +76,7 @@ class Language(models.TextChoices):
class ConfirmOrIgnore(models.TextChoices):
pending = '', _('Pending')
confirmed = 'confirmed', _('Confirmed')
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?",
"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",
"AccountDiscoverList": "Gather accounts",
"AccountDiscoverTaskCreate": "Create gather accounts task",
"AccountDiscoverTaskList": "Gather accounts tasks",
"AccountDiscoverTaskUpdate": "Update the gather accounts task",
"AccountDiscoverList": "Discover accounts",
"AccountDiscoverTaskCreate": "Create discover accounts task",
"AccountDiscoverTaskList": "Discover accounts tasks",
"AccountDiscoverTaskUpdate": "Update the discover accounts task",
"AccountList": "Accounts",
"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.",
@ -546,9 +546,9 @@
"GatewayList": "Gateways",
"GatewayPlatformHelpText": "Only platforms with names starting with Gateway can be used as gateways.",
"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.",
"GatheredAccountList": "Gathered accounts",
"DiscoveredAccountList": "Discovered accounts",
"General": "General",
"GeneralAccounts": "General accounts",
"GeneralSetting": "General",

View File

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

View File

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

View File

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