From c658252c0128f1c44344ff1b4f9dd7da342bcfd8 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:27:05 +0800 Subject: [PATCH] feat: oracle accounts gather (#14571) * feat: oracle accounts gather * feat: sqlserver accounts gather * feat: postgresql accounts gather * feat: mysql accounts gather --------- Co-authored-by: wangruidong <940853815@qq.com> --- .../database/sqlserver/main.yml | 24 ++++ .../database/sqlserver/manifest.yml | 10 ++ .../automations/gather_account/filter.py | 115 +++++++++++++++--- .../automations/gather_account/manager.py | 10 +- ...atheredaccount_authorized_keys_and_more.py | 30 +++++ .../models/automations/gather_account.py | 9 +- .../migrations/0011_auto_20241204_1516.py | 23 ++++ apps/libs/ansible/modules/oracle_info.py | 22 ++++ apps/ops/templates/ops/celery_task_log.html | 3 +- 9 files changed, 216 insertions(+), 30 deletions(-) create mode 100644 apps/accounts/automations/gather_account/database/sqlserver/main.yml create mode 100644 apps/accounts/automations/gather_account/database/sqlserver/manifest.yml create mode 100644 apps/accounts/migrations/0019_remove_gatheredaccount_authorized_keys_and_more.py create mode 100644 apps/assets/migrations/0011_auto_20241204_1516.py diff --git a/apps/accounts/automations/gather_account/database/sqlserver/main.yml b/apps/accounts/automations/gather_account/database/sqlserver/main.yml new file mode 100644 index 000000000..90bbe8cdb --- /dev/null +++ b/apps/accounts/automations/gather_account/database/sqlserver/main.yml @@ -0,0 +1,24 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /opt/py3/bin/python + + tasks: + - name: Test SQLServer connection + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: '{{ jms_asset.spec_info.db_name }}' + script: | + select * from sys.sql_logins + output: dict + register: db_info + + - name: Define info by set_fact + set_fact: + info: "{{ db_info.query_results_dict }}" + + - debug: + var: info diff --git a/apps/accounts/automations/gather_account/database/sqlserver/manifest.yml b/apps/accounts/automations/gather_account/database/sqlserver/manifest.yml new file mode 100644 index 000000000..aaf2891c7 --- /dev/null +++ b/apps/accounts/automations/gather_account/database/sqlserver/manifest.yml @@ -0,0 +1,10 @@ +id: gather_accounts_sqlserver +name: "{{ 'SQLServer account gather' | trans }}" +category: database +type: + - sqlserver +method: gather_accounts +i18n: + SQLServer account gather: + zh: SQLServer 账号收集 + ja: SQLServer アカウントの収集 \ No newline at end of file diff --git a/apps/accounts/automations/gather_account/filter.py b/apps/accounts/automations/gather_account/filter.py index 4b6a52562..63e2ebe17 100644 --- a/apps/accounts/automations/gather_account/filter.py +++ b/apps/accounts/automations/gather_account/filter.py @@ -4,16 +4,25 @@ from datetime import datetime __all__ = ['GatherAccountsFilter'] -def parse_date(date_str, default=''): +def parse_date(date_str, default=None): if not date_str: return default - if date_str == 'Never': - return None - try: - dt = datetime.strptime(date_str, '%Y/%m/%d %H:%M:%S') - return timezone.make_aware(dt, timezone.get_current_timezone()) - except ValueError: + if date_str in ['Never', 'null']: return default + formats = [ + '%Y/%m/%d %H:%M:%S', + '%Y-%m-%dT%H:%M:%S', + '%d-%m-%Y %H:%M:%S', + '%Y/%m/%d', + '%d-%m-%Y', + ] + for fmt in formats: + try: + dt = datetime.strptime(date_str, fmt) + return timezone.make_aware(dt, timezone.get_current_timezone()) + except ValueError: + continue + return default # TODO 后期会挪到 playbook 中 @@ -24,17 +33,83 @@ class GatherAccountsFilter: @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] = {} + for username, user_info in info.items(): + password_last_changed = parse_date(user_info.get('password_last_changed')) + password_lifetime = user_info.get('password_lifetime') + user = { + 'username': username, + 'date_password_change': password_last_changed, + 'date_password_expired': password_last_changed + timezone.timedelta( + days=password_lifetime) if password_last_changed and password_lifetime else None, + 'date_last_login': None, + 'groups': '', + } + result[username] = user return result @staticmethod def postgresql_filter(info): result = {} - for username in info: - result[username] = {} + for username, user_info in info.items(): + user = { + 'username': username, + 'date_password_change': None, + 'date_password_expired': parse_date(user_info.get('valid_until')), + 'date_last_login': None, + 'groups': '', + } + detail = { + 'canlogin': user_info.get('canlogin'), + 'superuser': user_info.get('superuser'), + } + user['detail'] = detail + result[username] = user + return result + + @staticmethod + def sqlserver_filter(info): + if not info: + return {} + result = {} + for user_info in info[0][0]: + user = { + 'username': user_info.get('name', ''), + 'date_password_change': None, + 'date_password_expired': None, + 'date_last_login': None, + 'groups': '', + } + detail = { + 'create_date': user_info.get('create_date', ''), + 'is_disabled': user_info.get('is_disabled', ''), + 'default_database_name': user_info.get('default_database_name', ''), + } + user['detail'] = detail + result[user['username']] = user + return result + + @staticmethod + def oracle_filter(info): + result = {} + for default_tablespace, users in info.items(): + for username, user_info in users.items(): + user = { + 'username': username, + 'date_password_change': parse_date(user_info.get('password_change_date')), + 'date_password_expired': parse_date(user_info.get('expiry_date')), + 'date_last_login': parse_date(user_info.get('last_login')), + 'groups': '', + } + detail = { + 'uid': user_info.get('user_id', ''), + 'create_date': user_info.get('created', ''), + 'account_status': user_info.get('account_status', ''), + 'default_tablespace': default_tablespace, + 'roles': user_info.get('roles', []), + 'privileges': user_info.get('privileges', []), + } + user['detail'] = detail + result[user['username']] = user return result @staticmethod @@ -105,10 +180,12 @@ class GatherAccountsFilter: 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 '' + detail = { + 'groups': username_groups.get(username) or '', + 'sudoers': username_sudo.get(username) or '', + 'authorized_keys': username_authorized.get(username) or '' + } + user['detail'] = detail result[username] = user return result @@ -125,13 +202,13 @@ class GatherAccountsFilter: if len(parts) == 2: key, value = parts user_info[key.strip()] = value.strip() + detail = {'groups': user_info.get('Global Group memberships', ''), } user = { 'username': user_info.get('User name', ''), - 'groups': user_info.get('Global Group memberships', ''), 'date_password_change': parse_date(user_info.get('Password last set', '')), 'date_password_expired': parse_date(user_info.get('Password expires', '')), 'date_last_login': parse_date(user_info.get('Last logon', '')), - 'can_change_password': user_info.get('User may change password', 'Yes') + 'groups': detail, } result[user['username']] = user return result diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index 5eeeb59b0..f2ee64bbb 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -16,9 +16,9 @@ from ..base.manager import AccountBasePlaybookManager logger = get_logger(__name__) risk_items = [ - "authorized_keys", - "sudoers", - "groups", + # "authorized_keys", + # "sudoers", + # "groups", ] diff_items = risk_items + [ @@ -81,8 +81,8 @@ class AnalyseAccountRisk: risks = [] for k, v in diff.items(): - if k not in risk_items: - continue + # if k not in risk_items: + # continue risks.append( dict( asset=ori_account.asset, diff --git a/apps/accounts/migrations/0019_remove_gatheredaccount_authorized_keys_and_more.py b/apps/accounts/migrations/0019_remove_gatheredaccount_authorized_keys_and_more.py new file mode 100644 index 000000000..27deeb463 --- /dev/null +++ b/apps/accounts/migrations/0019_remove_gatheredaccount_authorized_keys_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.13 on 2024-12-03 07:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0018_changesecretrecord_ignore_fail_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='gatheredaccount', + name='authorized_keys', + ), + migrations.RemoveField( + model_name='gatheredaccount', + name='groups', + ), + migrations.RemoveField( + model_name='gatheredaccount', + name='sudoers', + ), + migrations.AddField( + model_name='gatheredaccount', + name='detail', + field=models.JSONField(blank=True, default=dict, verbose_name='Detail'), + ), + ] diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py index 572a891a8..2e74b876f 100644 --- a/apps/accounts/models/automations/gather_account.py +++ b/apps/accounts/models/automations/gather_account.py @@ -9,7 +9,7 @@ from common.utils.timezone import is_date_more_than from orgs.mixins.models import JMSOrgBaseModel from .base import AccountBaseAutomation -__all__ = ['GatherAccountsAutomation', 'GatheredAccount',] +__all__ = ['GatherAccountsAutomation', 'GatheredAccount'] class GatheredAccount(JMSOrgBaseModel): @@ -17,14 +17,13 @@ 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")) date_last_login = models.DateTimeField(null=True, verbose_name=_("Date login")) - authorized_keys = models.TextField(default='', blank=True, verbose_name=_("Authorized keys")) - sudoers = models.TextField(default='', verbose_name=_("Sudoers"), blank=True) - 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_password_change = models.DateTimeField(null=True, verbose_name=_("Date change password")) date_password_expired = models.DateTimeField(null=True, verbose_name=_("Date password expired")) - status = models.CharField(max_length=32, default=ConfirmOrIgnore.pending, blank=True, choices=ConfirmOrIgnore.choices, verbose_name=_("Status")) + status = models.CharField(max_length=32, default=ConfirmOrIgnore.pending, blank=True, + choices=ConfirmOrIgnore.choices, verbose_name=_("Status")) + detail = models.JSONField(default=dict, blank=True, verbose_name=_("Detail")) @property def address(self): diff --git a/apps/assets/migrations/0011_auto_20241204_1516.py b/apps/assets/migrations/0011_auto_20241204_1516.py new file mode 100644 index 000000000..4cf910b63 --- /dev/null +++ b/apps/assets/migrations/0011_auto_20241204_1516.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.13 on 2024-12-04 07:16 + +from django.db import migrations + + +def migrate_platform_sqlserver_automation(apps, schema_editor): + platform_model = apps.get_model('assets', 'Platform') + platform = platform_model.objects.filter(name='SQLServer').first() + + if platform: + automation = platform.automation + automation.gather_accounts_method = 'gather_accounts_sqlserver' + automation.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0010_alter_automationexecution_duration'), + ] + + operations = [ + migrations.RunPython(migrate_platform_sqlserver_automation) + ] diff --git a/apps/libs/ansible/modules/oracle_info.py b/apps/libs/ansible/modules/oracle_info.py index 301f4d3fc..06a25d4d9 100644 --- a/apps/libs/ansible/modules/oracle_info.py +++ b/apps/libs/ansible/modules/oracle_info.py @@ -1,6 +1,7 @@ #!/usr/bin/python from __future__ import absolute_import, division, print_function + __metaclass__ = type DOCUMENTATION = r''' @@ -161,6 +162,7 @@ class OracleInfo(object): def __get_settings(self): """Get global variables (instance settings).""" + def _set_settings_value(item_dict): try: self.info['settings'][item_dict['name']] = item_dict['value'] @@ -178,11 +180,30 @@ class OracleInfo(object): def __get_users(self): """Get user info.""" + + def _set_users_roles(username, item_dict): + users_sql = f"SELECT GRANTED_ROLE FROM DBA_ROLE_PRIVS WHERE GRANTEE = '{username}';" + try: + rtn, err = self.oracle_client.execute(users_sql, exception_to_fail=True) + item_dict['roles'] = [r['role'] for r in rtn] + except Exception: + pass + + def _set_users_privileges(username, item_dict): + users_sql = f"SELECT PRIVILEGE FROM DBA_SYS_PRIVS WHERE GRANTEE = '{username}';" + try: + rtn, err = self.oracle_client.execute(users_sql, exception_to_fail=True) + item_dict['privileges'] = [r['privilege'] for r in rtn] + except Exception: + pass + def _set_users_value(item_dict): try: tablespace = item_dict.pop('default_tablespace') username = item_dict.pop('username') partial_users = self.info['users'].get(tablespace, {}) + _set_users_roles(username, item_dict) + _set_users_privileges(username, item_dict) partial_users[username] = item_dict self.info['users'][tablespace] = partial_users except KeyError: @@ -198,6 +219,7 @@ class OracleInfo(object): def __get_databases(self, exclude_fields): """Get info about databases.""" + def _set_databases_value(item_dict): try: tablespace_name = item_dict.pop('tablespace_name') diff --git a/apps/ops/templates/ops/celery_task_log.html b/apps/ops/templates/ops/celery_task_log.html index 25ab007e4..7ea1cd64d 100644 --- a/apps/ops/templates/ops/celery_task_log.html +++ b/apps/ops/templates/ops/celery_task_log.html @@ -125,7 +125,8 @@ fontSize: 13, lineHeight: 1.2, rightClickSelectsWord: true, - disableStdin: true + disableStdin: true, + scrollback: 9999999, }); term.open(document.getElementById('term')); window.fit.fit(term);