perf: update check account

pull/14387/merge
ibuler 2 weeks ago
parent e3f93a9410
commit 5257ea5f9f

@ -2,5 +2,5 @@ from .backup import *
from .base import *
from .change_secret import *
from .check_account import *
from .gather_accounts import *
from .gather_account import *
from .push_account import *

@ -5,28 +5,29 @@ from rest_framework.decorators import action
from accounts import serializers
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 .base import AutomationExecutionViewSet
__all__ = [
'CheckAccountAutomationViewSet', 'CheckAccountExecutionViewSet',
'AccountRiskViewSet', 'AccountCheckEngineViewSet',
'AccountRiskViewSet', 'CheckAccountEngineViewSet',
]
class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
model = AccountCheckAutomation
model = CheckAccountAutomation
filterset_fields = ('name',)
search_fields = filterset_fields
serializer_class = serializers.CheckAccountsAutomationSerializer
serializer_class = serializers.CheckAccountAutomationSerializer
class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = (
("list", "accounts.view_gatheraccountsexecution"),
("retrieve", "accounts.view_gatheraccountsexecution"),
("create", "accounts.add_gatheraccountsexecution"),
("list", "accounts.view_checkaccountexecution"),
("retrieve", "accounts.view_checkaccountsexecution"),
("create", "accounts.add_checkaccountexecution"),
)
tp = AutomationTypes.check_account
@ -71,25 +72,10 @@ class AccountRiskViewSet(OrgBulkModelViewSet):
return self.get_paginated_response_from_queryset(queryset)
class AccountCheckEngineViewSet(OrgBulkModelViewSet):
class CheckAccountEngineViewSet(JMSModelViewSet):
search_fields = ('name',)
serializer_class = serializers.AccountCheckEngineSerializer
rbac_perms = {
'list': 'assets.view_accountcheckautomation',
}
serializer_class = serializers.CheckAccountEngineSerializer
def get_queryset(self):
return [
{
'id': 1,
'name': 'check_gathered_account',
'display_name': '检查发现的账号',
'description': '基于自动发现的账号结果进行检查分析,检查 用户组、公钥、sudoers 等信息'
},
{
'id': 2,
'name': 'check_account_secret',
'display_name': '检查账号密码强弱',
'description': '基于账号密码的安全性进行检查分析, 检查密码强度、泄露等信息'
}
]
return CheckAccountEngine.objects.all()

