mirror of https://github.com/jumpserver/jumpserver
				
				
				
			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>pull/14586/head
							parent
							
								
									ab9d8afe80
								
							
						
					
					
						commit
						c658252c01
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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 アカウントの収集
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -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')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue