perf: 更新风险发现

pull/14517/head
ibuler 2024-11-11 11:12:10 +08:00
parent d3804156c8
commit f5d611acce
7 changed files with 232 additions and 81 deletions

View File

@ -39,11 +39,16 @@ class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
class AccountRiskViewSet(OrgBulkModelViewSet): class AccountRiskViewSet(OrgBulkModelViewSet):
model = AccountRisk model = AccountRisk
search_fields = ('username',) search_fields = ('username', 'asset')
filterset_fields = ('risk', 'status', 'asset')
serializer_classes = { serializer_classes = {
'default': serializers.AccountRiskSerializer, 'default': serializers.AccountRiskSerializer,
'assets': serializers.AssetRiskSerializer, 'assets': serializers.AssetRiskSerializer,
} }
ordering_fields = (
'asset', 'risk', 'status', 'username', 'date_created'
)
ordering = ('-asset', 'date_created')
rbac_perms = { rbac_perms = {
'sync_accounts': 'assets.add_accountrisk', 'sync_accounts': 'assets.add_accountrisk',
'assets': 'accounts.view_accountrisk' 'assets': 'accounts.view_accountrisk'

View File

@ -44,6 +44,14 @@ class GatherAccountsFilter:
continue continue
username_sudo[username.strip()] = sudo.strip() 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', []) user_authorized = info.pop('user_authorized', [])
username_authorized = {} username_authorized = {}
for line in user_authorized: for line in user_authorized:
@ -52,27 +60,39 @@ class GatherAccountsFilter:
username, authorized = line.split(':', 1) username, authorized = line.split(':', 1)
username_authorized[username.strip()] = authorized.strip() 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 = {} result = {}
users = info.pop('users', '') users = info.pop('users', '')
for line in users:
parts = line.split()
if len(parts) < 4:
continue
username = parts[0] for username in users:
if not username: if not username:
continue continue
user = dict() user = dict()
address = parts[2]
user['address_last_login'] = address
login_time = parts[3]
login = user_last_login.get(username) or ''
if login and len(login) == 3:
user['address_last_login'] = login[1][:32]
try: try:
login_date = timezone.datetime.fromisoformat(login_time) login_date = timezone.datetime.fromisoformat(login[2])
user['date_last_login'] = login_date user['date_last_login'] = login_date
except ValueError: except ValueError:
pass 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['groups'] = username_groups.get(username) or ''
user['sudoers'] = username_sudo.get(username) or '' user['sudoers'] = username_sudo.get(username) or ''
user['authorized_keys'] = username_authorized.get(username) or '' user['authorized_keys'] = username_authorized.get(username) or ''

View File

@ -4,23 +4,28 @@
- name: Get users - name: Get users
ansible.builtin.shell: ansible.builtin.shell:
cmd: > cmd: >
getent passwd | awk -F: '$7 !~ /(false|nologin)$/' | grep -v '^$' | awk -F":" '{ print $1 }' getent passwd | awk -F: '$7 !~ /(false|nologin|true|sync)$/' | grep -v '^$' | awk -F":" '{ print $1 }'
register: users register: users
- name: Gather posix account - name: Gather posix account last login
ansible.builtin.shell: | ansible.builtin.shell: |
for user in {{ users.stdout_lines | join(" ") }}; do for user in {{ users.stdout_lines | join(" ") }}; do
k=$(last -i --time-format iso -1 ${user} | head -1 | grep -v ^$ ) last -i --time-format iso -n 1 ${user} | awk '{ print $1,$3,$4, $NF }' | head -1 | grep -v ^$
if [ -n "$k" ]; then
echo $k
fi
done done
register: last_login 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 - name: Get user groups
ansible.builtin.shell: | ansible.builtin.shell: |
for user in {{ users.stdout_lines | join(" ") }}; do for user in {{ users.stdout_lines | join(" ") }}; do
echo "$(groups $user)" echo "$(groups $user)" | sed 's@ : @:@g'
done done
register: user_groups register: user_groups
@ -38,17 +43,19 @@
echo -n "$user:" echo -n "$user:"
if [[ -f ${home}/.ssh/authorized_keys ]]; then if [[ -f ${home}/.ssh/authorized_keys ]]; then
cat ${home}/.ssh/authorized_keys | tr '\n' ';' cat ${home}/.ssh/authorized_keys | tr '\n' ';'
echo
fi fi
echo
done done
register: user_authorized register: user_authorized
- set_fact: - set_fact:
info: info:
users: "{{ last_login.stdout_lines }}" users: "{{ users.stdout_lines }}"
last_login: "{{ last_login.stdout_lines }}"
user_groups: "{{ user_groups.stdout_lines }}" user_groups: "{{ user_groups.stdout_lines }}"
user_sudo: "{{ user_sudo.stdout_lines }}" user_sudo: "{{ user_sudo.stdout_lines }}"
user_authorized: "{{ user_authorized.stdout_lines }}" user_authorized: "{{ user_authorized.stdout_lines }}"
passwd_date: "{{ passwd_date.stdout_lines }}"
- debug: - debug:
var: info var: info

View File

@ -1,7 +1,9 @@
from collections import defaultdict from collections import defaultdict
from django.utils import timezone
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.models import GatheredAccount, Account, GatheredAccountDiff from accounts.models import GatheredAccount, Account, AccountRisk
from assets.models import Asset from assets.models import Asset
from common.const import ConfirmOrIgnore from common.const import ConfirmOrIgnore
from common.utils import get_logger from common.utils import get_logger
@ -16,7 +18,11 @@ logger = get_logger(__name__)
class GatherAccountsManager(AccountBasePlaybookManager): class GatherAccountsManager(AccountBasePlaybookManager):
diff_items = ['authorized_keys', 'sudoers', 'groups'] diff_items = [
'authorized_keys', 'sudoers', 'groups',
'date_password_change', 'date_password_expired',
]
long_time = timezone.timedelta(days=90)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -30,7 +36,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
self.is_sync_account = self.execution.snapshot.get('is_sync_account') self.is_sync_account = self.execution.snapshot.get('is_sync_account')
self.pending_add_accounts = [] self.pending_add_accounts = []
self.pending_update_accounts = [] self.pending_update_accounts = []
self.pending_add_diffs = [] self.pending_add_risks = []
@classmethod @classmethod
def method_type(cls): def method_type(cls):
@ -60,8 +66,6 @@ class GatherAccountsManager(AccountBasePlaybookManager):
self.asset_usernames_mapper[asset].add(username) self.asset_usernames_mapper[asset].add(username)
d = {'asset': asset, 'username': username, 'remote_present': True, **info} d = {'asset': asset, 'username': username, 'remote_present': True, **info}
if len(d['address_last_login']) > 32:
d['address_last_login'] = d['address_last_login'][:32]
accounts.append(d) accounts.append(d)
self.asset_account_info[asset] = accounts self.asset_account_info[asset] = accounts
@ -147,7 +151,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def batch_create_gathered_account(self, d, batch_size=20): def batch_create_gathered_account(self, d, batch_size=20):
if d is None: if d is None:
if self.pending_add_accounts: if self.pending_add_accounts:
GatheredAccount.objects.bulk_create(self.pending_add_accounts) GatheredAccount.objects.bulk_create(self.pending_add_accounts, ignore_conflicts=True)
self.pending_add_accounts = [] self.pending_add_accounts = []
return return
@ -159,32 +163,103 @@ class GatherAccountsManager(AccountBasePlaybookManager):
if len(self.pending_add_accounts) > batch_size: if len(self.pending_add_accounts) > batch_size:
self.batch_create_gathered_account(None) self.batch_create_gathered_account(None)
def batch_update_gathered_account(self, ori_account, d, batch_size=20): def _analyse_item_changed(self, ori_account, d):
if not ori_account or d is None: diff = self.get_items_diff(ori_account, d)
if self.pending_update_accounts:
GatheredAccount.objects.bulk_update(self.pending_update_accounts, [*self.diff_items])
self.pending_update_accounts = []
if self.pending_add_diffs: if not diff:
GatheredAccountDiff.objects.bulk_create(self.pending_add_diffs)
self.pending_add_diffs = []
return return
for k, v in diff.items():
self.pending_add_risks.append(AccountRisk(
asset=ori_account.asset, username=ori_account.username,
risk=k+'_changed', comment=v
))
@staticmethod
def perform_save_risks(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 for r in assets_risks}
for r in risks:
found = assets_risks.get(f"{r.asset_id}_{r.username}")
if not found:
r.save()
else:
found.comment = r.comment + '\n------\n' + found.comment
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
now = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
basic = {'asset': asset, 'username': d['username']}
if ori_account:
self._analyse_item_changed(ori_account, d)
else:
self.pending_add_risks.append(
AccountRisk(**basic, risk='ghost', comment='{}'.format(now))
)
last_login = d.get('date_last_login')
if last_login and last_login < timezone.now() - self.long_time:
self.pending_add_risks.append(
AccountRisk(**basic, risk='zombie', comment='{}'.format(last_login))
)
date_password_change = d.get('date_password_change')
if date_password_change and date_password_change < timezone.now() - self.long_time:
self.pending_add_risks.append(
AccountRisk(**basic, risk='long_time_password', comment='{}'.format(date_password_change))
)
date_password_expired = d.get('date_password_expired')
if date_password_expired and date_password_expired < timezone.now():
self.pending_add_risks.append(
AccountRisk(**basic, risk='password_expired', comment='{}'.format(date_password_expired))
)
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 = {} diff = {}
for item in self.diff_items: for item in self.diff_items:
ori = getattr(ori_account, item) ori = getattr(ori_account, item)
new = d.get(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: if new != ori:
setattr(ori_account, item, new)
diff[item] = get_text_diff(ori, new) 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: if diff:
for k in diff:
setattr(ori_account, k, d[k])
self.pending_update_accounts.append(ori_account) self.pending_update_accounts.append(ori_account)
for k, v in diff.items():
self.pending_add_diffs.append(
GatheredAccountDiff(account=ori_account, item=k, diff=v)
)
if len(self.pending_update_accounts) > batch_size: if len(self.pending_update_accounts) > batch_size:
self.batch_update_gathered_account(None, None) self.batch_update_gathered_account(None, None)
@ -202,11 +277,14 @@ class GatherAccountsManager(AccountBasePlaybookManager):
else: else:
self.batch_update_gathered_account(ori_account, d) self.batch_update_gathered_account(ori_account, d)
self.batch_analyse_risk(asset, ori_account, d)
self.update_gather_accounts_status(asset) self.update_gather_accounts_status(asset)
GatheredAccount.sync_accounts(gathered_accounts, self.is_sync_account) GatheredAccount.sync_accounts(gathered_accounts, self.is_sync_account)
self.batch_create_gathered_account(None) self.batch_create_gathered_account(None)
self.batch_update_gathered_account(None, None) self.batch_update_gathered_account(None, None)
self.batch_analyse_risk(None, None, {})
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
super().run(*args, **kwargs) super().run(*args, **kwargs)

View File

@ -0,0 +1,58 @@
# Generated by Django 4.1.13 on 2024-11-08 09:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("assets", "0006_baseautomation_start_time"),
("accounts", "0008_remove_accountrisk_confirmed_accountrisk_status_and_more"),
]
operations = [
migrations.AddField(
model_name="gatheredaccount",
name="date_change_password",
field=models.DateTimeField(null=True, verbose_name="Date change password"),
),
migrations.AddField(
model_name="gatheredaccount",
name="date_password_expired",
field=models.DateTimeField(null=True, verbose_name="Date password expired"),
),
migrations.AlterField(
model_name="accountrisk",
name="comment",
field=models.TextField(default="", verbose_name="Comment"),
),
migrations.AlterField(
model_name="accountrisk",
name="risk",
field=models.CharField(
choices=[
("zombie", "Long time no login"),
("ghost", "Not managed"),
("groups_changed", "Groups change"),
("sudoers_changed", "Sudo changed"),
("authorized_keys_changed", "Authorized keys changed"),
("account_deleted", "Account delete"),
("password_expired", "Password expired"),
("long_time_password", "Long time no change"),
("weak_password", "Weak password"),
("password_error", "Password error"),
("no_admin_account", "No admin account"),
("others", "Others"),
],
max_length=128,
verbose_name="Risk",
),
),
migrations.AlterUniqueTogether(
name="accountrisk",
unique_together={("asset", "username", "risk")},
),
migrations.DeleteModel(
name="GatheredAccountDiff",
),
]

View File

@ -37,16 +37,18 @@ class AccountCheckAutomation(AccountBaseAutomation):
class RiskChoice(TextChoices): class RiskChoice(TextChoices):
# 依赖自动发现的
zombie = 'zombie', _('Long time no login') # 好久没登录的账号, 禁用、删除 zombie = 'zombie', _('Long time no login') # 好久没登录的账号, 禁用、删除
ghost = 'ghost', _('Not managed') # 未被纳管的账号, 纳管, 删除, 禁用 ghost = 'ghost', _('Not managed') # 未被纳管的账号, 纳管, 删除, 禁用
long_time_password = 'long_time_password', _('Long time no change') # 好久没改密码的账号, 改密码 group_changed = 'groups_changed', _('Groups change') # 组变更, 确认
weak_password = 'weak_password', _('Weak password') # 弱密码, 改密 sudo_changed = 'sudoers_changed', _('Sudo changed') # sudo 变更, 确认
password_error = 'password_error', _('Password error') # 密码错误, 修改账号
password_expired = 'password_expired', _('Password expired') # 密码过期, 修改密码
group_changed = 'group_changed', _('Group change') # 组变更, 确认
sudo_changed = 'sudo_changed', _('Sudo changed') # sudo 变更, 确认
authorized_keys_changed = 'authorized_keys_changed', _('Authorized keys changed') # authorized_keys 变更, 确认 authorized_keys_changed = 'authorized_keys_changed', _('Authorized keys changed') # authorized_keys 变更, 确认
account_deleted = 'account_deleted', _('Account delete') # 账号被删除, 确认 account_deleted = 'account_deleted', _('Account delete') # 账号被删除, 确认
password_expired = 'password_expired', _('Password expired') # 密码过期, 修改密码
long_time_password = 'long_time_password', _('Long time no change') # 好久没改密码的账号, 改密码
weak_password = 'weak_password', _('Weak password') # 弱密码, 改密
password_error = 'password_error', _('Password error') # 密码错误, 修改账号
no_admin_account = 'no_admin_account', _('No admin account') # 无管理员账号, 设置账号 no_admin_account = 'no_admin_account', _('No admin account') # 无管理员账号, 设置账号
others = 'others', _('Others') # 其他风险, 确认 others = 'others', _('Others') # 其他风险, 确认
@ -56,25 +58,15 @@ class AccountRisk(JMSOrgBaseModel):
username = models.CharField(max_length=32, verbose_name=_('Username')) username = models.CharField(max_length=32, verbose_name=_('Username'))
risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices) risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices)
status = models.CharField(max_length=32, choices=ConfirmOrIgnore.choices, default='', blank=True, verbose_name=_('Status')) status = models.CharField(max_length=32, choices=ConfirmOrIgnore.choices, default='', blank=True, verbose_name=_('Status'))
comment = models.TextField(default='', verbose_name=_('Comment'))
class Meta: class Meta:
verbose_name = _('Account risk') verbose_name = _('Account risk')
unique_together = ('asset', 'username', 'risk')
def __str__(self): def __str__(self):
return f"{self.username}@{self.asset} - {self.risk}" return f"{self.username}@{self.asset} - {self.risk}"
def disable_account(self):
pass
def remove_account(self):
pass
def change_password(self):
pass
def handle_risk(self):
pass
@classmethod @classmethod
def gen_fake_data(cls, count=1000, batch_size=50): def gen_fake_data(cls, count=1000, batch_size=50):
from assets.models import Asset from assets.models import Asset

View File

@ -9,36 +9,27 @@ from common.utils.timezone import is_date_more_than
from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import JMSOrgBaseModel
from .base import AccountBaseAutomation from .base import AccountBaseAutomation
__all__ = ['GatherAccountsAutomation', 'GatheredAccount', 'GatheredAccountDiff'] __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): class GatheredAccount(JMSOrgBaseModel):
remote_present = models.BooleanField(default=True, verbose_name=_("Remote present")) # 远端资产上是否还存在
present = models.BooleanField(default=False, verbose_name=_("Present")) # 系统资产上是否还存在
date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login"))
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset")) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username')) 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")) 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")) date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login"))
authorized_keys = models.TextField(default='', blank=True, verbose_name=_("Authorized keys")) authorized_keys = models.TextField(default='', blank=True, verbose_name=_("Authorized keys"))
sudoers = models.TextField(default='', verbose_name=_("Sudoers"), blank=True) sudoers = models.TextField(default='', verbose_name=_("Sudoers"), blank=True)
groups = models.TextField(default='', blank=True, verbose_name=_("Groups")) groups = models.TextField(default='', blank=True, verbose_name=_("Groups"))
remote_present = models.BooleanField(default=True, verbose_name=_("Remote present")) # 远端资产上是否还存在
present = models.BooleanField(default=False, verbose_name=_("Present")) # 系统资产上是否还存在
date_change_password = models.DateTimeField(null=True, verbose_name=_("Date change password"))
date_password_expired = models.DateTimeField(null=True, verbose_name=_("Date password expired"))
status = models.CharField(max_length=32, default='', blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Status"))
@property @property
def address(self): def address(self):
return self.asset.address return self.asset.address
@classmethod
def find_account_risk(cls, gathered_account, accounts):
pass
@classmethod @classmethod
def update_exists_accounts(cls, gathered_account, accounts): def update_exists_accounts(cls, gathered_account, accounts):
if not gathered_account.date_last_login: if not gathered_account.date_last_login: