mirror of https://github.com/jumpserver/jumpserver
perf: update check account
parent
e3f93a9410
commit
5257ea5f9f
|
@ -2,5 +2,5 @@ from .backup import *
|
||||||
from .base import *
|
from .base import *
|
||||||
from .change_secret import *
|
from .change_secret import *
|
||||||
from .check_account import *
|
from .check_account import *
|
||||||
from .gather_accounts import *
|
from .gather_account import *
|
||||||
from .push_account import *
|
from .push_account import *
|
||||||
|
|
|
@ -5,28 +5,29 @@ from rest_framework.decorators import action
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import AccountCheckAutomation, AccountRisk, RiskChoice
|
from accounts.models import CheckAccountAutomation, AccountRisk, RiskChoice, CheckAccountEngine
|
||||||
|
from common.api import JMSModelViewSet
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from .base import AutomationExecutionViewSet
|
from .base import AutomationExecutionViewSet
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'CheckAccountAutomationViewSet', 'CheckAccountExecutionViewSet',
|
'CheckAccountAutomationViewSet', 'CheckAccountExecutionViewSet',
|
||||||
'AccountRiskViewSet', 'AccountCheckEngineViewSet',
|
'AccountRiskViewSet', 'CheckAccountEngineViewSet',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
|
class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
|
||||||
model = AccountCheckAutomation
|
model = CheckAccountAutomation
|
||||||
filterset_fields = ('name',)
|
filterset_fields = ('name',)
|
||||||
search_fields = filterset_fields
|
search_fields = filterset_fields
|
||||||
serializer_class = serializers.CheckAccountsAutomationSerializer
|
serializer_class = serializers.CheckAccountAutomationSerializer
|
||||||
|
|
||||||
|
|
||||||
class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
|
class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||||
rbac_perms = (
|
rbac_perms = (
|
||||||
("list", "accounts.view_gatheraccountsexecution"),
|
("list", "accounts.view_checkaccountexecution"),
|
||||||
("retrieve", "accounts.view_gatheraccountsexecution"),
|
("retrieve", "accounts.view_checkaccountsexecution"),
|
||||||
("create", "accounts.add_gatheraccountsexecution"),
|
("create", "accounts.add_checkaccountexecution"),
|
||||||
)
|
)
|
||||||
|
|
||||||
tp = AutomationTypes.check_account
|
tp = AutomationTypes.check_account
|
||||||
|
@ -71,25 +72,10 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
|
||||||
return self.get_paginated_response_from_queryset(queryset)
|
return self.get_paginated_response_from_queryset(queryset)
|
||||||
|
|
||||||
|
|
||||||
class AccountCheckEngineViewSet(OrgBulkModelViewSet):
|
class CheckAccountEngineViewSet(JMSModelViewSet):
|
||||||
search_fields = ('name',)
|
search_fields = ('name',)
|
||||||
serializer_class = serializers.AccountCheckEngineSerializer
|
serializer_class = serializers.CheckAccountEngineSerializer
|
||||||
rbac_perms = {
|
|
||||||
'list': 'assets.view_accountcheckautomation',
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return [
|
return CheckAccountEngine.objects.all()
|
||||||
{
|
|
||||||
'id': 1,
|
|
||||||
'name': 'check_gathered_account',
|
|
||||||
'display_name': '检查发现的账号',
|
|
||||||
'description': '基于自动发现的账号结果进行检查分析,检查 用户组、公钥、sudoers 等信息'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 2,
|
|
||||||
'name': 'check_account_secret',
|
|
||||||
'display_name': '检查账号密码强弱',
|
|
||||||
'description': '基于账号密码的安全性进行检查分析, 检查密码强度、泄露等信息'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
__all__ = ['GatherAccountsFilter']
|
|
||||||
|
|
||||||
|
|
||||||
# TODO 后期会挪到 playbook 中
|
|
||||||
class GatherAccountsFilter:
|
|
||||||
def __init__(self, tp):
|
|
||||||
self.tp = tp
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mysql_filter(info):
|
|
||||||
result = {}
|
|
||||||
for _, user_dict in info.items():
|
|
||||||
for username, _ in user_dict.items():
|
|
||||||
if len(username.split('.')) == 1:
|
|
||||||
result[username] = {}
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def postgresql_filter(info):
|
|
||||||
result = {}
|
|
||||||
for username in info:
|
|
||||||
result[username] = {}
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def posix_filter(info):
|
|
||||||
user_groups = info.pop('user_groups', [])
|
|
||||||
username_groups = {}
|
|
||||||
for line in user_groups:
|
|
||||||
if ':' not in line:
|
|
||||||
continue
|
|
||||||
username, groups = line.split(':', 1)
|
|
||||||
username_groups[username.strip()] = groups.strip()
|
|
||||||
|
|
||||||
user_sudo = info.pop('user_sudo', [])
|
|
||||||
username_sudo = {}
|
|
||||||
for line in user_sudo:
|
|
||||||
if ':' not in line:
|
|
||||||
continue
|
|
||||||
username, sudo = line.split(':', 1)
|
|
||||||
if not sudo.strip():
|
|
||||||
continue
|
|
||||||
username_sudo[username.strip()] = sudo.strip()
|
|
||||||
|
|
||||||
last_login = info.pop('last_login', '')
|
|
||||||
user_last_login = {}
|
|
||||||
for line in last_login:
|
|
||||||
if not line.strip() or ' ' not in line:
|
|
||||||
continue
|
|
||||||
username, login = line.split(' ', 1)
|
|
||||||
user_last_login[username] = login
|
|
||||||
|
|
||||||
user_authorized = info.pop('user_authorized', [])
|
|
||||||
username_authorized = {}
|
|
||||||
for line in user_authorized:
|
|
||||||
if ':' not in line:
|
|
||||||
continue
|
|
||||||
username, authorized = line.split(':', 1)
|
|
||||||
username_authorized[username.strip()] = authorized.strip()
|
|
||||||
|
|
||||||
passwd_date = info.pop('passwd_date', [])
|
|
||||||
username_password_date = {}
|
|
||||||
for line in passwd_date:
|
|
||||||
if ':' not in line:
|
|
||||||
continue
|
|
||||||
username, password_date = line.split(':', 1)
|
|
||||||
username_password_date[username.strip()] = password_date.strip().split()
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
users = info.pop('users', '')
|
|
||||||
|
|
||||||
for username in users:
|
|
||||||
if not username:
|
|
||||||
continue
|
|
||||||
user = dict()
|
|
||||||
|
|
||||||
login = user_last_login.get(username) or ''
|
|
||||||
if login and len(login) == 3:
|
|
||||||
user['address_last_login'] = login[1][:32]
|
|
||||||
try:
|
|
||||||
login_date = timezone.datetime.fromisoformat(login[2])
|
|
||||||
user['date_last_login'] = login_date
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
start_date = timezone.make_aware(timezone.datetime(1970, 1, 1))
|
|
||||||
_password_date = username_password_date.get(username) or ''
|
|
||||||
if _password_date and len(_password_date) == 2:
|
|
||||||
if _password_date[0] and _password_date[0] != '0':
|
|
||||||
user['date_password_change'] = start_date + timezone.timedelta(days=int(_password_date[0]))
|
|
||||||
if _password_date[1] and _password_date[1] != '0':
|
|
||||||
user['date_password_expired'] = start_date + timezone.timedelta(days=int(_password_date[1]))
|
|
||||||
|
|
||||||
user['groups'] = username_groups.get(username) or ''
|
|
||||||
user['sudoers'] = username_sudo.get(username) or ''
|
|
||||||
user['authorized_keys'] = username_authorized.get(username) or ''
|
|
||||||
result[username] = user
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def windows_filter(info):
|
|
||||||
info = info[4:-2]
|
|
||||||
result = {}
|
|
||||||
for i in info:
|
|
||||||
for username in i.split():
|
|
||||||
result[username] = {}
|
|
||||||
return result
|
|
||||||
|
|
||||||
def run(self, method_id_meta_mapper, info):
|
|
||||||
run_method_name = None
|
|
||||||
for k, v in method_id_meta_mapper.items():
|
|
||||||
if self.tp not in v['type']:
|
|
||||||
continue
|
|
||||||
run_method_name = k.replace(f'{v["method"]}_', '')
|
|
||||||
|
|
||||||
if not run_method_name:
|
|
||||||
return info
|
|
||||||
|
|
||||||
if hasattr(self, f'{run_method_name}_filter'):
|
|
||||||
return getattr(self, f'{run_method_name}_filter')(info)
|
|
||||||
return info
|
|
|
@ -1,61 +0,0 @@
|
||||||
- hosts: demo
|
|
||||||
gather_facts: no
|
|
||||||
tasks:
|
|
||||||
- name: Get users
|
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: >
|
|
||||||
getent passwd | awk -F: '$7 !~ /(false|nologin|true|sync)$/' | grep -v '^$' | awk -F":" '{ print $1 }'
|
|
||||||
register: users
|
|
||||||
|
|
||||||
- name: Gather posix account last login
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
|
||||||
last -i --time-format iso -n 1 ${user} | awk '{ print $1,$3,$4, $NF }' | head -1 | grep -v ^$
|
|
||||||
done
|
|
||||||
register: last_login
|
|
||||||
|
|
||||||
- name: Get user password change date and expiry
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
|
||||||
k=$(getent shadow $user | awk -F: '{ print $3, $5 }')
|
|
||||||
echo "$user:$k"
|
|
||||||
done
|
|
||||||
register: passwd_date
|
|
||||||
|
|
||||||
- name: Get user groups
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
|
||||||
echo "$(groups $user)" | sed 's@ : @:@g'
|
|
||||||
done
|
|
||||||
register: user_groups
|
|
||||||
|
|
||||||
- name: Get sudoers
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
|
||||||
echo "$user: $(grep "^$user " /etc/sudoers | tr '\n' ';' || echo '')"
|
|
||||||
done
|
|
||||||
register: user_sudo
|
|
||||||
|
|
||||||
- name: Get authorized keys
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
|
||||||
home=$(getent passwd $user | cut -d: -f6)
|
|
||||||
echo -n "$user:"
|
|
||||||
if [[ -f ${home}/.ssh/authorized_keys ]]; then
|
|
||||||
cat ${home}/.ssh/authorized_keys | tr '\n' ';'
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
register: user_authorized
|
|
||||||
|
|
||||||
- set_fact:
|
|
||||||
info:
|
|
||||||
users: "{{ users.stdout_lines }}"
|
|
||||||
last_login: "{{ last_login.stdout_lines }}"
|
|
||||||
user_groups: "{{ user_groups.stdout_lines }}"
|
|
||||||
user_sudo: "{{ user_sudo.stdout_lines }}"
|
|
||||||
user_authorized: "{{ user_authorized.stdout_lines }}"
|
|
||||||
passwd_date: "{{ passwd_date.stdout_lines }}"
|
|
||||||
|
|
||||||
- debug:
|
|
||||||
var: info
|
|
|
@ -1,13 +0,0 @@
|
||||||
id: gather_accounts_posix
|
|
||||||
name: "{{ 'Posix account gather' | trans }}"
|
|
||||||
category: host
|
|
||||||
type:
|
|
||||||
- linux
|
|
||||||
- unix
|
|
||||||
method: gather_accounts
|
|
||||||
|
|
||||||
i18n:
|
|
||||||
Posix account gather:
|
|
||||||
zh: 使用命令 getent passwd 收集 Posix 资产账号
|
|
||||||
ja: コマンド getent を使用してアセットアカウントを収集する
|
|
||||||
en: Using command getent to gather accounts
|
|
|
@ -1,14 +0,0 @@
|
||||||
- hosts: demo
|
|
||||||
gather_facts: no
|
|
||||||
tasks:
|
|
||||||
- name: Gather windows account
|
|
||||||
ansible.builtin.win_shell: net user
|
|
||||||
register: result
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Define info by set_fact
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
info: "{{ result.stdout_lines }}"
|
|
||||||
|
|
||||||
- debug:
|
|
||||||
var: info
|
|
|
@ -1,13 +0,0 @@
|
||||||
id: gather_accounts_windows
|
|
||||||
name: "{{ 'Windows account gather' | trans }}"
|
|
||||||
version: 1
|
|
||||||
method: gather_accounts
|
|
||||||
category: host
|
|
||||||
type:
|
|
||||||
- windows
|
|
||||||
|
|
||||||
i18n:
|
|
||||||
Windows account gather:
|
|
||||||
zh: 使用命令 net user 收集 Windows 账号
|
|
||||||
ja: コマンド net user を使用して Windows アカウントを収集する
|
|
||||||
en: Using command net user to gather accounts
|
|
|
@ -1,357 +1,122 @@
|
||||||
|
import re
|
||||||
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.models import Account, AccountRisk
|
||||||
from accounts.models import GatheredAccount, Account, AccountRisk
|
|
||||||
from assets.models import Asset
|
|
||||||
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
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class GatherAccountsManager(AccountBasePlaybookManager):
|
def is_weak_password(password):
|
||||||
diff_items = [
|
# 判断密码长度
|
||||||
'authorized_keys', 'sudoers', 'groups',
|
if len(password) < 8:
|
||||||
]
|
return True
|
||||||
long_time = timezone.timedelta(days=90)
|
|
||||||
datetime_check_items = [
|
|
||||||
{'field': 'date_last_login', 'risk': 'zombie', 'delta': long_time},
|
|
||||||
{'field': 'date_password_change', 'risk': 'long_time_password', 'delta': long_time},
|
|
||||||
{'field': 'date_password_expired', 'risk': 'password_expired', 'delta': timezone.timedelta(seconds=1)}
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
# 判断是否只有一种字符类型
|
||||||
super().__init__(*args, **kwargs)
|
if password.isdigit() or password.isalpha():
|
||||||
self.host_asset_mapper = {}
|
return True
|
||||||
self.asset_account_info = {}
|
|
||||||
|
|
||||||
self.asset_usernames_mapper = defaultdict(set)
|
# 判断是否只包含数字或字母
|
||||||
self.ori_asset_usernames = defaultdict(set)
|
if password.islower() or password.isupper():
|
||||||
self.ori_gathered_usernames = defaultdict(set)
|
return True
|
||||||
self.ori_gathered_accounts_mapper = dict()
|
|
||||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
|
||||||
self.pending_add_accounts = []
|
|
||||||
self.pending_update_accounts = []
|
|
||||||
self.pending_add_risks = []
|
|
||||||
self.now = timezone.now()
|
|
||||||
|
|
||||||
@classmethod
|
# 判断是否包含常见弱密码
|
||||||
def method_type(cls):
|
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
|
||||||
return AutomationTypes.gather_accounts
|
if password.lower() in common_passwords:
|
||||||
|
return True
|
||||||
|
|
||||||
def host_callback(self, host, asset=None, **kwargs):
|
# 正则表达式判断字符多样性(数字、字母、特殊字符)
|
||||||
super().host_callback(host, asset=asset, **kwargs)
|
if (not re.search(r'[A-Za-z]', password)
|
||||||
self.host_asset_mapper[host['name']] = asset
|
or not re.search(r'[0-9]', password)
|
||||||
return host
|
or not re.search(r'[\W_]', password)):
|
||||||
|
return True
|
||||||
|
|
||||||
def _filter_success_result(self, tp, result):
|
return False
|
||||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_nested_info(data, *keys):
|
|
||||||
for key in keys:
|
|
||||||
data = data.get(key, {})
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _collect_asset_account_info(self, asset, info):
|
def check_account_secrets(accounts, assets):
|
||||||
result = self._filter_success_result(asset.type, info)
|
now = timezone.now().isoformat()
|
||||||
accounts = []
|
risks = []
|
||||||
for username, info in result.items():
|
tmpl = "Check account %s: %s"
|
||||||
self.asset_usernames_mapper[asset].add(username)
|
RED = "\033[31m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
RESET = "\033[0m" # 还原默认颜色
|
||||||
|
|
||||||
d = {'asset': asset, 'username': username, 'remote_present': True, **info}
|
summary = defaultdict(int)
|
||||||
accounts.append(d)
|
for account in accounts:
|
||||||
self.asset_account_info[asset] = accounts
|
if not account.secret:
|
||||||
|
print(tmpl % (account, "no secret"))
|
||||||
|
summary['no_secret'] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
def on_runner_failed(self, runner, e):
|
if is_weak_password(account.secret):
|
||||||
print("Runner failed: ", e)
|
print(tmpl % (account, f"{RED}weak{RESET}"))
|
||||||
raise e
|
summary['weak'] += 1
|
||||||
|
risks.append({
|
||||||
def on_host_success(self, host, result):
|
'account': account,
|
||||||
info = self._get_nested_info(result, 'debug', 'res', 'info')
|
'risk': 'weak_password',
|
||||||
asset = self.host_asset_mapper.get(host)
|
})
|
||||||
if asset and info:
|
|
||||||
self._collect_asset_account_info(asset, info)
|
|
||||||
else:
|
else:
|
||||||
print(f'\033[31m Not found {host} info \033[0m\n')
|
summary['ok'] += 1
|
||||||
|
print(tmpl % (account, f"{GREEN}ok{RESET}"))
|
||||||
|
|
||||||
def prefetch_origin_account_usernames(self):
|
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}
|
||||||
提起查出来,避免每次 sql 查询
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
assets = self.asset_usernames_mapper.keys()
|
|
||||||
accounts = Account.objects.filter(asset__in=assets).values_list('asset', 'username')
|
|
||||||
|
|
||||||
for asset, username in accounts:
|
for d in risks:
|
||||||
self.ori_asset_usernames[asset].add(username)
|
key = f'{d["account"].asset_id}_{d["account"].username}_{d["risk"]}'
|
||||||
|
origin_risk = origin_risks_dict.get(key)
|
||||||
|
|
||||||
ga_accounts = GatheredAccount.objects.filter(asset__in=assets)
|
if origin_risk:
|
||||||
for account in ga_accounts:
|
origin_risk.details.append({'datetime': now})
|
||||||
self.ori_gathered_usernames[account.asset].add(account.username)
|
origin_risk.save(update_fields=['details'])
|
||||||
key = '{}_{}'.format(account.asset.id, account.username)
|
|
||||||
self.ori_gathered_accounts_mapper[key] = account
|
|
||||||
|
|
||||||
def update_gather_accounts_status(self, asset):
|
|
||||||
"""
|
|
||||||
远端账号,收集中的账号,vault 中的账号。
|
|
||||||
要根据账号新增见啥,标识 收集账号的状态, 让管理员关注
|
|
||||||
|
|
||||||
远端账号 -> 收集账号 -> 特权账号
|
|
||||||
"""
|
|
||||||
remote_users = self.asset_usernames_mapper[asset]
|
|
||||||
ori_users = self.ori_asset_usernames[asset]
|
|
||||||
ori_ga_users = self.ori_gathered_usernames[asset]
|
|
||||||
|
|
||||||
queryset = (GatheredAccount.objects
|
|
||||||
.filter(asset=asset)
|
|
||||||
.exclude(status=ConfirmOrIgnore.ignored))
|
|
||||||
|
|
||||||
# 远端账号 比 收集账号多的
|
|
||||||
# 新增创建,不用处理状态
|
|
||||||
|
|
||||||
# 远端上 比 收集账号少的
|
|
||||||
# 标识 remote_present=False, 标记为待处理
|
|
||||||
# 远端资产上不存在的,标识为待处理,需要管理员介入
|
|
||||||
lost_users = ori_ga_users - remote_users
|
|
||||||
if lost_users:
|
|
||||||
queryset.filter(username__in=lost_users).update(status='', remote_present=False)
|
|
||||||
|
|
||||||
# 收集的账号 比 账号列表多的, 有可能是账号中删掉了, 但这时候状态已经是 confirm 了
|
|
||||||
# 标识状态为 待处理, 让管理员去确认
|
|
||||||
ga_added_users = ori_ga_users - ori_users
|
|
||||||
if ga_added_users:
|
|
||||||
queryset.filter(username__in=ga_added_users).update(status='')
|
|
||||||
|
|
||||||
# 收集的账号 比 账号列表少的
|
|
||||||
# 这个好像不不用对比,原始情况就这样
|
|
||||||
|
|
||||||
# 远端账号 比 账号列表少的
|
|
||||||
# 创建收集账号,标识 remote_present=False, 状态待处理
|
|
||||||
|
|
||||||
# 远端账号 比 账号列表多的
|
|
||||||
# 正常情况, 不用处理,因为远端账号会创建到收集账号,收集账号再去对比
|
|
||||||
|
|
||||||
# 不过这个好像也处理一下 status,因为已存在,这是状态应该是确认
|
|
||||||
(queryset.filter(username__in=ori_users)
|
|
||||||
.exclude(status=ConfirmOrIgnore.confirmed)
|
|
||||||
.update(status=ConfirmOrIgnore.confirmed))
|
|
||||||
|
|
||||||
# 远端存在的账号,标识为已存在
|
|
||||||
queryset.filter(username__in=remote_users, remote_present=False).update(remote_present=True)
|
|
||||||
|
|
||||||
# 资产上没有的,标识为为存在
|
|
||||||
queryset.exclude(username__in=ori_users).filter(present=False).update(present=True)
|
|
||||||
|
|
||||||
def batch_create_gathered_account(self, d, batch_size=20):
|
|
||||||
if d is None:
|
|
||||||
if self.pending_add_accounts:
|
|
||||||
GatheredAccount.objects.bulk_create(self.pending_add_accounts, ignore_conflicts=True)
|
|
||||||
self.pending_add_accounts = []
|
|
||||||
return
|
|
||||||
|
|
||||||
gathered_account = GatheredAccount()
|
|
||||||
for k, v in d.items():
|
|
||||||
setattr(gathered_account, k, v)
|
|
||||||
self.pending_add_accounts.append(gathered_account)
|
|
||||||
|
|
||||||
if len(self.pending_add_accounts) > batch_size:
|
|
||||||
self.batch_create_gathered_account(None)
|
|
||||||
|
|
||||||
def _analyse_item_changed(self, ori_account, d):
|
|
||||||
diff = self.get_items_diff(ori_account, d)
|
|
||||||
|
|
||||||
if not diff:
|
|
||||||
return
|
|
||||||
|
|
||||||
for k, v in diff.items():
|
|
||||||
self.pending_add_risks.append(dict(
|
|
||||||
asset=ori_account.asset, username=ori_account.username,
|
|
||||||
risk=k+'_changed', detail={'diff': v}
|
|
||||||
))
|
|
||||||
|
|
||||||
def perform_save_risks(self, risks):
|
|
||||||
# 提前取出来,避免每次都查数据库
|
|
||||||
assets = {r['asset'] for r in risks}
|
|
||||||
assets_risks = AccountRisk.objects.filter(asset__in=assets)
|
|
||||||
assets_risks = {f"{r.asset_id}_{r.username}_{r.risk}": r for r in assets_risks}
|
|
||||||
|
|
||||||
for d in risks:
|
|
||||||
detail = d.pop('detail', {})
|
|
||||||
detail['datetime'] = self.now.isoformat()
|
|
||||||
key = f"{d['asset'].id}_{d['username']}_{d['risk']}"
|
|
||||||
found = assets_risks.get(key)
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
r = AccountRisk(**d, details=[detail])
|
|
||||||
r.save()
|
|
||||||
continue
|
|
||||||
|
|
||||||
found.details.append(detail)
|
|
||||||
found.save(update_fields=['details'])
|
|
||||||
|
|
||||||
def _analyse_datetime_changed(self, ori_account, d, asset, username):
|
|
||||||
basic = {'asset': asset, 'username': username}
|
|
||||||
|
|
||||||
for item in self.datetime_check_items:
|
|
||||||
field = item['field']
|
|
||||||
risk = item['risk']
|
|
||||||
delta = item['delta']
|
|
||||||
|
|
||||||
date = d.get(field)
|
|
||||||
if not date:
|
|
||||||
continue
|
|
||||||
|
|
||||||
pre_date = ori_account and getattr(ori_account, field)
|
|
||||||
if pre_date == date:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if date and date < timezone.now() - delta:
|
|
||||||
self.pending_add_risks.append(
|
|
||||||
dict(**basic, risk=risk, detail={'date': date.isoformat()})
|
|
||||||
)
|
|
||||||
|
|
||||||
def batch_analyse_risk(self, asset, ori_account, d, batch_size=20):
|
|
||||||
if asset is None:
|
|
||||||
if self.pending_add_risks:
|
|
||||||
self.perform_save_risks(self.pending_add_risks)
|
|
||||||
self.pending_add_risks = []
|
|
||||||
return
|
|
||||||
|
|
||||||
basic = {'asset': asset, 'username': d['username']}
|
|
||||||
if ori_account:
|
|
||||||
self._analyse_item_changed(ori_account, d)
|
|
||||||
else:
|
else:
|
||||||
self.pending_add_risks.append(
|
AccountRisk.objects.create(
|
||||||
dict(**basic, risk='ghost')
|
asset=d['account'].asset,
|
||||||
|
username=d['account'].username,
|
||||||
|
risk=d['risk'],
|
||||||
|
details=[{'datetime': now}],
|
||||||
)
|
)
|
||||||
|
return summary
|
||||||
|
|
||||||
self._analyse_datetime_changed(ori_account, d, asset, d['username'])
|
|
||||||
|
|
||||||
if len(self.pending_add_risks) > batch_size:
|
class CheckAccountManager:
|
||||||
self.batch_analyse_risk(None, None, {})
|
def __init__(self, execution):
|
||||||
|
self.execution = execution
|
||||||
|
self.date_start = timezone.now()
|
||||||
|
self.time_start = time.time()
|
||||||
|
self.date_end = None
|
||||||
|
self.time_end = None
|
||||||
|
self.timedelta = 0
|
||||||
|
self.assets = []
|
||||||
|
self.summary = {}
|
||||||
|
|
||||||
def get_items_diff(self, ori_account, d):
|
def pre_run(self):
|
||||||
if hasattr(ori_account, '_diff'):
|
self.assets = self.execution.get_all_assets()
|
||||||
return ori_account._diff
|
|
||||||
|
|
||||||
diff = {}
|
def batch_run(self, batch_size=100):
|
||||||
for item in self.diff_items:
|
for engine in self.execution.snapshot.get('engines', []):
|
||||||
ori = getattr(ori_account, item)
|
if engine == 'check_account_secret':
|
||||||
new = d.get(item, '')
|
handle = check_account_secrets
|
||||||
|
else:
|
||||||
if not ori:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(new, timezone.datetime):
|
for i in range(0, len(self.assets), batch_size):
|
||||||
new = ori.strftime('%Y-%m-%d %H:%M:%S')
|
_assets = self.assets[i:i + batch_size]
|
||||||
ori = ori.strftime('%Y-%m-%d %H:%M:%S')
|
accounts = Account.objects.filter(asset__in=_assets)
|
||||||
|
summary = handle(accounts, _assets)
|
||||||
|
self.summary.update(summary)
|
||||||
|
|
||||||
if new != ori:
|
def after_run(self):
|
||||||
diff[item] = get_text_diff(ori, new)
|
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
|
||||||
|
)
|
||||||
|
print(tmpl)
|
||||||
|
|
||||||
ori_account._diff = diff
|
def run(self,):
|
||||||
return diff
|
self.pre_run()
|
||||||
|
self.batch_run()
|
||||||
def batch_update_gathered_account(self, ori_account, d, batch_size=20):
|
self.after_run()
|
||||||
if not ori_account or d is None:
|
|
||||||
if self.pending_update_accounts:
|
|
||||||
GatheredAccount.objects.bulk_update(self.pending_update_accounts, [*self.diff_items])
|
|
||||||
self.pending_update_accounts = []
|
|
||||||
return
|
|
||||||
|
|
||||||
diff = self.get_items_diff(ori_account, d)
|
|
||||||
if diff:
|
|
||||||
for k in diff:
|
|
||||||
setattr(ori_account, k, d[k])
|
|
||||||
self.pending_update_accounts.append(ori_account)
|
|
||||||
|
|
||||||
if len(self.pending_update_accounts) > batch_size:
|
|
||||||
self.batch_update_gathered_account(None, None)
|
|
||||||
|
|
||||||
def update_or_create_accounts(self):
|
|
||||||
for asset, accounts_data in self.asset_account_info.items():
|
|
||||||
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('{}_{}'.format(asset.id, username))
|
|
||||||
|
|
||||||
if not ori_account:
|
|
||||||
self.batch_create_gathered_account(d)
|
|
||||||
else:
|
|
||||||
self.batch_update_gathered_account(ori_account, d)
|
|
||||||
|
|
||||||
self.batch_analyse_risk(asset, ori_account, d)
|
|
||||||
|
|
||||||
self.update_gather_accounts_status(asset)
|
|
||||||
GatheredAccount.sync_accounts(gathered_accounts, self.is_sync_account)
|
|
||||||
|
|
||||||
self.batch_create_gathered_account(None)
|
|
||||||
self.batch_update_gathered_account(None, None)
|
|
||||||
self.batch_analyse_risk(None, None, {})
|
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
|
||||||
super().run(*args, **kwargs)
|
|
||||||
self.prefetch_origin_account_usernames()
|
|
||||||
self.update_or_create_accounts()
|
|
||||||
# self.send_email_if_need()
|
|
||||||
|
|
||||||
def generate_send_users_and_change_info(self):
|
|
||||||
recipients = self.execution.recipients
|
|
||||||
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
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
asset_id_map = {str(asset.id): asset for asset in assets}
|
|
||||||
asset_id_username = list(assets.values_list('id', 'accounts__username'))
|
|
||||||
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
|
|
||||||
|
|
||||||
system_asset_usernames_mapper = defaultdict(set)
|
|
||||||
for asset_id, username in asset_id_username:
|
|
||||||
system_asset_usernames_mapper[str(asset_id)].add(username)
|
|
||||||
|
|
||||||
change_info = defaultdict(dict)
|
|
||||||
for asset_id, usernames in self.asset_usernames_mapper.items():
|
|
||||||
system_usernames = system_asset_usernames_mapper.get(asset_id)
|
|
||||||
if not system_usernames:
|
|
||||||
continue
|
|
||||||
|
|
||||||
add_usernames = usernames - system_usernames
|
|
||||||
remove_usernames = system_usernames - usernames
|
|
||||||
|
|
||||||
if not add_usernames and not remove_usernames:
|
|
||||||
continue
|
|
||||||
|
|
||||||
change_info[str(asset_id_map[asset_id])] = {
|
|
||||||
'add_usernames': add_usernames,
|
|
||||||
'remove_usernames': remove_usernames
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, dict(change_info)
|
|
||||||
|
|
||||||
def send_email_if_need(self):
|
|
||||||
users, change_info = self.generate_send_users_and_change_info()
|
|
||||||
if not users or not change_info:
|
|
||||||
return
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
GatherAccountChangeMsg(user, change_info).publish_async()
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from .backup_account.manager import AccountBackupManager
|
from .backup_account.manager import AccountBackupManager
|
||||||
from .change_secret.manager import ChangeSecretManager
|
from .change_secret.manager import ChangeSecretManager
|
||||||
|
from .check_account.manager import CheckAccountManager
|
||||||
from .gather_account.manager import GatherAccountsManager
|
from .gather_account.manager import GatherAccountsManager
|
||||||
from .push_account.manager import PushAccountManager
|
from .push_account.manager import PushAccountManager
|
||||||
from .remove_account.manager import RemoveAccountManager
|
from .remove_account.manager import RemoveAccountManager
|
||||||
|
@ -16,6 +17,7 @@ class ExecutionManager:
|
||||||
AutomationTypes.remove_account: RemoveAccountManager,
|
AutomationTypes.remove_account: RemoveAccountManager,
|
||||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||||
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
||||||
|
AutomationTypes.check_account: CheckAccountManager,
|
||||||
# TODO 后期迁移到自动化策略中
|
# TODO 后期迁移到自动化策略中
|
||||||
'backup_account': AccountBackupManager,
|
'backup_account': AccountBackupManager,
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,14 +34,14 @@ class AutomationTypes(models.TextChoices):
|
||||||
from accounts.models import (
|
from accounts.models import (
|
||||||
PushAccountAutomation, ChangeSecretAutomation,
|
PushAccountAutomation, ChangeSecretAutomation,
|
||||||
VerifyAccountAutomation, GatherAccountsAutomation,
|
VerifyAccountAutomation, GatherAccountsAutomation,
|
||||||
AccountCheckAutomation,
|
CheckAccountAutomation,
|
||||||
)
|
)
|
||||||
type_model_dict = {
|
type_model_dict = {
|
||||||
cls.push_account: PushAccountAutomation,
|
cls.push_account: PushAccountAutomation,
|
||||||
cls.change_secret: ChangeSecretAutomation,
|
cls.change_secret: ChangeSecretAutomation,
|
||||||
cls.verify_account: VerifyAccountAutomation,
|
cls.verify_account: VerifyAccountAutomation,
|
||||||
cls.gather_accounts: GatherAccountsAutomation,
|
cls.gather_accounts: GatherAccountsAutomation,
|
||||||
cls.check_account: AccountCheckAutomation,
|
cls.check_account: CheckAccountAutomation,
|
||||||
}
|
}
|
||||||
return type_model_dict.get(tp)
|
return type_model_dict.get(tp)
|
||||||
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Generated by Django 4.1.13 on 2024-10-21 09:00
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('accounts', '0007_alter_accountrisk_risk'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -47,4 +47,14 @@ class Migration(migrations.Migration):
|
||||||
verbose_name="Risk",
|
verbose_name="Risk",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Generated by Django 4.1.13 on 2024-11-14 11:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def init_account_check_engine(apps, schema_editor):
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'id': '00000000-0000-0000-0000-000000000001',
|
||||||
|
'slug': 'check_gathered_account',
|
||||||
|
'name': '检查发现的账号',
|
||||||
|
'comment': '基于自动发现的账号结果进行检查分析,检查 用户组、公钥、sudoers 等信息'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '00000000-0000-0000-0000-000000000002',
|
||||||
|
'slug': 'check_account_secret',
|
||||||
|
'name': '检查账号密码强弱',
|
||||||
|
'comment': '基于账号密码的安全性进行检查分析, 检查密码强度、泄露等信息'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
model_cls = apps.get_model('accounts', 'AccountCheckEngine')
|
||||||
|
for item in data:
|
||||||
|
model_cls.objects.create(**item)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assets", "0007_baseautomation_date_last_run_and_more"),
|
||||||
|
(
|
||||||
|
"accounts",
|
||||||
|
"0011_rename_date_change_password_gatheredaccount_date_password_change",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CheckAccountAutomation",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"baseautomation_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="assets.baseautomation",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "account check automation",
|
||||||
|
"permissions": [
|
||||||
|
("view_checkaccountexecution", "Can view check account execution"),
|
||||||
|
("add_checkaccountexecution", "Can add check account execution"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
bases=("accounts.accountbaseautomation",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CheckAccountEngine",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"created_by",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=128, null=True, verbose_name="Created by"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_by",
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=128, null=True, verbose_name="Updated by"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date_created",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True, null=True, verbose_name="Date created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"date_updated",
|
||||||
|
models.DateTimeField(auto_now=True, verbose_name="Date updated"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"comment",
|
||||||
|
models.TextField(blank=True, default="", verbose_name="Comment"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(max_length=128, unique=True, verbose_name="Name"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"slug",
|
||||||
|
models.SlugField(max_length=128, unique=True, verbose_name="Slug"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
models.BooleanField(default=True, verbose_name="Is active"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="accountbackupautomation",
|
||||||
|
name="date_last_run",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="Date last run"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="accountbackupautomation",
|
||||||
|
name="crontab",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default="", max_length=128, verbose_name="Crontab"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="AccountCheckAutomation",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="checkaccountautomation",
|
||||||
|
name="engines",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="check_automations",
|
||||||
|
to="accounts.checkaccountengine",
|
||||||
|
verbose_name="Engines",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(init_account_check_engine),
|
||||||
|
]
|
|
@ -4,27 +4,22 @@ from django.db import models
|
||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.const import Trigger, ConfirmOrIgnore
|
from common.const import ConfirmOrIgnore
|
||||||
|
from common.db.models import JMSBaseModel
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
from .base import AccountBaseAutomation
|
from .base import AccountBaseAutomation
|
||||||
from ...const import AutomationTypes
|
from ...const import AutomationTypes
|
||||||
|
|
||||||
__all__ = ['AccountCheckAutomation', 'AccountRisk', 'RiskChoice']
|
__all__ = ['CheckAccountAutomation', 'AccountRisk', 'RiskChoice', 'CheckAccountEngine']
|
||||||
|
|
||||||
|
|
||||||
class AccountCheckAutomation(AccountBaseAutomation):
|
class CheckAccountAutomation(AccountBaseAutomation):
|
||||||
|
engines = models.ManyToManyField('CheckAccountEngine', related_name='check_automations', verbose_name=_('Engines'))
|
||||||
def get_register_task(self):
|
|
||||||
from ...tasks import check_accounts_task
|
|
||||||
name = "check_accounts_task_period_{}".format(str(self.id)[:8])
|
|
||||||
task = check_accounts_task.name
|
|
||||||
args = (str(self.id), Trigger.timing)
|
|
||||||
kwargs = {}
|
|
||||||
return name, task, args, kwargs
|
|
||||||
|
|
||||||
def to_attr_json(self):
|
def to_attr_json(self):
|
||||||
attr_json = super().to_attr_json()
|
attr_json = super().to_attr_json()
|
||||||
attr_json.update({
|
attr_json.update({
|
||||||
|
'engines': [engine.slug for engine in self.engines.all()],
|
||||||
})
|
})
|
||||||
return attr_json
|
return attr_json
|
||||||
|
|
||||||
|
@ -33,7 +28,11 @@ class AccountCheckAutomation(AccountBaseAutomation):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Gather account automation')
|
verbose_name = _('account check automation')
|
||||||
|
permissions = [
|
||||||
|
('view_checkaccountexecution', _('Can view check account execution')),
|
||||||
|
('add_checkaccountexecution', _('Can add check account execution')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class RiskChoice(TextChoices):
|
class RiskChoice(TextChoices):
|
||||||
|
@ -91,3 +90,17 @@ class AccountRisk(JMSOrgBaseModel):
|
||||||
cls.objects.bulk_create(to_create)
|
cls.objects.bulk_create(to_create)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckAccountEngine(JMSBaseModel):
|
||||||
|
name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True)
|
||||||
|
slug = models.SlugField(max_length=128, verbose_name=_('Slug'), unique=True) #
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def internals(self):
|
||||||
|
return [
|
||||||
|
'check_gathered_account',
|
||||||
|
'check_account_secret'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from .base import *
|
from .base import *
|
||||||
from .change_secret import *
|
from .change_secret import *
|
||||||
from .check_accounts import *
|
from .check_account import *
|
||||||
from .gather_accounts import *
|
from .gather_account import *
|
||||||
from .push_account import *
|
from .push_account import *
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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 AccountCheckAutomation, AccountRisk, RiskChoice
|
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
|
||||||
|
@ -13,9 +13,9 @@ from .base import BaseAutomationSerializer
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'CheckAccountsAutomationSerializer',
|
'CheckAccountAutomationSerializer',
|
||||||
'AccountRiskSerializer',
|
'AccountRiskSerializer',
|
||||||
'AccountCheckEngineSerializer',
|
'CheckAccountEngineSerializer',
|
||||||
'AssetRiskSerializer',
|
'AssetRiskSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -53,12 +53,12 @@ class AssetRiskSerializer(serializers.Serializer):
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
class CheckAccountsAutomationSerializer(BaseAutomationSerializer):
|
class CheckAccountAutomationSerializer(BaseAutomationSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AccountCheckAutomation
|
model = CheckAccountAutomation
|
||||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||||
fields = BaseAutomationSerializer.Meta.fields \
|
fields = BaseAutomationSerializer.Meta.fields \
|
||||||
+ [] + read_only_fields
|
+ ['engines'] + read_only_fields
|
||||||
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -66,8 +66,11 @@ class CheckAccountsAutomationSerializer(BaseAutomationSerializer):
|
||||||
return AutomationTypes.check_account
|
return AutomationTypes.check_account
|
||||||
|
|
||||||
|
|
||||||
class AccountCheckEngineSerializer(serializers.Serializer):
|
class CheckAccountEngineSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(required=False)
|
class Meta:
|
||||||
name = serializers.CharField(max_length=128, required=True)
|
model = CheckAccountEngine
|
||||||
display_name = serializers.CharField(max_length=128, required=False)
|
fields = ['id', 'name', 'slug', 'is_active', 'comment']
|
||||||
description = serializers.CharField(required=False)
|
read_only_fields = ['slug']
|
||||||
|
extra_kwargs = {
|
||||||
|
'is_active': {'required': False},
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
|
||||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||||
fields = BaseAutomationSerializer.Meta.fields \
|
fields = BaseAutomationSerializer.Meta.fields \
|
||||||
+ ['is_sync_account', 'recipients'] + read_only_fields
|
+ ['is_sync_account', 'recipients'] + read_only_fields
|
||||||
|
|
||||||
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
@property
|
@property
|
|
@ -2,32 +2,5 @@
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
# __all__ = ['gather_asset_accounts_task']
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
#
|
|
||||||
# @org_aware_func("nodes")
|
|
||||||
# def gather_asset_accounts_util(nodes, task_name):
|
|
||||||
# from accounts.models import GatherAccountsAutomation
|
|
||||||
# task_name = GatherAccountsAutomation.generate_unique_name(task_name)
|
|
||||||
#
|
|
||||||
# task_snapshot = {
|
|
||||||
# 'nodes': [str(node.id) for node in nodes],
|
|
||||||
# }
|
|
||||||
# tp = AutomationTypes.verify_account
|
|
||||||
# quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# @shared_task(
|
|
||||||
# queue="ansible",
|
|
||||||
# verbose_name=_('Gather asset accounts'),
|
|
||||||
# activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
|
||||||
# description=_("Unused")
|
|
||||||
# )
|
|
||||||
# def gather_asset_accounts_task(node_ids, task_name=None):
|
|
||||||
# if task_name is None:
|
|
||||||
# task_name = gettext_noop("Gather assets accounts")
|
|
||||||
#
|
|
||||||
# nodes = Node.objects.filter(id__in=node_ids)
|
|
||||||
# gather_asset_accounts_util(nodes=nodes, task_name=task_name)
|
|
||||||
#
|
|
||||||
|
|
|
@ -1,19 +1,3 @@
|
||||||
from celery import shared_task
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
__all__ = [
|
|
||||||
'check_accounts_task',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(
|
|
||||||
queue="ansible",
|
|
||||||
verbose_name=_('Scan accounts'),
|
|
||||||
activity_callback=lambda self, node_ids, task_name=None, *args, **kwargs: (node_ids, None),
|
|
||||||
description=_("Unused")
|
|
||||||
)
|
|
||||||
def check_accounts_task(node_ids, task_name=None):
|
|
||||||
pass
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'pu
|
||||||
router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
|
router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
|
||||||
router.register(r'check-account-automations', api.CheckAccountAutomationViewSet, 'check-account-automation')
|
router.register(r'check-account-automations', api.CheckAccountAutomationViewSet, 'check-account-automation')
|
||||||
router.register(r'check-account-executions', api.CheckAccountExecutionViewSet, 'check-account-execution')
|
router.register(r'check-account-executions', api.CheckAccountExecutionViewSet, 'check-account-execution')
|
||||||
router.register(r'account-check-engines', api.AccountCheckEngineViewSet, 'account-check-engine')
|
router.register(r'account-check-engines', api.CheckAccountEngineViewSet, 'account-check-engine')
|
||||||
router.register(r'account-risks', api.AccountRiskViewSet, 'account-risks')
|
router.register(r'account-risks', api.AccountRiskViewSet, 'account-risks')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 4.1.13 on 2024-11-14 06:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("assets", "0006_baseautomation_start_time"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="baseautomation",
|
||||||
|
name="date_last_run",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="Date last run"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="baseautomation",
|
||||||
|
name="crontab",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default="", max_length=128, verbose_name="Crontab"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 4.1.13 on 2024-11-14 06:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("ops", "0004_historicaljob_start_time_job_start_time"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="historicaljob",
|
||||||
|
name="crontab",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default="", max_length=128, verbose_name="Crontab"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="job",
|
||||||
|
name="crontab",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, default="", max_length=128, verbose_name="Crontab"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,9 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import abc
|
import abc
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from celery.schedules import crontab
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, ClockedSchedule
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .celery.utils import (
|
from .celery.utils import (
|
||||||
|
@ -32,7 +36,7 @@ class PeriodTaskModelMixin(models.Model):
|
||||||
default=24, null=True, blank=True, verbose_name=_("Interval"),
|
default=24, null=True, blank=True, verbose_name=_("Interval"),
|
||||||
)
|
)
|
||||||
crontab = models.CharField(
|
crontab = models.CharField(
|
||||||
blank=True, max_length=128, null=True, verbose_name=_("Crontab"),
|
blank=True, max_length=128, default='', verbose_name=_("Crontab"),
|
||||||
)
|
)
|
||||||
start_time = models.DateTimeField(
|
start_time = models.DateTimeField(
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
|
@ -42,6 +46,7 @@ class PeriodTaskModelMixin(models.Model):
|
||||||
'triggering the task to run'
|
'triggering the task to run'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
date_last_run = models.DateTimeField(blank=True, null=True, verbose_name=_("Date last run"))
|
||||||
objects = PeriodTaskModelQuerySet.as_manager()
|
objects = PeriodTaskModelQuerySet.as_manager()
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -67,9 +72,9 @@ class PeriodTaskModelMixin(models.Model):
|
||||||
disable_celery_periodic_task(name)
|
disable_celery_periodic_task(name)
|
||||||
return
|
return
|
||||||
|
|
||||||
crontab = interval = None
|
cron = interval = None
|
||||||
if self.crontab:
|
if self.crontab:
|
||||||
crontab = self.crontab
|
cron = self.crontab
|
||||||
elif self.interval:
|
elif self.interval:
|
||||||
interval = self.interval * self.interval_ratio[0]
|
interval = self.interval * self.interval_ratio[0]
|
||||||
|
|
||||||
|
@ -77,7 +82,7 @@ class PeriodTaskModelMixin(models.Model):
|
||||||
name: {
|
name: {
|
||||||
'task': task,
|
'task': task,
|
||||||
'interval': interval,
|
'interval': interval,
|
||||||
'crontab': crontab,
|
'crontab': cron,
|
||||||
'args': args,
|
'args': args,
|
||||||
'kwargs': kwargs,
|
'kwargs': kwargs,
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
|
@ -86,6 +91,9 @@ class PeriodTaskModelMixin(models.Model):
|
||||||
}
|
}
|
||||||
create_or_update_celery_periodic_tasks(tasks)
|
create_or_update_celery_periodic_tasks(tasks)
|
||||||
|
|
||||||
|
def execute(self, *args, **kwargs):
|
||||||
|
self.date_last_run = timezone.now()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
instance = super().save(**kwargs)
|
instance = super().save(**kwargs)
|
||||||
self.set_period_schedule()
|
self.set_period_schedule()
|
||||||
|
@ -111,6 +119,45 @@ class PeriodTaskModelMixin(models.Model):
|
||||||
name = self.get_register_task()[0]
|
name = self.get_register_task()[0]
|
||||||
return PeriodicTask.objects.filter(name=name).first()
|
return PeriodicTask.objects.filter(name=name).first()
|
||||||
|
|
||||||
|
def get_next_run_time(self):
|
||||||
|
if not self.is_periodic:
|
||||||
|
return None
|
||||||
|
task = self.schedule
|
||||||
|
now = task.schedule.nowfun()
|
||||||
|
|
||||||
|
if self.start_time and self.start_time > now:
|
||||||
|
return self.start_time
|
||||||
|
|
||||||
|
scheduler = task.scheduler
|
||||||
|
# 根据不同的调度类型计算下次执行时间
|
||||||
|
if isinstance(scheduler, CrontabSchedule):
|
||||||
|
schedule = crontab(
|
||||||
|
minute=scheduler.minute,
|
||||||
|
hour=scheduler.hour,
|
||||||
|
day_of_week=scheduler.day_of_week,
|
||||||
|
day_of_month=scheduler.day_of_month,
|
||||||
|
month_of_year=scheduler.month_of_year,
|
||||||
|
)
|
||||||
|
next_run = schedule.remaining_estimate(now)
|
||||||
|
return now + next_run
|
||||||
|
elif isinstance(scheduler, IntervalSchedule):
|
||||||
|
interval = timedelta(
|
||||||
|
seconds=scheduler.every * {
|
||||||
|
IntervalSchedule.SECONDS: 1,
|
||||||
|
IntervalSchedule.MINUTES: 60,
|
||||||
|
IntervalSchedule.HOURS: 3600,
|
||||||
|
IntervalSchedule.DAYS: 86400,
|
||||||
|
}[scheduler.period]
|
||||||
|
)
|
||||||
|
last_run = task.last_run_at or now
|
||||||
|
return last_run + interval
|
||||||
|
|
||||||
|
elif isinstance(scheduler, ClockedSchedule):
|
||||||
|
return scheduler.clocked_time
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("不支持的任务调度类型")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue