diff --git a/apps/accounts/automations/gather_accounts/filter.py b/apps/accounts/automations/gather_accounts/filter.py index 1cca4d980..c0038efb0 100644 --- a/apps/accounts/automations/gather_accounts/filter.py +++ b/apps/accounts/automations/gather_accounts/filter.py @@ -5,7 +5,7 @@ from django.utils import timezone __all__ = ['GatherAccountsFilter'] -# TODO 后期会挪到playbook中 +# TODO 后期会挪到 playbook 中 class GatherAccountsFilter: def __init__(self, tp): diff --git a/apps/accounts/automations/gather_accounts/host/posix/main.yml b/apps/accounts/automations/gather_accounts/host/posix/main.yml index d3cbe9c75..9af249bcf 100644 --- a/apps/accounts/automations/gather_accounts/host/posix/main.yml +++ b/apps/accounts/automations/gather_accounts/host/posix/main.yml @@ -1,21 +1,64 @@ - hosts: demo gather_facts: no tasks: - - name: Gather posix account + - name: Get users ansible.builtin.shell: cmd: > - users=$(getent passwd | grep -v nologin | grep -v shutdown | awk -F":" '{ print $1 }');for i in $users; - do k=$(last -w -F $i -1 | head -1 | grep -v ^$ | awk '{ print $0 }') - if [ -n "$k" ]; then - echo $k - else - echo $i - fi;done - register: result + getent passwd | awk -F: '$7 !~ /(false|nologin)$/' | grep -v '^$' | awk -F":" '{ print $1 }' + register: users + + - name: Gather posix account + ansible.builtin.shell: | + for user in {{ users.stdout_lines | join(" ") }}; do + k=$(last --time-format iso $user -1 | head -1 | grep -v ^$ | awk '{ print $0 }') + if [ -n "$k" ]; then + echo $k + fi + done + register: last_login + + - name: Get user groups + ansible.builtin.shell: | + for user in {{ users.stdout_lines | join(" ") }}; do + echo "$(groups $user)" + done + register: user_groups + + - name: Get sudo permissions + 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' ';' + echo + fi + done + register: user_authorized + + - name: Display user groups + ansible.builtin.debug: + var: user_groups.stdout_lines + + - name: Display sudo permissions + ansible.builtin.debug: + var: user_sudo.stdout_lines + + - name: Display authorized keys + ansible.builtin.debug: + var: user_authorized.stdout_lines + + - name: Display last login + ansible.builtin.debug: + var: last_login.stdout_lines - name: Define info by set_fact ansible.builtin.set_fact: - info: "{{ result.stdout_lines }}" - - - debug: - var: info \ No newline at end of file + var: last_login.stdout_lines diff --git a/apps/accounts/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py index 65e6f26fa..f10b7196f 100644 --- a/apps/accounts/automations/gather_accounts/manager.py +++ b/apps/accounts/automations/gather_accounts/manager.py @@ -1,3 +1,4 @@ +import json from collections import defaultdict from accounts.const import AutomationTypes @@ -61,6 +62,9 @@ class GatherAccountsManager(AccountBasePlaybookManager): return data def on_host_success(self, host, result): + print("Result: ") + print(json.dumps(result, indent=4)) + print(">>>>>>>>>>>>>>>>.") info = self.get_nested_info(result, 'debug', 'res', 'info') asset = self.host_asset_mapper.get(host) if asset and info: @@ -69,9 +73,28 @@ class GatherAccountsManager(AccountBasePlaybookManager): else: print(f'\033[31m Not found {host} info \033[0m\n') + @staticmethod + def update_gather_accounts_status(asset): + """ + 对于资产上不存在的账号,标识为待处理 + 对于账号中不存在的,标识为待处理 + """ + asset_accounts_usernames = asset.accounts.values_list('username', flat=True) + # 账号中不存在的标识为待处理的, 有可能是账号那边删除了 + GatheredAccount.objects \ + .filter(asset=asset, present=True) \ + .exclude(username__in=asset_accounts_usernames) \ + .exclude(status=ConfirmOrIgnore.ignored) \ + .update(status='') + + # 远端资产上不存在的,标识为待处理,需要管理员介入 + GatheredAccount.objects \ + .filter(asset=asset, present=False) \ + .exclude(status=ConfirmOrIgnore.ignored) \ + .update(status='') + def update_or_create_accounts(self): for asset, data in self.asset_account_info.items(): - asset_accounts_usernames = set(asset.accounts.values_list('username', flat=True)) with (tmp_to_org(asset.org_id)): gathered_accounts = [] # 把所有的设置为 present = False, 创建的时候如果有就会更新 @@ -83,21 +106,8 @@ class GatherAccountsManager(AccountBasePlaybookManager): ) gathered_accounts.append(gathered_account) - # 账号中不存在的标识为待处理的, 有可能是账号那边删除了 - GatheredAccount.objects \ - .filter(asset=asset, present=True) \ - .exclude(username__in=asset_accounts_usernames) \ - .exclude(status=ConfirmOrIgnore.ignored) \ - .update(status='') - - # 远端资产上不存在的,标识为待处理,需要管理员介入 - GatheredAccount.objects \ - .filter(asset=asset, present=False) \ - .exclude(status=ConfirmOrIgnore.ignored) \ - .update(status='') - if not self.is_sync_account: - continue - GatheredAccount.sync_accounts(gathered_accounts) + self.update_gather_accounts_status(asset) + GatheredAccount.sync_accounts(gathered_accounts, self.is_sync_account) def run(self, *args, **kwargs): super().run(*args, **kwargs) @@ -115,7 +125,6 @@ class GatherAccountsManager(AccountBasePlaybookManager): return users, None asset_ids = self.asset_username_mapper.keys() - assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts') gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True) diff --git a/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py b/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py index 3dcd02c48..90c0a0a58 100644 --- a/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py +++ b/apps/accounts/migrations/0010_alter_accountrisk_options_and_more.py @@ -26,7 +26,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name="gatheredaccount", - name="action", + name="status", field=models.CharField( choices=[("confirmed", "Confirmed"), ("ignored", "Ignored")], default="", diff --git a/apps/accounts/migrations/0011_remove_gatheredaccount_action_and_more.py b/apps/accounts/migrations/0011_remove_gatheredaccount_action_and_more.py new file mode 100644 index 000000000..68a34aeb7 --- /dev/null +++ b/apps/accounts/migrations/0011_remove_gatheredaccount_action_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 4.1.13 on 2024-10-30 02:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0010_alter_accountrisk_options_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="gatheredaccount", + name="authorized_keys", + field=models.TextField( + blank=True, default="", verbose_name="Authorized keys" + ), + ), + migrations.AddField( + model_name="gatheredaccount", + name="groups", + field=models.TextField(blank=True, default="", verbose_name="Groups"), + ), + migrations.AddField( + model_name="gatheredaccount", + name="sudo", + field=models.TextField(default=False, verbose_name="Sudo"), + ), + migrations.CreateModel( + name="GatheredAccountDiff", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("diff", models.TextField(default="", verbose_name="Diff")), + ( + "item", + models.CharField(default="", max_length=32, verbose_name="Item"), + ), + ( + "date_created", + models.DateTimeField( + auto_now_add=True, verbose_name="Date created" + ), + ), + ( + "account", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="accounts.gatheredaccount", + verbose_name="Gathered account", + ), + ), + ], + ), + ] diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py index 270c9f23a..307ed04b3 100644 --- a/apps/accounts/models/automations/gather_account.py +++ b/apps/accounts/models/automations/gather_account.py @@ -1,17 +1,24 @@ from django.db import models from django.db.models import Q -from django.utils import timezone from django.utils.translation import gettext_lazy as _ from accounts.const import AutomationTypes, Source from accounts.models import Account from common.const import ConfirmOrIgnore +from common.utils.timezone import is_date_more_than from orgs.mixins.models import JMSOrgBaseModel from .base import AccountBaseAutomation __all__ = ['GatherAccountsAutomation', 'GatheredAccount'] +class GatheredAccountDiff(models.Model): + account = models.ForeignKey('GatheredAccount', on_delete=models.CASCADE, verbose_name=_("Gathered account")) + diff = models.TextField(default='', verbose_name=_("Diff")) + item = models.CharField(max_length=32, default='', verbose_name=_("Item")) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) + + class GatheredAccount(JMSOrgBaseModel): present = models.BooleanField(default=True, verbose_name=_("Remote present")) # 资产上是否还存在 date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login")) @@ -19,25 +26,32 @@ class GatheredAccount(JMSOrgBaseModel): username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username')) address_last_login = models.CharField(max_length=39, default='', verbose_name=_("Address login")) status = models.CharField(max_length=32, default='', blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Status")) + authorized_keys = models.TextField(default='', blank=True, verbose_name=_("Authorized keys")) + sudo = models.TextField(default=False, verbose_name=_("Sudo")) + groups = models.TextField(default='', blank=True, verbose_name=_("Groups")) @property def address(self): return self.asset.address + @classmethod + def find_account_risk(cls, gathered_account, accounts): + pass + @classmethod def update_exists_accounts(cls, gathered_account, accounts): if not gathered_account.date_last_login: return for account in accounts: - if (not account.date_last_login or - account.date_last_login - gathered_account.date_last_login > timezone.timedelta(minutes=5)): + # 这里是否可以考虑,标记成未从堡垒机登录风险 ? + if is_date_more_than(gathered_account.date_last_login, account.date_last_login, '5m'): account.date_last_login = gathered_account.date_last_login account.login_by = '{}({})'.format('unknown', gathered_account.address_last_login) account.save(update_fields=['date_last_login', 'login_by']) @classmethod - def create_accounts(cls, gathered_account, accounts): + def create_accounts(cls, gathered_account): account_objs = [] asset_id = gathered_account.asset_id username = gathered_account.username @@ -50,9 +64,14 @@ class GatheredAccount(JMSOrgBaseModel): ) account_objs.append(account) Account.objects.bulk_create(account_objs) + gathered_account.status = ConfirmOrIgnore.confirmed + gathered_account.save(update_fields=['status']) @classmethod - def sync_accounts(cls, gathered_accounts): + def sync_accounts(cls, gathered_accounts, auto_create=True): + """ + 更新为已存在的账号,或者创建新的账号, 原来的 sync 重构了,如果存在则自动更新一些信息 + """ for gathered_account in gathered_accounts: asset_id = gathered_account.asset_id username = gathered_account.username @@ -60,13 +79,11 @@ class GatheredAccount(JMSOrgBaseModel): Q(asset_id=asset_id, username=username) | Q(asset_id=asset_id, name=username) ) + if accounts.exists(): cls.update_exists_accounts(gathered_account, accounts) - else: - cls.create_accounts(gathered_account, accounts) - - gathered_account.status = ConfirmOrIgnore.confirmed - gathered_account.save(update_fields=['status']) + elif auto_create: + cls.create_accounts(gathered_account) class Meta: verbose_name = _("Gather asset accounts") diff --git a/apps/common/utils/timezone.py b/apps/common/utils/timezone.py index d680408c1..9db9d34d2 100644 --- a/apps/common/utils/timezone.py +++ b/apps/common/utils/timezone.py @@ -37,9 +37,11 @@ def local_monday(): return zero_hour_time - timedelta(zero_hour_time.weekday()) -def is_date_difference_than(d1, d2, threshold='1d'): - if d1 is None or d2 is None: +def is_date_more_than(d1, d2, threshold='1d'): + if d1 is None: return False + if d2 is None: + return True kwargs = {} if 'd' in threshold: @@ -52,8 +54,7 @@ def is_date_difference_than(d1, d2, threshold='1d'): raise ValueError('Invalid threshold format') delta = dj_timezone.timedelta(**kwargs) - - return abs((time1 - time2).days) > threshold_in_days + return d1 - d2 > delta _rest_dt_field = DateTimeField()