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);