@ -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 django.utils import timezone
from accounts.const import AutomationTypes
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):
diff_items = [
'authorized_keys', 'sudoers', 'groups',
]
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)
self.host_asset_mapper = {}
self.asset_account_info = {}
self.asset_usernames_mapper = defaultdict(set)
self.ori_asset_usernames = defaultdict(set)
self.ori_gathered_usernames = defaultdict(set)
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):
return AutomationTypes.gather_accounts
def host_callback(self, host, asset=None, **kwargs):
super().host_callback(host, asset=asset, **kwargs)
self.host_asset_mapper[host['name']] = asset
return host
def _filter_success_result(self, tp, result):
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):
result = self._filter_success_result(asset.type, info)
accounts = []
for username, info in result.items():
self.asset_usernames_mapper[asset].add(username)
d = {'asset': asset, 'username': username, 'remote_present': True, **info}
accounts.append(d)
self.asset_account_info[asset] = accounts
def on_runner_failed(self, runner, e):
print("Runner failed: ", e)
raise e
def on_host_success(self, host, result):
info = self._get_nested_info(result, 'debug', 'res', 'info')
asset = self.host_asset_mapper.get(host)
if asset and info:
self._collect_asset_account_info(asset, info)
from accounts.models import Account, AccountRisk
def is_weak_password(password):
# 判断密码长度
if len(password) < 8:
return True
# 判断是否只有一种字符类型
if password.isdigit() or password.isalpha():
return True
# 判断是否只包含数字或字母
if password.islower() or password.isupper():
return True
# 判断是否包含常见弱密码
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
if password.lower() in common_passwords:
return True
# 正则表达式判断字符多样性(数字、字母、特殊字符)
if (not re.search(r'[A-Za-z]', password)
or not re.search(r'[0-9]', password)
or not re.search(r'[\W_]', password)):
return True
return False
def check_account_secrets(accounts, assets):
now = timezone.now().isoformat()
risks = []
tmpl = "Check account %s: %s"
RED = "\033[31m"
GREEN = "\033[32m"
RESET = "\033[0m" # 还原默认颜色
summary = defaultdict(int)
for account in accounts:
if not account.secret:
print(tmpl % (account, "no secret"))
summary['no_secret'] += 1
continue
if is_weak_password(account.secret):
print(tmpl % (account, f"{RED}weak{RESET}"))
summary['weak'] += 1
risks.append({
'account': account,
'risk': 'weak_password',
})
else:
print(f'\033[31m Not found {host} info \033[0m\n')
def prefetch_origin_account_usernames(self):
"""
提起查出来避免每次 sql 查询
:return:
"""
assets = self.asset_usernames_mapper.keys()
accounts = Account.objects.filter(asset__in=assets).values_list('asset', 'username')
for asset, username in accounts:
self.ori_asset_usernames[asset].add(username)
ga_accounts = GatheredAccount.objects.filter(asset__in=assets)
for account in ga_accounts:
self.ori_gathered_usernames[account.asset].add(account.username)
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'])
summary['ok'] += 1
print(tmpl % (account, f"{GREEN}ok{RESET}"))
def _analyse_datetime_changed(self, ori_account, d, asset, username):
basic = {'asset': asset, 'username': username}
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}
for item in self.datetime_check_items:
field = item['field']
risk = item['risk']
delta = item['delta']
for d in risks:
key = f'{d["account"].asset_id}_{d["account"].username}_{d["risk"]}'
origin_risk = origin_risks_dict.get(key)
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)
if origin_risk:
origin_risk.details.append({'datetime': now})
origin_risk.save(update_fields=['details'])
else:
self.pending_add_risks.append(
dict(**basic, risk='ghost')
AccountRisk.objects.create(
asset=d['account'].asset,
username=d['account'].username,
risk=d['risk'],
details=[{'datetime': now}],
)
self._analyse_datetime_changed(ori_account, d, asset, d['username'])
if len(self.pending_add_risks) > batch_size:
self.batch_analyse_risk(None, None, {})
def get_items_diff(self, ori_account, d):
if hasattr(ori_account, '_diff'):
return ori_account._diff
diff = {}
for item in self.diff_items:
ori = getattr(ori_account, item)
new = d.get(item, '')
if not ori:
continue
if isinstance(new, timezone.datetime):
new = ori.strftime('%Y-%m-%d %H:%M:%S')
ori = ori.strftime('%Y-%m-%d %H:%M:%S')
if new != ori:
diff[item] = get_text_diff(ori, new)
ori_account._diff = diff
return diff
def batch_update_gathered_account(self, ori_account, d, batch_size=20):
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:
return summary
class CheckAccountManager:
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 pre_run(self):
self.assets = self.execution.get_all_assets()
def batch_run(self, batch_size=100):
for engine in self.execution.snapshot.get('engines', []):
if engine == 'check_account_secret':
handle = check_account_secrets
else:
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()
for i in range(0, len(self.assets), batch_size):
_assets = self.assets[i:i + batch_size]
accounts = Account.objects.filter(asset__in=_assets)
summary = handle(accounts, _assets)
self.summary.update(summary)
def after_run(self):
self.date_end = timezone.now()
self.time_end = time.time()
self.timedelta = self.time_end - self.time_start
tmpl = "\n-\nSummary: ok: %s, weak: %s, no_secret: %s, using time: %ss" % (
self.summary['ok'], self.summary['weak'], self.summary['no_secret'], self.timedelta
)
print(tmpl)
def run(self,):
self.pre_run()
self.batch_run()
self.after_run()

@ -1,5 +1,6 @@
from .backup_account.manager import AccountBackupManager
from .change_secret.manager import ChangeSecretManager
from .check_account.manager import CheckAccountManager
from .gather_account.manager import GatherAccountsManager
from .push_account.manager import PushAccountManager
from .remove_account.manager import RemoveAccountManager
@ -16,6 +17,7 @@ class ExecutionManager:
AutomationTypes.remove_account: RemoveAccountManager,
AutomationTypes.gather_accounts: GatherAccountsManager,
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
AutomationTypes.check_account: CheckAccountManager,
# TODO 后期迁移到自动化策略中
'backup_account': AccountBackupManager,
}

@ -34,14 +34,14 @@ class AutomationTypes(models.TextChoices):
from accounts.models import (
PushAccountAutomation, ChangeSecretAutomation,
VerifyAccountAutomation, GatherAccountsAutomation,
AccountCheckAutomation,
CheckAccountAutomation,
)
type_model_dict = {
cls.push_account: PushAccountAutomation,
cls.change_secret: ChangeSecretAutomation,
cls.verify_account: VerifyAccountAutomation,
cls.gather_accounts: GatherAccountsAutomation,
cls.check_account: AccountCheckAutomation,
cls.check_account: CheckAccountAutomation,
}
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",
),
),
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.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 .base import AccountBaseAutomation
from ...const import AutomationTypes
__all__ = ['AccountCheckAutomation', 'AccountRisk', 'RiskChoice']
__all__ = ['CheckAccountAutomation', 'AccountRisk', 'RiskChoice', 'CheckAccountEngine']
class AccountCheckAutomation(AccountBaseAutomation):
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
class CheckAccountAutomation(AccountBaseAutomation):
engines = models.ManyToManyField('CheckAccountEngine', related_name='check_automations', verbose_name=_('Engines'))
def to_attr_json(self):
attr_json = super().to_attr_json()
attr_json.update({
'engines': [engine.slug for engine in self.engines.all()],
})
return attr_json
@ -33,7 +28,11 @@ class AccountCheckAutomation(AccountBaseAutomation):
super().save(*args, **kwargs)
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):
@ -91,3 +90,17 @@ class AccountRisk(JMSOrgBaseModel):
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 .change_secret import *
from .check_accounts import *
from .gather_accounts import *
from .check_account import *
from .gather_account import *
from .push_account import *

@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
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 common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.utils import get_logger
@ -13,9 +13,9 @@ from .base import BaseAutomationSerializer
logger = get_logger(__file__)
__all__ = [
'CheckAccountsAutomationSerializer',
'CheckAccountAutomationSerializer',
'AccountRiskSerializer',
'AccountCheckEngineSerializer',
'CheckAccountEngineSerializer',
'AssetRiskSerializer',
]
@ -53,12 +53,12 @@ class AssetRiskSerializer(serializers.Serializer):
return summary
class CheckAccountsAutomationSerializer(BaseAutomationSerializer):
class CheckAccountAutomationSerializer(BaseAutomationSerializer):
class Meta:
model = AccountCheckAutomation
model = CheckAccountAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
fields = BaseAutomationSerializer.Meta.fields \
+ [] + read_only_fields
+ ['engines'] + read_only_fields
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
@property
@ -66,8 +66,11 @@ class CheckAccountsAutomationSerializer(BaseAutomationSerializer):
return AutomationTypes.check_account
class AccountCheckEngineSerializer(serializers.Serializer):
id = serializers.IntegerField(required=False)
name = serializers.CharField(max_length=128, required=True)
display_name = serializers.CharField(max_length=128, required=False)
description = serializers.CharField(required=False)
class CheckAccountEngineSerializer(serializers.ModelSerializer):
class Meta:
model = CheckAccountEngine
fields = ['id', 'name', 'slug', 'is_active', 'comment']
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
fields = BaseAutomationSerializer.Meta.fields \
+ ['is_sync_account', 'recipients'] + read_only_fields
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
@property

@ -2,32 +2,5 @@
from common.utils import get_logger
# __all__ = ['gather_asset_accounts_task']
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
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'check-account-automations', api.CheckAccountAutomationViewSet, 'check-account-automation')
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')
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 -*-
#
import abc
from datetime import timedelta
from celery.schedules import crontab
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import CrontabSchedule, IntervalSchedule, ClockedSchedule
from rest_framework import serializers
from .celery.utils import (
@ -32,7 +36,7 @@ class PeriodTaskModelMixin(models.Model):
default=24, null=True, blank=True, verbose_name=_("Interval"),
)
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(
blank=True, null=True,
@ -42,6 +46,7 @@ class PeriodTaskModelMixin(models.Model):
'triggering the task to run'
),
)
date_last_run = models.DateTimeField(blank=True, null=True, verbose_name=_("Date last run"))
objects = PeriodTaskModelQuerySet.as_manager()
@abc.abstractmethod
@ -67,9 +72,9 @@ class PeriodTaskModelMixin(models.Model):
disable_celery_periodic_task(name)
return
crontab = interval = None
cron = interval = None
if self.crontab:
crontab = self.crontab
cron = self.crontab
elif self.interval:
interval = self.interval * self.interval_ratio[0]
@ -77,7 +82,7 @@ class PeriodTaskModelMixin(models.Model):
name: {
'task': task,
'interval': interval,
'crontab': crontab,
'crontab': cron,
'args': args,
'kwargs': kwargs,
'enabled': True,
@ -86,6 +91,9 @@ class PeriodTaskModelMixin(models.Model):
}
create_or_update_celery_periodic_tasks(tasks)
def execute(self, *args, **kwargs):
self.date_last_run = timezone.now()
def save(self, *args, **kwargs):
instance = super().save(**kwargs)
self.set_period_schedule()
@ -111,6 +119,45 @@ class PeriodTaskModelMixin(models.Model):
name = self.get_register_task()[0]
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:
abstract = True

Loading…
Cancel
Save