diff --git a/apps/accounts/automations/gather_account/filter.py b/apps/accounts/automations/gather_account/filter.py index a12b78586..bc87a7511 100644 --- a/apps/accounts/automations/gather_account/filter.py +++ b/apps/accounts/automations/gather_account/filter.py @@ -13,6 +13,7 @@ def parse_date(date_str, default=None): formats = [ '%Y/%m/%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S', + '%Y-%m-%d %H:%M:%S', '%d-%m-%Y %H:%M:%S', '%Y/%m/%d', '%d-%m-%Y', @@ -26,7 +27,6 @@ def parse_date(date_str, default=None): return default -# TODO 后期会挪到 playbook 中 class GatherAccountsFilter: def __init__(self, tp): self.tp = tp @@ -208,14 +208,35 @@ class GatherAccountsFilter: key, value = parts user_info[key.strip()] = value.strip() detail = {'groups': user_info.get('Global Group memberships', ''), } - user = { - 'username': user_info.get('User name', ''), - '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', '')), + + username = user_info.get('User name') + if not username: + continue + + result[username] = { + 'username': username, + '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')), + 'groups': detail, + } + return result + + @staticmethod + def windows_ad_filter(info): + result = {} + for user_info in info['user_details']: + detail = {'groups': user_info.get('GlobalGroupMemberships', ''), } + username = user_info.get('SamAccountName') + if not username: + continue + result[username] = { + 'username': username, + 'date_password_change': parse_date(user_info.get('PasswordLastSet')), + 'date_password_expired': parse_date(user_info.get('PasswordExpires')), + 'date_last_login': parse_date(user_info.get('LastLogonDate')), 'groups': detail, } - result[user['username']] = user return result @staticmethod diff --git a/apps/accounts/automations/gather_account/host/windows/manifest.yml b/apps/accounts/automations/gather_account/host/windows/manifest.yml index 1d771d0fd..6cfd67a33 100644 --- a/apps/accounts/automations/gather_account/host/windows/manifest.yml +++ b/apps/accounts/automations/gather_account/host/windows/manifest.yml @@ -4,10 +4,10 @@ version: 1 method: gather_accounts category: - host - - ds + type: - windows - - windows_ad + i18n: Windows account gather: diff --git a/apps/accounts/automations/gather_account/host/windows_ad/main.yml b/apps/accounts/automations/gather_account/host/windows_ad/main.yml new file mode 100644 index 000000000..b1d57624e --- /dev/null +++ b/apps/accounts/automations/gather_account/host/windows_ad/main.yml @@ -0,0 +1,74 @@ +- hosts: demo + gather_facts: no + tasks: + - name: Import ActiveDirectory module + win_shell: Import-Module ActiveDirectory + args: + warn: false + + - name: Get the SamAccountName list of all AD users + win_shell: | + Import-Module ActiveDirectory + Get-ADUser -Filter * | Select-Object -ExpandProperty SamAccountName + register: ad_user_list + + - name: Set the all_users variable + set_fact: + all_users: "{{ ad_user_list.stdout_lines }}" + + - name: Get detailed information for each user + win_shell: | + Import-Module ActiveDirectory + + $user = Get-ADUser -Identity {{ item }} -Properties Name, SamAccountName, Enabled, LastLogonDate, PasswordLastSet, msDS-UserPasswordExpiryTimeComputed, MemberOf + + $globalGroups = @() + if ($user.MemberOf) { + $globalGroups = $user.MemberOf | ForEach-Object { + try { + $group = Get-ADGroup $_ -ErrorAction Stop + if ($group.GroupScope -eq 'Global') { $group.Name } + } catch { + } + } + } + + $passwordExpiry = $null + $expiryRaw = $user.'msDS-UserPasswordExpiryTimeComputed' + if ($expiryRaw) { + try { + $passwordExpiry = [datetime]::FromFileTime($expiryRaw) + } catch { + $passwordExpiry = $null + } + } + + $output = [PSCustomObject]@{ + Name = $user.Name + SamAccountName = $user.SamAccountName + Enabled = $user.Enabled + LastLogonDate = if ($user.LastLogonDate) { $user.LastLogonDate.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + PasswordLastSet = if ($user.PasswordLastSet) { $user.PasswordLastSet.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + PasswordExpires = if ($passwordExpiry) { $passwordExpiry.ToString("yyyy-MM-dd HH:mm:ss") } else { $null } + GlobalGroupMemberships = $globalGroups + } + + $output | ConvertTo-Json -Depth 3 + loop: "{{ all_users }}" + register: ad_user_details + ignore_errors: yes + + + - set_fact: + info: + user_details: >- + {{ + ad_user_details.results + | selectattr('rc', 'equalto', 0) + | map(attribute='stdout') + | select('truthy') + | map('from_json') + }} + + - debug: + var: info \ No newline at end of file diff --git a/apps/accounts/automations/gather_account/host/windows_ad/manifest.yml b/apps/accounts/automations/gather_account/host/windows_ad/manifest.yml new file mode 100644 index 000000000..2dbaddeb0 --- /dev/null +++ b/apps/accounts/automations/gather_account/host/windows_ad/manifest.yml @@ -0,0 +1,15 @@ +id: gather_accounts_windows_ad +name: "{{ 'Windows account gather' | trans }}" +version: 1 +method: gather_accounts +category: + - ds + +type: + - windows_ad + +i18n: + Windows account gather: + zh: 使用命令 Get-ADUser 收集 Windows 账号 + ja: コマンド Get-ADUser を使用して Windows アカウントを収集する + en: Using command Get-ADUser to gather accounts diff --git a/apps/accounts/automations/gather_account/manager.py b/apps/accounts/automations/gather_account/manager.py index f6af0d4d9..de677fa6d 100644 --- a/apps/accounts/automations/gather_account/manager.py +++ b/apps/accounts/automations/gather_account/manager.py @@ -1,6 +1,6 @@ +import time from collections import defaultdict -import time from django.utils import timezone from accounts.const import AutomationTypes @@ -222,6 +222,7 @@ class GatherAccountsManager(AccountBasePlaybookManager): def _collect_asset_account_info(self, asset, info): result = self._filter_success_result(asset.type, info) accounts = [] + for username, info in result.items(): self.asset_usernames_mapper[str(asset.id)].add(username) diff --git a/apps/assets/migrations/0016_auto_20250331_1149.bak b/apps/assets/migrations/0016_auto_20250331_1149.bak deleted file mode 100644 index 3b135e3ab..000000000 --- a/apps/assets/migrations/0016_auto_20250331_1149.bak +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 4.1.13 on 2025-03-31 02:49 - -import json - -import django -from django.db import migrations, models - -from assets.const.types import AllTypes - - -class Migration(migrations.Migration): - dependencies = [ - ("assets", "0015_automationexecution_type"), - ] - - operations = [ - migrations.RunPython(add_ad_host_type), - migrations.CreateModel( - name="DS", - fields=[ - ( - "asset_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="assets.asset", - ), - ), - ( - "domain_name", - models.CharField( - blank=True, - default="", - max_length=128, - verbose_name="Domain name", - ), - ), - ], - options={ - "verbose_name": "Active Directory", - }, - bases=("assets.asset",), - ), - migrations.AddField( - model_name="platform", - name="ds", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="ad_platforms", - to="assets.ds", - verbose_name="Active Directory", - ), - ), - migrations.AddField( - model_name="platform", - name="ds_enabled", - field=models.BooleanField(default=False, verbose_name="DS enabled"), - ), - ] diff --git a/apps/assets/migrations/0017_auto_20250407_1124.py b/apps/assets/migrations/0017_auto_20250407_1124.py index 756b7af76..b25cee40d 100644 --- a/apps/assets/migrations/0017_auto_20250407_1124.py +++ b/apps/assets/migrations/0017_auto_20250407_1124.py @@ -48,7 +48,7 @@ def add_ds_platforms(apps, schema_editor): }, "gather_accounts_enabled": true, - "gather_accounts_method": "gather_accounts_windows", + "gather_accounts_method": "gather_accounts_windows_ad", "gather_accounts_params": { }, diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 38627419e..f165a31d5 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -2,6 +2,7 @@ import json import os import re +import sys from collections import defaultdict import sys @@ -194,6 +195,7 @@ class JMSInventory: secret_info = {k: v for k, v in asset.secret_info.items() if v} host = { 'name': name, + 'local_python_interpreter': sys.executable, 'jms_asset': { 'id': str(asset.id), 'name': asset.name, 'address': asset.address, 'type': tp, 'category': category,